Compare commits

..

16 Commits

Author SHA1 Message Date
Flatlogic Bot
40a0d866cd Auto commit: 2025-12-19T15:08:47.391Z 2025-12-19 15:08:47 +00:00
Flatlogic Bot
30eacc0737 Auto commit: 2025-12-18T09:10:55.601Z 2025-12-18 09:10:55 +00:00
Flatlogic Bot
8be74c322f Auto commit: 2025-12-18T08:16:52.309Z 2025-12-18 08:16:52 +00:00
Flatlogic Bot
de3473c411 Auto commit: 2025-12-18T06:36:58.434Z 2025-12-18 06:36:58 +00:00
Flatlogic Bot
cff6fa6b35 Auto commit: 2025-12-18T06:13:19.239Z 2025-12-18 06:13:19 +00:00
Flatlogic Bot
0c361cd8ae Auto commit: 2025-12-18T03:56:08.868Z 2025-12-18 03:56:08 +00:00
Flatlogic Bot
8b3cd42fc5 Edit index.php via Editor 2025-12-18 03:43:24 +00:00
Flatlogic Bot
01014239c5 Auto commit: 2025-12-18T03:42:59.566Z 2025-12-18 03:42:59 +00:00
Flatlogic Bot
eff072b3ce Auto commit: 2025-12-18T02:31:04.349Z 2025-12-18 02:31:04 +00:00
Flatlogic Bot
b2a5953ed8 Auto commit: 2025-12-18T02:09:40.536Z 2025-12-18 02:09:40 +00:00
Flatlogic Bot
f06143ee16 Auto commit: 2025-12-18T01:59:13.858Z 2025-12-18 01:59:13 +00:00
Flatlogic Bot
7dedde4564 Auto commit: 2025-12-18T00:54:07.143Z 2025-12-18 00:54:07 +00:00
Flatlogic Bot
0244805935 Auto commit: 2025-12-18T00:11:46.638Z 2025-12-18 00:11:46 +00:00
Flatlogic Bot
a05604fc8e Auto commit: 2025-12-18T00:08:23.757Z 2025-12-18 00:08:23 +00:00
Flatlogic Bot
ec07369670 Auto commit: 2025-12-18T00:07:44.665Z 2025-12-18 00:07:44 +00:00
Flatlogic Bot
0ba5997fad Auto commit: 2025-12-17T23:56:00.480Z 2025-12-17 23:56:00 +00:00
26 changed files with 2468 additions and 161 deletions

View File

@ -1,18 +0,0 @@
DirectoryIndex index.php index.html
Options -Indexes
Options -MultiViews
RewriteEngine On
# 0) Serve existing files/directories as-is
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.+?)/?$ $1.php [L]
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ $1 [R=301,L]

28
admin/auth.php Normal file
View File

@ -0,0 +1,28 @@
<?php
session_start();
$admin_timeout = 3600; // 1 hour
// Check if user is logged in and is an admin
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
header('Location: ../login.php');
exit();
}
// Session timeout logic
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $admin_timeout)) {
session_unset();
session_destroy();
header('Location: ../login.php?timeout');
exit();
}
// IP binding logic
if (isset($_SESSION['admin_ip_address']) && $_SESSION['admin_ip_address'] !== $_SERVER['REMOTE_ADDR']) {
session_unset();
session_destroy();
header('Location: ../login.php?ip_changed');
exit();
}
$_SESSION['last_activity'] = time(); // Update last activity time

151
admin/categories.php Normal file
View File

@ -0,0 +1,151 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../db/config.php';
$pdo = db();
// Handle form submissions for categories
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['add_category'])) {
$name = trim($_POST['category_name']);
if (!empty($name)) {
$stmt = $pdo->prepare("INSERT INTO categories (name) VALUES (?)");
$stmt->execute([$name]);
}
} elseif (isset($_POST['update_category'])) {
$id = $_POST['category_id'];
$name = trim($_POST['category_name']);
$visibility = isset($_POST['visibility']) ? 1 : 0;
$order = (int)$_POST['display_order'];
$stmt = $pdo->prepare("UPDATE categories SET name = ?, visibility = ?, display_order = ? WHERE id = ?");
$stmt->execute([$name, $visibility, $order, $id]);
} elseif (isset($_POST['delete_category'])) {
$id = $_POST['category_id'];
$stmt = $pdo->prepare("DELETE FROM categories WHERE id = ?");
$stmt->execute([$id]);
} elseif (isset($_POST['add_subcategory'])) {
$name = trim($_POST['subcategory_name']);
$category_id = $_POST['category_id'];
if (!empty($name)) {
$stmt = $pdo->prepare("INSERT INTO subcategories (category_id, name) VALUES (?, ?)");
$stmt->execute([$category_id, $name]);
}
}
header("Location: categories.php");
exit;
}
// Fetch all categories and subcategories
$categories = $pdo->query("SELECT * FROM categories ORDER BY display_order ASC, name ASC")->fetchAll();
$subcategories = [];
$stmt = $pdo->query("SELECT * FROM subcategories ORDER BY name ASC");
while ($row = $stmt->fetch()) {
$subcategories[$row['category_id']][] = $row;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Categories - Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
<div class="auth-links">
<a href="../index.php">View Site</a>
<a href="../logout.php">Logout</a>
</div>
</header>
<div class="container my-4">
<div class="row">
<div class="col-md-3">
<aside class="admin-nav">
<h3>Menu</h3>
<nav class="nav flex-column">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link active" href="categories.php">Categories</a>
<a class="nav-link" href="users.php">Users</a>
<a class="nav-link" href="links.php">Links</a>
<a class="nav-link" href="settings.php">Settings</a>
</nav>
</aside>
</div>
<div class="col-md-9">
<main class="content">
<h2>Manage Categories</h2>
<!-- Add Category Form -->
<div class="card mb-4">
<div class="card-header">Add New Category</div>
<div class="card-body">
<form method="POST" action="categories.php">
<div class="input-group">
<input type="text" class="form-control" name="category_name" placeholder="New category name" required>
<button class="btn btn-primary" type="submit" name="add_category">Add Category</button>
</div>
</form>
</div>
</div>
<!-- Category List -->
<?php foreach ($categories as $category): ?>
<div class="card mb-3">
<div class="card-body">
<form method="POST" action="categories.php" class="d-flex align-items-center">
<input type="hidden" name="category_id" value="<?php echo $category['id']; ?>">
<div class="flex-grow-1">
<input type="text" class="form-control" name="category_name" value="<?php echo htmlspecialchars($category['name']); ?>">
</div>
<div class="ms-3">
<label class="form-check-label me-2">Visible:</label>
<input class="form-check-input" type="checkbox" name="visibility" <?php echo $category['visibility'] ? 'checked' : ''; ?>>
</div>
<div class="ms-3" style="width: 80px;">
<input type="number" class="form-control" name="display_order" value="<?php echo $category['display_order']; ?>">
</div>
<div class="ms-3">
<button type="submit" name="update_category" class="btn btn-sm btn-success">Save</button>
<button type="submit" name="delete_category" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">Del</button>
</div>
</form>
<!-- Subcategories -->
<div class="mt-3 ms-4">
<h6>Subcategories</h6>
<ul>
<?php if (isset($subcategories[$category['id']])): ?>
<?php foreach ($subcategories[$category['id']] as $sub): ?>
<li><?php echo htmlspecialchars($sub['name']); ?></li>
<?php endforeach; ?>
<?php else: ?>
<li>No subcategories yet.</li>
<?php endif; ?>
</ul>
<form method="POST" action="categories.php" class="input-group input-group-sm">
<input type="hidden" name="category_id" value="<?php echo $category['id']; ?>">
<input type="text" class="form-control" name="subcategory_name" placeholder="New subcategory" required>
<button class="btn btn-outline-secondary" type="submit" name="add_subcategory">Add</button>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</main>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
</body>
</html>

79
admin/index.php Normal file
View File

@ -0,0 +1,79 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../db/config.php';
// Fetch some basic stats for the dashboard
$pdo = db();
$user_count = $pdo->query("SELECT count(*) FROM users")->fetchColumn();
$link_count = $pdo->query("SELECT count(*) FROM links")->fetchColumn();
$category_count = $pdo->query("SELECT count(*) FROM categories")->fetchColumn();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
<div class="auth-links">
<a href="../index.php">View Site</a>
<a href="../logout.php">Logout</a>
</div>
</header>
<div class="container my-4">
<div class="row">
<div class="col-md-3">
<aside class="admin-nav">
<h3>Menu</h3>
<nav class="nav flex-column">
<a class="nav-link active" href="index.php">Dashboard</a>
<a class="nav-link" href="categories.php">Categories</a>
<a class="nav-link" href="users.php">Users</a>
<a class="nav-link" href="links.php">Links</a>
<a class="nav-link" href="settings.php">Settings</a>
</nav>
</aside>
</div>
<div class="col-md-9">
<main class="content">
<h2>Dashboard</h2>
<div class="row">
<div class="col-md-4">
<div class="card text-center p-3">
<h3><?php echo $user_count; ?></h3>
<p>Total Users</p>
</div>
</div>
<div class="col-md-4">
<div class="card text-center p-3">
<h3><?php echo $link_count; ?></h3>
<p>Total Links</p>
</div>
</div>
<div class="col-md-4">
<div class="card text-center p-3">
<h3><?php echo $category_count; ?></h3>
<p>Total Categories</p>
</div>
</div>
</div>
</main>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
</body>
</html>

418
admin/links.php Normal file
View File

@ -0,0 +1,418 @@
<?php
require_once __DIR__ . '/auth.php';
// Debug block for POST data - visible only to admin
require_once __DIR__ . '/../db/config.php';
$pdo = db();
// CSRF Protection
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$response = ['success' => false, 'message' => 'Invalid request.', 'debug_post' => $_POST ?? [] ];
if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$response['message'] = 'CSRF token validation failed.';
echo json_encode($response);
exit;
}
$action = $_POST['action'] ?? '';
$link_id = $_POST['link_id'] ?? null;
if (!$link_id || !is_numeric($link_id)) {
$response['message'] = 'Invalid Link ID.';
echo json_encode($response);
exit;
}
switch ($action) {
case 'delete':
try {
$stmt = $pdo->prepare("DELETE FROM links WHERE id = ?");
$stmt->execute([$link_id]);
if ($stmt->rowCount()) {
$response = ['success' => true, 'message' => 'Link deleted successfully.'];
} else {
$response['message'] = 'Link not found or could not be deleted.';
}
} catch (PDOException $e) {
$response['message'] = 'Database error: ' . $e->getMessage();
}
break;
case 'toggle_status':
$current_status = $_POST['current_status'] ?? '';
$new_status = ($current_status === 'paused') ? 'approved' : 'paused'; // Toggle between paused and approved
try {
$stmt = $pdo->prepare("UPDATE links SET status = ? WHERE id = ?");
$stmt->execute([$new_status, $link_id]);
if ($stmt->rowCount()) {
$response = ['success' => true, 'message' => 'Link status updated successfully to ' . $new_status . '.', 'new_status' => $new_status];
} else {
$response['message'] = 'Link not found or status could not be updated.';
}
} catch (PDOException $e) {
$response['message'] = 'Database error: ' . $e->getMessage();
}
break;
// Add 'edit' case later
case 'edit':
$title = trim($_POST['title'] ?? '');
$url = trim($_POST['url'] ?? '');
$description = trim($_POST['description'] ?? '');
$subcategory_id = $_POST['subcategory_id'] ?? null;
$status = $_POST['status'] ?? 'pending';
if (empty($title) || empty($url) || !filter_var($url, FILTER_VALIDATE_URL) || !is_numeric($subcategory_id)) {
$response['message'] = 'Invalid input for editing link.';
echo json_encode($response);
exit;
}
require_once __DIR__ . '/../includes/ImageProcessor.php';
$current_link = $pdo->prepare("SELECT thumbnail_url FROM links WHERE id = ?");
$current_link->execute([$link_id]);
$current_thumbnail_url = $current_link->fetchColumn();
$new_thumbnail_url = $current_thumbnail_url;
$remove_image = isset($_POST['remove_image']) && $_POST['remove_image'] === 'true';
// Handle image removal
if ($remove_image) {
if ($current_thumbnail_url && file_exists(__DIR__ . '/../' . $current_thumbnail_url)) {
unlink(__DIR__ . '/../' . $current_thumbnail_url);
}
$new_thumbnail_url = null;
}
// Handle new image upload
if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] === UPLOAD_ERR_OK) {
$upload_dir = __DIR__ . '/../assets/images/uploads/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$processor = new ImageProcessor($upload_dir);
$uploaded_path = $processor->uploadAndResize($_FILES['thumbnail']);
if ($uploaded_path) {
// Delete old thumbnail if a new one is uploaded
if ($current_thumbnail_url && file_exists(__DIR__ . '/../' . $current_thumbnail_url)) {
unlink(__DIR__ . '/../' . $current_thumbnail_url);
}
$new_thumbnail_url = 'assets/images/uploads/' . basename($uploaded_path);
} else {
$response['message'] = 'Failed to upload new thumbnail.';
echo json_encode($response);
exit;
}
}
try {
$stmt = $pdo->prepare("UPDATE links SET title = ?, url = ?, description = ?, subcategory_id = ?, status = ?, thumbnail_url = ? WHERE id = ?");
$stmt->execute([$title, $url, $description, $subcategory_id, $status, $new_thumbnail_url, $link_id]);
if ($stmt->rowCount()) {
$response = ['success' => true, 'message' => 'Link updated successfully.'];
} else {
$response['message'] = 'Link not found or no changes made.';
}
} catch (PDOException $e) {
$response['message'] = 'Database error: ' . $e->getMessage();
}
break;
default:
$response['message'] = 'Unknown action.';
break;
}
echo json_encode($response);
exit;
}
$links = $pdo->query("SELECT l.*, u.username, s.name as subcategory_name, c.name as category_name
FROM links l
JOIN users u ON l.user_id = u.id
JOIN subcategories s ON l.subcategory_id = s.id
JOIN categories c ON s.category_id = c.id
ORDER BY l.created_at DESC")->fetchAll();
// Fetch subcategories for the edit form
$subcategories = $pdo->query("SELECT sc.id, sc.name AS subcategory_name, c.name AS category_name FROM subcategories sc JOIN categories c ON sc.category_id = c.id ORDER BY c.name, sc.name")->fetchAll();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Links - Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
<div class="auth-links">
<a href="../index.php">View Site</a>
<a href="../logout.php">Logout</a>
</div>
</header>
<div class="container-fluid my-4">
<div class="row">
<div class="col-md-2">
<aside class="admin-nav">
<h3>Menu</h3>
<nav class="nav flex-column">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="categories.php">Categories</a>
<a class="nav-link" href="users.php">Users</a>
<a class="nav-link active" href="links.php">Links</a>
<a class="nav-link" href="settings.php">Settings</a>
</nav>
</aside>
</div>
<div class="col-md-10">
<main class="content">
<h2>Manage Links</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Title</th>
<th>URL</th>
<th>Category</th>
<th>User</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($links)): ?>
<tr><td colspan="7">No links submitted yet.</td></tr>
<?php else: ?>
<?php foreach ($links as $link): ?>
<tr data-id="<?php echo $link['id']; ?>" data-thumbnail-url="<?php echo htmlspecialchars($link['thumbnail_url']); ?>">
<td class="link-title"><?php echo htmlspecialchars($link['title']); ?></td>
<td class="link-url"><a href="<?php echo htmlspecialchars($link['url']); ?>" target="_blank"><?php echo htmlspecialchars(substr($link['url'], 0, 50)); ?>...</a></td>
<td><?php echo htmlspecialchars($link['category_name']); ?> > <span class="link-subcategory-name" data-id="<?php echo $link['subcategory_id']; ?>"><?php echo htmlspecialchars($link['subcategory_name']); ?></span></td>
<td><?php echo htmlspecialchars($link['username']); ?></td>
<td><span class="badge bg-<?php echo $link['status'] === 'approved' ? 'success' : ($link['status'] === 'pending' ? 'warning' : ($link['status'] === 'paused' ? 'info' : 'danger')); ?> link-status"><?php echo htmlspecialchars($link['status']); ?></span></td>
<td><?php echo date("Y-m-d", strtotime($link['created_at'])); ?></td>
<td>
<button class="btn btn-sm btn-primary edit-link-btn" data-id="<?php echo $link['id']; ?>" data-bs-toggle="modal" data-bs-target="#editLinkModal">Edit</button>
<button class="btn btn-sm btn-<?php echo $link['status'] === 'paused' ? 'success' : 'warning'; ?> toggle-status-btn" data-id="<?php echo $link['id']; ?>" data-status="<?php echo $link['status']; ?>"><?php echo $link['status'] === 'paused' ? 'Unpause' : 'Pause'; ?></button>
<button class="btn btn-sm btn-danger delete-link-btn" data-id="<?php echo $link['id']; ?>">Delete</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</main>
</div>
</div>
</div>
<!-- Edit Link Modal -->
<div class="modal fade" id="editLinkModal" tabindex="-1" aria-labelledby="editLinkModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editLinkModalLabel">Edit Link</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editLinkForm">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="link_id" id="editLinkId">
<div class="mb-3">
<label for="editLinkTitle" class="form-label">Title</label>
<input type="text" class="form-control" id="editLinkTitle" name="title" required>
</div>
<div class="mb-3">
<label for="editLinkUrl" class="form-label">URL</label>
<input type="url" class="form-control" id="editLinkUrl" name="url" required>
</div>
<div class="mb-3">
<label for="editLinkDescription" class="form-label">Description</label>
<textarea class="form-control" id="editLinkDescription" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="editLinkSubcategory" class="form-label">Subcategory</label>
<select class="form-select" id="editLinkSubcategory" name="subcategory_id" required>
<?php foreach ($subcategories as $sc): ?>
<option value="<?php echo $sc['id']; ?>"><?php echo htmlspecialchars($sc['category_name'] . ' > ' . $sc['subcategory_name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="editLinkStatus" class="form-label">Status</label>
<select class="form-select" id="editLinkStatus" name="status" required>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
<option value="paused">Paused</option>
</select>
</div>
<div class="mb-3">
<label for="editLinkThumbnail" class="form-label">Current Thumbnail</label>
<div id="currentThumbnailPreview" class="mb-2">
<!-- Image will be loaded here by JS -->
</div>
<input type="file" class="form-control" id="editLinkThumbnail" name="thumbnail" accept="image/*">
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" value="true" id="removeCurrentThumbnail" name="remove_image">
<label class="form-check-label" for="removeCurrentThumbnail">
Remove current thumbnail
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Save changes</button>
</form>
</div>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
document.querySelectorAll('.delete-link-btn').forEach(button => {
button.addEventListener('click', function() {
const linkId = this.dataset.id;
if (confirm('Are you sure you want to delete this link?')) {
const formData = new FormData();
formData.append('action', 'delete');
formData.append('link_id', linkId);
formData.append('csrf_token', csrfToken);
sendAction(formData);
}
});
});
document.querySelectorAll('.toggle-status-btn').forEach(button => {
button.addEventListener('click', function() {
const linkId = this.dataset.id;
const currentStatus = this.dataset.status;
// Determine new status based on current for toggling between paused and approved
// If current status is 'paused', next is 'approved'. Otherwise, 'paused'.
const newStatus = (currentStatus === 'paused') ? 'approved' : 'paused';
if (confirm(`Are you sure you want to ${newStatus === 'paused' ? 'pause' : 'unpause' } this link?`)) {
const formData = new FormData();
formData.append('action', 'toggle_status');
formData.append('link_id', linkId);
formData.append('current_status', currentStatus);
formData.append('csrf_token', csrfToken);
sendAction(formData);
}
});
});
document.querySelectorAll('.edit-link-btn').forEach(button => {
button.addEventListener('click', function() {
const linkId = this.dataset.id;
const row = this.closest('tr');
const linkId = this.dataset.id;
const row = this.closest('tr');
document.getElementById('editLinkId').value = linkId;
document.getElementById('editLinkTitle').value = row.querySelector('.link-title').textContent;
document.getElementById('editLinkUrl').value = row.querySelector('.link-url a').href;
document.getElementById('editLinkDescription').value = row.querySelector('.link-description') ? row.querySelector('.link-description').textContent : '';
document.getElementById('editLinkStatus').value = row.querySelector('.link-status').textContent.trim();
// Select the correct subcategory in the dropdown
const subcategoryId = row.querySelector('.link-subcategory-name').dataset.id;
const subcategorySelect = document.getElementById('editLinkSubcategory');
for (let i = 0; i < subcategorySelect.options.length; i++) {
if (subcategorySelect.options[i].value == subcategoryId) {
subcategorySelect.selectedIndex = i;
break;
}
}
// Reset file input and checkbox
document.getElementById('editLinkThumbnail').value = '';
document.getElementById('removeCurrentThumbnail').checked = false;
// Display current thumbnail if it exists
const currentThumbnailPreview = document.getElementById('currentThumbnailPreview');
currentThumbnailPreview.innerHTML = ''; // Clear previous preview
const thumbnailUrl = row.dataset.thumbnailUrl; // Assuming data-thumbnail-url attribute on tr
if (thumbnailUrl && thumbnailUrl !== 'null') {
const img = document.createElement('img');
img.src = '../' + thumbnailUrl;
img.alt = 'Current Thumbnail';
img.style.maxWidth = '100px';
img.style.maxHeight = '100px';
img.style.objectFit = 'cover';
currentThumbnailPreview.appendChild(img);
} else {
currentThumbnailPreview.innerHTML = '<small>No thumbnail set.</small>';
}
});
});
document.getElementById('editLinkForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
// Append the remove_image flag if checked
if (document.getElementById('removeCurrentThumbnail').checked) {
formData.append('remove_image', 'true');
}
// The action and link_id are already in the form as hidden inputs
// No need to manually add csrf_token if it's already a hidden input in the form
// sendAction now uses FormData directly
sendAction(formData);
});
function sendAction(formData) {
fetch('links.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('DEBUG POST Data:', data.debug_post);
if (data.success) {
alert(data.message);
window.location.reload(); // Simple reload for now, can be optimized later
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Fetch error:', error);
alert('An error occurred while processing your request.');
});
}
});
</script>
</body>
</html>

154
admin/settings.php Normal file
View File

@ -0,0 +1,154 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../includes/Settings.php';
$allSettings = Settings::getAllWithMetadata();
// Handle form submission to update settings
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Protection (consider adding this if not already in auth.php or a global handler)
// if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
// // Handle CSRF token mismatch
// $_SESSION['error_message'] = 'CSRF token validation failed.';
// header('Location: settings.php');
// exit;
// }
foreach ($allSettings as $setting) {
$key = $setting['setting_key'];
$newValue = $_POST[$key] ?? null;
// Special handling for checkboxes: if not present in POST, it means unchecked
if ($setting['setting_type'] === 'checkbox') {
$newValue = isset($_POST[$key]) ? '1' : '0';
}
// Update the setting using the Settings class
Settings::set(
$key,
$newValue,
$setting['setting_type'],
$setting['default_value'],
$setting['validation_rules'],
$setting['description']
);
}
Settings::clearCache(); // Clear cache after updating settings
$_SESSION['success_message'] = 'Settings updated successfully.';
header('Location: settings.php');
exit;
}
// Re-fetch settings after potential update
$allSettings = Settings::getAllWithMetadata();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Settings - Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
<div class="auth-links">
<a href="../index.php">View Site</a>
<a href="../logout.php">Logout</a>
</div>
</header>
<div class="container my-4">
<div class="row">
<div class="col-md-3">
<aside class="admin-nav">
<h3>Menu</h3>
<nav class="nav flex-column">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="categories.php">Categories</a>
<a class="nav-link" href="users.php">Users</a>
<a class="nav-link" href="links.php">Links</a>
<a class="nav-link active" href="settings.php">Settings</a>
</nav>
</aside>
</div>
<div class="col-md-9">
<main class="content">
<h2>Manage Settings</h2>
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success">
<?php echo $_SESSION['success_message']; unset($_SESSION['success_message']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-danger">
<?php echo $_SESSION['error_message']; unset($_SESSION['error_message']); ?>
</div>
<?php endif; ?>
<form action="settings.php" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?? ''; // Assuming CSRF token is available globally or generated here ?>">
<?php foreach ($allSettings as $setting): ?>
<div class="mb-3">
<label for="<?php echo htmlspecialchars($setting['setting_key']); ?>" class="form-label">
<?php echo htmlspecialchars($setting['description'] ?: ucwords(str_replace('_', ' ', $setting['setting_key']))); ?>
</label>
<?php
$currentValue = Settings::get($setting['setting_key'], $setting['default_value']);
switch ($setting['setting_type']) {
case 'text':
case 'string':
echo '<input type="text" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
break;
case 'number':
case 'integer':
echo '<input type="number" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
break;
case 'textarea':
echo '<textarea class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" rows="3">' . htmlspecialchars($currentValue) . '</textarea>';
break;
case 'checkbox':
case 'boolean':
echo '<div class="form-check form-switch">';
echo '<input class="form-check-input" type="checkbox" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="1" ' . ($currentValue ? 'checked' : '') . '>';
echo '<label class="form-check-label" for="' . htmlspecialchars($setting['setting_key']) . '"></label>';
echo '</div>';
break;
case 'dropdown':
case 'select':
$options = json_decode($setting['validation_rules'], true)['options'] ?? []; // Assuming options are stored in validation_rules as JSON
echo '<select class="form-select" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '>';
foreach ($options as $optionValue => $optionLabel) {
echo '<option value="' . htmlspecialchars($optionValue) . '" ' . ($currentValue == $optionValue ? 'selected' : '') . '>' . htmlspecialchars($optionLabel) . '</option>';
}
echo '</select>';
break;
// Add more types as needed (e.g., color, date, email)
default:
echo '<input type="text" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
break;
}
?>
<?php if (!empty($setting['description'])): ?>
<div class="form-text"><?php echo htmlspecialchars($setting['description']); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<button type="submit" class="btn btn-primary">Save Settings</button>
</form>
</main>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
</body>
</html>

101
admin/users.php Normal file
View File

@ -0,0 +1,101 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../db/config.php';
$pdo = db();
// Handle user role updates
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_role'])) {
$user_id = $_POST['user_id'];
$role = $_POST['role'];
// Add extra validation for role value
if (in_array($role, ['regular', 'power_user', 'admin'])) {
$stmt = $pdo->prepare("UPDATE users SET role = ? WHERE id = ?");
$stmt->execute([$role, $user_id]);
}
header("Location: users.php");
exit;
}
$users = $pdo->query("SELECT id, username, role, created_at FROM users ORDER BY created_at DESC")->fetchAll();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Users - Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
<div class="auth-links">
<a href="../index.php">View Site</a>
<a href="../logout.php">Logout</a>
</div>
</header>
<div class="container my-4">
<div class="row">
<div class="col-md-3">
<aside class="admin-nav">
<h3>Menu</h3>
<nav class="nav flex-column">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="categories.php">Categories</a>
<a class="nav-link active" href="users.php">Users</a>
<a class="nav-link" href="links.php">Links</a>
<a class="nav-link" href="settings.php">Settings</a>
</nav>
</aside>
</div>
<div class="col-md-9">
<main class="content">
<h2>Manage Users</h2>
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Registered</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?php echo htmlspecialchars($user['username']); ?></td>
<td>
<form method="POST" action="users.php" class="d-inline">
<input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
<select name="role" class="form-select form-select-sm" style="width: auto; display: inline-block;">
<option value="regular" <?php echo ($user['role'] === 'regular') ? 'selected' : ''; ?>>Regular</option>
<option value="power_user" <?php echo ($user['role'] === 'power_user') ? 'selected' : ''; ?>>Power User</option>
<option value="admin" <?php echo ($user['role'] === 'admin') ? 'selected' : ''; ?>>Admin</option>
</select>
<button type="submit" name="update_role" class="btn btn-sm btn-primary">Update</button>
</form>
</td>
<td><?php echo date("Y-m-d", strtotime($user['created_at'])); ?></td>
<td>
<!-- Future actions like delete or view profile -->
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</main>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
</body>
</html>

253
assets/css/custom.css Normal file
View File

@ -0,0 +1,253 @@
/* --- Modern Japanese Retro Theme with more Pizazz --- */
:root {
--primary-color: #007bff; /* Vibrant Blue */
--secondary-color: #ff4081; /* Pink Accent */
--tertiary-color: #f0f2f5; /* Light Gray Background */
--text-dark: #212529;
--text-medium: #495057;
--text-light: #ced4da;
--border-color: #dee2e6;
--shadow-light: rgba(0, 0, 0, 0.1);
--white: #ffffff;
--gradient-start: #e0f2f7; /* Light blue for gradient */
--gradient-end: #f0f8ff; /* Lighter blue for gradient */
}
body {
font-family: 'Noto Sans JP', sans-serif;
background: linear-gradient(to bottom right, var(--gradient-start), var(--gradient-end));
color: var(--text-dark);
line-height: 1.6;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.header {
background: var(--white);
color: var(--primary-color);
padding: 10px 25px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
box-shadow: 0 2px 8px var(--shadow-light);
}
.header h1 {
font-family: 'Zen Old Mincho', serif;
font-size: 2.5em;
color: var(--primary-color);
margin: 0;
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
}
.auth-links a {
margin-left: 20px;
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease, transform 0.2s ease;
}
.auth-links a:hover {
color: var(--secondary-color);
transform: translateY(-2px);
}
.main-wrapper {
padding-top: 25px; /* Increased space below the header */
}
.content-section {
background-color: var(--white);
border-radius: 12px;
overflow: hidden;
margin-bottom: 30px; /* Increased space between sections */
box-shadow: 0 4px 15px var(--shadow-light);
transition: all 0.3s ease;
}
.category-list {
background-color: var(--white);
padding: 25px;
border-bottom: 1px solid var(--border-color);
}
.category-list h3 {
font-family: 'Zen Old Mincho', serif;
font-size: 1.7rem;
color: var(--primary-color);
border-bottom: 3px solid var(--secondary-color);
padding-bottom: 12px;
margin-bottom: 20px;
}
.category-list .nav-link {
color: var(--text-dark);
padding: 8px 0;
font-size: 1rem;
text-decoration: none;
transition: color 0.3s ease, transform 0.2s ease;
}
.category-list .nav-link:hover {
color: var(--secondary-color);
transform: translateX(5px);
}
.content {
padding: 25px;
background-color: var(--white);
}
.content h2 {
font-family: 'Zen Old Mincho', serif;
color: var(--primary-color);
font-size: 2rem;
padding-bottom: 15px;
margin-bottom: 25px;
border-bottom: 1px solid var(--border-color);
}
.link-item {
margin-bottom: 20px;
border: 1px solid var(--border-color);
padding: 15px;
border-radius: 8px;
background-color: var(--white);
display: flex;
align-items: flex-start;
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
.link-item:hover {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
transform: translateY(-5px);
}
.link-item .thumbnail {
width: 140px;
height: 90px;
object-fit: cover;
border-radius: 4px;
margin-right: 20px;
flex-shrink: 0;
}
.link-item-body {
flex-grow: 1;
}
.link-item-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 5px;
}
.link-item-title a {
color: var(--primary-color);
text-decoration: none;
transition: color 0.3s ease;
}
.link-item-title a:hover {
color: var(--secondary-color);
text-decoration: underline;
}
.link-item-url {
font-size: 0.9rem;
color: var(--text-medium);
margin-bottom: 8px;
display: block;
}
.link-item-description {
font-size: 1rem;
color: var(--text-dark);
}
.footer {
text-align: center;
padding: 25px 0;
background-color: var(--primary-color);
color: var(--white);
font-size: 0.9rem;
margin-top: 40px;
box-shadow: 0 -2px 8px var(--shadow-light);
}
/* Featured Section Styles */
.featured-section {
background: linear-gradient(to bottom, #fff5e6, #ffe0b3); /* Warm gradient background */
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 15px rgba(255, 160, 0, 0.1);
border: 1px solid #ffcc80;
}
.featured-section h3 {
font-family: 'Zen Old Mincho', serif;
color: #e65100; /* Darker orange for heading */
font-size: 1.6rem;
border-bottom: 3px solid #ff9800; /* Orange underline */
padding-bottom: 10px;
margin-bottom: 20px;
}
.featured-item {
background-color: var(--white);
border: 1px solid #ffecb3;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease;
}
.featured-item:hover {
transform: translateY(-3px);
}
.featured-item h4 {
color: var(--primary-color);
font-size: 1.1rem;
margin-bottom: 8px;
}
.featured-item p {
font-size: 0.9rem;
color: var(--text-medium);
margin-bottom: 12px;
}
.featured-item .btn {
font-size: 0.85rem;
padding: 8px 15px;
border-radius: 5px;
transition: all 0.3s ease;
}
.featured-item .btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.featured-item .btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.featured-item .btn-secondary {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
}
.featured-item .btn-secondary:hover {
background-color: #c00c4e;
border-color: #c00c4e;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

1
assets/js/main.js Normal file
View File

@ -0,0 +1 @@
// Future javascript for interactivity

58
db/apply_migrations.php Normal file
View File

@ -0,0 +1,58 @@
<?php
require_once __DIR__ . '/config.php';
echo "Applying migrations...
";
try {
$pdo = db();
// Create migrations table if it doesn't exist
$pdo->exec("
CREATE TABLE IF NOT EXISTS `migrations` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`migration_name` VARCHAR(255) NOT NULL UNIQUE,
`applied_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
");
$migrationsDir = __DIR__ . '/migrations/';
$migrationFiles = glob($migrationsDir . '*.sql');
sort($migrationFiles);
foreach ($migrationFiles as $file) {
$migrationName = basename($file);
// Check if migration has already been applied
$stmt = $pdo->prepare("SELECT COUNT(*) FROM `migrations` WHERE `migration_name` = ?");
$stmt->execute([$migrationName]);
if ($stmt->fetchColumn() > 0) {
echo "Skipping already applied migration: $migrationName
";
continue;
}
echo "Applying migration: $migrationName
";
$sql = file_get_contents($file);
$pdo->exec($sql);
// Record the applied migration
$stmt = $pdo->prepare("INSERT INTO `migrations` (`migration_name`) VALUES (?)");
$stmt->execute([$migrationName]);
echo "Successfully applied migration: $migrationName
";
}
echo "All migrations applied.
";
} catch (PDOException $e) {
echo "Database error: " . $e->getMessage() . "
";
exit(1);
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "
";
exit(1);
}

View File

@ -0,0 +1,58 @@
CREATE TABLE IF NOT EXISTS `users` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL,
`role` ENUM('regular', 'power_user', 'admin') NOT NULL DEFAULT 'regular',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `categories` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`visibility` BOOLEAN NOT NULL DEFAULT TRUE,
`display_order` INT NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS `subcategories` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`category_id` INT NOT NULL,
`name` VARCHAR(255) NOT NULL,
FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `links` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`subcategory_id` INT NOT NULL,
`title` VARCHAR(255) NOT NULL,
`url` VARCHAR(2083) NOT NULL,
`description` TEXT,
`thumbnail_url` VARCHAR(2083),
`status` ENUM('pending', 'approved', 'rejected') NOT NULL DEFAULT 'pending',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
FOREIGN KEY (`subcategory_id`) REFERENCES `subcategories`(`id`)
);
CREATE TABLE IF NOT EXISTS `moderation_logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`link_id` INT NOT NULL,
`moderator_id` INT NOT NULL,
`action` ENUM('approved', 'rejected') NOT NULL,
`notes` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`link_id`) REFERENCES `links`(`id`),
FOREIGN KEY (`moderator_id`) REFERENCES `users`(`id`)
);
CREATE TABLE IF NOT EXISTS `visits` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`link_id` INT,
`user_id` INT,
`ip_address` VARCHAR(45),
`user_agent` TEXT,
`visited_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`link_id`) REFERENCES `links`(`id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
);

View File

@ -0,0 +1 @@
ALTER TABLE `links` MODIFY COLUMN `status` ENUM('pending', 'approved', 'rejected', 'paused') NOT NULL DEFAULT 'pending';

View File

@ -0,0 +1,13 @@
-- db/migrations/003_create_settings_table.sql
CREATE TABLE IF NOT EXISTS settings (
id INT AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(255) NOT NULL UNIQUE,
setting_value TEXT,
setting_type VARCHAR(50) NOT NULL DEFAULT 'text',
default_value TEXT NULL,
validation_rules TEXT NULL,
description TEXT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,10 @@
-- db/migrations/004_add_initial_settings.sql
-- Insert initial settings if they do not already exist
INSERT IGNORE INTO settings (setting_key, setting_value, setting_type, default_value, description)
VALUES
('site_title', 'My Web Directory', 'text', 'My Web Directory', 'The main title of the website.'),
('maintenance_mode', '0', 'checkbox', '0', 'Enable or disable maintenance mode for the site.'),
('items_per_page', '10', 'number', '10', 'Number of items to display per page in listings.'),
('admin_email', 'admin@example.com', 'text', 'admin@example.com', 'The email address for administrative notifications.'),
('tar_pit_delay', '0', 'number', '0', 'Delay (in seconds) for tar pit defense against brute-force attacks.');

230
export.mysql Normal file
View File

@ -0,0 +1,230 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: 127.0.0.1 Database: app_37018
-- ------------------------------------------------------
-- Server version 10.11.14-MariaDB-0+deb12u2
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `categories`
--
DROP TABLE IF EXISTS `categories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`visibility` tinyint(1) NOT NULL DEFAULT 1,
`display_order` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `categories`
--
LOCK TABLES `categories` WRITE;
/*!40000 ALTER TABLE `categories` DISABLE KEYS */;
INSERT INTO `categories` VALUES
(1,'Arts & Entertainment',1,0),
(2,'Business & Economy',1,0),
(3,'Computers & Internet',1,0),
(4,'Education',1,0),
(5,'Government',1,0),
(6,'Health & Fitness',1,0),
(7,'Home & Garden',1,0),
(8,'News & Media',1,0),
(9,'Recreation & Sports',1,0),
(10,'Reference',1,0),
(11,'Science & Technology',1,0),
(12,'Shopping',1,0),
(13,'Society & Culture',1,0),
(14,'Travel & Tourism',1,0),
(15,'Cars & Vehicles',1,0),
(16,'Food & Drink',1,0),
(17,'Law & Legal Issues',1,0),
(18,'Pets & Animals',1,0),
(19,'Real Estate',1,0),
(20,'Games',1,0);
/*!40000 ALTER TABLE `categories` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `links`
--
DROP TABLE IF EXISTS `links`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `links` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`subcategory_id` int(11) NOT NULL,
`title` varchar(255) NOT NULL,
`url` varchar(2083) NOT NULL,
`description` text DEFAULT NULL,
`thumbnail_url` varchar(2083) DEFAULT NULL,
`status` enum('pending','approved','rejected') NOT NULL DEFAULT 'pending',
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `subcategory_id` (`subcategory_id`),
CONSTRAINT `links_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `links_ibfk_2` FOREIGN KEY (`subcategory_id`) REFERENCES `subcategories` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `links`
--
LOCK TABLES `links` WRITE;
/*!40000 ALTER TABLE `links` DISABLE KEYS */;
INSERT INTO `links` VALUES
(1,1,1,'test title','https://title.com','this is a test description',NULL,'approved','2025-12-17 15:34:06');
/*!40000 ALTER TABLE `links` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `moderation_logs`
--
DROP TABLE IF EXISTS `moderation_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `moderation_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`link_id` int(11) NOT NULL,
`moderator_id` int(11) NOT NULL,
`action` enum('approved','rejected') NOT NULL,
`notes` text DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `link_id` (`link_id`),
KEY `moderator_id` (`moderator_id`),
CONSTRAINT `moderation_logs_ibfk_1` FOREIGN KEY (`link_id`) REFERENCES `links` (`id`),
CONSTRAINT `moderation_logs_ibfk_2` FOREIGN KEY (`moderator_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `moderation_logs`
--
LOCK TABLES `moderation_logs` WRITE;
/*!40000 ALTER TABLE `moderation_logs` DISABLE KEYS */;
/*!40000 ALTER TABLE `moderation_logs` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `subcategories`
--
DROP TABLE IF EXISTS `subcategories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `subcategories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`category_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `category_id` (`category_id`),
CONSTRAINT `subcategories_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `subcategories`
--
LOCK TABLES `subcategories` WRITE;
/*!40000 ALTER TABLE `subcategories` DISABLE KEYS */;
INSERT INTO `subcategories` VALUES
(1,1,'test Arts & Entertainment'),
(2,11,'Science & Technology syb');
/*!40000 ALTER TABLE `subcategories` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`role` enum('regular','power_user','admin') NOT NULL DEFAULT 'regular',
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `users`
--
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES
(1,'admin','$2y$10$ZXgcZZeRqeZmt3gD1hqnVedgdgGwQ4R5dFoY6YRT.GY0StKYwnx5.','admin','2025-12-17 15:27:26');
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `visits`
--
DROP TABLE IF EXISTS `visits`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `visits` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`link_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` text DEFAULT NULL,
`visited_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `link_id` (`link_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `visits_ibfk_1` FOREIGN KEY (`link_id`) REFERENCES `links` (`id`),
CONSTRAINT `visits_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `visits`
--
LOCK TABLES `visits` WRITE;
/*!40000 ALTER TABLE `visits` DISABLE KEYS */;
/*!40000 ALTER TABLE `visits` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2025-12-17 23:45:53

156
includes/ImageProcessor.php Normal file
View File

@ -0,0 +1,156 @@
<?php
class ImageProcessor {
private static $uploadDir = 'assets/images/uploads/';
private static $maxWidth = 400; // Default max width for thumbnails
private static $maxHeight = 800; // Default max height for thumbnails (double the max width)
/**
* Handles the uploaded image, moves it to the upload directory, and resizes it.
*
* @param array $file The $_FILES array entry for the uploaded file.
* @return string|false The path to the saved thumbnail or false on error.
*/
public static function processAndSaveImage(array $file) {
if (!isset($file['tmp_name']) || $file['error'] !== UPLOAD_ERR_OK) {
error_log("ImageProcessor: No file uploaded or an error occurred. File error code: " . ($file['error'] ?? 'unknown'));
return false; // No file uploaded or an error occurred
}
// Validate file type
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($file['type'], $allowedTypes)) {
error_log("ImageProcessor: Invalid file type uploaded: " . $file['type']);
return false; // Invalid file type
}
// Ensure upload directory exists
if (!is_dir(self::$uploadDir)) {
mkdir(self::$uploadDir, 0775, true);
}
// Generate a unique file name
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$fileName = uniqid('thumbnail_') . '.' . $extension;
$targetPath = self::$uploadDir . $fileName;
// Move the uploaded file
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
error_log("ImageProcessor: Failed to move uploaded file from " . $file['tmp_name'] . " to " . $targetPath);
return false; // Failed to move uploaded file
}
// Resize image
if (!self::resizeImage($targetPath, $file['type'])) {
error_log("ImageProcessor: Failed to resize image: " . $targetPath);
// If resizing fails, you might want to delete the original uploaded file
unlink($targetPath);
return false;
}
return $targetPath; // Return the path to the saved and resized image
}
/**
* Resizes an image to a maximum width, maintaining aspect ratio.
*
* @param string $imagePath The path to the image file.
* @param string $mimeType The MIME type of the image.
* @return bool True on success, false on failure.
*/
private static function resizeImage(string $imagePath, string $mimeType) {
list($width, $height) = getimagesize($imagePath);
if ($width <= self::$maxWidth && $height <= self::$maxHeight) {
return true; // No resizing needed
}
$aspectRatio = $width / $height;
$newWidth = $width;
$newHeight = $height;
// Resize based on width if it exceeds maxWidth or if new height based on maxWidth is less than maxHeight
if ($newWidth > self::$maxWidth) {
$newWidth = self::$maxWidth;
$newHeight = (int) (self::$maxWidth / $aspectRatio);
}
// Resize based on height if it exceeds maxHeight or if new width based on maxHeight is less than maxWidth
if ($newHeight > self::$maxHeight) {
$newHeight = self::$maxHeight;
$newWidth = (int) (self::$maxHeight * $aspectRatio);
}
// Final check to ensure it fits within both dimensions after initial adjustments
if ($newWidth > self::$maxWidth) {
$newWidth = self::$maxWidth;
$newHeight = (int) (self::$maxWidth / $aspectRatio);
}
if ($newHeight > self::$maxHeight) {
$newHeight = self::$maxHeight;
$newWidth = (int) (self::$maxHeight * $aspectRatio);
}
$image = null;
switch ($mimeType) {
case 'image/jpeg':
$image = imagecreatefromjpeg($imagePath);
break;
case 'image/png':
$image = imagecreatefrompng($imagePath);
break;
case 'image/gif':
$image = imagecreatefromgif($imagePath);
break;
default:
error_log("ImageProcessor: Unsupported image type for resizing: " . $mimeType);
return false; // Unsupported image type for resizing
}
if (!$image) {
return false;
}
$resizedImage = imagecreatetruecolor($newWidth, $newHeight);
// Preserve transparency for PNG and GIF
if ($mimeType == 'image/png') {
imagealphablending($resizedImage, false);
imagesavealpha($resizedImage, true);
$transparent = imagecolorallocatealpha($resizedImage, 255, 255, 255, 127);
imagefilledrectangle($resizedImage, 0, 0, $newWidth, $newHeight, $transparent);
} elseif ($mimeType == 'image/gif') {
$trnprt_indx = imagecolortransparent($image);
if ($trnprt_indx >= 0) {
// Get the transparent color from the original image
$trnprt_color = imagecolorsforindex($image, $trnprt_indx);
// Allocate the same color in the new image
$trnprt_indx = imagecolorallocate($resizedImage, $trnprt_color['red'], $trnprt_color['green'], $trnprt_color['blue']);
imagefill($resizedImage, 0, 0, $trnprt_indx);
imagecolortransparent($resizedImage, $trnprt_indx);
}
}
imagecopyresampled($resizedImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
$success = false;
switch ($mimeType) {
case 'image/jpeg':
$success = imagejpeg($resizedImage, $imagePath, 90); // 90% quality
break;
case 'image/png':
$success = imagepng($resizedImage, $imagePath);
break;
case 'image/gif':
$success = imagegif($resizedImage, $imagePath);
break;
}
imagedestroy($image);
imagedestroy($resizedImage);
return $success;
}
}

178
includes/Settings.php Normal file
View File

@ -0,0 +1,178 @@
<?php
require_once __DIR__ . '/../db/config.php';
class Settings
{
private static $cache = [];
/**
* Get a setting value.
*
* @param string $key The setting key.
* @param mixed $default The default value to return if the setting is not found.
* @return mixed The setting value or the default value.
*/
public static function get(string $key, $default = null)
{
if (isset(self::$cache[$key])) {
return self::$cache[$key];
}
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT setting_value, setting_type FROM settings WHERE setting_key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$value = self::castValue($row['setting_value'], $row['setting_type']);
self::$cache[$key] = $value;
return $value;
}
} catch (PDOException $e) {
error_log("Error getting setting '{$key}': " . $e->getMessage());
}
return $default;
}
/**
* Set or update a setting value.
*
* @param string $key The setting key.
* @param mixed $value The setting value.
* @param string $type The type of the setting (e.g., 'text', 'checkbox', 'number').
* @param mixed $defaultValue The default value for the setting.
* @param string|null $validationRules JSON string of validation rules.
* @param string|null $description A description for the setting.
* @return bool True on success, false on failure.
*/
public static function set(
string $key,
$value,
string $type = 'text',
$defaultValue = null,
?string $validationRules = null,
?string $description = null
): bool {
try {
$pdo = db();
// Check if setting exists
$stmt = $pdo->prepare("SELECT COUNT(*) FROM settings WHERE setting_key = ?");
$stmt->execute([$key]);
$exists = $stmt->fetchColumn();
$stringValue = self::valueToString($value);
$defaultValue = self::valueToString($defaultValue);
if ($exists) {
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ?, setting_type = ?, default_value = ?, validation_rules = ?, description = ?, updated_at = NOW() WHERE setting_key = ?");
$stmt->execute([$stringValue, $type, $defaultValue, $validationRules, $description, $key]);
} else {
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_type, default_value, validation_rules, description) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$key, $stringValue, $type, $defaultValue, $validationRules, $description]);
}
// Clear cache for this key
unset(self::$cache[$key]);
return true;
} catch (PDOException $e) {
error_log("Error setting '{$key}': " . $e->getMessage());
return false;
}
}
/**
* Casts a string value to its appropriate PHP type based on the setting type.
*
* @param string $value The string value from the database.
* @param string $type The declared type of the setting.
* @return mixed The type-casted value.
*/
private static function castValue(string $value, string $type)
{
switch ($type) {
case 'boolean':
case 'checkbox':
return (bool)$value;
case 'integer':
case 'number':
return (int)$value;
case 'float':
return (float)$value;
case 'json':
return json_decode($value, true);
case 'array': // Simple comma-separated array
return explode(',', $value);
default:
return $value;
}
}
/**
* Converts a PHP value to its string representation for storage.
*
* @param mixed $value The PHP value.
* @return string The string representation.
*/
private static function valueToString($value): string
{
if (is_bool($value)) {
return $value ? '1' : '0';
} elseif (is_array($value) || is_object($value)) {
return json_encode($value);
} else {
return (string)$value;
}
}
/**
* Clears the entire settings cache.
*/
public static function clearCache(): void
{
self::$cache = [];
}
/**
* Fetches all settings from the database.
*
* @return array An associative array of all settings.
*/
public static function getAll(): array
{
try {
$pdo = db();
$stmt = $pdo->query("SELECT setting_key, setting_value, setting_type FROM settings");
$allSettings = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
foreach ($allSettings as $setting) {
$result[$setting['setting_key']] = self::castValue($setting['setting_value'], $setting['setting_type']);
}
return $result;
} catch (PDOException $e) {
error_log("Error getting all settings: " . $e->getMessage());
return [];
}
}
/**
* Fetches all settings with their metadata from the database.
*
* @return array An associative array of all settings including metadata.
*/
public static function getAllWithMetadata(): array
{
try {
$pdo = db();
$stmt = $pdo->query("SELECT * FROM settings");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Error getting all settings with metadata: " . $e->getMessage());
return [];
}
}
}

318
index.php
View File

@ -1,150 +1,182 @@
<?php <?php session_start(); ?>
declare(strict_types=1); <!DOCTYPE html>
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Style</title>
<?php <title><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
// Read project preview data from environment <meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'A public web directory of curated links.'); ?>">
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?> <link rel="preconnect" href="https://fonts.googleapis.com">
<?php if ($projectDescription): ?> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Meta description --> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;700&family=Zen+Old+Mincho:wght@400;700&display=swap" rel="stylesheet">
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Open Graph meta tags --> <link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main> <?php
<div class="card"> require_once __DIR__ . '/db/config.php';
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> $pdo = db();
<span class="sr-only">Loading…</span>
</div> // Fetch visible categories from the database
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p> $stmt = $pdo->query("SELECT * FROM categories WHERE visibility = 1 ORDER BY display_order ASC, name ASC");
<p class="hint">This page will update automatically as the plan is implemented.</p> $categories = $stmt->fetchAll();
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
// Determine the current category
$current_category_id = null;
$current_category_name = 'All Categories';
if (isset($_GET['category']) && filter_var($_GET['category'], FILTER_VALIDATE_INT)) {
$category_id_from_get = (int)$_GET['category'];
$stmt = $pdo->prepare("SELECT id, name FROM categories WHERE id = ? AND visibility = 1");
$stmt->execute([$category_id_from_get]);
$cat = $stmt->fetch();
if ($cat) {
$current_category_id = $cat['id'];
$current_category_name = $cat['name'];
}
}
// Fetch links for the current category
$link_stmt = null;
if ($current_category_id) {
$link_stmt = $pdo->prepare(
"SELECT l.*, s.name as subcategory_name FROM links l " .
"JOIN subcategories s ON l.subcategory_id = s.id " .
"WHERE s.category_id = ? AND l.status = 'approved' ORDER BY s.name ASC, l.created_at DESC"
);
$link_stmt->execute([$current_category_id]);
} else {
$link_stmt = $pdo->query(
"SELECT l.*, s.name as subcategory_name, c.name as category_name " .
"FROM links l " .
"JOIN subcategories s ON l.subcategory_id = s.id " .
"JOIN categories c ON s.category_id = c.id " .
"WHERE l.status = 'approved' " .
"ORDER BY c.name ASC, s.name ASC, l.created_at DESC"
);
}
$current_links = $link_stmt->fetchAll();
?>
<pre style="background-color: #fdd; border: 1px solid #f99; padding: 10px; margin: 10px;">
DEBUG INFORMATION:
GET Parameters: <?php echo htmlspecialchars(json_encode($_GET, JSON_PRETTY_PRINT)); ?>
Current Category ID: <?php echo htmlspecialchars($current_category_id ?? "NULL"); ?>
Current Category Name: <?php echo htmlspecialchars($current_category_name ?? "Not Set"); ?>
Current Links Count: <?php echo htmlspecialchars(isset($current_links) ? count($current_links) : 0); ?>
Raw Query String: <?php echo htmlspecialchars($_SERVER['QUERY_STRING'] ?? 'Not Set'); ?>
SQL Query for links: <?php echo htmlspecialchars($link_stmt->queryString ?? "SQL Query not set yet."); ?>
</pre>
?>
<header class="header">
<h1><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></h1>
<div class="auth-links">
<?php if (isset($_SESSION['user_id'])): ?>
<a href="submit.php">Submit Link</a>
<?php if ($_SESSION['user_role'] === 'admin'): ?>
<a href="admin/index.php">Admin Panel</a>
<?php endif; ?>
<a href="logout.php">Logout</a>
<?php else: ?>
<a href="register.php">Register</a>
<a href="login.php">Login</a>
<?php endif; ?>
</div> </div>
</main> </header>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <div class="main-wrapper container my-4">
</footer> <div class="row">
<!-- Left Column - Categories -->
<div class="col-md-3">
<aside class="category-list">
<h3>Categories</h3>
<nav class="nav flex-column">
<a class="nav-link <?php echo ($current_category_id === null) ? 'fw-bold' : ''; ?>" href="index.php">All Categories</a>
<?php foreach ($categories as $category): ?>
<a class="nav-link <?php echo ($category['id'] === $current_category_id) ? 'fw-bold' : ''; ?>" href="?category=<?php echo $category['id']; ?>">
<?php echo htmlspecialchars($category['name']); ?>
</a>
<?php endforeach; ?>
</nav>
</aside>
</div>
<!-- Center Column - Main Content -->
<div class="col-md-6">
<main class="content">
<h2><?php echo htmlspecialchars($current_category_name); ?></h2>
<?php if (empty($current_links)): ?>
<p>No links found in this category yet.</p>
<?php else: ?>
<?php
$current_category_for_display = null;
$current_subcategory = null;
foreach ($current_links as $link):
if ($current_category_id === null && isset($link['category_name']) && $link['category_name'] !== $current_category_for_display) {
$current_category_for_display = $link['category_name'];
echo '<h3>' . htmlspecialchars($current_category_for_display) . '</h3>';
$current_subcategory = null; // Reset subcategory when category changes
}
if ($link['subcategory_name'] !== $current_subcategory) {
$current_subcategory = $link['subcategory_name'];
echo '<h4>' . htmlspecialchars($current_subcategory) . '</h4>';
}
?>
<div class="link-item">
<img src="<?php echo htmlspecialchars($link['thumbnail_url']); ?>" alt="Thumbnail for <?php echo htmlspecialchars($link['title']); ?>" class="thumbnail">
<div class="link-item-body">
<div class="link-item-title">
<a href="<?php echo htmlspecialchars($link['url']); ?>" target="_blank" rel="noopener noreferrer">
<?php echo htmlspecialchars($link['title']); ?>
</a>
</div>
<a href="<?php echo htmlspecialchars($link['url']); ?>" target="_blank" rel="noopener noreferrer" class="link-item-url"><?php echo htmlspecialchars($link['url']); ?></a>
<p class="link-item-description">
<?php echo htmlspecialchars($link['description']); ?>
</p>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</main>
</div>
<!-- Right Column -->
<div class="col-md-3">
<aside class="featured-section">
<h3>Featured Content</h3>
<div class="featured-item">
<h4>Special Link 1</h4>
<p>A description for a special featured link.</p>
<a href="#" class="btn btn-sm btn-primary">View More</a>
</div>
<div class="featured-item mt-3">
<h4>Announcement</h4>
<p>Check out our latest updates!</p>
<a href="#" class="btn btn-sm btn-secondary">Read Blog</a>
</div>
</aside>
</div>
</div>
</div><!-- /main-wrapper -->
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body> </body>
</html> </html>

105
login.php Normal file
View File

@ -0,0 +1,105 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
$errors = [];
if (isset($_SESSION['user_id'])) {
header("Location: index.php");
exit;
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username)) {
$errors[] = 'Username is required.';
}
if (empty($password)) {
$errors[] = 'Password is required.';
}
if (empty($errors)) {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT id, username, password, role FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['user_role'] = $user['role'];
// For admin panel security features
if ($user['role'] === 'admin') {
$_SESSION['admin_ip_address'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['last_activity'] = time();
}
header("Location: index.php");
exit;
} else {
$errors[] = 'Invalid username or password.';
}
} catch (PDOException $e) {
$errors[] = "Database error: " . $e->getMessage();
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;"><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></a></h1>
<div class="auth-links">
<a href="register.php">Register</a>
<a href="login.php">Login</a>
</div>
</header>
<div class="container my-4">
<div class="row justify-content-center">
<div class="col-md-6">
<main class="content p-4">
<h2>Login</h2>
<?php if (!empty($errors)): ?>
<div class="alert alert-danger">
<?php foreach ($errors as $error): ?>
<p class="mb-0"><?php echo $error; ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form action="login.php" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</main>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
</body>
</html>

6
logout.php Normal file
View File

@ -0,0 +1,6 @@
<?php
session_start();
session_unset();
session_destroy();
header("Location: index.php");
exit;

107
register.php Normal file
View File

@ -0,0 +1,107 @@
<?php
require_once __DIR__ . '/db/config.php';
$errors = [];
$success = false;
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$password_confirm = $_POST['password_confirm'] ?? '';
if (empty($username)) {
$errors[] = 'Username is required.';
}
if (empty($password)) {
$errors[] = 'Password is required.';
}
if ($password !== $password_confirm) {
$errors[] = 'Passwords do not match.';
}
if (empty($errors)) {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetch()) {
$errors[] = 'Username already taken.';
} else {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
// For now, all new users are 'regular'. The first admin will be created manually.
$stmt->execute([$username, $hashed_password, 'regular']);
$success = true;
}
} catch (PDOException $e) {
$errors[] = "Database error: " . $e->getMessage();
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;"><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></a></h1>
<div class="auth-links">
<a href="register.php">Register</a>
<a href="login.php">Login</a>
</div>
</header>
<div class="container my-4">
<div class="row justify-content-center">
<div class="col-md-6">
<main class="content p-4">
<h2>Register</h2>
<?php if (!empty($errors)): ?>
<div class="alert alert-danger">
<?php foreach ($errors as $error): ?>
<p class="mb-0"><?php echo $error; ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success">
<p class="mb-0">Registration successful! You can now <a href="login.php">login</a>.</p>
</div>
<?php else: ?>
<form action="register.php" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="password_confirm" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="password_confirm" name="password_confirm" required>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
<?php endif; ?>
</main>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
</body>
</html>

186
submit.php Normal file
View File

@ -0,0 +1,186 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/ImageProcessor.php';
error_log("submit.php: Script started.");
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
$pdo = db();
$errors = [];
$success = false;
// Fetch categories and subcategories for the form
$categories = $pdo->query("SELECT * FROM categories WHERE visibility = 1 ORDER BY display_order ASC, name ASC")->fetchAll();
$subcategories = [];
if (!empty($categories)) {
$stmt = $pdo->query("SELECT * FROM subcategories ORDER BY name ASC");
while ($row = $stmt->fetch()) {
$subcategories[$row['category_id']][] = $row;
}
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
error_log("submit.php: POST request received.");
$title = trim($_POST['title'] ?? '');
$url = trim($_POST['url'] ?? '');
$description = trim($_POST['description'] ?? '');
$subcategory_id = $_POST['subcategory_id'] ?? null;
if (empty($title)) $errors[] = 'Title is required.';
if (empty($url)) $errors[] = 'URL is required.';
if (!filter_var($url, FILTER_VALIDATE_URL)) $errors[] = 'Invalid URL.';
if (empty($subcategory_id)) $errors[] = 'Subcategory is required.';
if (empty($errors)) {
// Determine status based on user role
$status = ($_SESSION['user_role'] === 'admin' || $_SESSION['user_role'] === 'power_user') ? 'approved' : 'pending';
// For now, thumbnail is not implemented
$thumbnail_url = null;
if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] === UPLOAD_ERR_OK) {
error_log("submit.php: Image upload detected. Processing image...");
$uploadedImagePath = ImageProcessor::processAndSaveImage($_FILES['thumbnail']);
if ($uploadedImagePath) {
$thumbnail_url = $uploadedImagePath;
error_log("submit.php: Image processed successfully. Path: " . $uploadedImagePath);
} else {
$errors[] = 'Failed to process uploaded image. Please ensure it is a valid image file (JPEG, PNG, GIF).';
error_log("submit.php: Failed to process uploaded image.");
}
}
try {
error_log("submit.php: Attempting database insertion...");
$stmt = $pdo->prepare("INSERT INTO links (user_id, subcategory_id, title, url, description, thumbnail_url, status) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$_SESSION['user_id'], $subcategory_id, $title, $url, $description, $thumbnail_url, $status]);
$success = true;
error_log("submit.php: Database insertion successful.");
} catch (PDOException $e) {
$errors[] = "Database error: " . $e->getMessage();
error_log("submit.php: Database error: " . $e->getMessage());
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Submit a Link - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;"><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></a></h1>
<div class="auth-links">
<?php if (isset($_SESSION['user_id'])): ?>
<a href="submit.php">Submit Link</a>
<a href="logout.php">Logout</a>
<?php else: ?>
<a href="register.php">Register</a>
<a href="login.php">Login</a>
<?php endif; ?>
</div>
</header>
<div class="container my-4">
<div class="row justify-content-center">
<div class="col-md-8">
<main class="content p-4">
<h2>Submit a New Link</h2>
<?php if (!empty($errors)): ?>
<div class="alert alert-danger">
<?php foreach ($errors as $error): ?><p class="mb-0"><?php echo $error; ?></p><?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success">
<p class="mb-0">Thank you for your submission! It will be reviewed shortly.</p>
</div>
<?php else: ?>
<form action="submit.php" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="url" class="form-label">URL</label>
<input type="url" class="form-control" id="url" name="url" required>
</div>
<div class="mb-3">
<label for="category" class="form-label">Category</label>
<select class="form-select" id="category" name="category">
<option selected disabled>-- Select a Category --</option>
<?php foreach($categories as $cat): ?>
<option value="<?php echo $cat['id']; ?>"><?php echo htmlspecialchars($cat['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="subcategory_id" class="form-label">Subcategory</label>
<select class="form-select" id="subcategory_id" name="subcategory_id" required>
<option selected disabled>-- Select a Subcategory --</option>
<?php foreach($subcategories as $cat_id => $subs): ?>
<?php foreach($subs as $sub): ?>
<option class="d-none" data-category="<?php echo $cat_id; ?>" value="<?php echo $sub['id']; ?>"><?php echo htmlspecialchars($sub['name']); ?></option>
<?php endforeach; ?>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="thumbnail" class="form-label">Image (optional)</label>
<input type="file" class="form-control" id="thumbnail" name="thumbnail" accept="image/*">
<button type="submit" class="btn btn-primary">Submit Link</button>
</form>
<?php endif; ?>
</main>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
<script>
document.getElementById('category').addEventListener('change', function() {
const categoryId = this.value;
const subcategorySelect = document.getElementById('subcategory_id');
// Reset and show the default option
subcategorySelect.value = '-- Select a Subcategory --';
// Hide all subcategory options
Array.from(subcategorySelect.options).forEach(opt => {
if (opt.dataset.category) { // Skip the default disabled option
opt.classList.add('d-none');
}
});
// Show subcategories for the selected category
const relevantOptions = subcategorySelect.querySelectorAll(`[data-category="${categoryId}"]`);
relevantOptions.forEach(opt => opt.classList.remove('d-none'));
});
</script>
</body>
</html>