orm to frontend connected. for a time, i'll be trying multiple solutions at once.
This commit is contained in:
@@ -1,233 +0,0 @@
|
||||
<?php
|
||||
require_once 'db.php';
|
||||
require_once 'article_functions.php';
|
||||
|
||||
use Postmark\PostmarkClient;
|
||||
|
||||
function follow_user($follower_user_id, $following_user_id, $notify)
|
||||
{
|
||||
exec_stmt('INSERT INTO follows (follower_user_id, following_user_id, notify_user) VALUES (?, ?, ?)', 'iii', $follower_user_id, $following_user_id, $notify);
|
||||
}
|
||||
|
||||
function unfollow_user($follower_user_id, $following_user_id)
|
||||
{
|
||||
exec_stmt('DELETE FROM follows WHERE follower_user_id = ? AND following_user_id = ?', 'ii', $follower_user_id, $following_user_id);
|
||||
}
|
||||
|
||||
function user_id_exists($user_id)
|
||||
{
|
||||
$result = exec_stmt('SELECT user_id FROM user WHERE user_id = ?', 'i', $user_id)->fetch_assoc();
|
||||
|
||||
if ($result == null)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function username_exists($username)
|
||||
{
|
||||
$result = exec_stmt('SELECT username FROM user WHERE username = ?', 'i', $username)->fetch_assoc();
|
||||
|
||||
if ($result == null)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function logout()
|
||||
{
|
||||
foreach (array_keys($_SESSION) as $key)
|
||||
unset($_SESSION[$key]);
|
||||
|
||||
header('Location: .');
|
||||
}
|
||||
|
||||
function create_user($username, $email, $password, $verify_password, $role_id, $upload = null, $x = null, $y = null, $crop_width = null, $honeypot = null)
|
||||
{
|
||||
if (!empty($honeypot)) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
You are a bot. Leave now.
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
if ($password != $verify_password) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
Passwords do not match!
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
if (username_exists($username)) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
Username is taken!
|
||||
</div>
|
||||
';
|
||||
}
|
||||
|
||||
$password_hash = hash('sha256', $password);
|
||||
|
||||
$id = rand(1000, 999999999);
|
||||
while (user_id_exists($id))
|
||||
$id = rand(1000, 999999999);
|
||||
|
||||
if (!empty($upload['profile_picture']['tmp_name'])) {
|
||||
$orig_size = getimagesize($upload['profile_picture']['tmp_name']);
|
||||
$orig_width = $orig_size[0];
|
||||
$orig_height = $orig_size[1];
|
||||
|
||||
$crop_size = min($orig_width, $orig_height) * 0.56;
|
||||
|
||||
crop_image($upload['profile_picture']['tmp_name'], $x, $y, $crop_size, $crop_size);
|
||||
$target = upload($upload['profile_picture'], $_ENV['PROFILE_IMAGES_FQ_PATH'], $id);
|
||||
} else {
|
||||
$target = 'default-profile.png';
|
||||
}
|
||||
|
||||
exec_stmt('INSERT INTO user (user_id, username, email, password_hash, role_id, profile_picture) VALUES (?, ?, ?, ?, ?, ?)', 'isssis', $id, $username, $email, $password_hash, $role_id, $target);
|
||||
return true;
|
||||
}
|
||||
|
||||
function reset_password($user_id, $new_password, $verify_new_password)
|
||||
{
|
||||
if ($new_password != $verify_new_password) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
Passwords do not match!
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
$password_hash = hash('sha256', $new_password);
|
||||
exec_stmt('UPDATE user SET password_hash = ? WHERE user_id = ?', 'si', $password_hash, $user_id);
|
||||
}
|
||||
|
||||
function update_email($user_id, $new_email, $verify_new_email)
|
||||
{
|
||||
if ($new_email != $verify_new_email) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
Emails do not match!
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
exec_stmt('UPDATE user SET email = ? WHERE user_id = ?', 'si', $new_email, $user_id);
|
||||
}
|
||||
|
||||
function update_username($user_id, $new_username, $verify_new_username)
|
||||
{
|
||||
if ($new_username != $verify_new_username) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
Usernames do not match!
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
if ($result = username_exists($new_username))
|
||||
return $result;
|
||||
|
||||
exec_stmt('UPDATE user SET username = ? WHERE user_id = ?', 'si', $new_username, $user_id);
|
||||
}
|
||||
|
||||
function update_profile_picture($user_id) {}
|
||||
|
||||
function request_role_change($user_id, $new_role_id)
|
||||
{
|
||||
$user = exec_stmt('SELECT user_id, username, email, roles.role, created_at FROM user INNER JOIN roles WHERE user.role_id = roles.role_id AND user_id = ?', 'i', $user_id)->fetch_assoc();
|
||||
$new_role = exec_stmt('SELECT role FROM roles WHERE role_id = ?', 'i', $new_role_id)->fetch_array();
|
||||
|
||||
$email_html = '
|
||||
<h1>User Role Change Request</h1>
|
||||
<p>The following user has requested their role on vintagecoding.net to be changed:</p>
|
||||
<p><strong>User ID: </strong>' . $user['user_id'] . '</p>
|
||||
<p><strong>Username: </strong>' . $user['username'] . '</p>
|
||||
<p><strong>Email: </strong>' . $user['email'] . '</p>
|
||||
<p><strong>Current Role: </strong>' . $user['role'] . ' -> ' . $new_role[0] . '</p>
|
||||
<p><strong>Created At: </strong>' . $user['created_at'] . '</p>
|
||||
';
|
||||
|
||||
$client = new PostmarkClient($_ENV['POSTMARK_API_TOKEN']);
|
||||
|
||||
$client->sendEmail(
|
||||
'mailer@joshashton.dev',
|
||||
'me@joshashton.dev',
|
||||
'User Role Change Request - ' . $user['user_id'],
|
||||
$email_html
|
||||
);
|
||||
}
|
||||
|
||||
function delete_account($user_id, $password = null, $verify_password = null)
|
||||
{
|
||||
if ($user_id == 2025) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
I am the owner, and I cannot delete myself...
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
if ($password != $verify_password) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
Passwords do not match!
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
if ($password && $verify_password) {
|
||||
$password_hash = hash('sha256', $password);
|
||||
$user = exec_stmt('SELECT user_id, profile_picture FROM user WHERE user_id = ? AND password_hash = ?', 'is', $user_id, $password_hash)->fetch_assoc();
|
||||
|
||||
if ($user == null) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
Username and/or Password are invalid!
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
// Hardcoded prevention of deleting the owner's profile picture
|
||||
if (!empty($user['profile_picture']) && $user['profile_picture'] != '2025.jpg')
|
||||
delete_file($_ENV['PROFILE_IMAGES_FQ_PATH'] . $user['profile_picture']);
|
||||
|
||||
exec_stmt('DELETE FROM user WHERE user_id = ?', 'i', $user_id);
|
||||
logout();
|
||||
} else if ($_SESSION['role_id'] <= admin) {
|
||||
$user = exec_stmt('SELECT user_id, profile_picture FROM user WHERE user_id = ?', 'i', $user_id)->fetch_assoc();
|
||||
|
||||
if ($user['user_id'] == 2025) {
|
||||
$error_msg = '
|
||||
<div class="error">
|
||||
Cannot delete the owner!
|
||||
</div>
|
||||
';
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
// Hardcoded prevention of deleting the owner's profile picture
|
||||
if (!empty($user['profile_picture']) && $user['profile_picture'] != '2025.jpg')
|
||||
delete_file($_ENV['PROFILE_IMAGES_FQ_PATH'] . $user['profile_picture']);
|
||||
|
||||
exec_stmt('DELETE FROM user WHERE user_id = ?', 'i', $user_id);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
<?php
|
||||
require_once 'db.php';
|
||||
|
||||
function article_ids_by_newest()
|
||||
{
|
||||
$conn = get_connection();
|
||||
$results = $conn->query('SELECT article_id FROM article ORDER BY published_at DESC');
|
||||
$ids = [];
|
||||
|
||||
while ($row = $results->fetch_assoc())
|
||||
$ids[] = $row['article_id'];
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
function article_ids_by_oldest()
|
||||
{
|
||||
$conn = get_connection();
|
||||
$results = $conn->query('SELECT article_id FROM article ORDER BY published_at ASC');
|
||||
$ids = [];
|
||||
|
||||
while ($row = $results->fetch_assoc())
|
||||
$ids[] = $row['article_id'];
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
function articles_ids_by_author($author_id)
|
||||
{
|
||||
$results = exec_stmt('SELECT article_id FROM article WHERE author_id = ? ORDER BY published_at DESC', 'i', $author_id);
|
||||
$ids = [];
|
||||
|
||||
while ($row = $results->fetch_assoc())
|
||||
$ids[] = $row['article_id'];
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
function article_tags()
|
||||
{
|
||||
$conn = get_connection();
|
||||
$results = $conn->query('SELECT * FROM tags');
|
||||
$tags = [];
|
||||
|
||||
while ($row = $results->fetch_assoc())
|
||||
$tags[] = $row;
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
function article_ids_by_tag($tag)
|
||||
{
|
||||
$results = exec_stmt('SELECT article_id FROM article WHERE article.article_id IN ( SELECT article_tags.article_id FROM article_tags INNER JOIN tags ON article_tags.tag_id = tags.tag_id WHERE tag = ?) ORDER BY published_at DESC', 's', $tag);
|
||||
$ids = [];
|
||||
|
||||
while ($row = $results->fetch_assoc())
|
||||
$ids[] = $row['article_id'];
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
function create_article($author_id, $title, $excerpt, $tags, $markdown_file_contents)
|
||||
{
|
||||
$id = exec_stmt('INSERT INTO article (author_id, title, excerpt, published_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)', 'iss', $author_id, $title, $excerpt);
|
||||
|
||||
$path = $_ENV['ARTICLES_FQ_PATH'] . $id . '/';
|
||||
mkdir($path);
|
||||
|
||||
$fs = fopen($path . 'article.md', 'a');
|
||||
fwrite($fs, $markdown_file_contents);
|
||||
fclose($fs);
|
||||
|
||||
if (count($tags) > 0) {
|
||||
$tag_insert_stmt = 'INSERT INTO article_tags (article_id, tag_id) VALUES ';
|
||||
$types = '';
|
||||
for ($i = 0; $i < count($tags); $i++) {
|
||||
$tag_insert_stmt .= '(' . $id . ', ?)';
|
||||
|
||||
if ($i < count($tags) - 1)
|
||||
$tag_insert_stmt .= ', ';
|
||||
|
||||
$types .= 'i';
|
||||
}
|
||||
|
||||
exec_stmt($tag_insert_stmt, $types, ...$tags);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
function update_article($article_id, $title, $excerpt, $tags, $markdown_file_contents)
|
||||
{
|
||||
exec_stmt('UPDATE article SET title = ?, excerpt = ?, updated_at = CURRENT_TIMESTAMP WHERE article_id = ?', 'ssi', $title, $excerpt, $article_id);
|
||||
|
||||
$path = $_ENV['ARTICLES_FQ_PATH'] . $article_id . '/';
|
||||
$fs = fopen($path . 'article.md', 'w');
|
||||
fwrite($fs, $markdown_file_contents);
|
||||
fclose($fs);
|
||||
|
||||
exec_stmt('DELETE from article_tags WHERE article_id = ?', 'i', $article_id);
|
||||
|
||||
if (count($tags) > 0) {
|
||||
$tag_insert_stmt = 'INSERT INTO article_tags (article_id, tag_id) VALUES ';
|
||||
$types = '';
|
||||
for ($i = 0; $i < count($tags); $i++) {
|
||||
$tag_insert_stmt .= '(' . $article_id . ', ?)';
|
||||
|
||||
if ($i < count($tags) - 1)
|
||||
$tag_insert_stmt .= ', ';
|
||||
|
||||
$types .= 'i';
|
||||
}
|
||||
|
||||
exec_stmt($tag_insert_stmt, $types, ...$tags);
|
||||
}
|
||||
}
|
||||
|
||||
function delete_article($article_id)
|
||||
{
|
||||
exec_stmt('DELETE FROM article_tags WHERE article_id = ?', 'i', $article_id);
|
||||
exec_stmt('DELETE FROM article WHERE article_id = ?', 'i', $article_id);
|
||||
|
||||
delete_dir($_ENV['ARTICLES_FQ_PATH'] . $article_id . '/');
|
||||
}
|
||||
|
||||
function delete_articles_by_author($author_id)
|
||||
{
|
||||
exec_stmt('DELETE FROM article WHERE author_id = ?', 'i', $author_id);
|
||||
}
|
||||
|
||||
function increment_read_counter($article_id)
|
||||
{
|
||||
exec_stmt('UPDATE article SET read_count = read_count + 1 WHERE article_id = ?', 'i', $article_id);
|
||||
}
|
||||
@@ -1,11 +1,30 @@
|
||||
<?php
|
||||
include_once 'functions/init.php';
|
||||
require_once 'init.php';
|
||||
|
||||
use Postmark\PostmarkClient;
|
||||
|
||||
if (!empty($_FILES['upload']))
|
||||
upload($_FILES['upload'], $_ENV['UPLOAD_TMP_FQ_PATH'], isset($_POST['filename']) ? $_POST['filename'] : null);
|
||||
|
||||
/*
|
||||
* @* @param int $mode 0 for User, 1 for Post, 2 for Space
|
||||
*/
|
||||
function randomId(int $mode)
|
||||
{
|
||||
$id = rand(1000, 999999999999);
|
||||
switch ($mode) {
|
||||
case 0:
|
||||
if (User::exists($id))
|
||||
return randomId($mode);
|
||||
else
|
||||
return $id;
|
||||
case 1:
|
||||
break;
|
||||
case 2:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function notify_users($emails, $auther_username, $article_id, $article_title, $excerpt)
|
||||
{
|
||||
foreach ($emails as $email) {
|
||||
@@ -16,7 +35,6 @@ function notify_users($emails, $auther_username, $article_id, $article_title, $e
|
||||
<p>' . $excerpt . '</p>
|
||||
';
|
||||
|
||||
|
||||
$client = new PostmarkClient($_ENV['POSTMARK_API_TOKEN']);
|
||||
|
||||
$client->sendEmail(
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
|
||||
$dotenv = Dotenv\Dotenv::createImmutable('/home/jashton/dev/me/weave.space/');
|
||||
$dotenv->safeLoad();
|
||||
|
||||
// Use environment variables for the database password and IP address.
|
||||
|
||||
+6
-5
@@ -5,10 +5,12 @@ header("Content-Security-Policy: default-src 'self' localhost:7477; font-src 'se
|
||||
// Initialize Composer.
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'db.php';
|
||||
require_once 'account_functions.php';
|
||||
require_once 'user.php';
|
||||
require_once 'post.php';
|
||||
require_once 'spaces.php';
|
||||
|
||||
// Load environment variables.
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
|
||||
$dotenv = Dotenv\Dotenv::createImmutable('/home/jashton/dev/me/weave.space/.env');
|
||||
$dotenv->safeLoad();
|
||||
|
||||
// Set SESSION variable.
|
||||
@@ -21,9 +23,8 @@ if (!isset($_SESSION['initialized'])) {
|
||||
}
|
||||
|
||||
define('owner', 1);
|
||||
define('admin', 2);
|
||||
define('contributor', 3);
|
||||
define('reader', 4);
|
||||
define('developer', 2);
|
||||
define('user', 3);
|
||||
|
||||
// Update the database and cookies to keep the user logged in.
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
|
||||
@@ -0,0 +1,805 @@
|
||||
<?php
|
||||
|
||||
// It's good practice to use namespaces, e.g.:
|
||||
// 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';
|
||||
|
||||
// -- Data Transfer Object for Topics --
|
||||
class Topic
|
||||
{
|
||||
public int $topicId;
|
||||
public string $name;
|
||||
|
||||
public function __construct(int $topicId, string $name)
|
||||
{
|
||||
$this->topicId = $topicId;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->topicId;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
// -- 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 ?DateTime $createdAt;
|
||||
private ?DateTime $updatedAt;
|
||||
private ?DateTime $status;
|
||||
private bool $published;
|
||||
|
||||
public function __construct($postArray)
|
||||
{
|
||||
$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'];
|
||||
}
|
||||
|
||||
public function getPostId(): int
|
||||
{
|
||||
return $this->postId;
|
||||
}
|
||||
|
||||
public function getAuthor(): Author
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function getPostTypeId(): int
|
||||
{
|
||||
return $this->postTypeId;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getSlug(): ?string
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function getReadCount(): int
|
||||
{
|
||||
return $this->readCount;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?DateTime
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function getStatus(): ?DateTime
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function isPublished(): bool
|
||||
{
|
||||
return $this->published;
|
||||
}
|
||||
|
||||
public function incrementReadCount(): void
|
||||
{
|
||||
$this->readCount++;
|
||||
}
|
||||
|
||||
public function setSlug(?string $slug): void
|
||||
{
|
||||
$this->slug = $slug;
|
||||
}
|
||||
|
||||
public function setStatus(?string $status): void
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
public function setPublished(bool $published): void
|
||||
{
|
||||
$this->published = $published;
|
||||
}
|
||||
|
||||
public function setReadCount(int $count): void
|
||||
{
|
||||
$this->readCount = $count;
|
||||
}
|
||||
}
|
||||
|
||||
class Essay extends Post
|
||||
{
|
||||
private ?string $markdown;
|
||||
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)
|
||||
{
|
||||
parent::__construct($postId, $author, 1, $title, $slug, $createdAt, $updatedAt, $publishedAt, $published);
|
||||
$this->markdown = $markdown;
|
||||
$this->excerpt = $excerpt;
|
||||
$this->files = $files;
|
||||
}
|
||||
|
||||
public function getMarkdown(): ?string
|
||||
{
|
||||
return $this->markdown;
|
||||
}
|
||||
|
||||
public function getExcerpt(): ?string
|
||||
{
|
||||
return $this->excerpt;
|
||||
}
|
||||
|
||||
public function getFiles(): ?array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
public function setMarkdown(?string $markdown): void
|
||||
{
|
||||
$this->markdown = $markdown;
|
||||
}
|
||||
|
||||
public function setExcerpt(?string $excerpt): void
|
||||
{
|
||||
$this->excerpt = $excerpt;
|
||||
}
|
||||
|
||||
public function setFiles(?array $files): void
|
||||
{
|
||||
$this->files = $files;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
parent::__construct($postId, $author, 2, null, null, $createdAt, $updatedAt, $publishedAt, $published);
|
||||
$this->caption = $caption;
|
||||
$this->images = $images;
|
||||
}
|
||||
|
||||
public function getCaption(): ?string
|
||||
{
|
||||
return $this->caption;
|
||||
}
|
||||
|
||||
public function getImages(): ?array
|
||||
{
|
||||
return $this->images;
|
||||
}
|
||||
|
||||
public function setCaption(?string $caption): void
|
||||
{
|
||||
$this->caption = $caption;
|
||||
}
|
||||
|
||||
public function setImages(?array $images): void
|
||||
{
|
||||
$this->images = $images;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
parent::__construct($postId, $author, 3, null, null, $createdAt, $updatedAt, $publishedAt, $published);
|
||||
$this->thought = $thought;
|
||||
}
|
||||
|
||||
public function getThought(): ?string
|
||||
{
|
||||
return $this->thought;
|
||||
}
|
||||
|
||||
public function setThought(?string $thought): void
|
||||
{
|
||||
$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.
|
||||
*/
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
require_once 'core.php';
|
||||
require_once 'db.php';
|
||||
require_once 'user.php';
|
||||
require_once 'post.php';
|
||||
|
||||
class Space
|
||||
{
|
||||
private int $spaceId;
|
||||
private string $name;
|
||||
private string $description;
|
||||
private array $members;
|
||||
private array $posts;
|
||||
private ?Space $parent;
|
||||
|
||||
public function __construct(?int $spaceId, string $name, string $description, array $members, $posts, ?Space $parent = null)
|
||||
{
|
||||
$this->spaceId = $spaceId ?? randomId(2);
|
||||
$this->name = $name;
|
||||
$this->description = $description;
|
||||
$this->members = $members;
|
||||
$this->posts[] = $posts;
|
||||
$this->parent = $parent;
|
||||
|
||||
/* $stmt = 'INSERT INTO spaces (spaceId, name, description, parentSpaceIdk)'; */
|
||||
}
|
||||
|
||||
public static function retrieveFromDB(int $space, ?int $user): ?Space
|
||||
{
|
||||
if ($user) {
|
||||
$stmt = 'SELECT * FROM spaceMembers WHERE space = ? AND user = ?';
|
||||
$isMember = exec_stmt($stmt, 'ii', $space, $user)->fetch_assoc();
|
||||
if (!$isMember)
|
||||
return null;
|
||||
} else {
|
||||
$stmt = 'SELECT visibility FROM space WHERE id = ?';
|
||||
$isOpen = exec_stmt($stmt, 'i', $space);
|
||||
if (!$isOpen)
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = 'SELECT * FROM space WHERE id = ?';
|
||||
$space = exec_stmt($stmt, 'i', $space)->fetch_assoc();
|
||||
|
||||
if (!empty($space['parentSpace']))
|
||||
$parent = Space::retrieveFromDB($space['parentSpace'], $user);
|
||||
else
|
||||
$parent = null;
|
||||
|
||||
$stmt = 'SELECT user FROM spaceMembers WHERE space = ?';
|
||||
$members = exec_stmt($stmt, 'i', $space)->fetch_assoc();
|
||||
|
||||
$stmt = 'SELECT post FROM spacePosts WHERE space = ?';
|
||||
$posts = exec_stmt($stmt, 'i', $space)->fetch_assoc();
|
||||
|
||||
return new Space($space['id'], $space['name'], $space['description'], $members, $posts, $parent);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
echo 'todo: this should be JSON';
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->spaceId;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
// class Todo
|
||||
// {
|
||||
// private $todo_id;
|
||||
// private $user_id;
|
||||
// private $task;
|
||||
// private $description;
|
||||
// private $priority;
|
||||
// private $due_date;
|
||||
// private $category;
|
||||
// private $status;
|
||||
// private $created_at;
|
||||
// private $update_at;
|
||||
// private $completed_at;
|
||||
// private $is_recurring;
|
||||
// private $recurrence_rule;
|
||||
// private $reminder_at;
|
||||
//
|
||||
// function __construct(
|
||||
// $todo_id,
|
||||
// $user_id,
|
||||
// $task,
|
||||
// $description,
|
||||
// $priority,
|
||||
// $due_date,
|
||||
// $category,
|
||||
// $status,
|
||||
// $created_at,
|
||||
// $update_at,
|
||||
// $completed_at,
|
||||
// $is_recurring,
|
||||
// $recurrence_rule,
|
||||
// $reminder_at,
|
||||
// ) {
|
||||
// $this->todo_id = $todo_id;
|
||||
// $this->user_id = $user_id;
|
||||
// $this->task = $task;
|
||||
// $this->description = $description;
|
||||
// $this->priority = $priority;
|
||||
// $this->due_date = $due_date;
|
||||
// $this->category = $category;
|
||||
// $this->status = $status;
|
||||
// $this->created_at = $created_at;
|
||||
// $this->update_at = $update_at;
|
||||
// $this->completed_at = $completed_at;
|
||||
// $this->is_recurring = $is_recurring;
|
||||
// $this->recurrence_rule = $recurrence_rule;
|
||||
// $this->reminder_at = $reminder_at;
|
||||
// }
|
||||
// }
|
||||
|
||||
function change_status($todo_id, $status)
|
||||
{
|
||||
exec_stmt('UPDATE todos SET status = ? WHERE todo_id = ?', 'si', $status, $todo_id);
|
||||
}
|
||||
|
||||
function create_todo($todo)
|
||||
{
|
||||
exec_stmt('INSERT INTO todos (title, description, priority, due_date, category, status) VALUES (?,?,?,?,?,?)', 'ssssss', $todo['title'], $todo['description'], $todo['priority'], $todo['due_date'], $todo['category'], 'open');
|
||||
}
|
||||
function update_todo($todo_id, $todo)
|
||||
{
|
||||
exec_stmt('UPDATE todos SET title = ? WHERE todo_id = ?', 'si', $todo['title'], $todo_id);
|
||||
exec_stmt('UPDATE todos SET description = ? WHERE todo_id = ?', 'si', $todo['description'], $todo_id);
|
||||
exec_stmt('UPDATE todos SET priority = ? WHERE todo_id = ?', 'si', $todo['priority'], $todo_id);
|
||||
exec_stmt('UPDATE todos SET due_date = ? WHERE todo_id = ?', 'si', $todo['due_date'], $todo_id);
|
||||
exec_stmt('UPDATE todos SET category = ? WHERE todo_id = ?', 'si', $todo['category'], $todo_id);
|
||||
exec_stmt('UPDATE todos SET status = ? WHERE todo_id = ?', 'si', $todo['status'], $todo_id);
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
<?php
|
||||
|
||||
class LoginCredentials
|
||||
{
|
||||
private string $username;
|
||||
private string $password;
|
||||
|
||||
public function __construct(string $username, string $password)
|
||||
{
|
||||
// More intensive SQL injection checks (example - adapt as needed)
|
||||
if (preg_match('/[\'";\-\_]/', $username) || preg_match('/[\'";\-\_]/', $password)) {
|
||||
throw new InvalidArgumentException('Invalid characters in username or password.');
|
||||
}
|
||||
// Consider using a more robust validation library
|
||||
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
}
|
||||
|
||||
class User
|
||||
{
|
||||
private int $userId;
|
||||
private string $username;
|
||||
private ?DateTime $createdAt;
|
||||
private ?DateTime $lastActive;
|
||||
private bool $isActive;
|
||||
private ?string $profilePicture;
|
||||
private ?string $bio;
|
||||
private ?string $website;
|
||||
|
||||
public function __construct(int $userId, string $username, ?string $profilePicture = null, ?string $bio = null, ?string $website = null, ?mysqli $db = null)
|
||||
{
|
||||
// Basic XSS prevention on construction (can be enhanced)
|
||||
$username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
|
||||
$bio = htmlspecialchars($bio ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$website = htmlspecialchars($website ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$profilePicture = htmlspecialchars($profilePicture ?? 'default-profile.png', ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Basic SQL injection prevention (should primarily rely on prepared statements)
|
||||
if (preg_match('/[\'";\-\_]/', $username) || preg_match('/[\'";\-\_]/', $bio) || preg_match('/[\'";\-\_]/', $website) || preg_match('/[\'";\-\_]/', $profilePicture)) {
|
||||
throw new InvalidArgumentException('Invalid characters in user data.');
|
||||
}
|
||||
|
||||
$this->userId = $userId;
|
||||
$this->username = $username;
|
||||
$this->profilePicture = $profilePicture;
|
||||
$this->bio = $bio;
|
||||
$this->website = $website;
|
||||
$this->createdAt = new DateTime();
|
||||
$this->lastActive = null;
|
||||
$this->isActive = false;
|
||||
|
||||
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);
|
||||
}
|
||||
} 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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static function logout(): void
|
||||
{
|
||||
foreach (array_keys($_SESSION) as $key) {
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
// Consider destroying the session cookie as well: session_destroy();
|
||||
header('Location: .'); // Redirect to the homepage or login page
|
||||
exit();
|
||||
}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getLastActive(): ?DateTime
|
||||
{
|
||||
return $this->lastActive;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
|
||||
public function getProfilePicture(): ?string
|
||||
{
|
||||
return $this->profilePicture;
|
||||
}
|
||||
|
||||
public function getBio(): ?string
|
||||
{
|
||||
return $this->bio;
|
||||
}
|
||||
|
||||
public function getWebsite(): ?string
|
||||
{
|
||||
return $this->website;
|
||||
}
|
||||
|
||||
public function setLastActive(?DateTime $lastActive): void
|
||||
{
|
||||
$this->lastActive = $lastActive;
|
||||
}
|
||||
|
||||
public function setIsActive(bool $isActive): void
|
||||
{
|
||||
$this->isActive = $isActive;
|
||||
}
|
||||
|
||||
public function toggleActive(?mysqli $db): bool
|
||||
{
|
||||
if (!$db) {
|
||||
error_log('Database connection not provided for toggleActive.');
|
||||
return false;
|
||||
}
|
||||
$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);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
return $result;
|
||||
} else {
|
||||
error_log('Error preparing statement: ' . $db->error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function updateUsername(string $newUsername, ?mysqli $db): bool
|
||||
{
|
||||
$newUsername = htmlspecialchars($newUsername, ENT_QUOTES, 'UTF-8');
|
||||
if (preg_match('/[\'";\-\_]/', $newUsername)) {
|
||||
throw new InvalidArgumentException('Invalid characters in username.');
|
||||
}
|
||||
if (!$db) {
|
||||
error_log('Database connection not provided for updateUsername.');
|
||||
return false;
|
||||
}
|
||||
$stmt = $db->prepare('UPDATE user SET username = ? WHERE user_id = ?');
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('si', $newUsername, $this->userId);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
if ($result) {
|
||||
$this->username = $newUsername;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
error_log('Error preparing statement: ' . $db->error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updatePassword(string $newPassword, ?mysqli $db): bool
|
||||
{
|
||||
$newPasswordHash = hash('sha256', $newPassword);
|
||||
if (!$db) {
|
||||
error_log('Database connection not provided for updatePassword.');
|
||||
return false;
|
||||
}
|
||||
$stmt = $db->prepare('UPDATE user SET password_hash = ? WHERE user_id = ?');
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('si', $newPasswordHash, $this->userId);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
return $result;
|
||||
} else {
|
||||
error_log('Error preparing statement: ' . $db->error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updateProfilePicture(?string $newProfilePicture, ?mysqli $db): bool
|
||||
{
|
||||
$newProfilePicture = htmlspecialchars($newProfilePicture ?? 'default-profile.png', ENT_QUOTES, 'UTF-8');
|
||||
if (preg_match('/[\'";\-\_]/', $newProfilePicture)) {
|
||||
throw new InvalidArgumentException('Invalid characters in profile picture filename.');
|
||||
}
|
||||
if (!$db) {
|
||||
error_log('Database connection not provided for updateProfilePicture.');
|
||||
return false;
|
||||
}
|
||||
$stmt = $db->prepare('UPDATE user SET profile_picture = ? WHERE user_id = ?');
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('si', $newProfilePicture, $this->userId);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
if ($result) {
|
||||
$this->profilePicture = $newProfilePicture;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
error_log('Error preparing statement: ' . $db->error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function followUser(int $followingUserId, bool $notify = false, ?mysqli $db): bool
|
||||
{
|
||||
if (!$db) {
|
||||
error_log('Database connection not provided for followUser.');
|
||||
return false;
|
||||
}
|
||||
$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);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
return $result;
|
||||
} else {
|
||||
error_log('Error preparing statement: ' . $db->error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getFollowers(?mysqli $db): array
|
||||
{
|
||||
if (!$db) {
|
||||
error_log('Database connection not provided for getFollowers.');
|
||||
return [];
|
||||
}
|
||||
$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->execute();
|
||||
$result = $stmt->get_result();
|
||||
$followers = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$followers[] = new User(
|
||||
(int) $row['user_id'],
|
||||
$row['username'],
|
||||
$row['profile_picture'],
|
||||
$row['bio'],
|
||||
$row['website']
|
||||
);
|
||||
}
|
||||
$stmt->close();
|
||||
return $followers;
|
||||
} else {
|
||||
error_log('Error preparing statement: ' . $db->error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function getFollowing(?mysqli $db): array
|
||||
{
|
||||
if (!$db) {
|
||||
error_log('Database connection not provided for getFollowing.');
|
||||
return [];
|
||||
}
|
||||
$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->execute();
|
||||
$result = $stmt->get_result();
|
||||
$following = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$following[] = new User(
|
||||
(int) $row['user_id'],
|
||||
$row['username'],
|
||||
$row['profile_picture'],
|
||||
$row['bio'],
|
||||
$row['website']
|
||||
);
|
||||
}
|
||||
$stmt->close();
|
||||
return $following;
|
||||
} else {
|
||||
error_log('Error preparing statement: ' . $db->error);
|
||||
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 {}
|
||||
Reference in New Issue
Block a user