-
Notifications
You must be signed in to change notification settings - Fork 0
Build shared-hosting news site with ad flow and themes #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| 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] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Tin tức PHP cho shared hosting (dataonline.vn) | ||
|
|
||
| ## Cấu trúc & upload | ||
| - Giữ nguyên thư mục `public_html/` và upload thẳng vào hosting dataonline.vn (đúng tên public_html). | ||
| - File cấu hình duy nhất: `app/config/config.php` (DB_HOST, DB_NAME, DB_USER, DB_PASS, BASE_URL, SESSION_SALT, CACHE_PATH, CACHE_TTL, TELEGRAM_BOT_TOKEN, TELEGRAM_ADMIN_CHAT_ID). | ||
| - Import `sql/database.sql` vào MySQL (có seed admin + bài mẫu). | ||
| - Đảm bảo `app/cache` có quyền ghi (777 nếu cần). | ||
| - Apache mod_rewrite bật sẵn: `.htaccess` map `/slug` → `post.php?slug=...` và bỏ qua `/admin|/api|/bots|/assets|/includes|/app|/sql`. | ||
|
|
||
| ## Thiết lập nhanh | ||
| 1) Import DB: `sql/database.sql`. | ||
| 2) Sửa `app/config/config.php` với thông tin DB + BASE_URL + SESSION_SALT (tùy chọn đổi CACHE_TTL). | ||
| 3) (Tuỳ chọn) Điền TELEGRAM_BOT_TOKEN + TELEGRAM_ADMIN_CHAT_ID để bật notify. | ||
| 4) Kiểm tra quyền ghi `app/cache/` và thư mục `assets/img` (upload logo/banner). | ||
|
|
||
| ## Đăng nhập admin | ||
| - URL: `BASE_URL/admin/login.php` | ||
| - Tài khoản mẫu: `admin / admin123` (đổi ngay bằng UPDATE DB hoặc `password_hash` PHP). | ||
|
|
||
| ## Acceptance test checklist | ||
| 1) Slug: mở `/tin-nong-hom-nay` đúng bài. | ||
| 2) Quảng cáo Shopee (ads_enabled=1, ad_link có): | ||
| - Từ trang chủ hoặc truy cập trực tiếp `/slug`: overlay hiện ngay. | ||
| - Click overlay/nút X/anywhere: 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, không delay. | ||
| 3) Ads tắt: ads_enabled=0 hoặc ad_link rỗng → không overlay. | ||
| 4) Chống click ảo: spam `/api/track.php` trong 30s không tăng vô hạn (rate limit + hash IP/UA + token phiên). | ||
| 5) Theme: đổi theme 1/2/3 trong admin/settings, frontend thay CSS tương ứng. | ||
| 6) Telegram notify: publish bài public → nhận tin “Bài mới”; ad_click tích lũy → gửi gộp tối thiểu mỗi 5 phút. | ||
| 7) Responsive: kiểm tra mobile/laptop, menu hamburger 3D, admin/login cân đối. | ||
|
|
||
| ## Lưu ý hiệu năng | ||
| - Cache file: settings (5 phút), home (60s), bài (60s, key theo slug+updated_at). | ||
| - Có thể đặt cache-control dài cho assets tĩnh. | ||
| - Không dùng .env, không cần composer, thuần PHP 7.4+/PDO. |
| 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'; ?> |
| 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| </main> | ||
| </body> | ||
| </html> |
| 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"> |
| 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> |
| 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'); | ||
| 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'; ?> |
| 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'); |
| 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]); | ||
| } | ||
|
Comment on lines
+4
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| header('Location: '.BASE_URL.'/admin/posts.php'); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| <?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 | ||
| ]); | ||
| if ($article['status'] !== 'public' && $_POST['status'] === 'public') { | ||
| require_once __DIR__ . '/../app/lib/telegram.php'; | ||
| sendTelegramMessage('Bài mới: '.$_POST['title'].' - '.BASE_URL.'/'.$slug); | ||
| } | ||
| 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'; ?> |
| 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'; ?> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| <?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']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) { | ||
| $finfo = finfo_open(FILEINFO_MIME_TYPE); | ||
| $mime = finfo_file($finfo, $_FILES['logo']['tmp_name']); | ||
| finfo_close($finfo); | ||
| $allowed = ['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp']; | ||
| if (isset($allowed[$mime]) && $_FILES['logo']['size'] <= 2*1024*1024) { | ||
| $ext = $allowed[$mime]; | ||
| $name = 'assets/img/logo_'.time().'.'.$ext; | ||
| move_uploaded_file($_FILES['logo']['tmp_name'], __DIR__.'/../'.$name); | ||
| $logoPath = $name; | ||
| } | ||
| } | ||
| if (!empty($_FILES['banner']['name']) && $_FILES['banner']['error'] === UPLOAD_ERR_OK) { | ||
| $finfo = finfo_open(FILEINFO_MIME_TYPE); | ||
| $mime = finfo_file($finfo, $_FILES['banner']['tmp_name']); | ||
| finfo_close($finfo); | ||
| $allowed = ['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp']; | ||
| if (isset($allowed[$mime]) && $_FILES['banner']['size'] <= 3*1024*1024) { | ||
| $ext = $allowed[$mime]; | ||
| $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'; ?> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logout handler calls
admin_logout()(which only runssession_destroy()) without ever starting the session. Withoutsession_start(), PHP ignores the destroy request and leaves the existingadmin_idsession cookie intact, so hitting/admin/logout.phpdoes not actually log the user out—subsequent admin pages still see the old session. Start the session (and ideally clear session data) before destroying it to ensure logout works.Useful? React with 👍 / 👎.