Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
18
.htaccess
18
.htaccess
@ -0,0 +1,18 @@
|
|||||||
|
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]
|
||||||
@ -1,28 +0,0 @@
|
|||||||
<?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
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
<?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>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
<?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>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
418
admin/links.php
418
admin/links.php
@ -1,418 +0,0 @@
|
|||||||
<?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>© <?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>
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
<?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>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
101
admin/users.php
101
admin/users.php
@ -1,101 +0,0 @@
|
|||||||
<?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>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,253 +0,0 @@
|
|||||||
/* --- 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.
|
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@ -1 +0,0 @@
|
|||||||
// Future javascript for interactivity
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
<?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);
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
|
|
||||||
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`)
|
|
||||||
);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
ALTER TABLE `links` MODIFY COLUMN `status` ENUM('pending', 'approved', 'rejected', 'paused') NOT NULL DEFAULT 'pending';
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
-- 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
|
|
||||||
);
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
-- 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
230
export.mysql
@ -1,230 +0,0 @@
|
|||||||
/*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
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,178 +0,0 @@
|
|||||||
<?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
318
index.php
@ -1,182 +1,150 @@
|
|||||||
<?php session_start(); ?>
|
<?php
|
||||||
<!DOCTYPE html>
|
declare(strict_types=1);
|
||||||
|
@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.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>New Style</title>
|
||||||
<title><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?></title>
|
<?php
|
||||||
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'A public web directory of curated links.'); ?>">
|
// Read project preview data from environment
|
||||||
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
?>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<?php if ($projectDescription): ?>
|
||||||
<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 description -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
<!-- Open Graph meta tags -->
|
||||||
|
<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>
|
||||||
<?php
|
<main>
|
||||||
require_once __DIR__ . '/db/config.php';
|
<div class="card">
|
||||||
|
<h1>Analyzing your requirements and generating your website…</h1>
|
||||||
$pdo = db();
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
|
<span class="sr-only">Loading…</span>
|
||||||
// Fetch visible categories from the database
|
</div>
|
||||||
$stmt = $pdo->query("SELECT * FROM categories WHERE visibility = 1 ORDER BY display_order ASC, name ASC");
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
$categories = $stmt->fetchAll();
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
|
<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>
|
||||||
</header>
|
</main>
|
||||||
|
<footer>
|
||||||
<div class="main-wrapper container my-4">
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
<div class="row">
|
</footer>
|
||||||
<!-- 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>© <?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
105
login.php
@ -1,105 +0,0 @@
|
|||||||
<?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>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<?php
|
|
||||||
session_start();
|
|
||||||
session_unset();
|
|
||||||
session_destroy();
|
|
||||||
header("Location: index.php");
|
|
||||||
exit;
|
|
||||||
107
register.php
107
register.php
@ -1,107 +0,0 @@
|
|||||||
<?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>© <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
186
submit.php
186
submit.php
@ -1,186 +0,0 @@
|
|||||||
<?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>© <?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>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user