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
6 changes: 6 additions & 0 deletions public_html/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/(admin|api|bots|assets|includes|lib|cache|logs)/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^([A-Za-z0-9-]+)/?$ post.php?slug=$1 [L,QSA]

RewriteRule ^$ index.php [L]
35 changes: 35 additions & 0 deletions public_html/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Báo tin tức PHP thuần

Triển khai cho hosting share (PHP 7.4, MySQL) với ba theme, quản trị tiếng Việt và flow quảng cáo Shopee không delay, giữ link affiliate s.shopee.vn.

## Cách cài đặt
1. Upload toàn bộ thư mục `public_html` lên hosting (dataonline.vn hoặc tương tự).
2. Import MySQL file `sql/database.sql` (đã seed admin: `admin / admin567`).
3. Sửa file `public_html/config.php` với thông tin DB, `BASE_URL`, token Telegram, `APP_DEBUG`.
4. Đảm bảo file `.htaccess` hoạt động để rewrite slug: `/slug` -> `post.php?slug=...`.
5. Phân quyền ghi cho thư mục `cache/` và `logs/`.

## Đăng nhập admin
- URL: `/admin/login.php`
- Tài khoản mặc định: **admin / admin567** (mật khẩu đã được hash trong DB).

## Kiểm tra nhanh sau khi deploy
- **Quảng cáo Shopee**: Bật quảng cáo ở `Admin > Cài đặt`, nhập link s.shopee.vn. Truy cập bài viết: overlay xuất hiện, bấm bất kỳ nơi nào hoặc nút X sẽ mở bài ở tab mới với `?ad=0` và tab hiện tại đi Shopee (không delay). Truy cập trực tiếp `/slug` vẫn thấy overlay trừ khi URL có `?ad=0`.
- **Theme**: Vào `Admin > Cài đặt`, đổi giữa Theme 1/2/3, toàn site đổi màu ngay.
- **Chống click ảo**: Tracking dùng token phiên + rate limit IP/UA/slug (30s) trong `/api/track.php`; spam gọi API sẽ nhận `rate_limited`.
- **Log lỗi**: Khi `APP_DEBUG=false`, lỗi được ghi vào `logs/app.log` và hiển thị `error.php` thân thiện (không trắng trang).

## Thư mục chính
- `index.php`: trang chủ, phân trang, menu 3D, trigger quảng cáo trên click bài.
- `post.php`: trang bài, meta SEO/OG, hiển thị media Telegram, overlay quảng cáo nếu bật.
- `assets/css`: `base.css` + `theme1/2/3.css` + `admin.css`.
- `assets/js`: `app.js` (UI + menu), `adflow.js` (overlay Shopee đúng flow), `track.js` (tracking không chậm redirect).
- `admin/`: đăng nhập, CRUD bài, copy link, cài đặt, thống kê (view/ngày/tháng/ad click, reset).
- `api/track.php`: nhận tracking page/post/ad với rate limit và token, gộp thông báo Telegram.
- `lib/`: PDO, auth, cache file, csrf, slugify, rate-limit, telegram, helpers, error handler.

## Lưu ý vận hành
- Ảnh/logo/banner được lưu trong `assets/img/` (nhẹ, phù hợp share hosting).
- Cache file cho trang chủ/bài ~45s; cài đặt cache 5 phút.
- Không sử dụng framework hay CLI/composer.
- Gửi Telegram: cấu hình token/chat_id trong `config.php`; hàng đợi + gộp click trong `telegram_queue` và `telegram_notifications`.
27 changes: 27 additions & 0 deletions public_html/admin/dashboard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/helpers.php';
require_once __DIR__ . '/../lib/csrf.php';
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/telegram.php';
require_auth();

flushTelegramQueue();
$totalArticles = (int)$pdo->query("SELECT COUNT(*) FROM articles")->fetchColumn();
$publicArticles = (int)$pdo->query("SELECT COUNT(*) FROM articles WHERE status='public'")->fetchColumn();
$views = (int)$pdo->query("SELECT COUNT(*) FROM events WHERE event_type='post_view'")->fetchColumn();
$adClicks = (int)$pdo->query("SELECT COUNT(*) FROM events WHERE event_type='ad_click'")->fetchColumn();
include __DIR__ . '/includes/header.php';
?>
<div class="topbar">
<h1>Chào, <?php echo e($_SESSION['admin_username'] ?? 'admin'); ?></h1>
<a class="btn" href="/">Xem trang</a>
</div>
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:14px;">
<div class="card">Tổng bài viết: <strong><?php echo $totalArticles; ?></strong></div>
<div class="card">Bài public: <strong><?php echo $publicArticles; ?></strong></div>
<div class="card">Lượt xem bài: <strong><?php echo $views; ?></strong></div>
<div class="card">Click quảng cáo: <strong><?php echo $adClicks; ?></strong></div>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>
4 changes: 4 additions & 0 deletions public_html/admin/includes/auth_check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
require_once __DIR__ . '/../../lib/error_handler.php';
require_once __DIR__ . '/../../lib/auth.php';
require_auth();
4 changes: 4 additions & 0 deletions public_html/admin/includes/footer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
</main>
</div>
</body>
</html>
23 changes: 23 additions & 0 deletions public_html/admin/includes/header.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/../includes/auth_check.php';
?>
<!DOCTYPE html>
<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">
</head>
<body>
<div class="admin-shell">
<aside class="sidebar">
<h2>Admin</h2>
<a href="/admin/dashboard.php">Bảng tin</a>
<a href="/admin/posts.php">Bài viết</a>
<a href="/admin/post_add.php">Thêm bài</a>
<a href="/admin/settings.php">Cài đặt</a>
<a href="/admin/stats.php">Thống kê</a>
<a href="/admin/logout.php">Đăng xuất</a>
</aside>
<main class="main">
45 changes: 45 additions & 0 deletions public_html/admin/login.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/helpers.php';
require_once __DIR__ . '/../lib/csrf.php';

$message = '';
if (is_post()) {
$token = $_POST['csrf'] ?? '';
if (!verify_csrf($token)) {
$message = 'Token không hợp lệ';
} else {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (login($username, $password)) {
redirect('/admin/dashboard.php');
}
$message = 'Sai tài khoản hoặc mật khẩu';
}
}
?>
<!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 quản trị</title>
<link rel="stylesheet" href="/assets/css/admin.css">
</head>
<body style="display:grid; place-items:center; min-height:100vh;">
<div class="card" style="width:min(420px, 92%);">
<h2>Đăng nhập</h2>
<?php if ($message): ?><div class="alert"><?php echo e($message); ?></div><?php endif; ?>
<form method="POST">
<label>Tài khoản</label>
<input type="text" name="username" value="<?php echo e($_POST['username'] ?? ''); ?>" required>
<label>Mật khẩu</label>
<input type="password" name="password" required>
<input type="hidden" name="csrf" value="<?php echo csrf_token(); ?>">
<button class="btn" type="submit">Đăng nhập</button>
</form>
<p style="margin-top:12px; color:#94a3b8;">Tài khoản mặc định: admin / admin567</p>
</div>
</body>
</html>
6 changes: 6 additions & 0 deletions public_html/admin/logout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/auth.php';
logout();
header('Location: /admin/login.php');
exit;
74 changes: 74 additions & 0 deletions public_html/admin/post_add.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/helpers.php';
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/csrf.php';
require_once __DIR__ . '/../lib/slugify.php';
require_once __DIR__ . '/../lib/telegram.php';
require_auth();

$message = '';
if (is_post()) {
if (!verify_csrf($_POST['csrf'] ?? '')) {
$message = 'Token không hợp lệ';
} else {
$title = trim($_POST['title'] ?? '');
$slug = trim($_POST['slug'] ?? '') ?: slugify($title);
$excerpt = trim($_POST['excerpt'] ?? '');
$content = trim($_POST['content'] ?? '');
$telegram_media = trim($_POST['telegram_media'] ?? '');
$meta_title = trim($_POST['meta_title'] ?? '');
$meta_description = trim($_POST['meta_description'] ?? '');
$meta_keywords = trim($_POST['meta_keywords'] ?? '');
$status = $_POST['status'] === 'public' ? 'public' : 'draft';
$stmt = $pdo->prepare("INSERT INTO articles(title, slug, excerpt, content, telegram_media, meta_title, meta_description, meta_keywords, status, created_at, updated_at) VALUES(:title,:slug,:excerpt,:content,:media,:mt,:md,:mk,:status,NOW(),NOW())");
try {
$stmt->execute([
':title' => $title,
':slug' => $slug,
':excerpt' => $excerpt,
':content' => $content,
':media' => $telegram_media,
':mt' => $meta_title,
':md' => $meta_description,
':mk' => $meta_keywords,
':status' => $status,
]);
queueTelegramMessage('Bài mới: ' . $title . ' - ' . current_url($slug));
redirect('/admin/posts.php');
} catch (Throwable $e) {
$message = 'Không thể lưu bài: ' . $e->getMessage();
}
}
}
include __DIR__ . '/includes/header.php';
?>
<div class="topbar"><h1>Thêm bài</h1></div>
<?php if ($message): ?><div class="alert"><?php echo e($message); ?></div><?php endif; ?>
<form method="POST">
<label>Tiêu đề</label>
<input name="title" required value="<?php echo e($_POST['title'] ?? ''); ?>">
<label>Slug</label>
<input name="slug" value="<?php echo e($_POST['slug'] ?? ''); ?>" placeholder="tieu-de-khong-dau">
<label>Tóm tắt</label>
<textarea name="excerpt" rows="2"><?php echo e($_POST['excerpt'] ?? ''); ?></textarea>
<label>Nội dung</label>
<textarea name="content" rows="8" required><?php echo e($_POST['content'] ?? ''); ?></textarea>
<label>Telegram media (mỗi dòng 1 link)</label>
<textarea name="telegram_media" rows="3"><?php echo e($_POST['telegram_media'] ?? ''); ?></textarea>
<label>Meta title</label>
<input name="meta_title" value="<?php echo e($_POST['meta_title'] ?? ''); ?>">
<label>Meta description</label>
<input name="meta_description" value="<?php echo e($_POST['meta_description'] ?? ''); ?>">
<label>Meta keywords</label>
<input name="meta_keywords" value="<?php echo e($_POST['meta_keywords'] ?? ''); ?>">
<label>Trạng thái</label>
<select name="status">
<option value="draft">Nháp</option>
<option value="public">Public</option>
</select>
<input type="hidden" name="csrf" value="<?php echo csrf_token(); ?>">
<button class="btn" type="submit">Lưu</button>
</form>
<?php include __DIR__ . '/includes/footer.php'; ?>
32 changes: 32 additions & 0 deletions public_html/admin/post_copy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/helpers.php';
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/csrf.php';
require_auth();

$slug = $_GET['slug'] ?? '';
$link = $slug ? current_url($slug) : current_url();
?>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Copy link</title>
<link rel="stylesheet" href="/assets/css/admin.css">
</head>
<body style="display:grid; place-items:center; min-height:100vh;">
<div class="card" style="width:min(420px, 92%); text-align:center;">
<p>Link bài viết:</p>
<code style="display:block; margin-bottom:12px; word-break:break-all;"><?php echo e($link); ?></code>
<button class="btn" id="copy">Copy</button>
<p style="margin-top:12px; color:#94a3b8;">Dán link gửi cho bạn đọc.</p>
</div>
<script>
document.getElementById('copy').addEventListener('click', function(){
navigator.clipboard.writeText('<?php echo e($link); ?>').then(() => alert('Đã copy link!'));
});
</script>
</body>
</html>
12 changes: 12 additions & 0 deletions public_html/admin/post_delete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/helpers.php';
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/csrf.php';
require_auth();

$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare('DELETE FROM articles WHERE id = :id');
$stmt->execute([':id' => $id]);
Comment on lines +9 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.

P1 Badge Add CSRF protection to delete endpoint

post_delete.php performs the DELETE directly from a GET id without verifying a CSRF token. A malicious page loaded by a logged-in admin can fire a GET to this URL (e.g., via an image tag) and delete posts without server-side verification. This endpoint should require POST and call verify_csrf before executing the deletion.

Useful? React with 👍 / 👎.

redirect('/admin/posts.php');
80 changes: 80 additions & 0 deletions public_html/admin/post_edit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
require_once __DIR__ . '/../lib/error_handler.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/helpers.php';
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/csrf.php';
require_once __DIR__ . '/../lib/slugify.php';
require_once __DIR__ . '/../lib/telegram.php';
require_auth();

$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare('SELECT * FROM articles WHERE id = :id');
$stmt->execute([':id' => $id]);
$post = $stmt->fetch();
if (!$post) { echo 'Không tìm thấy'; exit; }

$message = '';
if (is_post()) {
if (!verify_csrf($_POST['csrf'] ?? '')) {
$message = 'Token không hợp lệ';
} else {
$title = trim($_POST['title'] ?? '');
$slug = trim($_POST['slug'] ?? '') ?: slugify($title);
$excerpt = trim($_POST['excerpt'] ?? '');
$content = trim($_POST['content'] ?? '');
$telegram_media = trim($_POST['telegram_media'] ?? '');
$meta_title = trim($_POST['meta_title'] ?? '');
$meta_description = trim($_POST['meta_description'] ?? '');
$meta_keywords = trim($_POST['meta_keywords'] ?? '');
$status = $_POST['status'] === 'public' ? 'public' : 'draft';
$stmtUpdate = $pdo->prepare("UPDATE articles SET title=:title, slug=:slug, excerpt=:excerpt, content=:content, telegram_media=:media, meta_title=:mt, meta_description=:md, meta_keywords=:mk, status=:status, updated_at=NOW() WHERE id=:id");
try {
$stmtUpdate->execute([
':title' => $title,
':slug' => $slug,
':excerpt' => $excerpt,
':content' => $content,
':media' => $telegram_media,
':mt' => $meta_title,
':md' => $meta_description,
':mk' => $meta_keywords,
':status' => $status,
':id' => $id,
]);
redirect('/admin/posts.php');
} catch (Throwable $e) {
$message = 'Không thể cập nhật: ' . $e->getMessage();
}
}
}
include __DIR__ . '/includes/header.php';
?>
<div class="topbar"><h1>Sửa bài</h1></div>
<?php if ($message): ?><div class="alert"><?php echo e($message); ?></div><?php endif; ?>
<form method="POST">
<label>Tiêu đề</label>
<input name="title" required value="<?php echo e($_POST['title'] ?? $post['title']); ?>">
<label>Slug</label>
<input name="slug" value="<?php echo e($_POST['slug'] ?? $post['slug']); ?>">
<label>Tóm tắt</label>
<textarea name="excerpt" rows="2"><?php echo e($_POST['excerpt'] ?? $post['excerpt']); ?></textarea>
<label>Nội dung</label>
<textarea name="content" rows="8" required><?php echo e($_POST['content'] ?? $post['content']); ?></textarea>
<label>Telegram media</label>
<textarea name="telegram_media" rows="3"><?php echo e($_POST['telegram_media'] ?? $post['telegram_media']); ?></textarea>
<label>Meta title</label>
<input name="meta_title" value="<?php echo e($_POST['meta_title'] ?? $post['meta_title']); ?>">
<label>Meta description</label>
<input name="meta_description" value="<?php echo e($_POST['meta_description'] ?? $post['meta_description']); ?>">
<label>Meta keywords</label>
<input name="meta_keywords" value="<?php echo e($_POST['meta_keywords'] ?? $post['meta_keywords']); ?>">
<label>Trạng thái</label>
<select name="status">
<option value="draft" <?php echo (($post['status'] ?? '') === 'draft') ? 'selected' : ''; ?>>Nháp</option>
<option value="public" <?php echo (($post['status'] ?? '') === 'public') ? 'selected' : ''; ?>>Public</option>
</select>
<input type="hidden" name="csrf" value="<?php echo csrf_token(); ?>">
<button class="btn" type="submit">Lưu thay đổi</button>
</form>
<?php include __DIR__ . '/includes/footer.php'; ?>
Loading