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
12 changes: 12 additions & 0 deletions public_html/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Options +FollowSymLinks
RewriteEngine On
RewriteBase /

RewriteCond %{REQUEST_URI} ^/(admin|api|bots|assets|sql|app|includes|uploads)/ [NC]
RewriteRule ^ - [L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([A-Za-z0-9-]+)/?$ post.php?slug=$1 [L,QSA]

RewriteRule ^$ index.php [L]
57 changes: 57 additions & 0 deletions public_html/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Báo điện tử PHP thuần (shared hosting)

## Công nghệ & Kiến trúc
- PHP 8.x + MySQL, HTML/CSS/JS thuần, PDO prepared statements.
- Cấu hình qua `app/config/config.php` (không .env), cache file tại `app/cache`.
- SEO slug rewrite bằng `.htaccess` (không ảnh hưởng `/admin`, `/api`, `/bots`, `/uploads`).
- 3 theme (A/B/C) đổi nhanh trong admin, mobile-first, hiệu ứng 3D nhẹ.

## Cài đặt nhanh trên shared hosting (DataOnline.vn)
1. Upload toàn bộ `public_html` lên root hosting (Apache + mod_rewrite).
2. Tạo database MySQL, import `sql/database.sql`.
3. Tạo cấu hình: truy cập `https://your-domain/install.php` để nhập DB/BASE_URL (xong nên xóa file này). Hoặc sửa tay `app/config/config.php`.
4. Đảm bảo `app/cache` và `uploads` có quyền ghi (chmod 755/775 tùy hosting).
5. Đăng nhập admin: `https://your-domain/admin/login.php` (mặc định `admin / admin123`, hãy đổi mật khẩu trong DB), cấu hình tên site, logo/banner, theme, slug mode.

## Chạy & URL
- Trang chủ: `/`
- Bài viết: `/slug` hoặc `/slug-id` (chọn trong Cài đặt > Slug mode).
- Admin: `/admin/`
- API tracking: `/api/track.php`
- Bot webhook: `/bots/telegram/webhook.php`

## Tính năng frontend
- Trang chủ: banner, logo (ẩn nếu trống), tìm kiếm, lọc danh mục, phân trang.
- Trang bài: breadcrumbs, chia sẻ, meta/OG tiếng Việt, schema NewsArticle, media Telegram (ảnh/video link), bài liên quan.

## Quảng cáo Shopee (interstitial hợp lệ)
- Cấu hình trong Admin > Cài đặt: bật/tắt, link Shopee, title/body, tần suất (một lần/phiên, mỗi N giờ, mỗi N bài).
- Overlay có 2 nút: “Đi tới Shopee (Ưu đãi)” mở tab mới Shopee (noopener) + tracking, “Vào đọc bài (Bỏ qua)” đóng overlay đọc ngay. Không hijack click toàn trang.

## Tracking & chống click ảo
- Event: `article_view`, `ad_forced_redirect`, `ad_close_click`.
- IP+UA hash (SESSION_SALT) + rate limit cấu hình (mặc định 5 sự kiện/60s) + chặn UA bot regex.
- Gửi bằng `sendBeacon`/fetch keepalive nên không cản redirect.

## Admin
- Đăng nhập bảo vệ CSRF + khóa tạm khi sai quá số lần (cấu hình).
- CRUD bài (title, slug, meta, tags, category, media Telegram, draft/publish, copy), ping Telegram khi publish.
- Cài đặt: theme A/B/C, slug mode, logo/banner upload an toàn (image mime), màu sắc, hero text, Shopee ad, rate limit, UA bot regex, Telegram token/chat id.
- Thống kê: view/click, CTR, reset (log), export CSV.

## Telegram bot
- Thư mục `bots/telegram/` (bot.php, webhook.php, keyboard.php).
- Webhook: `https://api.telegram.org/bot<token>/setWebhook?url=https://your-domain/bots/telegram/webhook.php`
- Lệnh: `/stats`, `/reset`, `/add title|excerpt|content|media`, `/ad link|title|body`.

## Rewrite & bảo trì
- `.htaccess` đã chặn rewrite thư mục hệ thống (`admin|api|bots|assets|sql|app|includes|uploads`).
- Backup/restore: export DB, sao lưu `app/config/config.php`, `uploads/`, `app/cache` có thể xóa để rebuild.

## Checklist kiểm thử nhanh
- Cài DB bằng install.php, xóa file sau khi hoàn tất.
- Slug rewrite hoạt động cho /slug và /slug-id.
- Quảng cáo: rỗng link => không hiển thị; có link => overlay với 2 nút, click “Shopee” mở tab mới + tracking.
- Tracking ghi `article_view` và `ad_forced_redirect`, CTR hiển thị ở dashboard.
- Admin CRUD + copy, publish/draft, đổi theme/slug mode, upload logo/banner.
- Bot Telegram nhận `/stats`, `/reset`, `/ad`, `/add` với admin ID whitelist.
21 changes: 21 additions & 0 deletions public_html/admin/dashboard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/posts.php';
require_once __DIR__ . '/../app/lib/tracking.php';
$stats = tracking_stats();
$totalPosts = count_posts();
$views = $stats['article_view'] ?? 0;
$clicks = $stats['ad_forced_redirect'] ?? 0;
$ctr = $views ? round($clicks / $views * 100, 2) : 0;
include __DIR__ . '/includes/header.php';
?>
<section class="card admin-card">
<h2>Tổng quan</h2>
<div class="stats-grid">
<div class="stat"><div class="stat-number"><?php echo $totalPosts; ?></div><div class="stat-label">Bài public</div></div>
<div class="stat"><div class="stat-number"><?php echo $stats['article_view'] ?? 0; ?></div><div class="stat-label">Lượt đọc</div></div>
<div class="stat"><div class="stat-number"><?php echo $stats['ad_forced_redirect'] ?? 0; ?></div><div class="stat-label">Redirect quảng cáo</div></div>
<div class="stat"><div class="stat-number"><?php echo $ctr; ?>%</div><div class="stat-label">CTR</div></div>
</div>
</section>
<?php include __DIR__ . '/includes/footer.php'; ?>
3 changes: 3 additions & 0 deletions public_html/admin/includes/auth_check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
require_once __DIR__ . '/../../app/lib/auth.php';
require_login();
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>
<footer class="footer admin-footer">© <?php echo date('Y'); ?> Tin tức</footer>
</body>
</html>
27 changes: 27 additions & 0 deletions public_html/admin/includes/header.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/../../app/lib/helpers.php';
require_once __DIR__ . '/../../app/lib/settings.php';
$settings = get_settings();
?>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin | <?php echo e($settings['site_name'] ?? 'News'); ?></title>
<link rel="stylesheet" href="/assets/css/style.css">
<link rel="stylesheet" href="/assets/css/custom.css">
</head>
<body class="admin">
<header class="admin-header">
<div class="admin-brand"><?php echo e($settings['site_name'] ?? 'News'); ?> Admin</div>
<nav class="admin-nav">
<a href="/admin/dashboard.php">Dashboard</a>
<a href="/admin/posts.php">Bài viết</a>
<a href="/admin/settings.php">Cài đặt</a>
<a href="/admin/stats.php">Thống kê</a>
<a href="/admin/theme.php">Giao diện</a>
<a href="/admin/logout.php">Đăng xuất</a>
</nav>
</header>
<main class="admin-container">
43 changes: 43 additions & 0 deletions public_html/admin/login.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/../app/lib/auth.php';
require_once __DIR__ . '/../app/lib/helpers.php';
require_once __DIR__ . '/../app/lib/settings.php';
$settings = get_settings();
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf($_POST['csrf'] ?? '')) {
$error = 'Phiên đăng nhập không hợp lệ';
} else {
$username = sanitize_text($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (login($username, $password)) {
header('Location: /admin/dashboard.php');
exit;
}
$error = 'Sai tài khoản hoặc mật khẩu / tài khoản tạm khóa';
}
}
?>
<!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/style.css">
</head>
<body class="admin auth">
<div class="auth-card">
<h2>Đăng nhập quản trị</h2>
<?php if ($error): ?><p class="error"><?php echo e($error); ?></p><?php endif; ?>
<form method="post">
<input type="hidden" name="csrf" value="<?php echo e(csrf_token()); ?>">
<label>Username</label>
<input type="text" name="username" required>
<label>Password</label>
<input type="password" name="password" required>
<button class="btn" type="submit">Đăng nhập</button>
</form>
</div>
</body>
</html>
5 changes: 5 additions & 0 deletions public_html/admin/logout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/../app/lib/auth.php';
logout();
header('Location: /admin/login.php');
exit;
82 changes: 82 additions & 0 deletions public_html/admin/post_add.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/posts.php';
require_once __DIR__ . '/../app/lib/helpers.php';

$post = ['title' => '', 'slug' => '', 'excerpt' => '', 'content' => '', 'media_url' => '', 'is_public' => 1, 'published_at' => date('Y-m-d H:i:s'), 'category'=>'', 'tags'=>'', 'meta_title'=>'', 'meta_description'=>'', 'meta_keywords'=>'', 'og_image'=>''];
if (!empty($_GET['copy'])) {
$copy = find_post_by_id((int)$_GET['copy']);
if ($copy) {
$post = $copy;
$post['slug'] = unique_slug($copy['slug'] . '-copy');
$post['title'] .= ' (Copy)';
}
}

$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf($_POST['csrf'] ?? '')) {
$error = 'Token không hợp lệ';
} else {
$title = sanitize_text($_POST['title'] ?? '');
$slug = unique_slug($_POST['slug'] ?: $title);
$data = [
'title' => $title,
'slug' => $slug,
'excerpt' => sanitize_text($_POST['excerpt'] ?? ''),
'content' => trim($_POST['content'] ?? ''),
'media_url' => sanitize_text($_POST['media_url'] ?? ''),
'is_public' => isset($_POST['is_public']) ? 1 : 0,
'published_at' => sanitize_text($_POST['published_at'] ?? date('Y-m-d H:i:s')),
'category' => sanitize_text($_POST['category'] ?? ''),
'tags' => sanitize_text($_POST['tags'] ?? ''),
'meta_title' => sanitize_text($_POST['meta_title'] ?? $title),
'meta_description' => sanitize_text($_POST['meta_description'] ?? ($_POST['excerpt'] ?? '')),
'meta_keywords' => sanitize_text($_POST['meta_keywords'] ?? ''),
'og_image' => sanitize_text($_POST['og_image'] ?? ($_POST['media_url'] ?? '')),
];
$id = create_post($data);
admin_log('create_post');
if ($data['is_public']) {
telegram_notify("Bài mới: " . $data['title'] . "\n" . base_url(ltrim(article_url(['slug'=>$data['slug'],'id'=>$id]), '/')));
}
header('Location: /admin/posts.php');
exit;
}
}
include __DIR__ . '/includes/header.php';
?>
<section class="card admin-card">
<h2>Thêm bài viết</h2>
<?php if ($error): ?><p class="error"><?php echo e($error); ?></p><?php endif; ?>
<form method="post">
<input type="hidden" name="csrf" value="<?php echo e(csrf_token()); ?>">
<label>Tiêu đề</label>
<input type="text" name="title" value="<?php echo e($post['title']); ?>" required>
<label>Slug</label>
<input type="text" name="slug" value="<?php echo e($post['slug']); ?>" placeholder="bai-viet-demo">
<label>Mô tả ngắn</label>
<textarea name="excerpt" required><?php echo e($post['excerpt']); ?></textarea>
<label>Nội dung</label>
<textarea name="content" rows="8" required><?php echo e($post['content']); ?></textarea>
<label>Media URL (Telegram)</label>
<input type="text" name="media_url" value="<?php echo e($post['media_url']); ?>">
<label>Danh mục</label>
<input type="text" name="category" value="<?php echo e($post['category'] ?? ''); ?>" placeholder="tin-nong">
<label>Tags (phân cách bởi dấu phẩy)</label>
<input type="text" name="tags" value="<?php echo e($post['tags'] ?? ''); ?>">
<label>Meta title</label>
<input type="text" name="meta_title" value="<?php echo e($post['meta_title'] ?? $post['title']); ?>">
<label>Meta description</label>
<input type="text" name="meta_description" value="<?php echo e($post['meta_description'] ?? $post['excerpt']); ?>">
<label>Từ khóa</label>
<input type="text" name="meta_keywords" value="<?php echo e($post['meta_keywords'] ?? ''); ?>">
<label>OG Image</label>
<input type="text" name="og_image" value="<?php echo e($post['og_image'] ?? ''); ?>">
<label>Ngày xuất bản</label>
<input type="text" name="published_at" value="<?php echo e($post['published_at']); ?>">
<label><input type="checkbox" name="is_public" value="1" <?php echo $post['is_public'] ? 'checked' : ''; ?>> Public</label>
<button class="btn" type="submit">Lưu</button>
</form>
</section>
<?php include __DIR__ . '/includes/footer.php'; ?>
14 changes: 14 additions & 0 deletions public_html/admin/post_delete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/posts.php';
require_once __DIR__ . '/../app/lib/helpers.php';

$id = (int)($_GET['id'] ?? 0);
$token = $_GET['csrf'] ?? '';

if ($id && verify_csrf($token)) {
delete_post($id);
admin_log('delete_post');
}
header('Location: /admin/posts.php');
exit;
78 changes: 78 additions & 0 deletions public_html/admin/post_edit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/posts.php';
require_once __DIR__ . '/../app/lib/helpers.php';

$id = (int)($_GET['id'] ?? 0);
$post = find_post_by_id($id);
if (!$post) {
exit('Không tìm thấy bài');
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf($_POST['csrf'] ?? '')) {
$error = 'Token không hợp lệ';
} else {
$title = sanitize_text($_POST['title'] ?? '');
$slugInput = $_POST['slug'] ?: $title;
$slug = slugify($slugInput);
if ($slug !== $post['slug']) {
$slug = unique_slug($slug);
}
$data = [
'title' => $title,
'slug' => $slug,
'excerpt' => sanitize_text($_POST['excerpt'] ?? ''),
'content' => trim($_POST['content'] ?? ''),
'media_url' => sanitize_text($_POST['media_url'] ?? ''),
'is_public' => isset($_POST['is_public']) ? 1 : 0,
'published_at' => sanitize_text($_POST['published_at'] ?? date('Y-m-d H:i:s')),
'category' => sanitize_text($_POST['category'] ?? ''),
'tags' => sanitize_text($_POST['tags'] ?? ''),
'meta_title' => sanitize_text($_POST['meta_title'] ?? $title),
'meta_description' => sanitize_text($_POST['meta_description'] ?? ($_POST['excerpt'] ?? '')),
'meta_keywords' => sanitize_text($_POST['meta_keywords'] ?? ''),
'og_image' => sanitize_text($_POST['og_image'] ?? ($_POST['media_url'] ?? '')),
];
update_post($id, $data);
admin_log('update_post');
header('Location: /admin/posts.php');
exit;
}
}
include __DIR__ . '/includes/header.php';
?>
<section class="card admin-card">
<h2>Sửa bài viết</h2>
<?php if ($error): ?><p class="error"><?php echo e($error); ?></p><?php endif; ?>
<form method="post">
<input type="hidden" name="csrf" value="<?php echo e(csrf_token()); ?>">
<label>Tiêu đề</label>
<input type="text" name="title" value="<?php echo e($post['title']); ?>" required>
<label>Slug</label>
<input type="text" name="slug" value="<?php echo e($post['slug']); ?>" required>
<label>Mô tả ngắn</label>
<textarea name="excerpt" required><?php echo e($post['excerpt']); ?></textarea>
<label>Nội dung</label>
<textarea name="content" rows="8" required><?php echo e($post['content']); ?></textarea>
<label>Media URL (Telegram)</label>
<input type="text" name="media_url" value="<?php echo e($post['media_url']); ?>">
<label>Danh mục</label>
<input type="text" name="category" value="<?php echo e($post['category']); ?>">
<label>Tags</label>
<input type="text" name="tags" value="<?php echo e($post['tags']); ?>">
<label>Meta title</label>
<input type="text" name="meta_title" value="<?php echo e($post['meta_title'] ?? $post['title']); ?>">
<label>Meta description</label>
<input type="text" name="meta_description" value="<?php echo e($post['meta_description'] ?? $post['excerpt']); ?>">
<label>Từ khóa</label>
<input type="text" name="meta_keywords" value="<?php echo e($post['meta_keywords'] ?? ''); ?>">
<label>OG Image</label>
<input type="text" name="og_image" value="<?php echo e($post['og_image'] ?? ''); ?>">
<label>Ngày xuất bản</label>
<input type="text" name="published_at" value="<?php echo e($post['published_at']); ?>">
<label><input type="checkbox" name="is_public" value="1" <?php echo $post['is_public'] ? 'checked' : ''; ?>> Public</label>
<button class="btn" type="submit">Cập nhật</button>
</form>
</section>
<?php include __DIR__ . '/includes/footer.php'; ?>
35 changes: 35 additions & 0 deletions public_html/admin/posts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/posts.php';
$posts = fetch_posts(200, 0, false); // admin view top 200
include __DIR__ . '/includes/header.php';
?>
<section class="card admin-card">
<div class="flex-between">
<h2>Bài viết</h2>
<div>
<a class="btn" href="/admin/post_add.php">Thêm bài</a>
</div>
</div>
<table class="table">
<thead><tr><th>ID</th><th>Tiêu đề</th><th>Danh mục</th><th>Slug</th><th>Ngày</th><th>TT</th><th>Hành động</th></tr></thead>
<tbody>
<?php foreach ($posts as $p): ?>
<tr>
<td><?php echo (int)$p['id']; ?></td>
<td><?php echo e($p['title']); ?></td>
<td><?php echo e($p['category'] ?? ''); ?></td>
<td><?php echo e($p['slug']); ?></td>
<td><?php echo e($p['published_at']); ?></td>
<td><?php echo $p['is_public'] ? 'Public' : 'Ẩn'; ?></td>
<td>
<a class="btn" href="/admin/post_edit.php?id=<?php echo (int)$p['id']; ?>">Sửa</a>
<a class="btn" href="/admin/post_delete.php?id=<?php echo (int)$p['id']; ?>&csrf=<?php echo e(csrf_token()); ?>" onclick="return confirm('Xoá?');">Xoá</a>
<a class="btn" href="/admin/post_add.php?copy=<?php echo (int)$p['id']; ?>">Copy</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
<?php include __DIR__ . '/includes/footer.php'; ?>
Loading