session_id(), 'items' => [], 'note' => null, 'attributes' => [], 'cart_level_discount_applications' => [], ]; } $cart = $_SESSION['cart']; $totalPrice = 0; $itemCount = 0; $totalWeight = 0; foreach ($cart['items'] as $item) { $linePrice = (int)($item['line_price'] ?? 0); $quantity = (int)($item['quantity'] ?? 0); $totalPrice += $linePrice; $itemCount += $quantity; } $cart['total_weight'] = $totalWeight; $cart['item_count'] = $itemCount; $cart['total_discounts'] = 0; $cart['total_price'] = $totalPrice; $cart['total_line_items_price'] = $totalPrice; $cart['requires_shipping'] = false; $_SESSION['cart'] = $cart; return $cart; } function saveCart(array $cart): void { $_SESSION['cart'] = $cart; } function buildLineItem(string $id, int $quantity, int $price, string $title = '', ?string $image = null, string $url = '#'): array { $title = $title !== '' ? $title : ('Sản phẩm ' . $id); $linePrice = $price * $quantity; $url = $url !== '' ? $url : '#'; return [ 'id' => $id, 'variant_id' => $id, 'product_id' => $id, 'quantity' => $quantity, 'title' => $title, 'product_title' => $title, 'variant_title' => '', 'price' => $price, 'line_price' => $linePrice, 'image' => $image, 'url' => $url, 'properties' => new stdClass(), ]; } function renderIndexWithInjectedScript(string $indexPath, string $script): void { header('Content-Type: text/html; charset=UTF-8'); $html = file_get_contents($indexPath); if ($html === false) { http_response_code(500); echo 'Missing site files.'; exit; } $needle = ''; $pos = stripos($html, $needle); if ($pos !== false) { $html = substr($html, 0, $pos) . $script . substr($html, $pos); } else { $html .= $script; } echo $html; exit; } function h(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function getCsrfToken(): string { if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token']) || $_SESSION['csrf_token'] === '') { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } function requireCsrfToken(string $token): void { $expected = $_SESSION['csrf_token'] ?? ''; if (!is_string($expected) || $expected === '' || !hash_equals($expected, $token)) { http_response_code(400); echo 'Invalid request.'; exit; } } function getMysqlConfig(): ?array { $host = trim((string)getenv('APP_DB_HOST')); $name = trim((string)getenv('APP_DB_NAME')); $user = trim((string)getenv('APP_DB_USER')); $pass = (string)getenv('APP_DB_PASS'); $port = trim((string)getenv('APP_DB_PORT')); $charset = trim((string)getenv('APP_DB_CHARSET')); if ($host === '' || $name === '' || $user === '') { return null; } if ($port === '') { $port = '3306'; } if ($charset === '') { $charset = 'utf8mb4'; } return [ 'host' => $host, 'port' => $port, 'name' => $name, 'user' => $user, 'pass' => $pass, 'charset' => $charset, ]; } function dbPdo(): ?PDO { static $pdo = null; static $tried = false; if ($tried) { return $pdo; } $tried = true; if (!class_exists('PDO')) { return null; } $drivers = PDO::getAvailableDrivers(); if (!in_array('mysql', $drivers, true)) { return null; } $cfg = getMysqlConfig(); if (!$cfg) { return null; } $dsn = 'mysql:host=' . $cfg['host'] . ';port=' . $cfg['port'] . ';dbname=' . $cfg['name'] . ';charset=' . $cfg['charset']; try { $pdo = new PDO($dsn, $cfg['user'], $cfg['pass'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (Throwable) { $pdo = null; } return $pdo; } function usersFilePath(): string { return __DIR__ . '/storage/users.json'; } function loadUsersFromJson(): array { $path = usersFilePath(); if (!is_file($path)) { return []; } $raw = file_get_contents($path); if ($raw === false || $raw === '') { return []; } $data = json_decode($raw, true); return is_array($data) ? $data : []; } function saveUsersToJson(array $users): void { $path = usersFilePath(); $dir = dirname($path); if (!is_dir($dir)) { mkdir($dir, 0755, true); } file_put_contents($path, json_encode($users, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX); } function ensureUsersTable(PDO $pdo): void { $sql = 'CREATE TABLE IF NOT EXISTS users (' . 'id VARCHAR(32) NOT NULL,' . 'email VARCHAR(190) NOT NULL,' . 'name VARCHAR(190) NOT NULL,' . 'password_hash VARCHAR(255) NOT NULL,' . 'is_admin TINYINT(1) NOT NULL DEFAULT 0,' . 'created_at INT NOT NULL,' . 'updated_at INT NULL,' . 'PRIMARY KEY (id),' . 'UNIQUE KEY uniq_email (email)' . ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'; $pdo->exec($sql); } function mysqlUsersCount(PDO $pdo): int { $stmt = $pdo->query('SELECT COUNT(*) FROM users'); $count = $stmt ? $stmt->fetchColumn() : 0; return (int)$count; } function normalizeUsersArray(array $users): array { $out = []; foreach ($users as $key => $user) { if (!is_array($user)) { continue; } $email = strtolower(trim((string)($user['email'] ?? $key))); if ($email === '') { continue; } $id = (string)($user['id'] ?? ''); if ($id === '') { $id = bin2hex(random_bytes(16)); } $out[$email] = [ 'id' => $id, 'email' => $email, 'name' => (string)($user['name'] ?? ''), 'password_hash' => (string)($user['password_hash'] ?? ''), 'is_admin' => !empty($user['is_admin']), 'created_at' => (int)($user['created_at'] ?? time()), 'updated_at' => isset($user['updated_at']) ? (int)$user['updated_at'] : null, ]; } return $out; } function mysqlLoadUsers(PDO $pdo): array { $stmt = $pdo->query('SELECT id, email, name, password_hash, is_admin, created_at, updated_at FROM users'); $rows = $stmt ? $stmt->fetchAll() : []; $out = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $email = strtolower((string)($row['email'] ?? '')); if ($email === '') { continue; } $out[$email] = [ 'id' => (string)($row['id'] ?? ''), 'email' => $email, 'name' => (string)($row['name'] ?? ''), 'password_hash' => (string)($row['password_hash'] ?? ''), 'is_admin' => !empty($row['is_admin']), 'created_at' => (int)($row['created_at'] ?? 0), 'updated_at' => isset($row['updated_at']) ? (int)$row['updated_at'] : null, ]; } return $out; } function mysqlSaveUsers(PDO $pdo, array $users): void { $users = normalizeUsersArray($users); $pdo->beginTransaction(); try { $existing = $pdo->query('SELECT LOWER(email) AS email FROM users'); $existingEmails = $existing ? $existing->fetchAll(PDO::FETCH_COLUMN) : []; $incomingEmails = array_keys($users); $incomingSet = []; foreach ($incomingEmails as $e) { $incomingSet[$e] = true; } $delete = []; foreach ($existingEmails as $e) { $e = strtolower((string)$e); if ($e !== '' && !isset($incomingSet[$e])) { $delete[] = $e; } } if ($delete) { $placeholders = implode(',', array_fill(0, count($delete), '?')); $delStmt = $pdo->prepare('DELETE FROM users WHERE LOWER(email) IN (' . $placeholders . ')'); $delStmt->execute($delete); } $upsert = $pdo->prepare( 'INSERT INTO users (id, email, name, password_hash, is_admin, created_at, updated_at) ' . 'VALUES (:id, :email, :name, :password_hash, :is_admin, :created_at, :updated_at) ' . 'ON DUPLICATE KEY UPDATE ' . 'name = VALUES(name), ' . 'password_hash = VALUES(password_hash), ' . 'is_admin = VALUES(is_admin), ' . 'created_at = VALUES(created_at), ' . 'updated_at = VALUES(updated_at)' ); foreach ($users as $user) { $upsert->execute([ ':id' => (string)($user['id'] ?? ''), ':email' => (string)($user['email'] ?? ''), ':name' => (string)($user['name'] ?? ''), ':password_hash' => (string)($user['password_hash'] ?? ''), ':is_admin' => !empty($user['is_admin']) ? 1 : 0, ':created_at' => (int)($user['created_at'] ?? time()), ':updated_at' => isset($user['updated_at']) ? (int)$user['updated_at'] : null, ]); } $pdo->commit(); } catch (Throwable $e) { $pdo->rollBack(); throw $e; } } function loadUsers(): array { $pdo = dbPdo(); if (!$pdo) { return normalizeUsersArray(loadUsersFromJson()); } try { ensureUsersTable($pdo); if (mysqlUsersCount($pdo) === 0) { $fromJson = loadUsersFromJson(); if ($fromJson) { mysqlSaveUsers($pdo, $fromJson); } } return mysqlLoadUsers($pdo); } catch (Throwable) { return normalizeUsersArray(loadUsersFromJson()); } } function saveUsers(array $users): void { $pdo = dbPdo(); if (!$pdo) { saveUsersToJson(normalizeUsersArray($users)); return; } try { ensureUsersTable($pdo); mysqlSaveUsers($pdo, $users); } catch (Throwable) { saveUsersToJson(normalizeUsersArray($users)); } } function getLoggedInUser(): ?array { $user = $_SESSION['user'] ?? null; return is_array($user) ? $user : null; } function isAdminUser(?array $user): bool { return is_array($user) && !empty($user['is_admin']); } function setLoggedInUser(array $user): void { $_SESSION['user'] = $user; } function logoutUser(): void { unset($_SESSION['user']); } function requireLogin(string $returnUrl): void { $user = getLoggedInUser(); if ($user) { return; } header('Location: /account/login?ReturnUrl=' . rawurlencode($returnUrl), true, 302); exit; } function requireAdmin(string $returnUrl = '/admin'): void { requireLogin($returnUrl); $user = getLoggedInUser(); if (!isAdminUser($user)) { http_response_code(403); echo 'Forbidden'; exit; } } function sanitizeReturnUrl(?string $returnUrl): string { if ($returnUrl === null) { return '/account'; } $returnUrl = trim($returnUrl); if ($returnUrl === '') { return '/account'; } if (!str_starts_with($returnUrl, '/')) { return '/account'; } if (str_contains($returnUrl, "\n") || str_contains($returnUrl, "\r")) { return '/account'; } if (str_contains($returnUrl, '://')) { return '/account'; } return $returnUrl; } function loadLayoutParts(string $indexPath): array { static $cache = null; if (is_array($cache)) { return $cache; } $html = file_get_contents($indexPath); if ($html === false) { http_response_code(500); echo 'Missing site files.'; exit; } $bodyPos = stripos($html, '', $bodyPos); if ($bodyTagEnd === false) { http_response_code(500); echo 'Invalid site template.'; exit; } $headAndBodyOpen = substr($html, 0, $bodyTagEnd + 1); $bodyClosePos = stripos($html, ''); if ($bodyClosePos === false) { http_response_code(500); echo 'Invalid site template.'; exit; } $bodyInner = substr($html, $bodyTagEnd + 1, $bodyClosePos - ($bodyTagEnd + 1)); $bgHomePos = stripos($bodyInner, '

Dự án

Trang chủ  /  Dự án
Chưa có dự án.
$headAndBodyOpen, 'prefix' => $prefix, 'suffix' => $suffix . $tail, ]; return $cache; } function applyAuthToHtml(string $html): string { $user = getLoggedInUser(); if (!$user) { return $html; } $adminLink = isAdminUser($user) ? 'Admin' : ''; $html = preg_replace( '/(
  • )([\s\S]*?)(<\/div>)/', '$1' . $adminLink . 'Tài khoảnĐăng xuất$3', $html, 1 ) ?? $html; $html = preg_replace( '/('; renderAdminPage($indexPath, 'Quản lý tin tức', 'posts', $html); } if (($path === '/admin/posts/new' || $path === '/admin/posts/edit') && $method === 'GET') { $isEdit = $path === '/admin/posts/edit'; $slug = $isEdit ? (string)($_GET['slug'] ?? '') : ''; $contentStore = loadContent(); $post = $isEdit && $slug !== '' && isset($contentStore['posts'][$slug]) && is_array($contentStore['posts'][$slug]) ? $contentStore['posts'][$slug] : []; if ($isEdit && $slug === '') { header('Location: /admin/posts?error=' . rawurlencode('Thiếu slug'), true, 302); exit; } if ($isEdit && $slug !== '' && $post === []) { header('Location: /admin/posts?error=' . rawurlencode('Không tìm thấy bài viết'), true, 302); exit; } $title = (string)($post['title'] ?? ''); $excerpt = (string)($post['excerpt'] ?? ''); $body = (string)($post['content'] ?? ''); $image = (string)($post['image'] ?? ''); $formSlug = $isEdit ? $slug : (string)($post['slug'] ?? ''); $editorStyle = ''; $editorScript = ''; $imagePreview = $image !== '' ? '
    ' : ''; $html = $flash . $editorStyle . '
    ' . '' . '' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '
    ' . '' . '
    ' . '
    ' . '
    Gợi ý: có thể dán trực tiếp nội dung từ Word vào ô soạn thảo.
    ' . '
    ' . '' . '' . 'Hủy' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    Thiết lập
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    Ảnh đại diện
    ' . $imagePreview . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . $editorScript . '
    '; renderAdminPage($indexPath, $isEdit ? 'Sửa bài viết' : 'Thêm bài viết', 'posts', $html); } if ($path === '/admin/posts/save' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $title = trim((string)($_POST['title'] ?? '')); $slugInput = trim((string)($_POST['slug'] ?? '')); $originalSlug = trim((string)($_POST['original_slug'] ?? '')); $excerpt = trim((string)($_POST['excerpt'] ?? '')); $body = sanitizeRichHtml((string)($_POST['content'] ?? '')); $imageUrl = trim((string)($_POST['image_url'] ?? '')); $uploaded = handleUpload('image_file'); $submitAction = (string)($_POST['submit_action'] ?? 'save'); if ($title === '') { header('Location: /admin/posts?error=' . rawurlencode('Tiêu đề không được để trống'), true, 302); exit; } $slug = $slugInput !== '' ? slugify($slugInput) : slugify($title); if ($slug === '') { header('Location: /admin/posts?error=' . rawurlencode('Không tạo được slug'), true, 302); exit; } $store = loadContent(); if (!is_array($store['posts'])) { $store['posts'] = []; } if ($originalSlug !== '' && $originalSlug !== $slug && isset($store['posts'][$slug])) { header('Location: /admin/posts?error=' . rawurlencode('Slug đã tồn tại'), true, 302); exit; } if ($originalSlug === '' && isset($store['posts'][$slug])) { header('Location: /admin/posts?error=' . rawurlencode('Slug đã tồn tại'), true, 302); exit; } $now = time(); $image = $uploaded !== null ? $uploaded : $imageUrl; $existing = $originalSlug !== '' && isset($store['posts'][$originalSlug]) && is_array($store['posts'][$originalSlug]) ? $store['posts'][$originalSlug] : []; $createdAt = isset($existing['created_at']) ? (int)$existing['created_at'] : $now; $record = [ 'slug' => $slug, 'title' => $title, 'excerpt' => $excerpt, 'content' => $body, 'image' => $image, 'created_at' => $createdAt, 'updated_at' => $now, ]; if ($originalSlug !== '' && $originalSlug !== $slug) { unset($store['posts'][$originalSlug]); } $store['posts'][$slug] = $record; saveContent($store); if ($submitAction === 'save_view') { header('Location: /' . $slug, true, 302); exit; } header('Location: /admin/posts?message=' . rawurlencode('Đã lưu bài viết'), true, 302); exit; } if ($path === '/admin/posts/delete' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $store = loadContent(); if ($slug !== '' && isset($store['posts'][$slug])) { unset($store['posts'][$slug]); saveContent($store); } header('Location: /admin/posts?message=' . rawurlencode('Đã xóa bài viết'), true, 302); exit; } if ($path === '/admin/products' && $method === 'GET') { $content = loadContent(); $products = is_array($content['products']) ? $content['products'] : []; $rows = ''; foreach ($products as $slug => $product) { if (!is_array($product)) { continue; } $title = (string)($product['title'] ?? ''); $excerpt = (string)($product['excerpt'] ?? ''); $price = (int)($product['price'] ?? 0); $priceText = $price > 0 ? number_format($price, 0, ',', '.') . '₫' : ''; $rows .= '' . '/san-pham/' . h((string)$slug) . '' . '' . h($title) . '
    ' . h($excerpt) . '
    ' . '' . h($priceText) . '' . '' . 'Sửa' . '
    ' . '' . '' . '' . '
    ' . '' . ''; } $html = $flash . '
    ' . '
    Sản phẩm
    ' . '+ Thêm sản phẩm' . '
    ' . '
    ' . '' . ($rows !== '' ? $rows : '') . '
    URLTiêu đềGiá
    Chưa có sản phẩm
    ' . '
    '; renderAdminPage($indexPath, 'Quản lý sản phẩm', 'products', $html); } if (($path === '/admin/products/new' || $path === '/admin/products/edit') && $method === 'GET') { $slug = trim((string)($_GET['slug'] ?? '')); $content = loadContent(); $products = is_array($content['products']) ? $content['products'] : []; $product = $slug !== '' && isset($products[$slug]) && is_array($products[$slug]) ? $products[$slug] : []; $titleVal = (string)($product['title'] ?? ''); $slugVal = $slug !== '' ? (string)$slug : ''; $excerptVal = (string)($product['excerpt'] ?? ''); $bodyVal = (string)($product['content'] ?? ''); $imageVal = (string)($product['image'] ?? ''); $priceVal = (string)($product['price'] ?? ''); $preview = $imageVal !== '' ? '
    ' : ''; $html = $flash . '
    ' . ($path === '/admin/products/new' ? 'Thêm sản phẩm' : 'Sửa sản phẩm') . '
    ' . '
    ' . '
    ' . '' . '' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . $preview . '
    ' . '
    ' . '' . 'Hủy' . '
    ' . '
    ' . '
    '; renderAdminPage($indexPath, $path === '/admin/products/new' ? 'Thêm sản phẩm' : 'Sửa sản phẩm', 'products', $html); } if ($path === '/admin/products/save' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $title = trim((string)($_POST['title'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $originalSlug = trim((string)($_POST['original_slug'] ?? '')); $excerpt = trim((string)($_POST['excerpt'] ?? '')); $body = trim((string)($_POST['content'] ?? '')); $image = trim((string)($_POST['image'] ?? '')); $priceRaw = preg_replace('/[^0-9]/', '', (string)($_POST['price'] ?? '')) ?? ''; $price = $priceRaw !== '' ? (int)$priceRaw : 0; if ($title === '') { header('Location: /admin/products?error=' . rawurlencode('Tiêu đề là bắt buộc'), true, 302); exit; } if ($slug === '') { $slug = slugify($title); } else { $slug = slugify($slug); } if ($slug === '') { header('Location: /admin/products?error=' . rawurlencode('Slug không hợp lệ'), true, 302); exit; } $upload = handleUpload('image_upload'); $uploadAttempted = isset($_FILES['image_upload']) && is_array($_FILES['image_upload']) && isset($_FILES['image_upload']['error']) && (int)$_FILES['image_upload']['error'] !== UPLOAD_ERR_NO_FILE; if ($uploadAttempted && $upload === null) { $target = $originalSlug !== '' ? ('/admin/products/edit?slug=' . rawurlencode($originalSlug)) : '/admin/products/new'; $join = str_contains($target, '?') ? '&' : '?'; header('Location: ' . $target . $join . 'error=' . rawurlencode('Upload ảnh thất bại. Vui lòng dùng ảnh JPG/PNG/GIF/WEBP và dung lượng <= 10MB.'), true, 302); exit; } if (is_string($upload) && $upload !== '') { $image = $upload; } $store = loadContent(); if (!isset($store['products']) || !is_array($store['products'])) { $store['products'] = []; } $existing = isset($store['products'][$slug]) && is_array($store['products'][$slug]) ? $store['products'][$slug] : []; $createdAt = isset($existing['created_at']) ? (int)$existing['created_at'] : time(); $now = time(); $record = [ 'title' => $title, 'excerpt' => $excerpt, 'content' => $body, 'image' => $image, 'price' => $price, 'created_at' => $createdAt, 'updated_at' => $now, ]; if ($originalSlug !== '' && $originalSlug !== $slug) { unset($store['products'][$originalSlug]); } $store['products'][$slug] = $record; saveContent($store); header('Location: /admin/products?message=' . rawurlencode('Đã lưu sản phẩm'), true, 302); exit; } if ($path === '/admin/products/delete' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $store = loadContent(); if ($slug !== '' && isset($store['products'][$slug])) { unset($store['products'][$slug]); saveContent($store); } header('Location: /admin/products?message=' . rawurlencode('Đã xóa sản phẩm'), true, 302); exit; } if ($path === '/admin/projects' && $method === 'GET') { $content = loadContent(); $projects = is_array($content['projects']) ? $content['projects'] : []; $rows = ''; foreach ($projects as $slug => $project) { if (!is_array($project)) { continue; } $title = (string)($project['title'] ?? ''); $excerpt = (string)($project['excerpt'] ?? ''); $rows .= '' . '/du-an/' . h((string)$slug) . '' . '' . h($title) . '
    ' . h($excerpt) . '
    ' . '' . 'Sửa' . '
    ' . '' . '' . '' . '
    ' . '' . ''; } $html = $flash . '
    ' . '
    Dự án
    ' . 'Thêm dự án' . '
    ' . '
    ' . '' . ($rows !== '' ? $rows : '') . '
    SlugTiêu đề
    Chưa có dự án
    ' . '
    '; renderAdminPage($indexPath, 'Quản lý dự án', 'projects', $html); } if (($path === '/admin/projects/new' || $path === '/admin/projects/edit') && $method === 'GET') { $isEdit = $path === '/admin/projects/edit'; $slug = $isEdit ? (string)($_GET['slug'] ?? '') : ''; $contentStore = loadContent(); $project = $isEdit && $slug !== '' && isset($contentStore['projects'][$slug]) && is_array($contentStore['projects'][$slug]) ? $contentStore['projects'][$slug] : []; if ($isEdit && $slug === '') { header('Location: /admin/projects?error=' . rawurlencode('Thiếu slug'), true, 302); exit; } if ($isEdit && $slug !== '' && $project === []) { header('Location: /admin/projects?error=' . rawurlencode('Không tìm thấy dự án'), true, 302); exit; } $title = (string)($project['title'] ?? ''); $excerpt = (string)($project['excerpt'] ?? ''); $body = (string)($project['content'] ?? ''); $image = (string)($project['image'] ?? ''); $formSlug = $isEdit ? $slug : (string)($project['slug'] ?? ''); $html = $flash . '
    ' . '
    ' . '' . '' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . ' ' . 'Hủy' . '
    ' . '
    '; renderAdminPage($indexPath, $isEdit ? 'Sửa dự án' : 'Thêm dự án', 'projects', $html); } if ($path === '/admin/projects/save' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $title = trim((string)($_POST['title'] ?? '')); $slugInput = trim((string)($_POST['slug'] ?? '')); $originalSlug = trim((string)($_POST['original_slug'] ?? '')); $excerpt = trim((string)($_POST['excerpt'] ?? '')); $body = (string)($_POST['content'] ?? ''); $imageUrl = trim((string)($_POST['image_url'] ?? '')); $uploaded = handleUpload('image_file'); if ($title === '') { header('Location: /admin/projects?error=' . rawurlencode('Tiêu đề không được để trống'), true, 302); exit; } $slug = $slugInput !== '' ? slugify($slugInput) : slugify($title); if ($slug === '') { header('Location: /admin/projects?error=' . rawurlencode('Không tạo được slug'), true, 302); exit; } $store = loadContent(); if (!is_array($store['projects'])) { $store['projects'] = []; } if ($originalSlug !== '' && $originalSlug !== $slug && isset($store['projects'][$slug])) { header('Location: /admin/projects?error=' . rawurlencode('Slug đã tồn tại'), true, 302); exit; } if ($originalSlug === '' && isset($store['projects'][$slug])) { header('Location: /admin/projects?error=' . rawurlencode('Slug đã tồn tại'), true, 302); exit; } $now = time(); $image = $uploaded !== null ? $uploaded : $imageUrl; $existing = $originalSlug !== '' && isset($store['projects'][$originalSlug]) && is_array($store['projects'][$originalSlug]) ? $store['projects'][$originalSlug] : []; $createdAt = isset($existing['created_at']) ? (int)$existing['created_at'] : $now; $record = [ 'slug' => $slug, 'title' => $title, 'excerpt' => $excerpt, 'content' => $body, 'image' => $image, 'created_at' => $createdAt, 'updated_at' => $now, ]; if ($originalSlug !== '' && $originalSlug !== $slug) { unset($store['projects'][$originalSlug]); } $store['projects'][$slug] = $record; saveContent($store); header('Location: /admin/projects?message=' . rawurlencode('Đã lưu dự án'), true, 302); exit; } if ($path === '/admin/projects/delete' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $store = loadContent(); if ($slug !== '' && isset($store['projects'][$slug])) { unset($store['projects'][$slug]); saveContent($store); } header('Location: /admin/projects?message=' . rawurlencode('Đã xóa dự án'), true, 302); exit; } if ($path === '/admin/users' && $method === 'GET') { $users = loadUsers(); $rows = ''; foreach ($users as $email => $user) { if (!is_array($user)) { continue; } $isAdmin = !empty($user['is_admin']); $rows .= '' . '' . h((string)($user['name'] ?? '')) . '' . '' . h((string)$email) . '' . '' . ($isAdmin ? 'Admin' : 'User') . '' . '' . '
    ' . '' . '' . '' . '' . '
    ' . '
    ' . '' . '' . '' . '' . '
    ' . '
    ' . '' . '' . '' . '
    ' . '' . ''; } $html = $flash . '
    Tài khoản
    ' . '
    ' . '' . ($rows !== '' ? $rows : '') . '
    Họ tênEmailQuyền
    Chưa có tài khoản
    ' . '
    '; renderAdminPage($indexPath, 'Quản lý tài khoản', 'users', $html); } if ($path === '/admin/users/role' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $email = strtolower(trim((string)($_POST['email'] ?? ''))); $action = (string)($_POST['action'] ?? ''); $users = loadUsers(); if (!isset($users[$email]) || !is_array($users[$email])) { header('Location: /admin/users?error=' . rawurlencode('Không tìm thấy tài khoản'), true, 302); exit; } if ($action === 'promote') { $users[$email]['is_admin'] = true; } elseif ($action === 'demote') { $users[$email]['is_admin'] = false; } saveUsers($users); header('Location: /admin/users?message=' . rawurlencode('Đã cập nhật quyền'), true, 302); exit; } if ($path === '/admin/users/reset-password' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $email = strtolower(trim((string)($_POST['email'] ?? ''))); $newPassword = (string)($_POST['new_password'] ?? ''); if (strlen($newPassword) < 6) { header('Location: /admin/users?error=' . rawurlencode('Mật khẩu phải từ 6 ký tự'), true, 302); exit; } $users = loadUsers(); if (!isset($users[$email]) || !is_array($users[$email])) { header('Location: /admin/users?error=' . rawurlencode('Không tìm thấy tài khoản'), true, 302); exit; } $users[$email]['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT); $users[$email]['updated_at'] = time(); saveUsers($users); header('Location: /admin/users?message=' . rawurlencode('Đã đổi mật khẩu'), true, 302); exit; } if ($path === '/admin/users/delete' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $email = strtolower(trim((string)($_POST['email'] ?? ''))); $session = getLoggedInUser(); $selfEmail = is_array($session) ? strtolower((string)($session['email'] ?? '')) : ''; if ($email !== '' && $email === $selfEmail) { header('Location: /admin/users?error=' . rawurlencode('Không thể xóa tài khoản đang đăng nhập'), true, 302); exit; } $users = loadUsers(); if ($email !== '' && isset($users[$email])) { unset($users[$email]); saveUsers($users); } header('Location: /admin/users?message=' . rawurlencode('Đã xóa tài khoản'), true, 302); exit; } if ($path === '/admin/settings' && $method === 'GET') { $s = getSiteSettings(); $html = $flash . '
    Cài đặt website
    ' . '
    ' . '
    ' . '' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    '; $pos = stripos($html, $needle); if ($pos !== false) { $html = substr($html, 0, $pos) . $script . substr($html, $pos); } else { $html .= $script; } echo $html; exit; } function h(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function getCsrfToken(): string { if (!isset($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token']) || $_SESSION['csrf_token'] === '') { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } function requireCsrfToken(string $token): void { $expected = $_SESSION['csrf_token'] ?? ''; if (!is_string($expected) || $expected === '' || !hash_equals($expected, $token)) { http_response_code(400); echo 'Invalid request.'; exit; } } function getMysqlConfig(): ?array { $host = trim((string)getenv('APP_DB_HOST')); $name = trim((string)getenv('APP_DB_NAME')); $user = trim((string)getenv('APP_DB_USER')); $pass = (string)getenv('APP_DB_PASS'); $port = trim((string)getenv('APP_DB_PORT')); $charset = trim((string)getenv('APP_DB_CHARSET')); if ($host === '' || $name === '' || $user === '') { return null; } if ($port === '') { $port = '3306'; } if ($charset === '') { $charset = 'utf8mb4'; } return [ 'host' => $host, 'port' => $port, 'name' => $name, 'user' => $user, 'pass' => $pass, 'charset' => $charset, ]; } function dbPdo(): ?PDO { static $pdo = null; static $tried = false; if ($tried) { return $pdo; } $tried = true; if (!class_exists('PDO')) { return null; } $drivers = PDO::getAvailableDrivers(); if (!in_array('mysql', $drivers, true)) { return null; } $cfg = getMysqlConfig(); if (!$cfg) { return null; } $dsn = 'mysql:host=' . $cfg['host'] . ';port=' . $cfg['port'] . ';dbname=' . $cfg['name'] . ';charset=' . $cfg['charset']; try { $pdo = new PDO($dsn, $cfg['user'], $cfg['pass'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (Throwable) { $pdo = null; } return $pdo; } function usersFilePath(): string { return __DIR__ . '/storage/users.json'; } function loadUsersFromJson(): array { $path = usersFilePath(); if (!is_file($path)) { return []; } $raw = file_get_contents($path); if ($raw === false || $raw === '') { return []; } $data = json_decode($raw, true); return is_array($data) ? $data : []; } function saveUsersToJson(array $users): void { $path = usersFilePath(); $dir = dirname($path); if (!is_dir($dir)) { mkdir($dir, 0755, true); } file_put_contents($path, json_encode($users, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX); } function ensureUsersTable(PDO $pdo): void { $sql = 'CREATE TABLE IF NOT EXISTS users (' . 'id VARCHAR(32) NOT NULL,' . 'email VARCHAR(190) NOT NULL,' . 'name VARCHAR(190) NOT NULL,' . 'password_hash VARCHAR(255) NOT NULL,' . 'is_admin TINYINT(1) NOT NULL DEFAULT 0,' . 'created_at INT NOT NULL,' . 'updated_at INT NULL,' . 'PRIMARY KEY (id),' . 'UNIQUE KEY uniq_email (email)' . ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'; $pdo->exec($sql); } function mysqlUsersCount(PDO $pdo): int { $stmt = $pdo->query('SELECT COUNT(*) FROM users'); $count = $stmt ? $stmt->fetchColumn() : 0; return (int)$count; } function normalizeUsersArray(array $users): array { $out = []; foreach ($users as $key => $user) { if (!is_array($user)) { continue; } $email = strtolower(trim((string)($user['email'] ?? $key))); if ($email === '') { continue; } $id = (string)($user['id'] ?? ''); if ($id === '') { $id = bin2hex(random_bytes(16)); } $out[$email] = [ 'id' => $id, 'email' => $email, 'name' => (string)($user['name'] ?? ''), 'password_hash' => (string)($user['password_hash'] ?? ''), 'is_admin' => !empty($user['is_admin']), 'created_at' => (int)($user['created_at'] ?? time()), 'updated_at' => isset($user['updated_at']) ? (int)$user['updated_at'] : null, ]; } return $out; } function mysqlLoadUsers(PDO $pdo): array { $stmt = $pdo->query('SELECT id, email, name, password_hash, is_admin, created_at, updated_at FROM users'); $rows = $stmt ? $stmt->fetchAll() : []; $out = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $email = strtolower((string)($row['email'] ?? '')); if ($email === '') { continue; } $out[$email] = [ 'id' => (string)($row['id'] ?? ''), 'email' => $email, 'name' => (string)($row['name'] ?? ''), 'password_hash' => (string)($row['password_hash'] ?? ''), 'is_admin' => !empty($row['is_admin']), 'created_at' => (int)($row['created_at'] ?? 0), 'updated_at' => isset($row['updated_at']) ? (int)$row['updated_at'] : null, ]; } return $out; } function mysqlSaveUsers(PDO $pdo, array $users): void { $users = normalizeUsersArray($users); $pdo->beginTransaction(); try { $existing = $pdo->query('SELECT LOWER(email) AS email FROM users'); $existingEmails = $existing ? $existing->fetchAll(PDO::FETCH_COLUMN) : []; $incomingEmails = array_keys($users); $incomingSet = []; foreach ($incomingEmails as $e) { $incomingSet[$e] = true; } $delete = []; foreach ($existingEmails as $e) { $e = strtolower((string)$e); if ($e !== '' && !isset($incomingSet[$e])) { $delete[] = $e; } } if ($delete) { $placeholders = implode(',', array_fill(0, count($delete), '?')); $delStmt = $pdo->prepare('DELETE FROM users WHERE LOWER(email) IN (' . $placeholders . ')'); $delStmt->execute($delete); } $upsert = $pdo->prepare( 'INSERT INTO users (id, email, name, password_hash, is_admin, created_at, updated_at) ' . 'VALUES (:id, :email, :name, :password_hash, :is_admin, :created_at, :updated_at) ' . 'ON DUPLICATE KEY UPDATE ' . 'name = VALUES(name), ' . 'password_hash = VALUES(password_hash), ' . 'is_admin = VALUES(is_admin), ' . 'created_at = VALUES(created_at), ' . 'updated_at = VALUES(updated_at)' ); foreach ($users as $user) { $upsert->execute([ ':id' => (string)($user['id'] ?? ''), ':email' => (string)($user['email'] ?? ''), ':name' => (string)($user['name'] ?? ''), ':password_hash' => (string)($user['password_hash'] ?? ''), ':is_admin' => !empty($user['is_admin']) ? 1 : 0, ':created_at' => (int)($user['created_at'] ?? time()), ':updated_at' => isset($user['updated_at']) ? (int)$user['updated_at'] : null, ]); } $pdo->commit(); } catch (Throwable $e) { $pdo->rollBack(); throw $e; } } function loadUsers(): array { $pdo = dbPdo(); if (!$pdo) { return normalizeUsersArray(loadUsersFromJson()); } try { ensureUsersTable($pdo); if (mysqlUsersCount($pdo) === 0) { $fromJson = loadUsersFromJson(); if ($fromJson) { mysqlSaveUsers($pdo, $fromJson); } } return mysqlLoadUsers($pdo); } catch (Throwable) { return normalizeUsersArray(loadUsersFromJson()); } } function saveUsers(array $users): void { $pdo = dbPdo(); if (!$pdo) { saveUsersToJson(normalizeUsersArray($users)); return; } try { ensureUsersTable($pdo); mysqlSaveUsers($pdo, $users); } catch (Throwable) { saveUsersToJson(normalizeUsersArray($users)); } } function getLoggedInUser(): ?array { $user = $_SESSION['user'] ?? null; return is_array($user) ? $user : null; } function isAdminUser(?array $user): bool { return is_array($user) && !empty($user['is_admin']); } function setLoggedInUser(array $user): void { $_SESSION['user'] = $user; } function logoutUser(): void { unset($_SESSION['user']); } function requireLogin(string $returnUrl): void { $user = getLoggedInUser(); if ($user) { return; } header('Location: /account/login?ReturnUrl=' . rawurlencode($returnUrl), true, 302); exit; } function requireAdmin(string $returnUrl = '/admin'): void { requireLogin($returnUrl); $user = getLoggedInUser(); if (!isAdminUser($user)) { http_response_code(403); echo 'Forbidden'; exit; } } function sanitizeReturnUrl(?string $returnUrl): string { if ($returnUrl === null) { return '/account'; } $returnUrl = trim($returnUrl); if ($returnUrl === '') { return '/account'; } if (!str_starts_with($returnUrl, '/')) { return '/account'; } if (str_contains($returnUrl, "\n") || str_contains($returnUrl, "\r")) { return '/account'; } if (str_contains($returnUrl, '://')) { return '/account'; } return $returnUrl; } function loadLayoutParts(string $indexPath): array { static $cache = null; if (is_array($cache)) { return $cache; } $html = file_get_contents($indexPath); if ($html === false) { http_response_code(500); echo 'Missing site files.'; exit; } $bodyPos = stripos($html, '', $bodyPos); if ($bodyTagEnd === false) { http_response_code(500); echo 'Invalid site template.'; exit; } $headAndBodyOpen = substr($html, 0, $bodyTagEnd + 1); $bodyClosePos = stripos($html, ''); if ($bodyClosePos === false) { http_response_code(500); echo 'Invalid site template.'; exit; } $bodyInner = substr($html, $bodyTagEnd + 1, $bodyClosePos - ($bodyTagEnd + 1)); $bgHomePos = stripos($bodyInner, '
    '); if ($bgHomePos === false) { http_response_code(500); echo 'Invalid site template.'; exit; } $prefix = substr($bodyInner, 0, $bgHomePos); $footerPos = stripos($bodyInner, ' $headAndBodyOpen, 'prefix' => $prefix, 'suffix' => $suffix . $tail, ]; return $cache; } function applyAuthToHtml(string $html): string { $user = getLoggedInUser(); if (!$user) { return $html; } $adminLink = isAdminUser($user) ? 'Admin' : ''; $html = preg_replace( '/(
  • )([\s\S]*?)(<\/div>)/', '$1' . $adminLink . 'Tài khoảnĐăng xuất$3', $html, 1 ) ?? $html; $html = preg_replace( '/('; renderAdminPage($indexPath, 'Quản lý tin tức', 'posts', $html); } if (($path === '/admin/posts/new' || $path === '/admin/posts/edit') && $method === 'GET') { $isEdit = $path === '/admin/posts/edit'; $slug = $isEdit ? (string)($_GET['slug'] ?? '') : ''; $contentStore = loadContent(); $post = $isEdit && $slug !== '' && isset($contentStore['posts'][$slug]) && is_array($contentStore['posts'][$slug]) ? $contentStore['posts'][$slug] : []; if ($isEdit && $slug === '') { header('Location: /admin/posts?error=' . rawurlencode('Thiếu slug'), true, 302); exit; } if ($isEdit && $slug !== '' && $post === []) { header('Location: /admin/posts?error=' . rawurlencode('Không tìm thấy bài viết'), true, 302); exit; } $title = (string)($post['title'] ?? ''); $excerpt = (string)($post['excerpt'] ?? ''); $body = (string)($post['content'] ?? ''); $image = (string)($post['image'] ?? ''); $formSlug = $isEdit ? $slug : (string)($post['slug'] ?? ''); $editorStyle = ''; $editorScript = ''; $imagePreview = $image !== '' ? '
    ' : ''; $html = $flash . $editorStyle . '
    ' . '' . '' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '
    ' . '' . '
    ' . '
    ' . '
    Gợi ý: có thể dán trực tiếp nội dung từ Word vào ô soạn thảo.
    ' . '
    ' . '' . '' . 'Hủy' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    Thiết lập
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    Ảnh đại diện
    ' . $imagePreview . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . $editorScript . '
    '; renderAdminPage($indexPath, $isEdit ? 'Sửa bài viết' : 'Thêm bài viết', 'posts', $html); } if ($path === '/admin/posts/save' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $title = trim((string)($_POST['title'] ?? '')); $slugInput = trim((string)($_POST['slug'] ?? '')); $originalSlug = trim((string)($_POST['original_slug'] ?? '')); $excerpt = trim((string)($_POST['excerpt'] ?? '')); $body = sanitizeRichHtml((string)($_POST['content'] ?? '')); $imageUrl = trim((string)($_POST['image_url'] ?? '')); $uploaded = handleUpload('image_file'); $submitAction = (string)($_POST['submit_action'] ?? 'save'); if ($title === '') { header('Location: /admin/posts?error=' . rawurlencode('Tiêu đề không được để trống'), true, 302); exit; } $slug = $slugInput !== '' ? slugify($slugInput) : slugify($title); if ($slug === '') { header('Location: /admin/posts?error=' . rawurlencode('Không tạo được slug'), true, 302); exit; } $store = loadContent(); if (!is_array($store['posts'])) { $store['posts'] = []; } if ($originalSlug !== '' && $originalSlug !== $slug && isset($store['posts'][$slug])) { header('Location: /admin/posts?error=' . rawurlencode('Slug đã tồn tại'), true, 302); exit; } if ($originalSlug === '' && isset($store['posts'][$slug])) { header('Location: /admin/posts?error=' . rawurlencode('Slug đã tồn tại'), true, 302); exit; } $now = time(); $image = $uploaded !== null ? $uploaded : $imageUrl; $existing = $originalSlug !== '' && isset($store['posts'][$originalSlug]) && is_array($store['posts'][$originalSlug]) ? $store['posts'][$originalSlug] : []; $createdAt = isset($existing['created_at']) ? (int)$existing['created_at'] : $now; $record = [ 'slug' => $slug, 'title' => $title, 'excerpt' => $excerpt, 'content' => $body, 'image' => $image, 'created_at' => $createdAt, 'updated_at' => $now, ]; if ($originalSlug !== '' && $originalSlug !== $slug) { unset($store['posts'][$originalSlug]); } $store['posts'][$slug] = $record; saveContent($store); if ($submitAction === 'save_view') { header('Location: /' . $slug, true, 302); exit; } header('Location: /admin/posts?message=' . rawurlencode('Đã lưu bài viết'), true, 302); exit; } if ($path === '/admin/posts/delete' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $store = loadContent(); if ($slug !== '' && isset($store['posts'][$slug])) { unset($store['posts'][$slug]); saveContent($store); } header('Location: /admin/posts?message=' . rawurlencode('Đã xóa bài viết'), true, 302); exit; } if ($path === '/admin/products' && $method === 'GET') { $content = loadContent(); $products = is_array($content['products']) ? $content['products'] : []; $rows = ''; foreach ($products as $slug => $product) { if (!is_array($product)) { continue; } $title = (string)($product['title'] ?? ''); $excerpt = (string)($product['excerpt'] ?? ''); $price = (int)($product['price'] ?? 0); $priceText = $price > 0 ? number_format($price, 0, ',', '.') . '₫' : ''; $rows .= '' . '/san-pham/' . h((string)$slug) . '' . '' . h($title) . '
    ' . h($excerpt) . '
    ' . '' . h($priceText) . '' . '' . 'Sửa' . '
    ' . '' . '' . '' . '
    ' . '' . ''; } $html = $flash . '
    ' . '
    Sản phẩm
    ' . '+ Thêm sản phẩm' . '
    ' . '
    ' . '' . ($rows !== '' ? $rows : '') . '
    URLTiêu đềGiá
    Chưa có sản phẩm
    ' . '
    '; renderAdminPage($indexPath, 'Quản lý sản phẩm', 'products', $html); } if (($path === '/admin/products/new' || $path === '/admin/products/edit') && $method === 'GET') { $slug = trim((string)($_GET['slug'] ?? '')); $content = loadContent(); $products = is_array($content['products']) ? $content['products'] : []; $product = $slug !== '' && isset($products[$slug]) && is_array($products[$slug]) ? $products[$slug] : []; $titleVal = (string)($product['title'] ?? ''); $slugVal = $slug !== '' ? (string)$slug : ''; $excerptVal = (string)($product['excerpt'] ?? ''); $bodyVal = (string)($product['content'] ?? ''); $imageVal = (string)($product['image'] ?? ''); $priceVal = (string)($product['price'] ?? ''); $preview = $imageVal !== '' ? '
    ' : ''; $html = $flash . '
    ' . ($path === '/admin/products/new' ? 'Thêm sản phẩm' : 'Sửa sản phẩm') . '
    ' . '
    ' . '
    ' . '' . '' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . $preview . '
    ' . '
    ' . '' . 'Hủy' . '
    ' . '
    ' . '
    '; renderAdminPage($indexPath, $path === '/admin/products/new' ? 'Thêm sản phẩm' : 'Sửa sản phẩm', 'products', $html); } if ($path === '/admin/products/save' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $title = trim((string)($_POST['title'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $originalSlug = trim((string)($_POST['original_slug'] ?? '')); $excerpt = trim((string)($_POST['excerpt'] ?? '')); $body = trim((string)($_POST['content'] ?? '')); $image = trim((string)($_POST['image'] ?? '')); $priceRaw = preg_replace('/[^0-9]/', '', (string)($_POST['price'] ?? '')) ?? ''; $price = $priceRaw !== '' ? (int)$priceRaw : 0; if ($title === '') { header('Location: /admin/products?error=' . rawurlencode('Tiêu đề là bắt buộc'), true, 302); exit; } if ($slug === '') { $slug = slugify($title); } else { $slug = slugify($slug); } if ($slug === '') { header('Location: /admin/products?error=' . rawurlencode('Slug không hợp lệ'), true, 302); exit; } $upload = handleUpload('image_upload'); $uploadAttempted = isset($_FILES['image_upload']) && is_array($_FILES['image_upload']) && isset($_FILES['image_upload']['error']) && (int)$_FILES['image_upload']['error'] !== UPLOAD_ERR_NO_FILE; if ($uploadAttempted && $upload === null) { $target = $originalSlug !== '' ? ('/admin/products/edit?slug=' . rawurlencode($originalSlug)) : '/admin/products/new'; $join = str_contains($target, '?') ? '&' : '?'; header('Location: ' . $target . $join . 'error=' . rawurlencode('Upload ảnh thất bại. Vui lòng dùng ảnh JPG/PNG/GIF/WEBP và dung lượng <= 10MB.'), true, 302); exit; } if (is_string($upload) && $upload !== '') { $image = $upload; } $store = loadContent(); if (!isset($store['products']) || !is_array($store['products'])) { $store['products'] = []; } $existing = isset($store['products'][$slug]) && is_array($store['products'][$slug]) ? $store['products'][$slug] : []; $createdAt = isset($existing['created_at']) ? (int)$existing['created_at'] : time(); $now = time(); $record = [ 'title' => $title, 'excerpt' => $excerpt, 'content' => $body, 'image' => $image, 'price' => $price, 'created_at' => $createdAt, 'updated_at' => $now, ]; if ($originalSlug !== '' && $originalSlug !== $slug) { unset($store['products'][$originalSlug]); } $store['products'][$slug] = $record; saveContent($store); header('Location: /admin/products?message=' . rawurlencode('Đã lưu sản phẩm'), true, 302); exit; } if ($path === '/admin/products/delete' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $store = loadContent(); if ($slug !== '' && isset($store['products'][$slug])) { unset($store['products'][$slug]); saveContent($store); } header('Location: /admin/products?message=' . rawurlencode('Đã xóa sản phẩm'), true, 302); exit; } if ($path === '/admin/projects' && $method === 'GET') { $content = loadContent(); $projects = is_array($content['projects']) ? $content['projects'] : []; $rows = ''; foreach ($projects as $slug => $project) { if (!is_array($project)) { continue; } $title = (string)($project['title'] ?? ''); $excerpt = (string)($project['excerpt'] ?? ''); $rows .= '' . '/du-an/' . h((string)$slug) . '' . '' . h($title) . '
    ' . h($excerpt) . '
    ' . '' . 'Sửa' . '
    ' . '' . '' . '' . '
    ' . '' . ''; } $html = $flash . '
    ' . '
    Dự án
    ' . 'Thêm dự án' . '
    ' . '
    ' . '' . ($rows !== '' ? $rows : '') . '
    SlugTiêu đề
    Chưa có dự án
    ' . '
    '; renderAdminPage($indexPath, 'Quản lý dự án', 'projects', $html); } if (($path === '/admin/projects/new' || $path === '/admin/projects/edit') && $method === 'GET') { $isEdit = $path === '/admin/projects/edit'; $slug = $isEdit ? (string)($_GET['slug'] ?? '') : ''; $contentStore = loadContent(); $project = $isEdit && $slug !== '' && isset($contentStore['projects'][$slug]) && is_array($contentStore['projects'][$slug]) ? $contentStore['projects'][$slug] : []; if ($isEdit && $slug === '') { header('Location: /admin/projects?error=' . rawurlencode('Thiếu slug'), true, 302); exit; } if ($isEdit && $slug !== '' && $project === []) { header('Location: /admin/projects?error=' . rawurlencode('Không tìm thấy dự án'), true, 302); exit; } $title = (string)($project['title'] ?? ''); $excerpt = (string)($project['excerpt'] ?? ''); $body = (string)($project['content'] ?? ''); $image = (string)($project['image'] ?? ''); $formSlug = $isEdit ? $slug : (string)($project['slug'] ?? ''); $html = $flash . '
    ' . '
    ' . '' . '' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . ' ' . 'Hủy' . '
    ' . '
    '; renderAdminPage($indexPath, $isEdit ? 'Sửa dự án' : 'Thêm dự án', 'projects', $html); } if ($path === '/admin/projects/save' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $title = trim((string)($_POST['title'] ?? '')); $slugInput = trim((string)($_POST['slug'] ?? '')); $originalSlug = trim((string)($_POST['original_slug'] ?? '')); $excerpt = trim((string)($_POST['excerpt'] ?? '')); $body = (string)($_POST['content'] ?? ''); $imageUrl = trim((string)($_POST['image_url'] ?? '')); $uploaded = handleUpload('image_file'); if ($title === '') { header('Location: /admin/projects?error=' . rawurlencode('Tiêu đề không được để trống'), true, 302); exit; } $slug = $slugInput !== '' ? slugify($slugInput) : slugify($title); if ($slug === '') { header('Location: /admin/projects?error=' . rawurlencode('Không tạo được slug'), true, 302); exit; } $store = loadContent(); if (!is_array($store['projects'])) { $store['projects'] = []; } if ($originalSlug !== '' && $originalSlug !== $slug && isset($store['projects'][$slug])) { header('Location: /admin/projects?error=' . rawurlencode('Slug đã tồn tại'), true, 302); exit; } if ($originalSlug === '' && isset($store['projects'][$slug])) { header('Location: /admin/projects?error=' . rawurlencode('Slug đã tồn tại'), true, 302); exit; } $now = time(); $image = $uploaded !== null ? $uploaded : $imageUrl; $existing = $originalSlug !== '' && isset($store['projects'][$originalSlug]) && is_array($store['projects'][$originalSlug]) ? $store['projects'][$originalSlug] : []; $createdAt = isset($existing['created_at']) ? (int)$existing['created_at'] : $now; $record = [ 'slug' => $slug, 'title' => $title, 'excerpt' => $excerpt, 'content' => $body, 'image' => $image, 'created_at' => $createdAt, 'updated_at' => $now, ]; if ($originalSlug !== '' && $originalSlug !== $slug) { unset($store['projects'][$originalSlug]); } $store['projects'][$slug] = $record; saveContent($store); header('Location: /admin/projects?message=' . rawurlencode('Đã lưu dự án'), true, 302); exit; } if ($path === '/admin/projects/delete' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $slug = trim((string)($_POST['slug'] ?? '')); $store = loadContent(); if ($slug !== '' && isset($store['projects'][$slug])) { unset($store['projects'][$slug]); saveContent($store); } header('Location: /admin/projects?message=' . rawurlencode('Đã xóa dự án'), true, 302); exit; } if ($path === '/admin/users' && $method === 'GET') { $users = loadUsers(); $rows = ''; foreach ($users as $email => $user) { if (!is_array($user)) { continue; } $isAdmin = !empty($user['is_admin']); $rows .= '' . '' . h((string)($user['name'] ?? '')) . '' . '' . h((string)$email) . '' . '' . ($isAdmin ? 'Admin' : 'User') . '' . '' . '
    ' . '' . '' . '' . '' . '
    ' . '
    ' . '' . '' . '' . '' . '
    ' . '
    ' . '' . '' . '' . '
    ' . '' . ''; } $html = $flash . '
    Tài khoản
    ' . '
    ' . '' . ($rows !== '' ? $rows : '') . '
    Họ tênEmailQuyền
    Chưa có tài khoản
    ' . '
    '; renderAdminPage($indexPath, 'Quản lý tài khoản', 'users', $html); } if ($path === '/admin/users/role' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $email = strtolower(trim((string)($_POST['email'] ?? ''))); $action = (string)($_POST['action'] ?? ''); $users = loadUsers(); if (!isset($users[$email]) || !is_array($users[$email])) { header('Location: /admin/users?error=' . rawurlencode('Không tìm thấy tài khoản'), true, 302); exit; } if ($action === 'promote') { $users[$email]['is_admin'] = true; } elseif ($action === 'demote') { $users[$email]['is_admin'] = false; } saveUsers($users); header('Location: /admin/users?message=' . rawurlencode('Đã cập nhật quyền'), true, 302); exit; } if ($path === '/admin/users/reset-password' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $email = strtolower(trim((string)($_POST['email'] ?? ''))); $newPassword = (string)($_POST['new_password'] ?? ''); if (strlen($newPassword) < 6) { header('Location: /admin/users?error=' . rawurlencode('Mật khẩu phải từ 6 ký tự'), true, 302); exit; } $users = loadUsers(); if (!isset($users[$email]) || !is_array($users[$email])) { header('Location: /admin/users?error=' . rawurlencode('Không tìm thấy tài khoản'), true, 302); exit; } $users[$email]['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT); $users[$email]['updated_at'] = time(); saveUsers($users); header('Location: /admin/users?message=' . rawurlencode('Đã đổi mật khẩu'), true, 302); exit; } if ($path === '/admin/users/delete' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $email = strtolower(trim((string)($_POST['email'] ?? ''))); $session = getLoggedInUser(); $selfEmail = is_array($session) ? strtolower((string)($session['email'] ?? '')) : ''; if ($email !== '' && $email === $selfEmail) { header('Location: /admin/users?error=' . rawurlencode('Không thể xóa tài khoản đang đăng nhập'), true, 302); exit; } $users = loadUsers(); if ($email !== '' && isset($users[$email])) { unset($users[$email]); saveUsers($users); } header('Location: /admin/users?message=' . rawurlencode('Đã xóa tài khoản'), true, 302); exit; } if ($path === '/admin/settings' && $method === 'GET') { $s = getSiteSettings(); $html = $flash . '
    Cài đặt website
    ' . '
    ' . '
    ' . '' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '
    ' . '' . 'Quay lại' . '
    ' . '
    ' . '
    '; renderAdminPage($indexPath, 'Cài đặt', 'settings', $html); } if ($path === '/admin/settings/save' && $method === 'POST') { requireCsrfToken((string)($_POST['csrf'] ?? '')); $store = loadContent(); if (!isset($store['settings']) || !is_array($store['settings'])) { $store['settings'] = []; } $keys = [ 'company_name', 'address', 'email', 'phone', 'about_top_title', 'about_title', 'about_desc', 'footer_services_html', 'footer_projects_html', 'newsletter_title', 'newsletter_subtitle', 'newsletter_placeholder', 'newsletter_button', 'copyright_html', ]; foreach ($keys as $k) { $store['settings'][$k] = trim((string)($_POST[$k] ?? '')); } saveContent($store); header('Location: /admin/settings?message=' . rawurlencode('Đã lưu cài đặt'), true, 302); exit; } header('Location: /admin', true, 302); exit; } if ($path === '/tin-tuc' && $method === 'GET') { $store = loadContent(); $posts = is_array($store['posts']) ? $store['posts'] : []; $items = []; foreach ($posts as $slug => $post) { if (!is_array($post)) { continue; } $items[] = ['slug' => (string)$slug] + $post; } usort($items, function(array $a, array $b): int { return (int)($b['updated_at'] ?? 0) <=> (int)($a['updated_at'] ?? 0); }); $cards = ''; foreach ($items as $p) { $img = (string)($p['image'] ?? ''); $cards .= '
    ' . '
    ' . ($img !== '' ? '' . h((string)($p['title'] ?? '')) . '' : '') . '
    ' . '
    ' . '

    ' . h((string)($p['title'] ?? '')) . '

    ' . '
    ' . h((string)($p['excerpt'] ?? '')) . '
    ' . '
    ' . '
    '; } $mainHtml = '
    ' . '
    ' . '

    Tin tức

    ' . '
    Trang chủ  /  Tin tức
    ' . '
    ' . '
    ' . ($cards !== '' ? $cards : '
    Chưa có bài viết.
    ') . '
    '; renderLayoutPage($indexPath, 'Tin tức', $mainHtml); } if (in_array($path, ['/du-an-tieu-bieu', '/du-an-da-thi-cong', '/du-an-dang-thi-cong'], true) && $method === 'GET') { $store = loadContent(); $projects = is_array($store['projects']) ? $store['projects'] : []; $items = []; foreach ($projects as $slug => $project) { if (!is_array($project)) { continue; } $items[] = ['slug' => (string)$slug] + $project; } usort($items, function(array $a, array $b): int { return (int)($b['updated_at'] ?? 0) <=> (int)($a['updated_at'] ?? 0); }); $cards = ''; foreach ($items as $p) { $img = (string)($p['image'] ?? ''); $cards .= '
    ' . '
    ' . ($img !== '' ? '' . h((string)($p['title'] ?? '')) . '' : '') . '
    ' . '
    ' . '

    ' . h((string)($p['title'] ?? '')) . '

    ' . '
    ' . h((string)($p['excerpt'] ?? '')) . '
    ' . '
    ' . '
    '; } $mainHtml = '
    ' . '
    ' . '

    Dự án

    ' . '
    Trang chủ  /  Dự án
    ' . '
    ' . '
    ' . ($cards !== '' ? $cards : '
    Chưa có dự án.
    ') . '
    '; renderLayoutPage($indexPath, 'Dự án', $mainHtml); } if (preg_match('#^/du-an/([a-z0-9-]+)$#', $path, $m) && $method === 'GET') { $slug = (string)$m[1]; $store = loadContent(); $projects = is_array($store['projects']) ? $store['projects'] : []; $project = isset($projects[$slug]) && is_array($projects[$slug]) ? $projects[$slug] : null; if (!$project) { http_response_code(404); renderLayoutPage($indexPath, 'Không tìm thấy', '
    Không tìm thấy dự án.
    '); } $img = (string)($project['image'] ?? ''); $mainHtml = '
    ' . '
    ' . '

    ' . h((string)($project['title'] ?? '')) . '

    ' . '
    Trang chủ  /  Dự án
    ' . '
    ' . '
    ' . ($img !== '' ? '
    ' . h((string)($project['title'] ?? '')) . '
    ' : '') . '
    ' . (string)($project['content'] ?? '') . '
    ' . '
    '; renderLayoutPage($indexPath, (string)($project['title'] ?? 'Dự án'), $mainHtml); } if (preg_match('#^/([a-z0-9-]+)$#', $path, $m) && $method === 'GET') { $slug = (string)$m[1]; $store = loadContent(); $posts = is_array($store['posts']) ? $store['posts'] : []; $post = isset($posts[$slug]) && is_array($posts[$slug]) ? $posts[$slug] : null; if ($post) { $img = (string)($post['image'] ?? ''); $mainHtml = '
    ' . '
    ' . '

    ' . h((string)($post['title'] ?? '')) . '

    ' . '
    Trang chủ  /  Tin tức
    ' . '
    ' . '
    ' . ($img !== '' ? '
    ' . h((string)($post['title'] ?? '')) . '
    ' : '') . '
    ' . (string)($post['content'] ?? '') . '
    ' . '
    '; renderLayoutPage($indexPath, (string)($post['title'] ?? 'Tin tức'), $mainHtml); } } if ($path !== '/' && $method === 'GET') { http_response_code(404); renderLayoutPage($indexPath, 'Không tìm thấy', '
    Trang không tồn tại.
    '); } header('Content-Type: text/html; charset=UTF-8'); renderHomePage($indexPath);