Skip to content

Add full AVSTube PHP/MySQL video sharing site with admin, popup ads, and SQL schema#46

Open
huzely wants to merge 1 commit intomasterfrom
codex/generate-complete-avstube-source-code-wggp5h
Open

Add full AVSTube PHP/MySQL video sharing site with admin, popup ads, and SQL schema#46
huzely wants to merge 1 commit intomasterfrom
codex/generate-complete-avstube-source-code-wggp5h

Conversation

@huzely
Copy link
Copy Markdown
Owner

@huzely huzely commented Mar 9, 2026

Motivation

  • Implement a complete production-ready AVSTube web application that exactly reproduces the provided dark video-grid UI and behavior requirements (black background, #111 header, #ddd text, red brand #c00, thumbnail/duration, hover scale, responsive grid).
  • Provide a full stack solution using the required technologies: HTML/CSS/Bootstrap 5/JS frontend and PHP 8 + MySQL backend with an admin panel and DB schema.
  • Support admin-managed advertising including full-screen popup ads that redirect on click and are suppressed after first view using localStorage.

Description

  • Added full frontend and backend source tree with files: index.php, video.php, search.php, comment.php, config.php, assets/css/style.css, assets/js/main.js, uploads/, admin/*, and sql/avstube.sql with initial seed data and admin account.
  • Implemented video listing grid, video player supporting embed and mp4, comments (stored in comments table), search by title via search.php, and banner/Google ad zones.
  • Built admin area (login/logout, dashboard with Chart.js, videos CRUD with file uploads, ads, popup config, announcements, settings) using a dark sidebar layout and prepared PDO statements for DB access.
  • Implemented popup ad flow that reads a popup ads record, displays full-screen image, redirects on click or close, and sets localStorage.popupViewed = true so the popup does not reappear; also added daily view stats (video_stats) updated on each video view.

Testing

  • Ran PHP syntax checks with rg --files -g '*.php' | xargs -n1 php -l and all PHP files reported No syntax errors detected.
  • Started a local dev server with php -S 0.0.0.0:8080 to smoke-test the app startup successfully.
  • Attempted an automated Playwright screenshot to validate the UI, but the Chromium process crashed with SIGSEGV in this environment so a screenshot could not be captured.

Codex Task

Summary by CodeRabbit

  • New Features

    • Launched AVSTube video platform with search and video player functionality
    • Added user comment system for viewer engagement
    • Introduced admin dashboard with video analytics and daily view tracking
    • Implemented advertisement management for popup and banner ads
    • Added admin authentication system with login and settings panel
    • Introduced announcement feature for site-wide messaging
  • Documentation

    • Updated README with Vietnamese project documentation
  • Style

    • Implemented dark-themed responsive UI design

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

This PR introduces AVSTube, a complete Vietnamese-language video platform with a public-facing video gallery and admin control panel. It includes a MySQL database schema with tables for videos, comments, users, ads, announcements, and analytics; admin pages for content and settings management; and public pages for browsing, searching, and commenting on videos.

Changes

Cohort / File(s) Summary
Documentation
README.md
Replaced Decrypt project README with AVSTube Vietnamese documentation, including project overview, installation, and features.
Admin Authentication & Layout
admin/auth.php, admin/index.php, admin/login.php, admin/logout.php, admin/partials.php
Implements admin authentication flow: login form with credential verification, session management, role-based routing to dashboard or login, and reusable header/footer UI components.
Admin Content Management
admin/videos.php, admin/ads.php, admin/announcements.php, admin/popup.php, admin/settings.php
CRUD pages for managing videos (with file uploads), ads (banner/google types), announcements, popup advertisements, and site settings (logo/banner). Each includes form submission, data validation, and table listing.
Admin Dashboard
admin/dashboard.php
Displays aggregate statistics (total videos, total views, today's views, top video) and a 7-day daily views chart using Chart.js.
Configuration & Helpers
config.php
Database connection setup via PDO with error handling; defines global constants (DB credentials, BASE_URL); provides utility functions: e() for HTML escaping, isAdmin() for session checks, adminOnly() for access control, setting() for fetching site settings.
Public Website Core
index.php, video.php, search.php, comment.php
Homepage with video gallery grid, search form, and featured announcements/banners; video detail page with embedded/HTML5 player, view tracking, and comments section; search redirect; and comment submission handler.
Frontend Assets
assets/css/style.css, assets/js/main.js
Dark-themed responsive stylesheet for video gallery, cards, player, and modals; JavaScript to manage popup overlay display (single-view via localStorage, random link redirect).
Database Schema
sql/avstube.sql
MySQL schema with utf8mb4 encoding; creates tables for videos, comments, users, ads, announcements, settings, and video_stats (daily views tracking); includes primary/foreign keys, cascading deletes, and seeds an admin user plus sample videos.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~95 minutes


🐰 Whiskers twitch with digital glee
AVSTube hops into history,
With videos, comments, and ads galore,
Admin dashboards to explore,
A rabbit's dream platform—hip-hip-hooray! 🎬✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing a complete AVSTube video sharing platform with PHP/MySQL backend, admin panel, and ads functionality. It is clear, specific, and directly reflects the substantial codebase addition.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/generate-complete-avstube-source-code-wggp5h

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d2fe40d48

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +12 to +13
$stats = $pdo->prepare('INSERT INTO video_stats (video_id, view_date, daily_views) VALUES (:video_id, CURDATE(), 1) ON DUPLICATE KEY UPDATE daily_views = daily_views + 1');
$stats->execute(['video_id' => $id]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate video before writing to video_stats

This inserts a stats row before confirming the video exists, so requests like video.php?id=<nonexistent> hit the video_stats.video_id foreign key and throw an uncaught PDOException instead of following the later redirect path. In production this turns a normal invalid-ID request into a 500 error; fetch/validate the video first (or gate the insert on a successful lookup) to avoid hard failures.

Useful? React with 👍 / 👎.

<?php endif; ?>
</div>
<h2><?= e($video['title']) ?></h2>
<p class="views mb-0"><i class="fa fa-eye"></i> <?= number_format((int) $video['views'] + 1) ?> lượt xem</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show persisted view count without adding another increment

The page increments views before loading the record, so $video['views'] already reflects the current view. Adding + 1 here makes the displayed counter consistently one higher than the stored value and dashboard totals, which will confuse users and admins when comparing counts.

Useful? React with 👍 / 👎.

Comment on lines +5 to +8
if (isset($_GET['delete'])) {
$id = (int) $_GET['delete'];
$stmt = $pdo->prepare('DELETE FROM videos WHERE id = :id');
$stmt->execute(['id' => $id]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require CSRF-safe method for video deletion

Deleting videos is triggered directly from a GET parameter with no CSRF token or origin check, so any site can force an authenticated admin’s browser to request videos.php?delete=<id> and delete content unintentionally. Destructive actions should require a POST (or DELETE) plus CSRF validation to prevent cross-site request forgery.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

🧹 Nitpick comments (6)
sql/avstube.sql (1)

4-14: Consider adding an index on created_at for query performance.

The homepage and search queries order by created_at DESC. Without an index, this requires a full table scan as the videos table grows.

📈 Suggested index
     views INT NOT NULL DEFAULT 0,
-    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_created_at (created_at)
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/avstube.sql` around lines 4 - 14, The videos table lacks an index on the
created_at column which causes ORDER BY created_at DESC queries
(homepage/search) to do full table scans; add a dedicated index on
videos.created_at (e.g., create an index named like idx_videos_created_at) via
your schema migration or ALTER TABLE so ordering and date-range filters use the
index; if you also filter by other columns in those queries consider a composite
index including those filter columns plus created_at for best performance.
README.md (1)

47-51: Add security warning for default credentials.

While documenting default credentials is helpful for initial setup, consider adding a prominent warning to change them before deploying to production.

📝 Suggested addition
 ## 3) Tài khoản admin mặc định

+> ⚠️ **Bảo mật**: Đổi mật khẩu admin ngay sau khi cài đặt!
+
 - Username: `admin`
 - Password: `admin2006`
 - URL: `/admin/login.php`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 47 - 51, Update the "3) Tài khoản admin mặc định"
README section to include a clear security warning that the default Username
`admin` and Password `admin2006` must be changed before any production
deployment; explicitly instruct users to log in to `/admin/login.php` and
immediately change the admin password to a strong, unique password (or create a
new admin user and disable/remove the default), and optionally note best
practices like using a password manager, enforcing strong password policies, and
rotating credentials regularly.
admin/logout.php (1)

1-5: Incomplete session cleanup on logout.

session_destroy() alone doesn't unset the $_SESSION superglobal or clear the session cookie. For a more secure logout, clear session data and optionally invalidate the cookie.

🔒 Proposed fix for complete session cleanup
 <?php
 require_once __DIR__ . '/../config.php';
+$_SESSION = [];
+if (ini_get('session.use_cookies')) {
+    $p = session_get_cookie_params();
+    setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
+}
 session_destroy();
 header('Location: login.php');
 exit;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/logout.php` around lines 1 - 5, The logout flow currently only calls
session_destroy(); improve it to fully clear session state by ensuring the
session is started (call session_start() if needed), unset all session data (set
$_SESSION = array()), remove the session cookie using session_name() and
session_get_cookie_params() with setcookie(..., time()-3600, ...) to match
path/domain/secure/httponly, then call session_destroy() and proceed with the
existing header('Location: login.php') and exit; update the logout.php logic to
perform these steps around the existing session_destroy/header/exit sequence.
admin/settings.php (2)

5-15: Missing URL validation for logo and banner inputs.

The settings accept arbitrary strings for URLs. Consider validating that inputs are valid URLs or empty strings to prevent storing malformed data.

🛡️ Optional validation
 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
     $logo = trim($_POST['site_logo'] ?? '');
     $banner = trim($_POST['site_banner'] ?? '');
+    
+    // Optional: validate URLs if not empty
+    if ($logo !== '' && !filter_var($logo, FILTER_VALIDATE_URL)) {
+        // Handle invalid URL
+    }
+    if ($banner !== '' && !filter_var($banner, FILTER_VALIDATE_URL)) {
+        // Handle invalid URL
+    }
 
     foreach (['site_logo' => $logo, 'site_banner' => $banner] as $key => $value) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/settings.php` around lines 5 - 15, The POST handling in settings.php
currently stores whatever is submitted into $logo and $banner without
validation; update the logic before the foreach loop to validate that $logo and
$banner are either empty or valid URLs using filter_var(...,
FILTER_VALIDATE_URL) (or equivalent) and only persist those sanitized values,
otherwise set them to an empty string or return an error response; keep the
existing prepared statement and loop (the $logo/$banner variables and the
foreach mapping) but replace/overwrite invalid inputs with a safe value so
malformed strings are never written to the settings table.

9-12: Update deprecated MySQL syntax in ON DUPLICATE KEY UPDATE.

VALUES(column) in ON DUPLICATE KEY UPDATE is deprecated as of MySQL 8.0.20. Use row aliases instead.

♻️ Modernize with alias syntax
     foreach (['site_logo' => $logo, 'site_banner' => $banner] as $key => $value) {
-        $stmt = $pdo->prepare('INSERT INTO settings (setting_key, setting_value) VALUES (:k, :v) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)');
+        $stmt = $pdo->prepare('INSERT INTO settings (setting_key, setting_value) VALUES (:k, :v) AS new ON DUPLICATE KEY UPDATE setting_value = new.setting_value');
         $stmt->execute(['k' => $key, 'v' => $value]);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/settings.php` around lines 9 - 12, The INSERT uses the deprecated
VALUES(column) form in the ON DUPLICATE KEY UPDATE clause; change the query in
admin/settings.php (the $stmt prepared in the foreach loop) to use an INSERT
alias and reference that alias instead — e.g. add "AS new" after the VALUES(...)
clause and replace "setting_value = VALUES(setting_value)" with "setting_value =
new.setting_value" so the prepared statement uses the modern alias syntax.
admin/partials.php (1)

12-18: Consider adding active state detection for sidebar links.

All sidebar links have the same styling. Adding active state detection would improve navigation UX.

♻️ Example approach using basename comparison
$current = basename($_SERVER['PHP_SELF']);
echo '<a href="dashboard.php"' . ($current === 'dashboard.php' ? ' class="active"' : '') . '>...';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/partials.php` around lines 12 - 18, Sidebar anchors in
admin/partials.php lack an active state; compute the current page (e.g.,
$current = basename($_SERVER['PHP_SELF'])) and append a conditional class
attribute to each echoed anchor so the link whose href matches $current gets
class="active" (apply this logic to the existing anchors for "dashboard.php",
"videos.php", "ads.php", "popup.php", "announcements.php", "settings.php",
"logout.php"); ensure you only alter the echo strings to include the
ternary/conditional that injects ' class="active"' when matched so styling picks
up the active state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@admin/ads.php`:
- Around line 5-15: The toggle/delete actions are implemented as GET handlers
using $_GET['toggle'] and $_GET['delete'], which must be converted to POST-only
and CSRF-protected: change the checks from
isset($_GET['toggle'])/isset($_GET['delete']) to validating $_POST['toggle'] and
$_POST['delete'] respectively and verify a server-side CSRF token (e.g., compare
$_POST['csrf_token'] to the session token) before executing the PDO statements
in ads.php; also ensure the UI uses POST forms (or JS POST) with the CSRF token
for the toggle and delete controls and keep the redirect/header('Location:
ads.php') behavior after successful POST handling.
- Around line 5-24: The toggle and delete handlers currently operate on any
ads.id; restrict their mutations to banner/google only by adding a type filter
to the prepared statements used in toggle and delete (e.g., change the UPDATE in
the toggle handler and the DELETE in the delete handler to include AND type IN
('banner','google')), and keep the existing POST type validation (type variable
in the POST branch) for inserts; optionally check the execute() rowCount/return
to detect when no row was affected. This ensures only banner/google rows (as
selected by $ads) can be toggled or removed.

In `@admin/announcements.php`:
- Around line 5-17: The code currently allows data mutation via a GET "delete"
param and accepts POST inserts without any CSRF verification; change the delete
flow to require a POST (remove GET-based deletion) and implement a
session-backed CSRF token check for all state-changing endpoints (the POST
insert handling and the delete handling previously using $_GET['delete']).
Specifically: generate a CSRF token per-session and embed it as a hidden field
in the announcement create/delete forms, store it in $_SESSION['csrf_token'],
and in the request handlers verify the submitted token using a timing-safe
comparison (e.g., hash_equals) before executing the PDO queries in the POST
branch (where $_SERVER['REQUEST_METHOD'] === 'POST') and in the new POST-based
delete branch instead of relying on $_GET['delete']; only proceed to execute the
DELETE/INSERT and redirect after successful token validation.

In `@admin/dashboard.php`:
- Around line 32-37: The embedded JSON for Chart.js uses json_encode($labels)
and json_encode($values) without safety flags; update those calls to include
safe encoding flags (e.g. JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)
so special characters like </script> are escaped before output, keeping the rest
of the Chart initialization (element id 'viewsChart', dataset label 'Views',
borderColor and tension, and options) unchanged.

In `@admin/login.php`:
- Around line 18-23: After successful authentication in the login handling block
(the if that checks $user, password_verify($password, $user['password']) and
$user['role'] === 'admin'), regenerate the session ID to prevent session
fixation by calling session_regenerate_id(true) immediately before setting
$_SESSION['admin_id'] and $_SESSION['admin_username'], then proceed with the
existing header('Location: dashboard.php') and exit; ensure session_start() has
been called earlier in the request flow.
- Around line 11-12: The login code trims both username and password; remove
trimming for the password to avoid altering legitimate credentials. Keep
trimming the username ($username = trim($_POST['username'] ?? '')) but change
handling of $password so it is read raw from $_POST (use $password =
$_POST['password'] ?? ''), and ensure downstream authentication functions (where
$password is used) expect the raw value from the $password variable.

In `@admin/popup.php`:
- Around line 7-19: The code currently concatenates $link1 and $link2 into a
comma-delimited $links, inserts it into the ads.link column, and later
rehydrates it with explode(',', ...) which will break URLs containing commas;
update the flow to store links as JSON instead: when handling form submission
replace the implode step that builds $links with json_encode of an array
filtered from $link1/$link2 and bind that JSON to the INSERT for ads (refer to
$link1, $link2, $links, the INSERT prepared statement), and when reading the
popup replace the explode(...) and array_map/array_filter sequence with
json_decode on $popup['link'] (handle null/empty safely and cast to array).
Ensure the database column contains valid JSON strings and adjust any callers
expecting comma-delimited values.
- Around line 5-15: Add CSRF token generation and verification around the POST
handler in popup.php: start a session (session_start()) and when rendering the
form generate and store a random token in $_SESSION['csrf_token'] and include it
as a hidden form field (e.g., name="csrf_token"); then in the POST branch
validate that $_POST['csrf_token'] exists and matches $_SESSION['csrf_token']
before performing the DELETE FROM ads and INSERT INTO ads operations (and reject
or exit with an error if the token is missing/invalid). Ensure the token is
single-use by unsetting $_SESSION['csrf_token'] after a successful validation
and before redirecting with header('Location: popup.php?saved=1').
- Around line 11-13: The delete-then-insert for popup ads is not atomic; wrap
the operations using $pdo->beginTransaction(), perform the DELETE and the
prepared INSERT via $pdo->exec and $pdo->prepare/$stmt->execute, then call
$pdo->commit(); on any exception call $pdo->rollBack() and rethrow/log the error
so the original popup remains if the INSERT fails. Alternatively, replace the
DELETE + INSERT with an UPDATE query on the ads row of type='popup' and INSERT
only if no row exists (upsert) so the change is durable and atomic.

In `@admin/videos.php`:
- Around line 5-10: The delete handler currently uses $_GET['delete'] for state
changes and create/update POSTs lack CSRF checks; change the delete action to
require a POST with a CSRF token and add CSRF validation to the existing
create/update POST handling (the POST-processing block that handles
insert/update around lines 13-65 and the delete handling that checks
$_GET['delete']). Implement generation of a per-session token stored in
$_SESSION['csrf_token'], output it as a hidden input named e.g. csrf_token in
all mutation forms, and verify it on each POST using a constant-time comparison
(hash_equals) before executing SQL; if missing/invalid, abort the action and
respond with an error. Ensure the deletion code stops reading $_GET['delete']
and instead reads the id from $_POST['delete'] (or a named delete_id) and that
redirects/exits remain after successful operations.
- Around line 15-20: The code currently allows empty video_url/thumbnail to be
inserted; add server-side validation in the create/update handlers to reject
incomplete records based on $type: if $type === 'embed' require non-empty
$videoUrl; if $type === 'mp4' require non-empty $videoUrl and $thumbnail; if
validation fails, stop the insert/update, set an error message and return to the
form (do not write to the videos table). Apply these checks around the blocks
that use $title/$type/$videoUrl/$thumbnail (the create and edit code paths) so
both insert and update paths enforce the same rules.
- Around line 22-39: The current upload logic uses move_uploaded_file() but
unconditionally sets $videoUrl and $thumbnail even if the move fails, which can
create DB records pointing to non-existent files; update the blocks handling
$_FILES['mp4_file'] and $_FILES['thumbnail_file'] to check the boolean return of
move_uploaded_file() before assigning $videoUrl or $thumbnail (e.g., only set
$videoUrl = 'uploads/videos/'.$name after
move_uploaded_file($_FILES['mp4_file']['tmp_name'], $target) returns true, and
similarly for $thumbnail), and on failure handle the error (log, set an error
flag, or stop processing) so you never persist a path unless the file was
successfully moved.

In `@assets/js/main.js`:
- Around line 22-28: The popup's click listener (popup.addEventListener('click',
redirect)) causes clicks on the close button to bubble and trigger redirect; add
a dedicated handler for the close button (select the element with class
'popup-close') that calls event.stopPropagation(), marks the popup as dismissed
(e.g., set localStorage 'popupViewed' as appropriate) and hides/removes the
popup, and keep the existing redirect() handler only for clicks that are not
from the close control (or alternatively guard inside the popup click handler
with if (event.target.closest('.popup-close')) return;). Update references to
redirect, popup, and the '.popup-close' element accordingly so the X button
dismisses without navigating.

In `@comment.php`:
- Around line 8-18: Add CSRF token validation around the comment insertion:
require a session-based token (e.g. $_SESSION['csrf_token']) and check the
submitted token (e.g. $_POST['csrf_token']) before running the existing
insertion logic that uses $videoId, $username, $comment; if the token is missing
or invalid, abort and do not execute $stmt->execute(...). Also ensure the token
is generated and injected as a hidden field in the comment form (video.php) so
the submitted form includes the expected token.

In `@config.php`:
- Around line 21-23: The catch block that handles PDOException ($e) currently
dies with the exception message (die('Database connection failed: ' .
htmlspecialchars($e->getMessage()))), which can leak sensitive DB details;
change it to log the full exception internally (use error_log or your
application logger with $e->getMessage() and stack trace) and replace the die()
output with a generic user-facing message like "Database connection error" (no
exception details), then exit with a non-zero status; ensure you still
htmlspecialchars any user-visible text and keep the exception logging call
adjacent to the catch handling for PDOException.
- Around line 4-8: The config file currently hardcodes DB_HOST, DB_NAME,
DB_USER, DB_PASS and BASE_URL; replace these constant definitions to read from
environment/config instead (use getenv or $_ENV for DB_HOST, DB_NAME, DB_USER,
DB_PASS, BASE_URL), provide sensible defaults only for non-sensitive values, and
validate required DB credentials at startup (throw/log a clear error if
DB_USER/DB_PASS/DB_NAME are missing) so secrets are not committed—also document
that secrets should be set via environment variables or a non-committed local
config file.

In `@index.php`:
- Around line 95-97: The foreach rendering of $googleCodes in index.php emits
$code['image'] unescaped which creates XSS risk; either sanitize/validate the
image HTML before output or explicitly-document/limit this as intentional raw
output. Update the loop that outputs $code['image'] to (a) sanitize/strip
disallowed tags or run it through a safe sanitizer function (e.g.,
sanitizeAdHtml or a whitelist-based purifier) and/or (b) add a clear inline
comment above the output explaining why raw HTML is used and that input is
restricted to trusted admins, and consider adding CSP headers elsewhere;
reference the foreach over $googleCodes and the $code['image'] key when making
the change.

In `@video.php`:
- Around line 62-67: The comment form in video.php must include a CSRF token and
server-side validation to match comment.php; generate a session token if missing
(use $_SESSION['csrf_token'] = $_SESSION['csrf_token'] ??
bin2hex(random_bytes(32))) and add a hidden input named csrf_token inside the
existing form (near the other hidden video_id input), then ensure comment.php
validates the submitted csrf_token against $_SESSION['csrf_token' ] (and rejects
the request if missing/invalid); escape the token output with your existing e()
helper when rendering the hidden input.
- Around line 9-22: The code updates views and inserts into video_stats before
checking the video exists, causing a foreign key PDOException for invalid $id;
move the SELECT query (the $stmt prepared with 'SELECT * FROM videos WHERE id =
:id') and the fetch($stmt->fetch()) ahead of the $update and $stats statements,
check if $video is falsy and redirect/exit if so, and only then execute the
$update (views increment) and $stats (INSERT ... ON DUPLICATE KEY UPDATE) using
the same $id; ensure to reuse or re-prepare the $update and $stats statements
after the existence check if needed.

---

Nitpick comments:
In `@admin/logout.php`:
- Around line 1-5: The logout flow currently only calls session_destroy();
improve it to fully clear session state by ensuring the session is started (call
session_start() if needed), unset all session data (set $_SESSION = array()),
remove the session cookie using session_name() and session_get_cookie_params()
with setcookie(..., time()-3600, ...) to match path/domain/secure/httponly, then
call session_destroy() and proceed with the existing header('Location:
login.php') and exit; update the logout.php logic to perform these steps around
the existing session_destroy/header/exit sequence.

In `@admin/partials.php`:
- Around line 12-18: Sidebar anchors in admin/partials.php lack an active state;
compute the current page (e.g., $current = basename($_SERVER['PHP_SELF'])) and
append a conditional class attribute to each echoed anchor so the link whose
href matches $current gets class="active" (apply this logic to the existing
anchors for "dashboard.php", "videos.php", "ads.php", "popup.php",
"announcements.php", "settings.php", "logout.php"); ensure you only alter the
echo strings to include the ternary/conditional that injects ' class="active"'
when matched so styling picks up the active state.

In `@admin/settings.php`:
- Around line 5-15: The POST handling in settings.php currently stores whatever
is submitted into $logo and $banner without validation; update the logic before
the foreach loop to validate that $logo and $banner are either empty or valid
URLs using filter_var(..., FILTER_VALIDATE_URL) (or equivalent) and only persist
those sanitized values, otherwise set them to an empty string or return an error
response; keep the existing prepared statement and loop (the $logo/$banner
variables and the foreach mapping) but replace/overwrite invalid inputs with a
safe value so malformed strings are never written to the settings table.
- Around line 9-12: The INSERT uses the deprecated VALUES(column) form in the ON
DUPLICATE KEY UPDATE clause; change the query in admin/settings.php (the $stmt
prepared in the foreach loop) to use an INSERT alias and reference that alias
instead — e.g. add "AS new" after the VALUES(...) clause and replace
"setting_value = VALUES(setting_value)" with "setting_value = new.setting_value"
so the prepared statement uses the modern alias syntax.

In `@README.md`:
- Around line 47-51: Update the "3) Tài khoản admin mặc định" README section to
include a clear security warning that the default Username `admin` and Password
`admin2006` must be changed before any production deployment; explicitly
instruct users to log in to `/admin/login.php` and immediately change the admin
password to a strong, unique password (or create a new admin user and
disable/remove the default), and optionally note best practices like using a
password manager, enforcing strong password policies, and rotating credentials
regularly.

In `@sql/avstube.sql`:
- Around line 4-14: The videos table lacks an index on the created_at column
which causes ORDER BY created_at DESC queries (homepage/search) to do full table
scans; add a dedicated index on videos.created_at (e.g., create an index named
like idx_videos_created_at) via your schema migration or ALTER TABLE so ordering
and date-range filters use the index; if you also filter by other columns in
those queries consider a composite index including those filter columns plus
created_at for best performance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0471d1dd-7ac6-425f-ab9c-0679b7fed89a

📥 Commits

Reviewing files that changed from the base of the PR and between c5b1ce9 and 2d2fe40.

📒 Files selected for processing (20)
  • README.md
  • admin/ads.php
  • admin/announcements.php
  • admin/auth.php
  • admin/dashboard.php
  • admin/index.php
  • admin/login.php
  • admin/logout.php
  • admin/partials.php
  • admin/popup.php
  • admin/settings.php
  • admin/videos.php
  • assets/css/style.css
  • assets/js/main.js
  • comment.php
  • config.php
  • index.php
  • search.php
  • sql/avstube.sql
  • video.php

Comment on lines +5 to +15
if (isset($_GET['toggle'])) {
$id = (int) $_GET['toggle'];
$pdo->prepare('UPDATE ads SET status = IF(status=1,0,1) WHERE id=:id')->execute(['id' => $id]);
header('Location: ads.php');
exit;
}
if (isset($_GET['delete'])) {
$pdo->prepare('DELETE FROM ads WHERE id=:id')->execute(['id' => (int) $_GET['delete']]);
header('Location: ads.php');
exit;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't toggle or delete ads through GET.

The action links at Line 35 hit state-changing GET handlers, so a logged-in admin can be tricked into enabling, disabling, or deleting ads via CSRF. These actions should be POST-only and protected by a CSRF token.

Also applies to: 35-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/ads.php` around lines 5 - 15, The toggle/delete actions are implemented
as GET handlers using $_GET['toggle'] and $_GET['delete'], which must be
converted to POST-only and CSRF-protected: change the checks from
isset($_GET['toggle'])/isset($_GET['delete']) to validating $_POST['toggle'] and
$_POST['delete'] respectively and verify a server-side CSRF token (e.g., compare
$_POST['csrf_token'] to the session token) before executing the PDO statements
in ads.php; also ensure the UI uses POST forms (or JS POST) with the CSRF token
for the toggle and delete controls and keep the redirect/header('Location:
ads.php') behavior after successful POST handling.

Comment on lines +5 to +24
if (isset($_GET['toggle'])) {
$id = (int) $_GET['toggle'];
$pdo->prepare('UPDATE ads SET status = IF(status=1,0,1) WHERE id=:id')->execute(['id' => $id]);
header('Location: ads.php');
exit;
}
if (isset($_GET['delete'])) {
$pdo->prepare('DELETE FROM ads WHERE id=:id')->execute(['id' => (int) $_GET['delete']]);
header('Location: ads.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$type = in_array($_POST['type'] ?? '', ['banner', 'google'], true) ? $_POST['type'] : 'banner';
$image = trim($_POST['image'] ?? '');
$link = trim($_POST['link'] ?? '');
$pdo->prepare('INSERT INTO ads (type,image,link,status) VALUES (:type,:image,:link,1)')->execute(compact('type', 'image', 'link'));
header('Location: ads.php');
exit;
}
$ads = $pdo->query("SELECT * FROM ads WHERE type IN ('banner','google') ORDER BY id DESC")->fetchAll();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scope this page's mutations to banner/google ads.

admin/popup.php manages popup ads separately, but the handlers here update and delete any ads.id regardless of type. A crafted request can therefore disable or remove the popup record even though it is not listed on this page.

Suggested fix
-    $pdo->prepare('UPDATE ads SET status = IF(status=1,0,1) WHERE id=:id')->execute(['id' => $id]);
+    $pdo->prepare("UPDATE ads SET status = IF(status=1,0,1) WHERE id=:id AND type IN ('banner','google')")->execute(['id' => $id]);
@@
-    $pdo->prepare('DELETE FROM ads WHERE id=:id')->execute(['id' => (int) $_GET['delete']]);
+    $pdo->prepare("DELETE FROM ads WHERE id=:id AND type IN ('banner','google')")->execute(['id' => (int) $_GET['delete']]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isset($_GET['toggle'])) {
$id = (int) $_GET['toggle'];
$pdo->prepare('UPDATE ads SET status = IF(status=1,0,1) WHERE id=:id')->execute(['id' => $id]);
header('Location: ads.php');
exit;
}
if (isset($_GET['delete'])) {
$pdo->prepare('DELETE FROM ads WHERE id=:id')->execute(['id' => (int) $_GET['delete']]);
header('Location: ads.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$type = in_array($_POST['type'] ?? '', ['banner', 'google'], true) ? $_POST['type'] : 'banner';
$image = trim($_POST['image'] ?? '');
$link = trim($_POST['link'] ?? '');
$pdo->prepare('INSERT INTO ads (type,image,link,status) VALUES (:type,:image,:link,1)')->execute(compact('type', 'image', 'link'));
header('Location: ads.php');
exit;
}
$ads = $pdo->query("SELECT * FROM ads WHERE type IN ('banner','google') ORDER BY id DESC")->fetchAll();
if (isset($_GET['toggle'])) {
$id = (int) $_GET['toggle'];
$pdo->prepare("UPDATE ads SET status = IF(status=1,0,1) WHERE id=:id AND type IN ('banner','google')")->execute(['id' => $id]);
header('Location: ads.php');
exit;
}
if (isset($_GET['delete'])) {
$pdo->prepare("DELETE FROM ads WHERE id=:id AND type IN ('banner','google')")->execute(['id' => (int) $_GET['delete']]);
header('Location: ads.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$type = in_array($_POST['type'] ?? '', ['banner', 'google'], true) ? $_POST['type'] : 'banner';
$image = trim($_POST['image'] ?? '');
$link = trim($_POST['link'] ?? '');
$pdo->prepare('INSERT INTO ads (type,image,link,status) VALUES (:type,:image,:link,1)')->execute(compact('type', 'image', 'link'));
header('Location: ads.php');
exit;
}
$ads = $pdo->query("SELECT * FROM ads WHERE type IN ('banner','google') ORDER BY id DESC")->fetchAll();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/ads.php` around lines 5 - 24, The toggle and delete handlers currently
operate on any ads.id; restrict their mutations to banner/google only by adding
a type filter to the prepared statements used in toggle and delete (e.g., change
the UPDATE in the toggle handler and the DELETE in the delete handler to include
AND type IN ('banner','google')), and keep the existing POST type validation
(type variable in the POST branch) for inserts; optionally check the execute()
rowCount/return to detect when no row was affected. This ensures only
banner/google rows (as selected by $ads) can be toggled or removed.

Comment on lines +5 to +17
if (isset($_GET['delete'])) {
$pdo->prepare('DELETE FROM announcements WHERE id=:id')->execute(['id' => (int) $_GET['delete']]);
header('Location: announcements.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title'] ?? '');
$content = trim($_POST['content'] ?? '');
if ($title !== '' && $content !== '') {
$pdo->prepare('INSERT INTO announcements (title, content) VALUES (:title, :content)')->execute(compact('title', 'content'));
}
header('Location: announcements.php');
exit;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Protect announcement writes with POST + CSRF.

Both the POST insert and the GET delete mutate data without any CSRF check. A malicious page can use an admin's session to publish or remove announcements.

Also applies to: 24-24

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/announcements.php` around lines 5 - 17, The code currently allows data
mutation via a GET "delete" param and accepts POST inserts without any CSRF
verification; change the delete flow to require a POST (remove GET-based
deletion) and implement a session-backed CSRF token check for all state-changing
endpoints (the POST insert handling and the delete handling previously using
$_GET['delete']). Specifically: generate a CSRF token per-session and embed it
as a hidden field in the announcement create/delete forms, store it in
$_SESSION['csrf_token'], and in the request handlers verify the submitted token
using a timing-safe comparison (e.g., hash_equals) before executing the PDO
queries in the POST branch (where $_SERVER['REQUEST_METHOD'] === 'POST') and in
the new POST-based delete branch instead of relying on $_GET['delete']; only
proceed to execute the DELETE/INSERT and redirect after successful token
validation.

Comment on lines +32 to +37
<script>
new Chart(document.getElementById('viewsChart'), {
type: 'line',
data: { labels: <?= json_encode($labels) ?>, datasets: [{label: 'Views', data: <?= json_encode($values) ?>, borderColor: '#c00', tension: 0.4}]},
options: { plugins: {legend: {labels:{color:'#ddd'}}}, scales: {x:{ticks:{color:'#aaa'}}, y:{ticks:{color:'#aaa'}}}}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use safe JSON encoding flags when embedding in HTML.

json_encode() without proper flags can be vulnerable to XSS if data contains characters like </script> or HTML entities. Use encoding flags for safety when embedding JSON in script tags.

🔒 Proposed fix
+<?php
+$jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
+?>
 <script>
 new Chart(document.getElementById('viewsChart'), {
     type: 'line',
-    data: { labels: <?= json_encode($labels) ?>, datasets: [{label: 'Views', data: <?= json_encode($values) ?>, borderColor: '#c00', tension: 0.4}]},
+    data: { labels: <?= json_encode($labels, $jsonFlags) ?>, datasets: [{label: 'Views', data: <?= json_encode($values, $jsonFlags) ?>, borderColor: '#c00', tension: 0.4}]},
     options: { plugins: {legend: {labels:{color:'#ddd'}}}, scales: {x:{ticks:{color:'#aaa'}}, y:{ticks:{color:'#aaa'}}}}
 });
 </script>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script>
new Chart(document.getElementById('viewsChart'), {
type: 'line',
data: { labels: <?= json_encode($labels) ?>, datasets: [{label: 'Views', data: <?= json_encode($values) ?>, borderColor: '#c00', tension: 0.4}]},
options: { plugins: {legend: {labels:{color:'#ddd'}}}, scales: {x:{ticks:{color:'#aaa'}}, y:{ticks:{color:'#aaa'}}}}
});
<?php
$jsonFlags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
?>
<script>
new Chart(document.getElementById('viewsChart'), {
type: 'line',
data: { labels: <?= json_encode($labels, $jsonFlags) ?>, datasets: [{label: 'Views', data: <?= json_encode($values, $jsonFlags) ?>, borderColor: '#c00', tension: 0.4}]},
options: { plugins: {legend: {labels:{color:'#ddd'}}}, scales: {x:{ticks:{color:'#aaa'}}, y:{ticks:{color:'#aaa'}}}}
});
</script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/dashboard.php` around lines 32 - 37, The embedded JSON for Chart.js
uses json_encode($labels) and json_encode($values) without safety flags; update
those calls to include safe encoding flags (e.g.
JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT) so special characters
like </script> are escaped before output, keeping the rest of the Chart
initialization (element id 'viewsChart', dataset label 'Views', borderColor and
tension, and options) unchanged.

Comment on lines +11 to +12
$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid trimming passwords.

trim() on passwords can cause authentication failures if a user's password legitimately contains leading/trailing whitespace. Passwords should be used as-is.

🔧 Proposed fix
     $username = trim($_POST['username'] ?? '');
-    $password = trim($_POST['password'] ?? '');
+    $password = $_POST['password'] ?? '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/login.php` around lines 11 - 12, The login code trims both username and
password; remove trimming for the password to avoid altering legitimate
credentials. Keep trimming the username ($username = trim($_POST['username'] ??
'')) but change handling of $password so it is read raw from $_POST (use
$password = $_POST['password'] ?? ''), and ensure downstream authentication
functions (where $password is used) expect the raw value from the $password
variable.

Comment on lines +4 to +8
define('DB_HOST', 'localhost');
define('DB_NAME', 'avstube');
define('DB_USER', 'root');
define('DB_PASS', '');
define('BASE_URL', '');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded database credentials pose a security risk for production.

Storing credentials directly in source code exposes them in version control and makes deployment configuration inflexible. Consider using environment variables or a separate non-committed config file.

♻️ Suggested approach using environment variables
-define('DB_HOST', 'localhost');
-define('DB_NAME', 'avstube');
-define('DB_USER', 'root');
-define('DB_PASS', '');
-define('BASE_URL', '');
+define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
+define('DB_NAME', getenv('DB_NAME') ?: 'avstube');
+define('DB_USER', getenv('DB_USER') ?: 'root');
+define('DB_PASS', getenv('DB_PASS') ?: '');
+define('BASE_URL', getenv('BASE_URL') ?: '');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
define('DB_HOST', 'localhost');
define('DB_NAME', 'avstube');
define('DB_USER', 'root');
define('DB_PASS', '');
define('BASE_URL', '');
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_NAME', getenv('DB_NAME') ?: 'avstube');
define('DB_USER', getenv('DB_USER') ?: 'root');
define('DB_PASS', getenv('DB_PASS') ?: '');
define('BASE_URL', getenv('BASE_URL') ?: '');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.php` around lines 4 - 8, The config file currently hardcodes DB_HOST,
DB_NAME, DB_USER, DB_PASS and BASE_URL; replace these constant definitions to
read from environment/config instead (use getenv or $_ENV for DB_HOST, DB_NAME,
DB_USER, DB_PASS, BASE_URL), provide sensible defaults only for non-sensitive
values, and validate required DB credentials at startup (throw/log a clear error
if DB_USER/DB_PASS/DB_NAME are missing) so secrets are not committed—also
document that secrets should be set via environment variables or a non-committed
local config file.

Comment on lines +21 to +23
} catch (PDOException $e) {
die('Database connection failed: ' . htmlspecialchars($e->getMessage()));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Exposing database exception details can leak sensitive information.

The error message may reveal database host, schema names, or driver details to end users. Log the full error internally and show a generic message to users.

🛡️ Proposed fix
 } catch (PDOException $e) {
-    die('Database connection failed: ' . htmlspecialchars($e->getMessage()));
+    error_log('Database connection failed: ' . $e->getMessage());
+    die('Database connection failed. Please try again later.');
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (PDOException $e) {
die('Database connection failed: ' . htmlspecialchars($e->getMessage()));
}
} catch (PDOException $e) {
error_log('Database connection failed: ' . $e->getMessage());
die('Database connection failed. Please try again later.');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config.php` around lines 21 - 23, The catch block that handles PDOException
($e) currently dies with the exception message (die('Database connection failed:
' . htmlspecialchars($e->getMessage()))), which can leak sensitive DB details;
change it to log the full exception internally (use error_log or your
application logger with $e->getMessage() and stack trace) and replace the die()
output with a generic user-facing message like "Database connection error" (no
exception details), then exit with a non-zero status; ensure you still
htmlspecialchars any user-visible text and keep the exception logging call
adjacent to the catch handling for PDOException.

Comment on lines +95 to +97
<?php foreach ($googleCodes as $code): ?>
<div class="panel-dark p-3 mb-4"><?= $code['image'] ?></div>
<?php endforeach; ?>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unescaped output creates XSS risk if admin account is compromised.

The $code['image'] field is rendered without escaping, allowing arbitrary HTML/JS execution. While this may be intentional for embedding Google ad scripts, a compromised admin account could inject malicious code affecting all visitors.

Consider adding a sanitization layer or Content Security Policy headers to mitigate the risk. At minimum, add a comment documenting this intentional raw output:

         <?php foreach ($googleCodes as $code): ?>
-            <div class="panel-dark p-3 mb-4"><?= $code['image'] ?></div>
+            <?php // Raw HTML output for Google ad code - admin-only input ?>
+            <div class="panel-dark p-3 mb-4"><?= $code['image'] ?></div>
         <?php endforeach; ?>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<?php foreach ($googleCodes as $code): ?>
<div class="panel-dark p-3 mb-4"><?= $code['image'] ?></div>
<?php endforeach; ?>
<?php foreach ($googleCodes as $code): ?>
<?php // Raw HTML output for Google ad code - admin-only input ?>
<div class="panel-dark p-3 mb-4"><?= $code['image'] ?></div>
<?php endforeach; ?>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.php` around lines 95 - 97, The foreach rendering of $googleCodes in
index.php emits $code['image'] unescaped which creates XSS risk; either
sanitize/validate the image HTML before output or explicitly-document/limit this
as intentional raw output. Update the loop that outputs $code['image'] to (a)
sanitize/strip disallowed tags or run it through a safe sanitizer function
(e.g., sanitizeAdHtml or a whitelist-based purifier) and/or (b) add a clear
inline comment above the output explaining why raw HTML is used and that input
is restricted to trusted admins, and consider adding CSP headers elsewhere;
reference the foreach over $googleCodes and the $code['image'] key when making
the change.

Comment on lines +9 to +22
$update = $pdo->prepare('UPDATE videos SET views = views + 1 WHERE id = :id');
$update->execute(['id' => $id]);

$stats = $pdo->prepare('INSERT INTO video_stats (video_id, view_date, daily_views) VALUES (:video_id, CURDATE(), 1) ON DUPLICATE KEY UPDATE daily_views = daily_views + 1');
$stats->execute(['video_id' => $id]);

$stmt = $pdo->prepare('SELECT * FROM videos WHERE id = :id LIMIT 1');
$stmt->execute(['id' => $id]);
$video = $stmt->fetch();

if (!$video) {
header('Location: index.php');
exit;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

View count update before existence check causes exception for invalid IDs.

If a non-existent video ID is provided, the UPDATE silently affects 0 rows, but the INSERT INTO video_stats will throw a PDOException due to the foreign key constraint violation. This crashes the page instead of redirecting gracefully.

🐛 Proposed fix: fetch video first, then update stats
 if ($id <= 0) {
     header('Location: index.php');
     exit;
 }

-$update = $pdo->prepare('UPDATE videos SET views = views + 1 WHERE id = :id');
-$update->execute(['id' => $id]);
-
-$stats = $pdo->prepare('INSERT INTO video_stats (video_id, view_date, daily_views) VALUES (:video_id, CURDATE(), 1) ON DUPLICATE KEY UPDATE daily_views = daily_views + 1');
-$stats->execute(['video_id' => $id]);
-
 $stmt = $pdo->prepare('SELECT * FROM videos WHERE id = :id LIMIT 1');
 $stmt->execute(['id' => $id]);
 $video = $stmt->fetch();

 if (!$video) {
     header('Location: index.php');
     exit;
 }
+
+$update = $pdo->prepare('UPDATE videos SET views = views + 1 WHERE id = :id');
+$update->execute(['id' => $id]);
+
+$stats = $pdo->prepare('INSERT INTO video_stats (video_id, view_date, daily_views) VALUES (:video_id, CURDATE(), 1) ON DUPLICATE KEY UPDATE daily_views = daily_views + 1');
+$stats->execute(['video_id' => $id]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$update = $pdo->prepare('UPDATE videos SET views = views + 1 WHERE id = :id');
$update->execute(['id' => $id]);
$stats = $pdo->prepare('INSERT INTO video_stats (video_id, view_date, daily_views) VALUES (:video_id, CURDATE(), 1) ON DUPLICATE KEY UPDATE daily_views = daily_views + 1');
$stats->execute(['video_id' => $id]);
$stmt = $pdo->prepare('SELECT * FROM videos WHERE id = :id LIMIT 1');
$stmt->execute(['id' => $id]);
$video = $stmt->fetch();
if (!$video) {
header('Location: index.php');
exit;
}
if ($id <= 0) {
header('Location: index.php');
exit;
}
$stmt = $pdo->prepare('SELECT * FROM videos WHERE id = :id LIMIT 1');
$stmt->execute(['id' => $id]);
$video = $stmt->fetch();
if (!$video) {
header('Location: index.php');
exit;
}
$update = $pdo->prepare('UPDATE videos SET views = views + 1 WHERE id = :id');
$update->execute(['id' => $id]);
$stats = $pdo->prepare('INSERT INTO video_stats (video_id, view_date, daily_views) VALUES (:video_id, CURDATE(), 1) ON DUPLICATE KEY UPDATE daily_views = daily_views + 1');
$stats->execute(['video_id' => $id]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@video.php` around lines 9 - 22, The code updates views and inserts into
video_stats before checking the video exists, causing a foreign key PDOException
for invalid $id; move the SELECT query (the $stmt prepared with 'SELECT * FROM
videos WHERE id = :id') and the fetch($stmt->fetch()) ahead of the $update and
$stats statements, check if $video is falsy and redirect/exit if so, and only
then execute the $update (views increment) and $stats (INSERT ... ON DUPLICATE
KEY UPDATE) using the same $id; ensure to reuse or re-prepare the $update and
$stats statements after the existence check if needed.

Comment on lines +62 to +67
<form method="post" action="comment.php" class="mb-3">
<input type="hidden" name="video_id" value="<?= (int) $video['id'] ?>">
<input class="form-control bg-dark text-light border-secondary mb-2" name="username" placeholder="Tên của bạn" required maxlength="50">
<textarea class="form-control bg-dark text-light border-secondary mb-2" name="comment" rows="3" placeholder="Nội dung bình luận" required maxlength="500"></textarea>
<button class="btn btn-red w-100" type="submit">Gửi bình luận</button>
</form>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Comment form lacks CSRF protection.

Same issue as in comment.php — the form should include a CSRF token to prevent cross-site request forgery.

Add a hidden CSRF token field:

<?php $_SESSION['csrf_token'] = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32)); ?>
<input type="hidden" name="csrf_token" value="<?= e($_SESSION['csrf_token']) ?>">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@video.php` around lines 62 - 67, The comment form in video.php must include a
CSRF token and server-side validation to match comment.php; generate a session
token if missing (use $_SESSION['csrf_token'] = $_SESSION['csrf_token'] ??
bin2hex(random_bytes(32))) and add a hidden input named csrf_token inside the
existing form (near the other hidden video_id input), then ensure comment.php
validates the submitted csrf_token against $_SESSION['csrf_token' ] (and rejects
the request if missing/invalid); escape the token output with your existing e()
helper when rendering the hidden input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant