Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 0 additions & 74 deletions dec.py

This file was deleted.

11 changes: 11 additions & 0 deletions public_html/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Options -Indexes
RewriteEngine On

RewriteCond %{REQUEST_URI} !^/admin
RewriteCond %{REQUEST_URI} !^/assets
RewriteCond %{REQUEST_URI} !^/api
RewriteCond %{REQUEST_URI} !^/sql
RewriteCond %{REQUEST_URI} !^/logs
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([A-Za-z0-9\-]+)/?$ post.php?slug=$1 [L,QSA]
Comment on lines +1 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: /sql and /logs directories are publicly accessible.

Options -Indexes only suppresses directory listing. It does not block direct file access — http://domain/sql/database.sql is fully readable by anyone, exposing the entire schema and the seeded admin bcrypt hash. If log files land in /logs, they are similarly exposed (stack traces, DB errors, IP addresses).

The safest fix is to move both directories outside public_html. If that isn't feasible, add explicit deny rules:

🔒 Proposed .htaccess fix (deny access to sensitive directories)
 Options -Indexes
 RewriteEngine On

+# Block direct access to sensitive directories
+<DirectoryMatch "^(.*/)(sql|logs)/">
+    Require all denied
+</DirectoryMatch>
+
 RewriteCond %{REQUEST_URI} !^/admin

Alternatively, place a deny.htaccess inside each directory:

# public_html/sql/.htaccess  and  public_html/logs/.htaccess
Require all denied
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public_html/.htaccess` around lines 1 - 11, The /sql and /logs directories
are still web-accessible despite "Options -Indexes"; move the physical
directories out of public_html so they cannot be served, and update any paths
that reference them; if moving is not possible, add per-directory deny files
(create public_html/sql/.htaccess and public_html/logs/.htaccess containing
"Require all denied") or add root-level deny rules that block requests to ^/sql
and ^/logs before the existing RewriteCond/RewriteRule block (ensure the deny
runs prior to the RewriteEngine rules that route to post.php) so direct file
access is forbidden.

13 changes: 13 additions & 0 deletions public_html/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Website tin tức đơn giản

Upload toàn bộ thư mục `public_html` lên hosting (DataOnline.vn), import `sql/database.sql`, cập nhật thông tin DB trong `config.php`. Sau khi import có sẵn tài khoản admin/admin567.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Default credentials (admin/admin567) must not be documented in a public repository.

The README + database.sql together tell anyone the exact admin username and password before the site owner has a chance to change it. Any deployment where the password is not immediately rotated is trivially compromisable.

Recommendations:

  1. Remove the password from the README entirely. Instruct the operator to set their own password with password_hash() after import, or add a first-run setup wizard.
  2. At minimum, add a prominent ⚠️ warning that the default password must be changed immediately after the SQL import.
  3. Consider replacing the seeded bcrypt hash in database.sql with a placeholder comment rather than a real working hash.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public_html/README.md` at line 3, Remove the hardcoded default credentials
from public README and instead instruct operators to set their own admin
password after import; update README to (a) remove "admin/admin567" and replace
with a short instruction to run a one-time setup or call password_hash() to
create an admin password, and (b) add a prominent ⚠️ warning that any default
password must be changed immediately after import; also update database.sql to
replace the seeded bcrypt/hash for the admin user with a clear placeholder
comment (or no hash) so no working credentials are stored in the repo, and
mention in README to update config.php DB settings as part of the secure setup.


## Đường dẫn
- Trang chủ: `/`
- Trang bài: `/slug`
- Đăng nhập: `/admin/login.php`
- Trang quản trị một trang: `/admin/panel.php`

## Lưu ý
- Bật/tắt quảng cáo, link Shopee, link liên hệ nằm trong tab Cài đặt của panel.
- Khi quảng cáo bật và người dùng mở trực tiếp `/slug`, overlay sẽ xuất hiện ngay; đóng overlay sẽ mở tab mới `/slug?ad=0` và tab hiện tại chuyển sang link Shopee.
40 changes: 40 additions & 0 deletions public_html/admin/login.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/error_handler.php';

ensure_session();
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (admin_login($username, $password)) {
header('Location: /admin/panel.php');
exit;
}
$error = 'Sai tài khoản hoặc mật khẩu';
Comment on lines +8 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Regenerate session ID after login; add CSRF to the login form.

Successful authentication should call session_regenerate_id(true) to mitigate session fixation. Also consider adding a CSRF token to the login form to prevent login CSRF.

🔒 Minimal fix for session fixation
     if (admin_login($username, $password)) {
+        session_regenerate_id(true);
         header('Location: /admin/panel.php');
         exit;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (admin_login($username, $password)) {
header('Location: /admin/panel.php');
exit;
}
$error = 'Sai tài khoản hoặc mật khẩu';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (admin_login($username, $password)) {
session_regenerate_id(true);
header('Location: /admin/panel.php');
exit;
}
$error = 'Sai tài khoản hoặc mật khẩu';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public_html/admin/login.php` around lines 8 - 15, After successful
authentication via admin_login($username, $password) call, immediately call
session_regenerate_id(true) before redirecting (i.e., insert
session_regenerate_id(true) between the admin_login success branch and the
header('Location: /admin/panel.php')/exit) to prevent session fixation;
additionally, add CSRF protection by generating a token into the session when
rendering the login form and validating that token on POST (compare
$_POST['csrf_token'] to the session token) before calling admin_login.

}
?><!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Đăng nhập</title>
<link rel="stylesheet" href="/assets/css/admin.css">
</head>
<body>
<div class="container">
<div class="card">
<h2>Đăng nhập admin</h2>
<?php if ($error): ?><p class="alert"><?php echo htmlspecialchars($error, ENT_QUOTES, 'UTF-8'); ?></p><?php endif; ?>
<form method="post" autocomplete="off">
<label>Tên đăng nhập</label>
<input name="username" required>
<label>Mật khẩu</label>
<input type="password" name="password" required>
<button type="submit">Đăng nhập</button>
</form>
</div>
</div>
</body>
</html>
7 changes: 7 additions & 0 deletions public_html/admin/logout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/error_handler.php';

admin_logout();
header('Location: /admin/login.php');
Comment on lines +5 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Logout endpoint is vulnerable to CSRF.

Any page an admin visits can silently trigger a GET /admin/logout.php (e.g., via <img src="...">), forcing the admin out of their session. Add a CSRF token check before calling admin_logout().

🛡️ Proposed fix
 <?php
 require_once __DIR__ . '/../lib/auth.php';
+require_once __DIR__ . '/../lib/csrf.php';
 require_once __DIR__ . '/../lib/error_handler.php';

+admin_require();
+csrf_verify();   // verify POST token, or gate on POST method
 admin_logout();
 header('Location: /admin/login.php');
 exit;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public_html/admin/logout.php` around lines 5 - 6, The logout endpoint
currently calls admin_logout() on any GET, so add a CSRF token check before
invoking admin_logout(): require a non-GET method (e.g., POST) and validate a
submitted token (e.g., $_POST['csrf_token']) against the stored session token
(e.g., $_SESSION['csrf_token']); only call admin_logout() and then
header('Location: /admin/login.php') when the token matches, otherwise return a
400/403 response and do not perform the logout; ensure the token name matches
how other forms generate it and clear/regenerate the session token after
successful logout.

exit;
181 changes: 181 additions & 0 deletions public_html/admin/panel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/csrf.php';
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/slugify.php';
require_once __DIR__ . '/../lib/track.php';

admin_require();
$pdo = db();
$message = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!csrf_check($_POST['csrf'] ?? '')) {
die('CSRF không hợp lệ');
}

$action = $_POST['action'] ?? '';
if ($action === 'create_post') {
$title = trim($_POST['title'] ?? '');
$slug = trim($_POST['slug'] ?? '');
$slug = $slug !== '' ? $slug : slugify($title);
$content = trim($_POST['content'] ?? '');
$images = array_filter(array_map('trim', explode("\n", $_POST['images'] ?? '')));
$videos = array_filter(array_map('trim', explode("\n", $_POST['videos'] ?? '')));
$stmt = $pdo->prepare('INSERT INTO posts (title, slug, content, images_json, videos_json, created_at) VALUES (?, ?, ?, ?, ?, NOW())');
$stmt->execute([$title, $slug, $content, json_encode(array_values($images)), json_encode(array_values($videos))]);
$message = 'Đã thêm bài';
} elseif ($action === 'update_post') {
$postId = (int)($_POST['post_id'] ?? 0);
$title = trim($_POST['title'] ?? '');
$slug = trim($_POST['slug'] ?? '');
$slug = $slug !== '' ? $slug : slugify($title);
$content = trim($_POST['content'] ?? '');
$images = array_filter(array_map('trim', explode("\n", $_POST['images'] ?? '')));
$videos = array_filter(array_map('trim', explode("\n", $_POST['videos'] ?? '')));
$stmt = $pdo->prepare('UPDATE posts SET title=?, slug=?, content=?, images_json=?, videos_json=? WHERE id=?');
$stmt->execute([$title, $slug, $content, json_encode(array_values($images)), json_encode(array_values($videos)), $postId]);
$message = 'Đã cập nhật';
} elseif ($action === 'delete_post') {
$postId = (int)($_POST['post_id'] ?? 0);
$stmt = $pdo->prepare('DELETE FROM posts WHERE id = ?');
$stmt->execute([$postId]);
$message = 'Đã xóa';
} elseif ($action === 'save_settings') {
$adsEnabled = isset($_POST['ads_enabled']) ? 1 : 0;
$adLink = trim($_POST['ad_link'] ?? '');
$adTitle = trim($_POST['ad_title'] ?? '');
$adBody = trim($_POST['ad_body'] ?? '');
$contactLink = trim($_POST['contact_link'] ?? '');
$stmt = $pdo->prepare('UPDATE settings SET ads_enabled=?, ad_link=?, ad_title=?, ad_body=?, contact_link=? WHERE id = 1');
$stmt->execute([$adsEnabled, $adLink, $adTitle, $adBody, $contactLink]);
Comment on lines +46 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate ad_link / contact_link schemes before saving.

Unvalidated URLs allow javascript: or other unsafe schemes that later get rendered on public pages. Restrict to http/https (or empty) before persisting.

✅ Example validation guard
     } elseif ($action === 'save_settings') {
         $adsEnabled = isset($_POST['ads_enabled']) ? 1 : 0;
         $adLink = trim($_POST['ad_link'] ?? '');
         $adTitle = trim($_POST['ad_title'] ?? '');
         $adBody = trim($_POST['ad_body'] ?? '');
         $contactLink = trim($_POST['contact_link'] ?? '');
+        foreach (['adLink' => $adLink, 'contactLink' => $contactLink] as $label => $link) {
+            if ($link !== '' && !preg_match('~^https?://~i', $link)) {
+                $message = 'Link không hợp lệ';
+                goto render_after_post;
+            }
+        }
         $stmt = $pdo->prepare('UPDATE settings SET ads_enabled=?, ad_link=?, ad_title=?, ad_body=?, contact_link=? WHERE id = 1');
         $stmt->execute([$adsEnabled, $adLink, $adTitle, $adBody, $contactLink]);
         $message = 'Đã lưu cài đặt';
     }
+render_after_post:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$adsEnabled = isset($_POST['ads_enabled']) ? 1 : 0;
$adLink = trim($_POST['ad_link'] ?? '');
$adTitle = trim($_POST['ad_title'] ?? '');
$adBody = trim($_POST['ad_body'] ?? '');
$contactLink = trim($_POST['contact_link'] ?? '');
$stmt = $pdo->prepare('UPDATE settings SET ads_enabled=?, ad_link=?, ad_title=?, ad_body=?, contact_link=? WHERE id = 1');
$stmt->execute([$adsEnabled, $adLink, $adTitle, $adBody, $contactLink]);
$adsEnabled = isset($_POST['ads_enabled']) ? 1 : 0;
$adLink = trim($_POST['ad_link'] ?? '');
$adTitle = trim($_POST['ad_title'] ?? '');
$adBody = trim($_POST['ad_body'] ?? '');
$contactLink = trim($_POST['contact_link'] ?? '');
foreach (['adLink' => $adLink, 'contactLink' => $contactLink] as $label => $link) {
if ($link !== '' && !preg_match('~^https?://~i', $link)) {
$message = 'Link không hợp lệ';
goto render_after_post;
}
}
$stmt = $pdo->prepare('UPDATE settings SET ads_enabled=?, ad_link=?, ad_title=?, ad_body=?, contact_link=? WHERE id = 1');
$stmt->execute([$adsEnabled, $adLink, $adTitle, $adBody, $contactLink]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public_html/admin/panel.php` around lines 46 - 52, The ad_link and
contact_link values ($adLink and $contactLink) are not validated and may contain
unsafe schemes; before executing the UPDATE in the panel.php handler, validate
each link's scheme (e.g., using parse_url or filter_var) and only allow empty or
schemes "http" and "https"; if a link fails validation, either normalize it to
an empty string or return a validation error to the caller, then proceed to bind
the sanitized $adLink and $contactLink into the existing $stmt->execute call so
only safe URLs are persisted.

$message = 'Đã lưu cài đặt';
}
}

$settings = load_settings($pdo);
$posts = $pdo->query('SELECT * FROM posts ORDER BY created_at DESC')->fetchAll();
$totalPosts = count($posts);
$totalViews = stat_total($pdo, 'view');
$totalClicks = stat_total($pdo, 'ad_click');
$csrf = csrf_token();
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
$baseUrl = $scheme . ($_SERVER['HTTP_HOST'] ?? 'domain');
?><!DOCTYPE html>
Comment on lines +63 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t trust HTTP_HOST for base URL construction.

HTTP_HOST is user-controlled and can inject malicious hostnames into the admin UI. Use a configured BASE_URL (from config) or a strict allowlist.

🔧 Example (config-based)
-$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
-$baseUrl = $scheme . ($_SERVER['HTTP_HOST'] ?? 'domain');
+$baseUrl = rtrim(BASE_URL, '/');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public_html/admin/panel.php` around lines 63 - 65, The current construction
of $baseUrl using $_SERVER['HTTP_HOST'] is unsafe because HTTP_HOST is
user-controlled; replace it so $baseUrl is derived from a trusted configuration
value (e.g. a BASE_URL constant/setting) or validated against a strict allowlist
of allowed hostnames before use; update the code paths that set $baseUrl (look
for $scheme and $baseUrl in this file) to prefer the configured BASE_URL, fall
back to building from $_SERVER only after validating against the allowlist, and
log or reject requests with invalid hosts.

<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quản trị</title>
<link rel="stylesheet" href="/assets/css/admin.css">
<script>
function switchTab(id){
document.querySelectorAll('.tab').forEach(el=>el.classList.add('hidden'));
document.getElementById(id).classList.remove('hidden');
}
function copyLink(link){
navigator.clipboard.writeText(link).then(()=>{
alert('Đã copy link');
});
}
</script>
</head>
<body>
<div class="container">
<div class="card">
<div class="top-row">
<h2>Xin chào, <?php echo htmlspecialchars(admin_name(), ENT_QUOTES, 'UTF-8'); ?></h2>
<a class="logout" href="/admin/logout.php">Đăng xuất</a>
</div>
<div class="nav">
<button onclick="switchTab('tab-post')">Bài viết</button>
<button onclick="switchTab('tab-settings')">Cài đặt</button>
<button onclick="switchTab('tab-stats')">Thống kê</button>
</div>
<?php if ($message): ?><p class="badge"><?php echo htmlspecialchars($message, ENT_QUOTES, 'UTF-8'); ?></p><?php endif; ?>

<div id="tab-post" class="tab">
<h3>Thêm bài</h3>
<form method="post">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($csrf, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="create_post">
<label>Tiêu đề</label>
<input name="title" required>
<label>Slug (để trống tự tạo)</label>
<input name="slug">
<label>Nội dung</label>
<textarea name="content" rows="4"></textarea>
<label>Link ảnh (mỗi dòng một link Telegram)</label>
<textarea name="images" rows="4"></textarea>
<label>Link video (mỗi dòng một link Telegram)</label>
<textarea name="videos" rows="4"></textarea>
<button type="submit">Thêm bài</button>
</form>

<h3>Danh sách bài</h3>
<?php foreach ($posts as $item): $itemId = (int)$item['id']; $slugVal = $item['slug']; $full = $baseUrl . '/' . $slugVal; ?>
<div class="card inner">
<div class="row">
<strong><?php echo htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8'); ?></strong>
<span class="badge">ID <?php echo $itemId; ?></span>
</div>
<p class="link-copy">Link: <?php echo htmlspecialchars($full, ENT_QUOTES, 'UTF-8'); ?></p>
<button type="button" onclick="copyLink('<?php echo htmlspecialchars($full, ENT_QUOTES, 'UTF-8'); ?>')">Copy link</button>
Comment on lines +123 to +124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape for JavaScript context, not just HTML.

htmlspecialchars doesn’t prevent JS string breakouts inside onclick. Use json_encode for the JS literal to avoid injection.

🔐 Safe JS string encoding
-<button type="button" onclick="copyLink('<?php echo htmlspecialchars($full, ENT_QUOTES, 'UTF-8'); ?>')">Copy link</button>
+<button type="button" onclick="copyLink(<?php echo json_encode($full, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); ?>)">Copy link</button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p class="link-copy">Link: <?php echo htmlspecialchars($full, ENT_QUOTES, 'UTF-8'); ?></p>
<button type="button" onclick="copyLink('<?php echo htmlspecialchars($full, ENT_QUOTES, 'UTF-8'); ?>')">Copy link</button>
<p class="link-copy">Link: <?php echo htmlspecialchars($full, ENT_QUOTES, 'UTF-8'); ?></p>
<button type="button" onclick="copyLink(<?php echo json_encode($full, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); ?>)">Copy link</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public_html/admin/panel.php` around lines 123 - 124, The onclick embeds the
PHP string into JavaScript but currently uses htmlspecialchars which only
escapes HTML; change the onclick argument to use a JS-safe literal by emitting
json_encode($full) (e.g., onclick="copyLink(<?php echo json_encode($full); ?>)")
while keeping htmlspecialchars($full, ENT_QUOTES, 'UTF-8') for the visible <p>
content; update the copyLink(...) call to use the json_encoded value so the
string cannot break out into JS.

<details>
<summary>Sửa / Xóa</summary>
<form method="post">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($csrf, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="update_post">
<input type="hidden" name="post_id" value="<?php echo $itemId; ?>">
<label>Tiêu đề</label>
<input name="title" value="<?php echo htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8'); ?>" required>
<label>Slug</label>
<input name="slug" value="<?php echo htmlspecialchars($item['slug'], ENT_QUOTES, 'UTF-8'); ?>" required>
<label>Nội dung</label>
<textarea name="content" rows="4"><?php echo htmlspecialchars($item['content'], ENT_QUOTES, 'UTF-8'); ?></textarea>
<label>Link ảnh</label>
<textarea name="images" rows="3"><?php echo htmlspecialchars(implode("\n", json_decode($item['images_json'], true) ?: []), ENT_QUOTES, 'UTF-8'); ?></textarea>
<label>Link video</label>
<textarea name="videos" rows="3"><?php echo htmlspecialchars(implode("\n", json_decode($item['videos_json'], true) ?: []), ENT_QUOTES, 'UTF-8'); ?></textarea>
<button type="submit">Lưu</button>
</form>
<form method="post" onsubmit="return confirm('Xóa bài này?');">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($csrf, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="delete_post">
<input type="hidden" name="post_id" value="<?php echo $itemId; ?>">
<button type="submit" class="danger">Xóa</button>
</form>
</details>
</div>
<?php endforeach; ?>
</div>

<div id="tab-settings" class="tab hidden">
<h3>Cài đặt</h3>
<form method="post">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($csrf, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="save_settings">
<label><input type="checkbox" name="ads_enabled" <?php echo !empty($settings['ads_enabled']) ? 'checked' : ''; ?>> Bật quảng cáo</label>
<label>Link Shopee</label>
<input name="ad_link" value="<?php echo htmlspecialchars($settings['ad_link'], ENT_QUOTES, 'UTF-8'); ?>" placeholder="https://s.shopee.vn/xxxx">
<label>Tiêu đề quảng cáo</label>
<input name="ad_title" value="<?php echo htmlspecialchars($settings['ad_title'], ENT_QUOTES, 'UTF-8'); ?>">
<label>Nội dung quảng cáo</label>
<textarea name="ad_body" rows="3"><?php echo htmlspecialchars($settings['ad_body'], ENT_QUOTES, 'UTF-8'); ?></textarea>
<label>Link liên hệ admin</label>
<input name="contact_link" value="<?php echo htmlspecialchars($settings['contact_link'], ENT_QUOTES, 'UTF-8'); ?>" placeholder="Facebook hoặc Telegram">
<button type="submit">Lưu</button>
</form>
</div>

<div id="tab-stats" class="tab hidden">
<h3>Thống kê</h3>
<p>Tổng số bài báo: <strong><?php echo $totalPosts; ?></strong></p>
<p>Tổng lượt xem bài: <strong><?php echo $totalViews; ?></strong></p>
<p>Tổng lượt click quảng cáo: <strong><?php echo $totalClicks; ?></strong></p>
</div>
</div>
</div>
</body>
</html>
22 changes: 22 additions & 0 deletions public_html/api/track.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/track.php';

$pdo = db();
$type = $_POST['type'] ?? 'view';
$slug = $_POST['slug'] ?? '';
$postId = null;

if ($slug !== '') {
$stmt = $pdo->prepare('SELECT id FROM posts WHERE slug = ? LIMIT 1');
$stmt->execute([$slug]);
$row = $stmt->fetch();
if ($row) {
$postId = (int)$row['id'];
}
}

record_stat($pdo, $type === 'ad_click' ? 'ad_click' : 'view', $postId, 60);

echo json_encode(['ok' => true]);
Comment on lines +1 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing Content-Type header and no HTTP method guard.

Two minor issues:

  1. The JSON response is missing header('Content-Type: application/json').
  2. No method check means a plain GET request (e.g., a stray browser prefetch) will execute the slug lookup and call record_stat against the DB with $postId = null.
🔧 Proposed fix
 <?php
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+    http_response_code(405);
+    exit;
+}
 require_once __DIR__ . '/../lib/db.php';
 require_once __DIR__ . '/../lib/error_handler.php';
 require_once __DIR__ . '/../lib/track.php';
 
 // ...
 
+header('Content-Type: application/json');
 echo json_encode(['ok' => true]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public_html/api/track.php` around lines 1 - 22, The endpoint lacks a
Content-Type header and doesn't guard the HTTP method; update the top of the
script to only proceed when $_SERVER['REQUEST_METHOD'] === 'POST' (otherwise
return a 405 JSON response) and ensure you set header('Content-Type:
application/json') before sending the JSON reply; keep the existing logic that
computes $type/$slug, does the slug lookup, and calls record_stat($pdo, $type
=== 'ad_click' ? 'ad_click' : 'view', $postId, 60) so that DB updates only occur
on POST and the response is correctly labeled as application/json.

24 changes: 24 additions & 0 deletions public_html/assets/css/admin.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
body {
margin: 0;
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background: #0b1222;
color: #e2e8f0;
}
.container { max-width: 880px; margin: 0 auto; padding: 18px 14px 60px; }
.card { background: #111827; border-radius: 12px; padding: 16px; box-shadow: 0 12px 32px rgba(0,0,0,0.35); }
.card + .card { margin-top: 12px; }
input, textarea, button { width: 100%; padding: 12px; margin: 6px 0 12px; border-radius: 8px; border: 1px solid #1f2937; background: #0d152b; color: #e2e8f0; }
label { font-weight: 600; display: block; margin-top: 4px; }
button { cursor: pointer; background: #2563eb; border: none; font-weight: 600; }
button.danger { background: #ef4444; }
.nav { display: flex; gap: 8px; margin: 12px 0; }
.nav button { flex: 1; }
.badge { display: inline-block; padding: 6px 10px; background: #1d4ed8; border-radius: 8px; font-size: 12px; }
.alert { color: #fca5a5; }
.hidden { display: none; }
.link-copy { font-size: 13px; color: #93c5fd; word-break: break-all; }
.row { display: flex; justify-content: space-between; align-items: center; }
.top-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.logout { color: #93c5fd; text-decoration: none; }
.card.inner { margin-top: 10px; }
@media (max-width: 600px) { .nav { flex-direction: column; } .top-row { flex-direction: column; align-items: flex-start; } }
Loading