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
28 changes: 0 additions & 28 deletions README.md

This file was deleted.

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
RewriteBase /
RewriteCond %{REQUEST_URI} !^/(admin|api|bots|assets|includes|app|sql)/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([^/]+)/?$ post.php?slug=$1 [QSA,L]
33 changes: 33 additions & 0 deletions public_html/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Tin tức PHP cho shared hosting (dataonline.vn)

## Cấu trúc thư mục
- Upload toàn bộ thư mục `public_html` lên hosting (đặt đúng tên public_html).
- `app/config/config.php`: sửa thông tin DB, BASE_URL, SESSION_SALT, đường dẫn cache, token Telegram.
- `sql/database.sql`: import vào MySQL (có dữ liệu mẫu + tài khoản admin/admin123 thay đổi ngay).

## Cài đặt
1. Tạo database MySQL, import `sql/database.sql`.
2. Mở `app/config/config.php` và điền DB_HOST/DB_NAME/DB_USER/DB_PASS + BASE_URL.
3. Đảm bảo thư mục `app/cache` có quyền ghi.
4. Bật mod_rewrite trên Apache (dataonline.vn bật sẵn). `.htaccess` đã cấu hình slug `/slug` → `post.php?slug=...` và bỏ qua `/admin|/api|/bots|/assets|/includes|/app|/sql`.

## Đăng nhập admin
- URL: `BASE_URL/admin/login.php`
- Tài khoản mẫu: admin / admin123 (hãy đổi trong DB hoặc cập nhật mật khẩu bằng `password_hash`).

## Kiểm thử bắt buộc
- **Slug**: `/tin-nong-hom-nay` mở đúng bài.
- **Quảng cáo Shopee** (ads_enabled=1, ad_link có):
- Click bài từ trang chủ hoặc truy cập trực tiếp `/slug` → overlay hiện.
- Click overlay hoặc nút X: tab hiện tại chuyển ngay đến ad_link, đồng thời mở tab mới `/slug?ad=0` không quảng cáo.
- **Tắt quảng cáo**: ads_enabled=0 hoặc ad_link rỗng → không overlay.
- **Chống click ảo**: gửi nhiều lần `/api/track.php` trong 30s không tăng vô hạn (rate limit + token + hash IP/UA).
- **Theme**: đổi theme 1/2/3 trong admin/settings, frontend đổi CSS.
- **Telegram**: điền TELEGRAM_BOT_TOKEN + TELEGRAM_ADMIN_CHAT_ID rồi publish bài mới (status=public) hoặc có ad_click → Telegram nhận thông báo (gộp click, gửi tối thiểu mỗi 5 phút).
- **Responsive**: kiểm tra mobile/laptop, hamburger 3D mở menu.

## Lưu ý
- Không dùng .env, không cần composer. Thuần PHP/PDO.
- Cache file: settings (5 phút), home (60s), bài viết (60s).
- Assets đã thêm lazy-load (iframe/JS), nên đặt header cache-control dài hạn trên hosting nếu muốn.
- Nếu cần webhook Telegram, trỏ `bots/telegram/webhook.php` và bật HTTPS.
14 changes: 14 additions & 0 deletions public_html/admin/dashboard.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/db.php';
include __DIR__ . '/includes/header.php';
$stats = db()->query('SELECT event_type, COUNT(*) as c FROM events GROUP BY event_type')->fetchAll();
$byType = []; foreach ($stats as $s){$byType[$s['event_type']] = $s['c'];}
?>
<div class="card">
<h3>Tổng quan</h3>
<p>Page views: <?= $byType['page_view'] ?? 0 ?></p>
<p>Post views: <?= $byType['post_view'] ?? 0 ?></p>
<p>Ad clicks: <?= $byType['ad_click'] ?? 0 ?></p>
</div>
<?php include __DIR__ . '/includes/footer.php'; ?>
7 changes: 7 additions & 0 deletions public_html/admin/includes/auth_check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php
session_start();
require_once __DIR__ . '/../../app/config/config.php';
if (empty($_SESSION['admin_id'])) {
header('Location: ' . BASE_URL . '/admin/login.php');
exit;
}
3 changes: 3 additions & 0 deletions public_html/admin/includes/footer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
</main>
</body>
</html>
16 changes: 16 additions & 0 deletions public_html/admin/includes/header.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php require_once __DIR__ . '/../../app/config/config.php'; ?>
<!DOCTYPE html>
<html lang="vi">
<head><meta charset="UTF-8"><title>Admin</title><link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/admin.css"></head>
<body>
<header class="admin-header">
<div>Admin Panel</div>
<nav>
<a href="<?= BASE_URL ?>/admin/dashboard.php">Dashboard</a>
<a href="<?= BASE_URL ?>/admin/posts.php">Bài viết</a>
<a href="<?= BASE_URL ?>/admin/settings.php">Cài đặt</a>
<a href="<?= BASE_URL ?>/admin/stats.php">Thống kê</a>
<a href="<?= BASE_URL ?>/admin/logout.php">Logout</a>
</nav>
</header>
<main class="admin-main">
28 changes: 28 additions & 0 deletions public_html/admin/login.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
session_start();
require_once __DIR__ . '/../app/lib/auth.php';
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
require_once __DIR__ . '/../app/lib/csrf.php';
csrf_verify();
if (admin_login($_POST['username'], $_POST['password'])) {
header('Location: ' . BASE_URL . '/admin/dashboard.php');
exit;
}
$message = 'Sai tài khoản hoặc mật khẩu';
}
?>
<!DOCTYPE html>
<html lang="vi">
<head><meta charset="UTF-8"><title>Đăng nhập</title><link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/admin.css"></head>
<body class="login-page">
<form method="post" class="login-form">
<h2>Admin Login</h2>
<?php if ($message): ?><div class="alert"><?= htmlspecialchars($message) ?></div><?php endif; ?>
<?php if (function_exists('csrf_field')) echo csrf_field(); ?>
<input type="text" name="username" placeholder="Tên đăng nhập" required>
<input type="password" name="password" placeholder="Mật khẩu" required>
<button type="submit">Đăng nhập</button>
</form>
</body>
</html>
4 changes: 4 additions & 0 deletions public_html/admin/logout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
require_once __DIR__ . '/../app/lib/auth.php';
admin_logout();
header('Location: ' . BASE_URL . '/admin/login.php');
Comment on lines +1 to +4
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 Logout endpoint never invalidates the session

The logout handler calls admin_logout() without ever calling session_start(), while admin_logout() only invokes session_destroy(). PHP ignores session_destroy() when no session is active, so this endpoint redirects without clearing the session cookie or data and the admin remains logged in. Start the session before destroying it (or call session_start() inside admin_logout()) so logout actually ends the session.

Useful? React with 👍 / 👎.

45 changes: 45 additions & 0 deletions public_html/admin/post_add.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/db.php';
require_once __DIR__ . '/../app/lib/slugify.php';
require_once __DIR__ . '/../app/lib/csrf.php';
require_once __DIR__ . '/../app/lib/telegram.php';
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$slug = $_POST['slug'] ?: slugify($_POST['title']);
$stmt = db()->prepare('INSERT INTO articles (slug,title,excerpt,content,telegram_media,meta_title,meta_description,meta_keywords,status) VALUES (:slug,:title,:excerpt,:content,:media,:mt,:md,:mk,:status)');
$stmt->execute([
':slug'=>$slug,
':title'=>$_POST['title'],
':excerpt'=>$_POST['excerpt'],
':content'=>$_POST['content'],
':media'=>$_POST['telegram_media'],
':mt'=>$_POST['meta_title'],
':md'=>$_POST['meta_description'],
':mk'=>$_POST['meta_keywords'],
':status'=>$_POST['status']
]);
if ($_POST['status']==='public') {
sendTelegramMessage('Bài mới: '.$_POST['title'].' - '.BASE_URL.'/'.$slug);
}
header('Location: '.BASE_URL.'/admin/posts.php');
exit;
}
include __DIR__ . '/includes/header.php';
?>
<h3>Thêm bài</h3>
<form method="post">
<?= csrf_field() ?>
<label>Tiêu đề<input type="text" name="title" required></label>
<label>Slug<input type="text" name="slug"></label>
<label>Tóm tắt<textarea name="excerpt"></textarea></label>
<label>Nội dung<textarea name="content" rows="10"></textarea></label>
<label>Telegram media (mỗi dòng một link)<textarea name="telegram_media"></textarea></label>
<label>Meta title<input type="text" name="meta_title"></label>
<label>Meta description<input type="text" name="meta_description"></label>
<label>Meta keywords<input type="text" name="meta_keywords"></label>
<label>Trạng thái<select name="status"><option value="draft">Draft</option><option value="public">Public</option></select></label>
<button class="btn" type="submit">Lưu</button>
</form>
<?php include __DIR__ . '/includes/footer.php'; ?>
14 changes: 14 additions & 0 deletions public_html/admin/post_copy.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/db.php';
require_once __DIR__ . '/../app/lib/slugify.php';
$id = (int)($_GET['id'] ?? 0);
$stmt = db()->prepare('SELECT * FROM articles WHERE id=:id');
$stmt->execute([':id'=>$id]);
$a = $stmt->fetch();
if($a){
$newSlug = slugify($a['slug'].'-copy-'.rand(100,999));
$ins = db()->prepare('INSERT INTO articles (slug,title,excerpt,content,telegram_media,meta_title,meta_description,meta_keywords,status) VALUES (:slug,:title,:excerpt,:content,:media,:mt,:md,:mk,:status)');
$ins->execute([':slug'=>$newSlug,':title'=>$a['title'].' (copy)',':excerpt'=>$a['excerpt'],':content'=>$a['content'],':media'=>$a['telegram_media'],':mt'=>$a['meta_title'],':md'=>$a['meta_description'],':mk'=>$a['meta_keywords'],':status'=>'draft']);
}
header('Location: '.BASE_URL.'/admin/posts.php');
9 changes: 9 additions & 0 deletions public_html/admin/post_delete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/db.php';
$id = (int)($_GET['id'] ?? 0);
if ($id) {
$stmt = db()->prepare('DELETE FROM articles WHERE id=:id');
$stmt->execute([':id'=>$id]);
}
header('Location: '.BASE_URL.'/admin/posts.php');
37 changes: 37 additions & 0 deletions public_html/admin/post_edit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/db.php';
require_once __DIR__ . '/../app/lib/slugify.php';
require_once __DIR__ . '/../app/lib/csrf.php';
$id = (int)($_GET['id'] ?? 0);
$stmt = db()->prepare('SELECT * FROM articles WHERE id=:id');
$stmt->execute([':id'=>$id]);
$article = $stmt->fetch();
if(!$article){exit('Not found');}
if($_SERVER['REQUEST_METHOD']==='POST'){
csrf_verify();
$slug = $_POST['slug'] ?: slugify($_POST['title']);
$u = db()->prepare('UPDATE articles SET slug=:slug,title=:title,excerpt=:excerpt,content=:content,telegram_media=:media,meta_title=:mt,meta_description=:md,meta_keywords=:mk,status=:status WHERE id=:id');
$u->execute([
':slug'=>$slug,':title'=>$_POST['title'],':excerpt'=>$_POST['excerpt'],':content'=>$_POST['content'],':media'=>$_POST['telegram_media'],':mt'=>$_POST['meta_title'],':md'=>$_POST['meta_description'],':mk'=>$_POST['meta_keywords'],':status'=>$_POST['status'],':id'=>$id
]);
header('Location: '.BASE_URL.'/admin/posts.php');
exit;
}
include __DIR__ . '/includes/header.php';
?>
<h3>Sửa bài</h3>
<form method="post">
<?= csrf_field() ?>
<label>Tiêu đề<input type="text" name="title" value="<?= htmlspecialchars($article['title']) ?>" required></label>
<label>Slug<input type="text" name="slug" value="<?= htmlspecialchars($article['slug']) ?>"></label>
<label>Tóm tắt<textarea name="excerpt"><?= htmlspecialchars($article['excerpt']) ?></textarea></label>
<label>Nội dung<textarea name="content" rows="10"><?= htmlspecialchars($article['content']) ?></textarea></label>
<label>Telegram media<textarea name="telegram_media"><?= htmlspecialchars($article['telegram_media']) ?></textarea></label>
<label>Meta title<input type="text" name="meta_title" value="<?= htmlspecialchars($article['meta_title']) ?>"></label>
<label>Meta description<input type="text" name="meta_description" value="<?= htmlspecialchars($article['meta_description']) ?>"></label>
<label>Meta keywords<input type="text" name="meta_keywords" value="<?= htmlspecialchars($article['meta_keywords']) ?>"></label>
<label>Trạng thái<select name="status"><option value="draft" <?= $article['status']==='draft'?'selected':'' ?>>Draft</option><option value="public" <?= $article['status']==='public'?'selected':'' ?>>Public</option></select></label>
<button class="btn" type="submit">Lưu</button>
</form>
<?php include __DIR__ . '/includes/footer.php'; ?>
23 changes: 23 additions & 0 deletions public_html/admin/posts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/db.php';
include __DIR__ . '/includes/header.php';
$articles = db()->query('SELECT * FROM articles ORDER BY created_at DESC')->fetchAll();
?>
<div class="actions"><a class="btn" href="<?= BASE_URL ?>/admin/post_add.php">Thêm bài</a></div>
<table>
<tr><th>Tiêu đề</th><th>Slug</th><th>Trạng thái</th><th>Hành động</th></tr>
<?php foreach ($articles as $a): ?>
<tr>
<td><?= htmlspecialchars($a['title']) ?></td>
<td><?= htmlspecialchars($a['slug']) ?></td>
<td><?= $a['status'] ?></td>
<td>
<a href="<?= BASE_URL ?>/admin/post_edit.php?id=<?= $a['id'] ?>">Sửa</a> |
<a href="<?= BASE_URL ?>/admin/post_copy.php?id=<?= $a['id'] ?>">Copy</a> |
<a href="<?= BASE_URL ?>/admin/post_delete.php?id=<?= $a['id'] ?>" onclick="return confirm('Xóa?')">Xóa</a>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php include __DIR__ . '/includes/footer.php'; ?>
50 changes: 50 additions & 0 deletions public_html/admin/settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/db.php';
require_once __DIR__ . '/../app/lib/csrf.php';
$settings = db()->query('SELECT * FROM site_settings LIMIT 1')->fetch();
if($_SERVER['REQUEST_METHOD']==='POST'){
csrf_verify();
$logoPath = $settings['logo_path'];
$bannerPath = $settings['banner_path'];
if (!empty($_FILES['logo']['name'])) {
$ext = pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION);
$name = 'assets/img/logo_'.time().'.'.$ext;
move_uploaded_file($_FILES['logo']['tmp_name'], __DIR__.'/../'.$name);
$logoPath = $name;
Comment on lines +10 to +14
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 File uploads accept arbitrary executable types

The settings form saves uploaded logo/banner files directly to public_html/assets/img using the user-supplied extension and without any MIME/type whitelist or path checks. An authenticated admin can therefore upload a PHP file (or other executable) and have it written to a web-accessible location, leading to straightforward remote code execution. Validate the file type and restrict extensions/paths before moving the upload.

Useful? React with 👍 / 👎.

}
if (!empty($_FILES['banner']['name'])) {
$ext = pathinfo($_FILES['banner']['name'], PATHINFO_EXTENSION);
$name = 'assets/img/banner_'.time().'.'.$ext;
move_uploaded_file($_FILES['banner']['tmp_name'], __DIR__.'/../'.$name);
$bannerPath = $name;
}
$upd = db()->prepare('UPDATE site_settings SET site_name=:sn, site_description=:sd, logo_path=:logo, banner_path=:banner, theme=:theme, ads_enabled=:ads, ad_link=:link, ad_title=:title, ad_body=:body, updated_at=NOW() WHERE id=1');
$upd->execute([
':sn'=>$_POST['site_name'], ':sd'=>$_POST['site_description'], ':logo'=>$logoPath, ':banner'=>$bannerPath,
':theme'=>$_POST['theme'], ':ads'=>!empty($_POST['ads_enabled'])?1:0, ':link'=>$_POST['ad_link'], ':title'=>$_POST['ad_title'], ':body'=>$_POST['ad_body']
]);
header('Location: '.BASE_URL.'/admin/settings.php');
exit;
}
include __DIR__ . '/includes/header.php';
?>
<h3>Cài đặt</h3>
<form method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<label>Tên site<input type="text" name="site_name" value="<?= htmlspecialchars($settings['site_name']) ?>"></label>
<label>Mô tả<input type="text" name="site_description" value="<?= htmlspecialchars($settings['site_description']) ?>"></label>
<label>Logo<input type="file" name="logo"></label>
<label>Banner<input type="file" name="banner"></label>
<label>Theme<select name="theme">
<option value="1" <?= $settings['theme']==='1'?'selected':'' ?>>Theme 1</option>
<option value="2" <?= $settings['theme']==='2'?'selected':'' ?>>Theme 2</option>
<option value="3" <?= $settings['theme']==='3'?'selected':'' ?>>Theme 3</option>
</select></label>
<label><input type="checkbox" name="ads_enabled" value="1" <?= $settings['ads_enabled']?'checked':'' ?>> Bật quảng cáo</label>
<label>Link Shopee<input type="url" name="ad_link" value="<?= htmlspecialchars($settings['ad_link']) ?>"></label>
<label>Tiêu đề quảng cáo<input type="text" name="ad_title" value="<?= htmlspecialchars($settings['ad_title']) ?>"></label>
<label>Nội dung quảng cáo<textarea name="ad_body"><?= htmlspecialchars($settings['ad_body']) ?></textarea></label>
<button class="btn" type="submit">Lưu</button>
</form>
<?php include __DIR__ . '/includes/footer.php'; ?>
28 changes: 28 additions & 0 deletions public_html/admin/stats.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/includes/auth_check.php';
require_once __DIR__ . '/../app/lib/db.php';
require_once __DIR__ . '/../app/lib/csrf.php';

if($_SERVER['REQUEST_METHOD']==='POST'){
csrf_verify();
if(isset($_POST['reset_all'])){
db()->exec('TRUNCATE TABLE events');
}
header('Location: '.BASE_URL.'/admin/stats.php');
exit;
}
$daily = db()->query('SELECT DATE(created_at) d, event_type, COUNT(*) c FROM events GROUP BY d, event_type ORDER BY d DESC LIMIT 30')->fetchAll();
include __DIR__ . '/includes/header.php';
?>
<h3>Thống kê</h3>
<table>
<tr><th>Ngày</th><th>Loại</th><th>Số</th></tr>
<?php foreach($daily as $row): ?>
<tr><td><?= $row['d'] ?></td><td><?= $row['event_type'] ?></td><td><?= $row['c'] ?></td></tr>
<?php endforeach; ?>
</table>
<form method="post" onsubmit="return confirm('Reset toàn bộ?')">
<?= csrf_field() ?>
<button class="btn" name="reset_all" value="1">Reset thống kê</button>
</form>
<?php include __DIR__ . '/includes/footer.php'; ?>
5 changes: 5 additions & 0 deletions public_html/api/stats.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/../app/lib/db.php';
header('Content-Type: application/json');
$totals = db()->query('SELECT event_type, COUNT(*) c FROM events GROUP BY event_type')->fetchAll();
echo json_encode($totals);
16 changes: 16 additions & 0 deletions public_html/api/track.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
session_start();
require_once __DIR__ . '/../app/lib/db.php';
require_once __DIR__ . '/../app/lib/rate_limit.php';
header('Content-Type: application/json');
$event = $_POST['event'] ?? $_GET['event'] ?? '';
$slug = $_POST['slug'] ?? $_GET['slug'] ?? null;
$token = $_POST['token'] ?? $_GET['token'] ?? '';
if (!$event || !$token || ($event==='post_view' && !$slug)) { http_response_code(400); echo json_encode(['ok'=>false]); exit; }
if (!hash_equals($_SESSION['track_token'] ?? '', $token)) { http_response_code(403); echo json_encode(['ok'=>false]); exit; }
if (!in_array($event, ['page_view','post_view','ad_click'])) { http_response_code(400); echo json_encode(['ok'=>false]); exit; }
$window = $event==='ad_click' ? 120 : 30;
if (!rate_limit($event, $slug, $window)) { echo json_encode(['ok'=>false,'rate_limited'=>true]); exit; }
log_event($event, $slug, $token);
if ($event==='ad_click') { require_once __DIR__ . '/../app/lib/telegram.php'; queue_click_notify(1); }
echo json_encode(['ok'=>true]);
14 changes: 14 additions & 0 deletions public_html/app/config/config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
// Cấu hình hệ thống (không dùng .env)
define('DB_HOST', 'localhost');
define('DB_NAME', 'news_site');
define('DB_USER', 'db_user');
define('DB_PASS', 'db_pass');

define('BASE_URL', 'https://yourdomain.com');
define('SESSION_SALT', 'change_me_salt');
define('CACHE_PATH', __DIR__ . '/../cache');
define('CACHE_TTL', 300); // giây

define('TELEGRAM_BOT_TOKEN', '');
define('TELEGRAM_ADMIN_CHAT_ID', '');
Loading