diff --git a/public_html/.htaccess b/public_html/.htaccess new file mode 100644 index 0000000..bf55d4c --- /dev/null +++ b/public_html/.htaccess @@ -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] diff --git a/public_html/README.md b/public_html/README.md new file mode 100644 index 0000000..c29f778 --- /dev/null +++ b/public_html/README.md @@ -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`. diff --git a/public_html/admin/dashboard.php b/public_html/admin/dashboard.php new file mode 100644 index 0000000..e367db5 --- /dev/null +++ b/public_html/admin/dashboard.php @@ -0,0 +1,27 @@ +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'; +?> +
+

Chào,

+ Xem trang +
+
+
Tổng bài viết:
+
Bài public:
+
Lượt xem bài:
+
Click quảng cáo:
+
+ diff --git a/public_html/admin/includes/auth_check.php b/public_html/admin/includes/auth_check.php new file mode 100644 index 0000000..fc8d2ed --- /dev/null +++ b/public_html/admin/includes/auth_check.php @@ -0,0 +1,4 @@ + + + + diff --git a/public_html/admin/includes/header.php b/public_html/admin/includes/header.php new file mode 100644 index 0000000..372b158 --- /dev/null +++ b/public_html/admin/includes/header.php @@ -0,0 +1,23 @@ + + + + + + + Quản trị + + + +
+ +
diff --git a/public_html/admin/login.php b/public_html/admin/login.php new file mode 100644 index 0000000..ea40463 --- /dev/null +++ b/public_html/admin/login.php @@ -0,0 +1,45 @@ + + + + + + + Đăng nhập quản trị + + + +
+

Đăng nhập

+
+
+ + + + + + +
+

Tài khoản mặc định: admin / admin567

+
+ + diff --git a/public_html/admin/logout.php b/public_html/admin/logout.php new file mode 100644 index 0000000..de8fca6 --- /dev/null +++ b/public_html/admin/logout.php @@ -0,0 +1,6 @@ +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'; +?> +

Thêm bài

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ diff --git a/public_html/admin/post_copy.php b/public_html/admin/post_copy.php new file mode 100644 index 0000000..c243d80 --- /dev/null +++ b/public_html/admin/post_copy.php @@ -0,0 +1,32 @@ + + + + + + Copy link + + + +
+

Link bài viết:

+ + +

Dán link gửi cho bạn đọc.

+
+ + + diff --git a/public_html/admin/post_delete.php b/public_html/admin/post_delete.php new file mode 100644 index 0000000..3e0e596 --- /dev/null +++ b/public_html/admin/post_delete.php @@ -0,0 +1,12 @@ +prepare('DELETE FROM articles WHERE id = :id'); +$stmt->execute([':id' => $id]); +redirect('/admin/posts.php'); diff --git a/public_html/admin/post_edit.php b/public_html/admin/post_edit.php new file mode 100644 index 0000000..9c1fdb3 --- /dev/null +++ b/public_html/admin/post_edit.php @@ -0,0 +1,80 @@ +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'; +?> +

Sửa bài

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ diff --git a/public_html/admin/posts.php b/public_html/admin/posts.php new file mode 100644 index 0000000..4164ce9 --- /dev/null +++ b/public_html/admin/posts.php @@ -0,0 +1,62 @@ +prepare($query); +$stmt->execute($params); +$posts = $stmt->fetchAll(); +include __DIR__ . '/includes/header.php'; +?> +
+

Bài viết

+ +
+
+ + + + + + + + + + + + + + +
Tiêu đềSlugTrạng tháiHành động
+ Sửa + Xóa + +
+
+ + diff --git a/public_html/admin/settings.php b/public_html/admin/settings.php new file mode 100644 index 0000000..74a8901 --- /dev/null +++ b/public_html/admin/settings.php @@ -0,0 +1,113 @@ +query('SELECT * FROM site_settings LIMIT 1'); +$settings = $stmt->fetch() ?: [ + 'site_name' => 'Tin nhanh', + 'site_description' => '', + 'theme' => 1, + 'logo' => '', + 'banner' => '', + 'ads_enabled' => 0, + 'ad_link' => '', + 'ad_title' => '', + 'ad_body' => '' +]; + +$message = ''; +if (is_post()) { + if (!verify_csrf($_POST['csrf'] ?? '')) { + $message = 'Token không hợp lệ'; + } else { + $site_name = trim($_POST['site_name'] ?? ''); + $site_description = trim($_POST['site_description'] ?? ''); + $theme = (int)($_POST['theme'] ?? 1); + $ads_enabled = isset($_POST['ads_enabled']) ? 1 : 0; + $ad_link = trim($_POST['ad_link'] ?? ''); + $ad_title = trim($_POST['ad_title'] ?? ''); + $ad_body = trim($_POST['ad_body'] ?? ''); + $logoPath = $settings['logo'] ?? ''; + $bannerPath = $settings['banner'] ?? ''; + + if (!empty($_FILES['logo']['name'])) { + $logoPath = '/assets/img/logo_' . time() . '_' . basename($_FILES['logo']['name']); + move_uploaded_file($_FILES['logo']['tmp_name'], __DIR__ . '/../' . ltrim($logoPath, '/')); + } + if (!empty($_FILES['banner']['name'])) { + $bannerPath = '/assets/img/banner_' . time() . '_' . basename($_FILES['banner']['name']); + move_uploaded_file($_FILES['banner']['tmp_name'], __DIR__ . '/../' . ltrim($bannerPath, '/')); + } + + if (isset($settings['id'])) { + $stmtU = $pdo->prepare('UPDATE site_settings SET site_name=:sn, site_description=:sd, theme=:t, logo=:logo, banner=:banner, ads_enabled=:ae, ad_link=:al, ad_title=:ati, ad_body=:abo WHERE id=:id'); + $stmtU->execute([ + ':sn' => $site_name, + ':sd' => $site_description, + ':t' => $theme, + ':logo' => $logoPath, + ':banner' => $bannerPath, + ':ae' => $ads_enabled, + ':al' => $ad_link, + ':ati' => $ad_title, + ':abo' => $ad_body, + ':id' => $settings['id'], + ]); + } else { + $stmtI = $pdo->prepare('INSERT INTO site_settings(site_name, site_description, theme, logo, banner, ads_enabled, ad_link, ad_title, ad_body) VALUES(:sn,:sd,:t,:logo,:banner,:ae,:al,:ati,:abo)'); + $stmtI->execute([ + ':sn' => $site_name, + ':sd' => $site_description, + ':t' => $theme, + ':logo' => $logoPath, + ':banner' => $bannerPath, + ':ae' => $ads_enabled, + ':al' => $ad_link, + ':ati' => $ad_title, + ':abo' => $ad_body, + ]); + } + cache_set('site_settings', null, 1); + $message = 'Đã lưu cài đặt'; + $settings = array_merge($settings, compact('site_name','site_description','theme','logoPath','bannerPath','ads_enabled','ad_link','ad_title','ad_body')); + $settings['logo'] = $logoPath; + $settings['banner'] = $bannerPath; + } +} +include __DIR__ . '/includes/header.php'; +?> +

Cài đặt

+
+
+ + + + + + + + + logo + + + banner + + + + + + + + + + +
+ diff --git a/public_html/admin/stats.php b/public_html/admin/stats.php new file mode 100644 index 0000000..d95bc83 --- /dev/null +++ b/public_html/admin/stats.php @@ -0,0 +1,78 @@ +exec("DELETE FROM events WHERE event_type='ad_click'"); + } elseif ($type === 'post_view') { + $pdo->exec("DELETE FROM events WHERE event_type='post_view'"); + } else { + $pdo->exec('TRUNCATE TABLE events'); + } + $message = 'Đã reset thống kê'; + } +} + +$todayViews = (int)$pdo->query("SELECT COUNT(*) FROM events WHERE event_type='post_view' AND DATE(created_at)=CURDATE()")->fetchColumn(); +$monthViews = (int)$pdo->query("SELECT COUNT(*) FROM events WHERE event_type='post_view' AND MONTH(created_at)=MONTH(CURDATE()) AND YEAR(created_at)=YEAR(CURDATE())")->fetchColumn(); +$adClicks = (int)$pdo->query("SELECT COUNT(*) FROM events WHERE event_type='ad_click'")->fetchColumn(); +$daily = $pdo->query("SELECT DATE(created_at) as d, COUNT(*) as c FROM events WHERE event_type='post_view' GROUP BY DATE(created_at) ORDER BY d DESC LIMIT 15")->fetchAll(); +$monthly = $pdo->query("SELECT DATE_FORMAT(created_at,'%Y-%m') as m, COUNT(*) as c FROM events WHERE event_type='post_view' GROUP BY DATE_FORMAT(created_at,'%Y-%m') ORDER BY m DESC LIMIT 12")->fetchAll(); +$topStmt = $pdo->query("SELECT slug, COUNT(*) as c FROM events WHERE event_type='post_view' GROUP BY slug ORDER BY c DESC LIMIT 5"); +$topPosts = $topStmt->fetchAll(); +include __DIR__ . '/includes/header.php'; +?> +

Thống kê

+
+
+
View hôm nay:
+
View tháng:
+
Ad click:
+
+
+

Top bài

+
    + +
  • - lượt
  • + +
+
+
+

Theo ngày

+
    + +
  • : view
  • + +
+
+
+

Theo tháng

+
    + +
  • : view
  • + +
+
+
+

Reset thống kê

+
+ + + +
+
+ diff --git a/public_html/api/stats.php b/public_html/api/stats.php new file mode 100644 index 0000000..701396f --- /dev/null +++ b/public_html/api/stats.php @@ -0,0 +1,8 @@ +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(); +$today = (int)$pdo->query("SELECT COUNT(*) FROM events WHERE DATE(created_at)=CURDATE()")->fetchColumn(); +echo json_encode(['views' => $views, 'ad_clicks' => $adClicks, 'today' => $today]); diff --git a/public_html/api/track.php b/public_html/api/track.php new file mode 100644 index 0000000..4c7d4bf --- /dev/null +++ b/public_html/api/track.php @@ -0,0 +1,38 @@ + $_GET['event'] ?? '', + 'slug' => $_GET['slug'] ?? '', + 'token' => $_GET['token'] ?? '', + ]; +} + +$event = $data['event'] ?? ''; +$slug = $data['slug'] ?? ''; +$token = $data['token'] ?? ''; + +if (!$event || !$slug || !$token || !verify_csrf($token)) { + http_response_code(400); + echo 'invalid'; + exit; +} + +if (rate_limited($event, $slug, $token)) { + echo 'rate_limited'; + exit; +} + +if ($event === 'ad_click') { + notifyAdClick($slug); +} + +echo 'ok'; diff --git a/public_html/assets/css/admin.css b/public_html/assets/css/admin.css new file mode 100644 index 0000000..1894101 --- /dev/null +++ b/public_html/assets/css/admin.css @@ -0,0 +1,16 @@ +body { font-family: 'Inter', 'Segoe UI', sans-serif; margin:0; background:#0b1021; color:#e2e8f0; } +.admin-shell { display:flex; min-height:100vh; } +.sidebar { width:240px; background:#0f172a; padding:20px; position:sticky; top:0; height:100vh; } +.sidebar h2 { margin:0 0 12px; font-size:18px; } +.sidebar a { display:block; color:#e2e8f0; padding:10px 12px; border-radius:10px; text-decoration:none; margin-bottom:6px; transition:.2s; } +.sidebar a:hover, .sidebar a.active { background:#11182f; } +.main { flex:1; padding:24px; } +.card { background:#11182f; border-radius:12px; padding:16px; box-shadow:0 10px 30px rgba(0,0,0,0.3); } +.table { width:100%; border-collapse:collapse; } +.table th, .table td { padding:10px; text-align:left; border-bottom:1px solid rgba(255,255,255,0.05); } +.btn { background:#38bdf8; color:#0b1021; padding:10px 14px; border-radius:10px; border:none; cursor:pointer; font-weight:700; text-decoration:none; display:inline-block; } +input, textarea, select { width:100%; padding:10px; border-radius:8px; border:1px solid rgba(255,255,255,0.1); background:#0b1021; color:#e2e8f0; } +form { display:grid; gap:12px; } +.alert { padding:12px; border-radius:10px; background:rgba(56,189,248,0.1); color:#67e8f9; } +.actions { display:flex; gap:8px; flex-wrap:wrap; } +.topbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:18px; } diff --git a/public_html/assets/css/base.css b/public_html/assets/css/base.css new file mode 100644 index 0000000..29dd852 --- /dev/null +++ b/public_html/assets/css/base.css @@ -0,0 +1,52 @@ +:root { + --primary: #e63946; + --dark: #1d1f27; + --muted: #7a7d87; + --radius: 12px; +} +* { box-sizing: border-box; } +body { font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; margin:0; background:#f6f7fb; color:#1f2933; } +a { color: inherit; text-decoration:none; } +img { max-width:100%; display:block; } +.container { width: min(1100px, 95%); margin: 0 auto; } +.btn { display:inline-block; padding:10px 16px; border-radius: var(--radius); background: var(--primary); color:#fff; font-weight:600; } +.grid { display:grid; gap:18px; } +.card { background:#fff; border-radius:var(--radius); padding:18px; box-shadow:0 10px 35px rgba(0,0,0,0.06); } +header.site-header { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:20px 0; } +.logo { display:flex; align-items:center; gap:12px; font-weight:800; font-size:20px; text-transform:uppercase; letter-spacing:1px; } +.banner img { border-radius: var(--radius); } +.hero-title { font-size: 30px; margin:0 0 8px; } +.hero-desc { color: var(--muted); margin:0 0 18px; } +.posts-list { display:grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap:18px; } +.post-card h3 { margin:0 0 8px; font-size:20px; } +.post-card p { color: var(--muted); margin:0; } +.footer { text-align:center; color: var(--muted); padding:30px 0; font-size:14px; } +.error-page { display:flex; min-height:100vh; align-items:center; justify-content:center; } +.error-container { text-align:center; max-width:480px; padding:24px; background:#fff; border-radius:var(--radius); box-shadow:0 10px 30px rgba(0,0,0,0.07); } + +.hamburger { width:46px; height:46px; border-radius:50%; background:#fff; display:grid; place-items:center; cursor:pointer; box-shadow:0 10px 30px rgba(0,0,0,0.12); position:relative; } +.hamburger span { position:absolute; width:22px; height:2px; background:#111; transition:.3s; } +.hamburger span:nth-child(1) { transform: translateY(-7px); } +.hamburger span:nth-child(3) { transform: translateY(7px); } +.hamburger.active span:nth-child(1) { transform: translateY(0) rotate(45deg); } +.hamburger.active span:nth-child(2) { opacity:0; } +.hamburger.active span:nth-child(3) { transform: translateY(0) rotate(-45deg); } +.mobile-menu { position:fixed; inset:0; background:rgba(0,0,0,0.6); backdrop-filter:blur(4px); display:none; align-items:center; justify-content:center; z-index:20; } +.mobile-menu.active { display:flex; } +.mobile-menu .panel { width:90%; max-width:320px; background:#fff; border-radius:20px; padding:30px; transform:rotateY(90deg); transform-origin:right; transition:.4s; } +.mobile-menu.active .panel { transform:rotateY(0deg); } +.overlay-ad { position:fixed; inset:0; background:rgba(0,0,0,0.75); color:#fff; display:flex; align-items:center; justify-content:center; z-index:40; } +.overlay-ad .box { background:#111; border-radius:20px; padding:24px; max-width:360px; width:90%; text-align:center; box-shadow:0 15px 50px rgba(0,0,0,0.35); } +.overlay-ad button { background:#fff; color:#111; border:none; padding:10px 16px; border-radius:12px; font-weight:700; cursor:pointer; margin-top:12px; } +.theme-1 body { background:#f6f7fb; } +.theme-2 body { background:#0f172a; color:#e2e8f0; } +.theme-3 body { background:#fdf6e3; color:#2f2a1d; } + +@media (max-width: 768px) { + header.site-header { flex-direction: column; align-items: flex-start; } + .logo { font-size:18px; } + .hero-title { font-size:24px; } + .card { padding:14px; } + .posts-list { grid-template-columns: 1fr; } + .container { width: 92%; } +} diff --git a/public_html/assets/css/theme1.css b/public_html/assets/css/theme1.css new file mode 100644 index 0000000..9ba00d9 --- /dev/null +++ b/public_html/assets/css/theme1.css @@ -0,0 +1,3 @@ +body { background:#f6f7fb; color:#1f2933; } +.card { background:#fff; } +.btn { background: linear-gradient(120deg, #ff6b6b, #f06595); } diff --git a/public_html/assets/css/theme2.css b/public_html/assets/css/theme2.css new file mode 100644 index 0000000..f52b91f --- /dev/null +++ b/public_html/assets/css/theme2.css @@ -0,0 +1,4 @@ +body { background:#0f172a; color:#e2e8f0; } +.card { background:#11182f; } +.btn { background: linear-gradient(120deg, #38bdf8, #6366f1); color:#0b1021; } +a { color:#e2e8f0; } diff --git a/public_html/assets/css/theme3.css b/public_html/assets/css/theme3.css new file mode 100644 index 0000000..44483e2 --- /dev/null +++ b/public_html/assets/css/theme3.css @@ -0,0 +1,4 @@ +body { background:#fdf6e3; color:#2f2a1d; } +.card { background:#fff7d6; } +.btn { background: linear-gradient(120deg, #f6d365, #fda085); color:#2f2a1d; } +a { color:#2f2a1d; } diff --git a/public_html/assets/js/adflow.js b/public_html/assets/js/adflow.js new file mode 100644 index 0000000..5f879d2 --- /dev/null +++ b/public_html/assets/js/adflow.js @@ -0,0 +1,48 @@ +(function () { + 'use strict'; + + var S = window.__SITE || {}; + + function truthy(v){ return v === 1 || v === true || v === '1'; } + + if (!truthy(S.adsEnabled) || !S.adLink || S.bypass === true) return; + + function openFlow(slug){ + if (typeof window.trackEvent === 'function' && S.token) { + try { window.trackEvent('ad_click', slug || '', S.token); } catch(e){} + } + try { + window.open('/' + slug + '?ad=0', '_blank', 'noopener'); + } catch(e){} + window.location.href = S.adLink; + } + + function buildOverlay(){ + var o = document.createElement('div'); + o.id = 'adOverlay'; + o.style.cssText = + 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.75);display:flex;align-items:center;justify-content:center;'; + var b = document.createElement('div'); + b.style.cssText = + 'background:#fff;border-radius:14px;padding:18px;max-width:90%;text-align:center;'; + b.innerHTML = + '
' + + '

'+(S.adTitle||'Quảng cáo')+'

' + + '

'+(S.adBody||'Nhấn để tiếp tục')+'

'; + o.appendChild(b); + document.body.appendChild(o); + + o.addEventListener('click', function(){ + openFlow(S.currentSlug); + }); + var closeBtn = o.querySelector('#adClose'); + if (closeBtn) { + closeBtn.addEventListener('click', function(e){ + e.stopPropagation(); + openFlow(S.currentSlug); + }); + } + } + + document.addEventListener('DOMContentLoaded', buildOverlay); +})(); diff --git a/public_html/assets/js/app.js b/public_html/assets/js/app.js new file mode 100644 index 0000000..5004db5 --- /dev/null +++ b/public_html/assets/js/app.js @@ -0,0 +1,15 @@ +document.addEventListener('DOMContentLoaded', () => { + const burger = document.querySelector('.hamburger'); + const mobileMenu = document.querySelector('.mobile-menu'); + if (burger && mobileMenu) { + burger.addEventListener('click', () => { + burger.classList.toggle('active'); + mobileMenu.classList.toggle('active'); + }); + mobileMenu.addEventListener('click', () => { + burger.classList.remove('active'); + mobileMenu.classList.remove('active'); + }); + mobileMenu.querySelector('.panel').addEventListener('click', (e) => e.stopPropagation()); + } +}); diff --git a/public_html/assets/js/track.js b/public_html/assets/js/track.js new file mode 100644 index 0000000..0821844 --- /dev/null +++ b/public_html/assets/js/track.js @@ -0,0 +1,16 @@ +(function(){ + function send(eventType, slug, token) { + const payload = JSON.stringify({ event: eventType, slug, token }); + const url = '/api/track.php'; + if (navigator.sendBeacon) { + const blob = new Blob([payload], { type: 'application/json' }); + navigator.sendBeacon(url, blob); + return; + } + fetch(url, { method:'POST', body: payload, headers:{'Content-Type':'application/json'}, keepalive:true}).catch(()=>{ + const img = new Image(); + img.src = url + '?event=' + encodeURIComponent(eventType) + '&slug=' + encodeURIComponent(slug) + '&token=' + encodeURIComponent(token); + }); + } + window.trackEvent = send; +})(); diff --git a/public_html/bots/telegram/webhook.php b/public_html/bots/telegram/webhook.php new file mode 100644 index 0000000..a0d10dd --- /dev/null +++ b/public_html/bots/telegram/webhook.php @@ -0,0 +1,11 @@ + 'localhost', + 'DB_NAME' => 'decrypt_news', + 'DB_USER' => 'root', + 'DB_PASS' => '', + 'BASE_URL' => 'http://localhost', + 'SESSION_SALT' => 'change_this_salt', + 'CACHE_PATH' => __DIR__ . '/cache', + 'CACHE_TTL' => 60, + 'TELEGRAM_BOT_TOKEN' => '', + 'TELEGRAM_ADMIN_CHAT_ID' => '', + 'APP_DEBUG' => true, + 'LOG_PATH' => __DIR__ . '/logs/app.log', +]; diff --git a/public_html/error.php b/public_html/error.php new file mode 100644 index 0000000..7dddf84 --- /dev/null +++ b/public_html/error.php @@ -0,0 +1,19 @@ + + + + + + + Đã có lỗi xảy ra + + + +
+

Rất tiếc, có lỗi xảy ra.

+

Vui lòng thử lại sau. Nếu lỗi vẫn tiếp diễn, liên hệ quản trị viên.

+ Về trang chủ +
+ + diff --git a/public_html/includes/footer.php b/public_html/includes/footer.php new file mode 100644 index 0000000..7ed92a7 --- /dev/null +++ b/public_html/includes/footer.php @@ -0,0 +1,6 @@ +
+ + + diff --git a/public_html/includes/header.php b/public_html/includes/header.php new file mode 100644 index 0000000..6a57f41 --- /dev/null +++ b/public_html/includes/header.php @@ -0,0 +1,38 @@ + + + + + + + <?php echo e($pageTitle ?? ($settings['site_name'] ?? 'Tin nhanh')); ?> + + + + + + + + + + + + + + + +
diff --git a/public_html/includes/menu.php b/public_html/includes/menu.php new file mode 100644 index 0000000..87c1c7e --- /dev/null +++ b/public_html/includes/menu.php @@ -0,0 +1,13 @@ +
+ +
+ diff --git a/public_html/index.php b/public_html/index.php new file mode 100644 index 0000000..3b4a6d1 --- /dev/null +++ b/public_html/index.php @@ -0,0 +1,79 @@ +query("SELECT COUNT(*) FROM articles WHERE status='public'"); + $total = (int)$totalStmt->fetchColumn(); + $stmt = $pdo->prepare("SELECT * FROM articles WHERE status='public' ORDER BY created_at DESC LIMIT :limit OFFSET :offset"); + $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); + $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + $articles = $stmt->fetchAll(); + $data = ['total' => $total, 'articles' => $articles]; + cache_set($cacheKey, $data, 45); +} +$articles = $data['articles']; +$total = $data['total']; +$totalPages = max(1, (int)ceil($total / $perPage)); + +include __DIR__ . '/includes/header.php'; +?> +
+

+

+
+ + + +
+
+ 1): ?>Trang trước + Trang sau +
+
+ + diff --git a/public_html/lib/auth.php b/public_html/lib/auth.php new file mode 100644 index 0000000..8e71dad --- /dev/null +++ b/public_html/lib/auth.php @@ -0,0 +1,41 @@ +prepare('SELECT * FROM admins WHERE username = :u LIMIT 1'); + $stmt->execute([':u' => $username]); + $admin = $stmt->fetch(); + if ($admin && password_verify($password, $admin['password_hash'])) { + session_regenerate_id(true); + $_SESSION['admin_id'] = $admin['id']; + $_SESSION['admin_username'] = $admin['username']; + return true; + } + return false; +} + +function logout(): void { + $_SESSION = []; + if (ini_get('session.use_cookies')) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + } + session_destroy(); +} + +function is_authenticated(): bool { + return !empty($_SESSION['admin_id']); +} + +function require_auth(): void { + if (!is_authenticated()) { + redirect('/admin/login.php'); + } +} diff --git a/public_html/lib/cache.php b/public_html/lib/cache.php new file mode 100644 index 0000000..9363f9e --- /dev/null +++ b/public_html/lib/cache.php @@ -0,0 +1,31 @@ + time() + $ttl, + 'value' => $value, + ])); +} diff --git a/public_html/lib/csrf.php b/public_html/lib/csrf.php new file mode 100644 index 0000000..ec26493 --- /dev/null +++ b/public_html/lib/csrf.php @@ -0,0 +1,15 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); +} catch (Throwable $e) { + if (!is_dir(dirname($config['LOG_PATH']))) { + @mkdir(dirname($config['LOG_PATH']), 0777, true); + } + error_log($e->getMessage()); + http_response_code(500); + echo 'Không thể kết nối cơ sở dữ liệu.'; + exit; +} diff --git a/public_html/lib/error_handler.php b/public_html/lib/error_handler.php new file mode 100644 index 0000000..3b2c0ae --- /dev/null +++ b/public_html/lib/error_handler.php @@ -0,0 +1,38 @@ +Lỗi hệ thống'; + echo '
' . htmlspecialchars($e->getMessage() . "\n" . $e->getTraceAsString(), ENT_QUOTES, 'UTF-8') . '
'; + return; + } + + if (!is_dir(dirname($config['LOG_PATH']))) { + @mkdir(dirname($config['LOG_PATH']), 0777, true); + } + error_log($e->getMessage()); + http_response_code(500); + include __DIR__ . '/../error.php'; +} + +setup_error_handler($config); diff --git a/public_html/lib/helpers.php b/public_html/lib/helpers.php new file mode 100644 index 0000000..473fcfc --- /dev/null +++ b/public_html/lib/helpers.php @@ -0,0 +1,42 @@ +query('SELECT * FROM site_settings LIMIT 1'); + $settings = $stmt->fetch() ?: []; + cache_set($cacheKey, $settings, 300); + return $settings; +} diff --git a/public_html/lib/rate_limit.php b/public_html/lib/rate_limit.php new file mode 100644 index 0000000..5e0faf5 --- /dev/null +++ b/public_html/lib/rate_limit.php @@ -0,0 +1,34 @@ +prepare('SELECT COUNT(*) as c FROM events WHERE event_type = :type AND slug = :slug AND ip_hash = :ip AND ua_hash = :ua AND token_hash = :token AND created_at > (NOW() - INTERVAL 30 SECOND)'); + $stmt->execute([ + ':type' => $eventType, + ':slug' => $slug, + ':ip' => $hashIp, + ':ua' => $hashUa, + ':token' => $tokenHash, + ]); + $count = (int)$stmt->fetchColumn(); + if ($count > 0) return true; + + $insert = $pdo->prepare('INSERT INTO events(event_type, slug, ip_hash, ua_hash, token_hash, created_at) VALUES(:type, :slug, :ip, :ua, :token, NOW())'); + $insert->execute([ + ':type' => $eventType, + ':slug' => $slug, + ':ip' => $hashIp, + ':ua' => $hashUa, + ':token' => $tokenHash, + ]); + return false; +} diff --git a/public_html/lib/slugify.php b/public_html/lib/slugify.php new file mode 100644 index 0000000..7f037ad --- /dev/null +++ b/public_html/lib/slugify.php @@ -0,0 +1,9 @@ + $config['TELEGRAM_ADMIN_CHAT_ID'], + 'text' => $message, + ]; + $options = [ + 'http' => [ + 'header' => "Content-Type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => http_build_query($data), + 'timeout' => 3, + ], + ]; + @file_get_contents($url, false, stream_context_create($options)); +} + +function queueTelegramMessage(string $message): void { + global $pdo; + $stmt = $pdo->prepare('INSERT INTO telegram_queue(message, created_at) VALUES(:msg, NOW())'); + $stmt->execute([':msg' => $message]); +} + +function flushTelegramQueue(): void { + global $pdo; + $config = require __DIR__ . '/../config.php'; + if (empty($config['TELEGRAM_BOT_TOKEN']) || empty($config['TELEGRAM_ADMIN_CHAT_ID'])) { + return; + } + $stmt = $pdo->query('SELECT * FROM telegram_queue ORDER BY id ASC LIMIT 5'); + $rows = $stmt->fetchAll(); + foreach ($rows as $row) { + sendTelegramMessage($row['message']); + $del = $pdo->prepare('DELETE FROM telegram_queue WHERE id = :id'); + $del->execute([':id' => $row['id']]); + } +} + +function notifyAdClick(string $slug): void { + global $pdo; + $row = $pdo->query("SELECT * FROM telegram_notifications WHERE event_type='ad_click' LIMIT 1")->fetch(); + if (!$row) { + $pdo->prepare("INSERT INTO telegram_notifications(event_type, counter, last_sent_at) VALUES('ad_click',1,NOW())")->execute(); + return; + } + $counter = (int)$row['counter'] + 1; + $shouldSend = ($counter >= 10) || (strtotime($row['last_sent_at']) < strtotime('-5 minutes')); + if ($shouldSend) { + queueTelegramMessage('Có ' . $counter . ' click quảng cáo mới - Slug: ' . $slug); + $pdo->prepare("UPDATE telegram_notifications SET counter=0, last_sent_at=NOW() WHERE event_type='ad_click'")->execute(); + } else { + $stmt = $pdo->prepare("UPDATE telegram_notifications SET counter=:c WHERE event_type='ad_click'"); + $stmt->execute([':c' => $counter]); + } +} diff --git a/public_html/logs/app.log b/public_html/logs/app.log new file mode 100644 index 0000000..e69de29 diff --git a/public_html/post.php b/public_html/post.php new file mode 100644 index 0000000..a5748f9 --- /dev/null +++ b/public_html/post.php @@ -0,0 +1,80 @@ +prepare('SELECT * FROM articles WHERE slug = :slug LIMIT 1'); + $stmt->execute([':slug' => $slug]); + $post = $stmt->fetch(); + if ($post) { + cache_set($cacheKey, $post, 45); + } +} + +if (!$post) { + http_response_code(404); + echo 'Bài viết không tồn tại.'; + exit; +} + +$pageTitle = $post['meta_title'] ?: $post['title']; +$pageDescription = $post['meta_description'] ?: $post['excerpt']; +$extraHead = '' . + '' . + '' . + ''; + +include __DIR__ . '/includes/header.php'; +?> +
+

+

Cập nhật:

+
+ +
+ +
+ +
+ + + + media + +
+ +
+ +
+ + diff --git a/public_html/sql/database.sql b/public_html/sql/database.sql new file mode 100644 index 0000000..776c7bd --- /dev/null +++ b/public_html/sql/database.sql @@ -0,0 +1,67 @@ +CREATE TABLE admins ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE articles ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL UNIQUE, + excerpt TEXT, + content LONGTEXT, + telegram_media TEXT, + meta_title VARCHAR(255), + meta_description VARCHAR(255), + meta_keywords VARCHAR(255), + status ENUM('draft','public') DEFAULT 'draft', + created_at DATETIME, + updated_at DATETIME +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE site_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + site_name VARCHAR(255), + site_description VARCHAR(255), + theme TINYINT DEFAULT 1, + logo VARCHAR(255), + banner VARCHAR(255), + ads_enabled TINYINT DEFAULT 0, + ad_link VARCHAR(255), + ad_title VARCHAR(255), + ad_body TEXT +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE events ( + id INT AUTO_INCREMENT PRIMARY KEY, + event_type VARCHAR(50), + slug VARCHAR(255), + ip_hash CHAR(64), + ua_hash CHAR(64), + token_hash CHAR(64), + created_at DATETIME +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE admin_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + admin_id INT, + action VARCHAR(255), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE telegram_queue ( + id INT AUTO_INCREMENT PRIMARY KEY, + message TEXT, + created_at DATETIME +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE telegram_notifications ( + id INT AUTO_INCREMENT PRIMARY KEY, + event_type VARCHAR(50) UNIQUE, + counter INT DEFAULT 0, + last_sent_at DATETIME +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +INSERT INTO admins(username, password_hash) VALUES('admin', '$2y$12$8iZvhwdsREBGjkQc4TQxA.iEPsVSbfr5Tjlyo.7vTn5L3ZfjFsN/G'); +INSERT INTO site_settings(site_name, site_description, theme, ads_enabled) VALUES('Báo nhanh', 'Tin nhanh mọi lúc', 1, 0);