diff --git a/README.md b/README.md deleted file mode 100644 index cdc16c5..0000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Decrypt -### [+] Created By HTR-TECH (@***tahmid.rayat***) -### [+] Disclaimer : -***Decrypter is a tool to decrypt Encrypted Bash Scripts into a Readable Format.This Tool is created for Educational Purpose only.I am not responsible for any misuse of this tool.*** - - - -### [+] Installation -```apt update``` - -```apt install git python2 -y``` - -```git clone https://github.com/hax0rtahm1d/decrypt``` - -```cd decrypt``` - -```python2 dec.py``` - -### Or, Use Single Command - -``` -apt update && apt install git python2 -y && git clone https://github.com/hax0rtahm1d/decrypt && cd decrypt && python2 dec.py -``` - -## [+] Find Me on : -[![Github](https://img.shields.io/badge/Github-HTR--TECH-green?style=for-the-badge&logo=github)](https://github.com/htr-tech) -[![Instagram](https://img.shields.io/badge/IG-%40tahmid.rayat-red?style=for-the-badge&logo=instagram)](https://www.instagram.com/tahmid.rayat) -[![Messenger](https://img.shields.io/badge/Chat-Messenger-blue?style=for-the-badge&logo=messenger)](https://m.me/tahmid.rayat.official) diff --git a/public_html/.htaccess b/public_html/.htaccess new file mode 100644 index 0000000..cd960f0 --- /dev/null +++ b/public_html/.htaccess @@ -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] diff --git a/public_html/README.md b/public_html/README.md new file mode 100644 index 0000000..766d5ba --- /dev/null +++ b/public_html/README.md @@ -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. diff --git a/public_html/admin/dashboard.php b/public_html/admin/dashboard.php new file mode 100644 index 0000000..079627f --- /dev/null +++ b/public_html/admin/dashboard.php @@ -0,0 +1,14 @@ +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'];} +?> +
+

Tổng quan

+

Page views:

+

Post views:

+

Ad clicks:

+
+ diff --git a/public_html/admin/includes/auth_check.php b/public_html/admin/includes/auth_check.php new file mode 100644 index 0000000..e4e7c33 --- /dev/null +++ b/public_html/admin/includes/auth_check.php @@ -0,0 +1,7 @@ + + + diff --git a/public_html/admin/includes/header.php b/public_html/admin/includes/header.php new file mode 100644 index 0000000..ec6fe60 --- /dev/null +++ b/public_html/admin/includes/header.php @@ -0,0 +1,16 @@ + + + +Admin + +
+
Admin Panel
+
+ Dashboard + Bài viết + Cài đặt + Thống kê + Logout +
+
+
diff --git a/public_html/admin/login.php b/public_html/admin/login.php new file mode 100644 index 0000000..74674aa --- /dev/null +++ b/public_html/admin/login.php @@ -0,0 +1,28 @@ + + + +Đăng nhập + +
+

Admin Login

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

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..178c3d5 --- /dev/null +++ b/public_html/admin/post_copy.php @@ -0,0 +1,14 @@ +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'); diff --git a/public_html/admin/post_delete.php b/public_html/admin/post_delete.php new file mode 100644 index 0000000..6e75cb0 --- /dev/null +++ b/public_html/admin/post_delete.php @@ -0,0 +1,9 @@ +prepare('DELETE FROM articles WHERE id=:id'); + $stmt->execute([':id'=>$id]); +} +header('Location: '.BASE_URL.'/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..bda2b27 --- /dev/null +++ b/public_html/admin/post_edit.php @@ -0,0 +1,41 @@ +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'; +?> +

Sửa bài

+
+ + + + + + + + + + + +
+ diff --git a/public_html/admin/posts.php b/public_html/admin/posts.php new file mode 100644 index 0000000..c84de8a --- /dev/null +++ b/public_html/admin/posts.php @@ -0,0 +1,23 @@ +query('SELECT * FROM articles ORDER BY created_at DESC')->fetchAll(); +?> +
Thêm bài
+ + + + + + + + + + +
Tiêu đềSlugTrạng tháiHành động
+ Sửa | + Copy | + Xóa +
+ diff --git a/public_html/admin/settings.php b/public_html/admin/settings.php new file mode 100644 index 0000000..dbe4399 --- /dev/null +++ b/public_html/admin/settings.php @@ -0,0 +1,62 @@ +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'; +?> +

Cài đặt

+
+ + + + + + + + + + + +
+ diff --git a/public_html/admin/stats.php b/public_html/admin/stats.php new file mode 100644 index 0000000..a6a72f0 --- /dev/null +++ b/public_html/admin/stats.php @@ -0,0 +1,56 @@ +exec('TRUNCATE TABLE events'); + } + if(isset($_POST['reset_selected']) && in_array($_POST['reset_type'], ['page_view','post_view','ad_click'])){ + $del = db()->prepare('DELETE FROM events WHERE event_type = :t'); + $del->execute([':t'=>$_POST['reset_type']]); + } + 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(); +$topPosts = db()->query("SELECT slug, SUM(event_type='post_view') pv, SUM(event_type='ad_click') ac FROM events WHERE slug IS NOT NULL GROUP BY slug ORDER BY pv DESC LIMIT 10")->fetchAll(); +$totals = db()->query("SELECT SUM(event_type='page_view') pv_all, SUM(event_type='post_view') post_all, SUM(event_type='ad_click') ad_all FROM events")->fetch(); +$counts = db()->query("SELECT COUNT(*) total, SUM(status='public') public_count FROM articles")->fetch(); +include __DIR__ . '/includes/header.php'; +?> +

Thống kê

+ + + + + +
NgàyLoạiSố
+
+

Tổng

+

Page view: | Post view: | Ad click:

+

Số bài: (public: )

+
+

Top bài

+ + + + + +
SlugViewAd click
+
+ + +
+
+ + + +
+ diff --git a/public_html/api/stats.php b/public_html/api/stats.php new file mode 100644 index 0000000..b6191dc --- /dev/null +++ b/public_html/api/stats.php @@ -0,0 +1,5 @@ +query('SELECT event_type, COUNT(*) c FROM events GROUP BY event_type')->fetchAll(); +echo json_encode($totals); diff --git a/public_html/api/track.php b/public_html/api/track.php new file mode 100644 index 0000000..7b7a7cf --- /dev/null +++ b/public_html/api/track.php @@ -0,0 +1,16 @@ +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); flush_telegram_queue(); } +echo json_encode(['ok'=>true]); diff --git a/public_html/app/cache/.gitkeep b/public_html/app/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public_html/app/config/config.php b/public_html/app/config/config.php new file mode 100644 index 0000000..d941b1d --- /dev/null +++ b/public_html/app/config/config.php @@ -0,0 +1,14 @@ +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_name'] = $admin['username']; + return true; + } + return false; +} + +function admin_require_login(): void { + if (empty($_SESSION['admin_id'])) { + header('Location: ' . BASE_URL . '/admin/login.php'); + exit; + } +} + +function admin_logout(): void { + session_destroy(); +} diff --git a/public_html/app/lib/cache.php b/public_html/app/lib/cache.php new file mode 100644 index 0000000..d8ac361 --- /dev/null +++ b/public_html/app/lib/cache.php @@ -0,0 +1,15 @@ +'; +} + +function csrf_verify(): void { + $token = $_POST['csrf_token'] ?? ''; + if (!$token || !hash_equals($_SESSION['csrf_token'] ?? '', $token)) { + http_response_code(403); + exit('CSRF token invalid'); + } +} diff --git a/public_html/app/lib/db.php b/public_html/app/lib/db.php new file mode 100644 index 0000000..8270019 --- /dev/null +++ b/public_html/app/lib/db.php @@ -0,0 +1,12 @@ + PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false]; + $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); + } + return $pdo; +} diff --git a/public_html/app/lib/rate_limit.php b/public_html/app/lib/rate_limit.php new file mode 100644 index 0000000..464d9ed --- /dev/null +++ b/public_html/app/lib/rate_limit.php @@ -0,0 +1,28 @@ +prepare('SELECT created_at FROM events WHERE ip_hash = :fp AND event_type = :event AND (slug = :slug OR (:slug IS NULL AND slug IS NULL)) ORDER BY created_at DESC LIMIT 1'); + $stmt->execute([':fp' => $fp, ':event' => $event, ':slug' => $slug]); + $last = $stmt->fetchColumn(); + if ($last && (time() - strtotime($last) < $window)) { + return false; + } + return true; +} + +function log_event(string $event, ?string $slug, string $token): void { + $fp = fingerprint(); + $uaHash = hash('sha256', SESSION_SALT . '|' . ($_SERVER['HTTP_USER_AGENT'] ?? 'unknown')); + $tokenHash = hash('sha256', SESSION_SALT . '|' . $token); + $stmt = db()->prepare('INSERT INTO events (slug, event_type, ip_hash, ua_hash, token_hash, created_at) VALUES (:slug, :event, :ip, :ua, :token, NOW())'); + $stmt->execute([':slug' => $slug, ':event' => $event, ':ip' => $fp, ':ua' => $uaHash, ':token' => $tokenHash]); +} diff --git a/public_html/app/lib/slugify.php b/public_html/app/lib/slugify.php new file mode 100644 index 0000000..4866f4e --- /dev/null +++ b/public_html/app/lib/slugify.php @@ -0,0 +1,9 @@ + TELEGRAM_ADMIN_CHAT_ID, 'text' => $text]; + @file_get_contents($url . '?' . http_build_query($payload)); +} + +function queue_click_notify(int $count): void { + $stmt = db()->prepare('INSERT INTO telegram_queue (type, counter, last_sent_at) VALUES ("click", :cnt, NOW()) ON DUPLICATE KEY UPDATE counter = counter + :cnt'); + $stmt->execute([':cnt' => $count]); +} + +function flush_telegram_queue(): void { + $stmt = db()->query('SELECT * FROM telegram_queue'); + foreach ($stmt->fetchAll() as $row) { + if ($row['type'] === 'click' && strtotime($row['last_sent_at']) < time() - 300 && $row['counter'] > 0) { + sendTelegramMessage('Có ' . $row['counter'] . ' click Shopee mới.'); + db()->prepare('UPDATE telegram_queue SET counter = 0, last_sent_at = NOW() WHERE id = :id')->execute([':id' => $row['id']]); + } + } +} diff --git a/public_html/assets/css/admin.css b/public_html/assets/css/admin.css new file mode 100644 index 0000000..5fa2678 --- /dev/null +++ b/public_html/assets/css/admin.css @@ -0,0 +1 @@ +body{font-family:'Inter',system-ui;background:#f3f4f6;margin:0;color:#111827;} .admin-header{background:#0f172a;color:#fff;padding:12px 16px;display:flex;justify-content:space-between;align-items:center;} .admin-header a{color:#fff;margin-left:10px;text-decoration:none;} .admin-main{padding:18px;} table{width:100%;border-collapse:collapse;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.05);} th,td{padding:10px;border-bottom:1px solid #e5e7eb;text-align:left;} .btn{display:inline-block;padding:10px 14px;border-radius:8px;background:#2563eb;color:#fff;text-decoration:none;} .card{background:#fff;padding:14px;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,0.05);margin-bottom:12px;} .login-page{display:flex;align-items:center;justify-content:center;height:100vh;background:#0f172a;} .login-form{background:#fff;padding:20px;border-radius:12px;width:340px;} .login-form input{width:100%;padding:10px;margin-bottom:10px;border-radius:8px;border:1px solid #e5e7eb;} .login-form button{width:100%;padding:12px;border:none;border-radius:8px;background:#2563eb;color:#fff;} label{display:block;margin-bottom:10px;} input[type=text],input[type=password],input[type=url],textarea,select{width:100%;padding:10px;border-radius:8px;border:1px solid #e5e7eb;} textarea{min-height:120px;} diff --git a/public_html/assets/css/base.css b/public_html/assets/css/base.css new file mode 100644 index 0000000..d0715de --- /dev/null +++ b/public_html/assets/css/base.css @@ -0,0 +1,15 @@ +:root { --accent:#ff5a1f; --bg:#f8f9fb; --text:#1f2937; --card:#fff; --shadow:0 10px 30px rgba(0,0,0,0.08); } +*{box-sizing:border-box;} body{margin:0;font-family:'Inter',system-ui;background:var(--bg);color:var(--text);} a{text-decoration:none;color:var(--accent);} img{max-width:100%;display:block;} +.site-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:var(--card);box-shadow:var(--shadow);position:sticky;top:0;z-index:50;} +.logo{height:42px;} +.banner img{height:56px;object-fit:cover;border-radius:8px;} +.hamburger{font-size:28px;cursor:pointer;display:block;} +.mobile-menu{position:fixed;top:0;left:-240px;width:220px;height:100vh;background:#0f172a;color:#e2e8f0;padding:70px 16px;transition:transform .5s, box-shadow .3s;transform:translateX(0) rotateY(25deg);transform-origin:left;box-shadow:0 0 0 rgba(0,0,0,0);} .mobile-menu.open{transform:translateX(240px) rotateY(0);box-shadow:6px 0 18px rgba(0,0,0,0.35);} .mobile-menu ul{list-style:none;padding:0;margin:0;} .mobile-menu li{margin-bottom:12px;} .mobile-menu a{color:#e2e8f0;font-weight:600;} +.container{max-width:1100px;margin:0 auto;padding:16px;} +.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;} +.card{background:var(--card);border-radius:14px;padding:14px;box-shadow:var(--shadow);transition:transform .2s;} .card:hover{transform:translateY(-4px);} .card h2{margin:0 0 6px;} .card .meta{color:#6b7280;font-size:13px;} +.article-body img{border-radius:12px;} +.interstitial{position:fixed;inset:0;background:rgba(0,0,0,0.6);display:none;align-items:center;justify-content:center;z-index:60;} .interstitial.show{display:flex;} .interstitial .box{background:#fff;padding:18px;border-radius:12px;text-align:center;max-width:420px;width:90%;} +.btn{padding:10px 14px;border-radius:10px;border:none;cursor:pointer;} .btn-primary{background:var(--accent);color:#fff;} .btn-ghost{background:#e5e7eb;} +.site-footer{padding:18px;text-align:center;color:#6b7280;} +@media(min-width:768px){.hamburger{display:none;}.mobile-menu{display:none;}} diff --git a/public_html/assets/css/theme1.css b/public_html/assets/css/theme1.css new file mode 100644 index 0000000..e23b40e --- /dev/null +++ b/public_html/assets/css/theme1.css @@ -0,0 +1 @@ +body{--accent:#ff5a1f;--bg:#fef5ed;--card:#fff;--text:#1f2937;} diff --git a/public_html/assets/css/theme2.css b/public_html/assets/css/theme2.css new file mode 100644 index 0000000..bbf1f1c --- /dev/null +++ b/public_html/assets/css/theme2.css @@ -0,0 +1,4 @@ +body{--accent:#2563eb;--bg:#eef2ff;--card:#fff;--text:#0f172a;} +.card{border:1px solid #dbeafe;} +.site-header{background:#0f172a;color:#fff;} +.site-header a{color:#fff;} diff --git a/public_html/assets/css/theme3.css b/public_html/assets/css/theme3.css new file mode 100644 index 0000000..0c6f56e --- /dev/null +++ b/public_html/assets/css/theme3.css @@ -0,0 +1,4 @@ +body{--accent:#16a34a;--bg:#0b1224;--card:#111827;--text:#e2e8f0;} +.card{border:1px solid #1f2937;} +.site-header{background:#0f172a;color:#e2e8f0;} +.site-footer{color:#cbd5e1;} diff --git a/public_html/assets/js/adflow.js b/public_html/assets/js/adflow.js new file mode 100644 index 0000000..6fb7074 --- /dev/null +++ b/public_html/assets/js/adflow.js @@ -0,0 +1,25 @@ +(function(){ + const overlay=document.getElementById('adOverlay'); + if(!overlay) return; + const slug=overlay.dataset.slug; + const adLink=overlay.dataset.link; + const token=overlay.dataset.token; + const close = () => { + if(!adLink) return; + // send tracking asynchronously + if(navigator.sendBeacon){ + const data=new URLSearchParams({event:'ad_click',slug:slug,token:token}); + navigator.sendBeacon('/api/track.php',data); + } else if(window.fetch){ + fetch('/api/track.php',{method:'POST',body:new URLSearchParams({event:'ad_click',slug:slug,token:token}),keepalive:true}); + } else { + const img=new Image(); + img.src='/api/track.php?event=ad_click&slug='+encodeURIComponent(slug)+'&token='+encodeURIComponent(token); + } + window.open('/'+slug+'?ad=0','_blank','noopener'); + window.location.href=adLink; + }; + overlay.addEventListener('click',close); + const btnClose=document.getElementById('adClose'); + if(btnClose) btnClose.addEventListener('click',close); +})(); diff --git a/public_html/assets/js/app.js b/public_html/assets/js/app.js new file mode 100644 index 0000000..5852e5d --- /dev/null +++ b/public_html/assets/js/app.js @@ -0,0 +1,5 @@ +(function(){ + const hamburger=document.getElementById('hamburger'); + const menu=document.getElementById('mobileMenu'); + if(hamburger&&menu){hamburger.addEventListener('click',()=>{menu.classList.toggle('open');});} +})(); diff --git a/public_html/assets/js/track.js b/public_html/assets/js/track.js new file mode 100644 index 0000000..f534676 --- /dev/null +++ b/public_html/assets/js/track.js @@ -0,0 +1,11 @@ +(function(){ + const token=window.__trackToken; + if(!token) return; + const slug=document.body.dataset.slug||null; + const send=(event)=>{ + const payload=new URLSearchParams({event:event,slug:slug||'',token:token}); + if(navigator.sendBeacon){navigator.sendBeacon('/api/track.php',payload);} else {fetch('/api/track.php',{method:'POST',body:payload,keepalive:true});} + }; + if(document.body.dataset.pageview==='1'){send('page_view');} + if(document.body.dataset.postview==='1'){send('post_view');} +})(); diff --git a/public_html/bots/telegram/webhook.php b/public_html/bots/telegram/webhook.php new file mode 100644 index 0000000..3b56229 --- /dev/null +++ b/public_html/bots/telegram/webhook.php @@ -0,0 +1,2 @@ + +
+

+
+ + diff --git a/public_html/includes/header.php b/public_html/includes/header.php new file mode 100644 index 0000000..5a65278 --- /dev/null +++ b/public_html/includes/header.php @@ -0,0 +1,43 @@ +query('SELECT * FROM site_settings LIMIT 1')->fetch(); + cache_set('site_settings', $settings); +} +$theme = $settings['theme'] ?? '1'; +?> + + + + + + <?= htmlspecialchars($settings['site_name'] ?? 'Tin tức nhanh') ?> + + + + + + + + +
+
+ + Logo + + + +
+
+ + Banner + +
+
☰
+
+ +
diff --git a/public_html/includes/menu.php b/public_html/includes/menu.php new file mode 100644 index 0000000..b16dc3c --- /dev/null +++ b/public_html/includes/menu.php @@ -0,0 +1,5 @@ +
+
    +
  • Trang chủ
  • +
+
diff --git a/public_html/index.php b/public_html/index.php new file mode 100644 index 0000000..50ef46d --- /dev/null +++ b/public_html/index.php @@ -0,0 +1,49 @@ +query('SELECT * FROM site_settings LIMIT 1')->fetch(); cache_set('site_settings', $settings); } +$page = max(1, (int)($_GET['page'] ?? 1)); +$perPage = 9; +$offset = ($page - 1) * $perPage; + +$cacheKey = 'home_' . $page; +$data = cache_get($cacheKey, 60); +if (!$data) { + $stmt = db()->prepare('SELECT * FROM articles WHERE status = "public" ORDER BY created_at DESC LIMIT :offset,:limit'); + $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); + $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); + $stmt->execute(); + $articles = $stmt->fetchAll(); + $total = db()->query('SELECT COUNT(*) FROM articles WHERE status="public"')->fetchColumn(); + $data = ['articles'=>$articles,'total'=>$total]; + cache_set($cacheKey, $data); +} +$trackToken = bin2hex(random_bytes(8)); +$_SESSION['track_token'] = $trackToken; +include __DIR__ . '/includes/header.php'; +?> + +
+ +
+ +

+
+

...

+
+
+ +
+1): ?> +
+ + Trang + +
+ + diff --git a/public_html/post.php b/public_html/post.php new file mode 100644 index 0000000..5a6f763 --- /dev/null +++ b/public_html/post.php @@ -0,0 +1,54 @@ +prepare('SELECT * FROM articles WHERE slug = :slug AND status="public" LIMIT 1'); +$stmt->execute([':slug' => $slug]); +$article = $stmt->fetch(); +if (!$article) { http_response_code(404); exit('Not found'); } +$cacheKey = 'post_'.$slug.'_'.$article['updated_at']; +if ($cached = cache_get($cacheKey, 60)) { + $article = $cached; +} else { + cache_set($cacheKey, $article); +} + +$settings = cache_get('site_settings', 300); +if (!$settings) { $settings = db()->query('SELECT * FROM site_settings LIMIT 1')->fetch(); cache_set('site_settings', $settings); } + +$trackToken = bin2hex(random_bytes(8)); +$_SESSION['track_token'] = $trackToken; +$adsEnabled = !empty($settings['ads_enabled']) && !empty($settings['ad_link']) && ($_GET['ad'] ?? '1') !== '0'; +include __DIR__ . '/includes/header.php'; +?> + + + + + + +
+
+

+

+ +
+
+ +
+

+
Ngày đăng:
+
+ +
+ + + +
+ +
+ diff --git a/public_html/sql/database.sql b/public_html/sql/database.sql new file mode 100644 index 0000000..a481e00 --- /dev/null +++ b/public_html/sql/database.sql @@ -0,0 +1,71 @@ +CREATE TABLE admins ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(120) UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE articles ( + id INT AUTO_INCREMENT PRIMARY KEY, + slug VARCHAR(190) UNIQUE NOT NULL, + title VARCHAR(255) NOT NULL, + excerpt TEXT, + content MEDIUMTEXT, + telegram_media TEXT, + meta_title VARCHAR(255), + meta_description VARCHAR(255), + meta_keywords VARCHAR(255), + status ENUM('draft','public') DEFAULT 'draft', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE site_settings ( + id INT PRIMARY KEY, + site_name VARCHAR(255), + site_description TEXT, + logo_path VARCHAR(255), + banner_path VARCHAR(255), + theme ENUM('1','2','3') DEFAULT '1', + ads_enabled TINYINT(1) DEFAULT 0, + ad_link VARCHAR(255), + ad_title VARCHAR(255), + ad_body TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE events ( + id INT AUTO_INCREMENT PRIMARY KEY, + slug VARCHAR(190) NULL, + event_type ENUM('page_view','post_view','ad_click') NOT NULL, + ip_hash CHAR(64), + ua_hash CHAR(64), + token_hash CHAR(64), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX(event_type), INDEX(slug) +); + +CREATE TABLE admin_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + admin_id INT NULL, + action VARCHAR(255), + payload TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE telegram_queue ( + id INT AUTO_INCREMENT PRIMARY KEY, + type VARCHAR(50) UNIQUE, + counter INT DEFAULT 0, + last_sent_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE telegram_queue ADD UNIQUE KEY uq_type (type); +INSERT INTO telegram_queue (type, counter, last_sent_at) VALUES ('click', 0, NOW()) + ON DUPLICATE KEY UPDATE last_sent_at = last_sent_at; + +INSERT INTO admins (username, password_hash) VALUES ('admin', '$2y$10$3bQ7N06j8VnYX3yvGN28M.dQ21FdWyF/pD2IJVBGLe7.tuMnIKPQe'); +INSERT INTO site_settings (id, site_name, site_description, theme, ads_enabled, ad_title, ad_body) VALUES (1, 'Báo điện tử', 'Tin nhanh cho mọi người', '1', 0, 'Mua sắm Shopee', 'Ưu đãi hot chờ bạn!'); +INSERT INTO articles (slug, title, excerpt, content, telegram_media, meta_title, meta_description, meta_keywords, status) VALUES +('tin-nong-hom-nay', 'Tin nóng hôm nay', 'Bản tin ngắn gọn', '

Nội dung bài mẫu.

', 'https://t.me/yourchannel/1', 'Tin nóng', 'Tin nóng hôm nay', 'tin,nong,hom,nay', 'public'), +('the-thao-sang', 'Thể thao sáng', 'Tin thể thao', '

Bài thể thao.

', 'https://t.me/yourchannel/2', 'Thể thao', 'Bản tin thể thao', 'the thao', 'public');