Skip to content

Build GENZMOVIE full-stack PHP movie streaming platform scaffold#44

Open
huzely wants to merge 1 commit intomasterfrom
codex/create-complete-movie-streaming-website
Open

Build GENZMOVIE full-stack PHP movie streaming platform scaffold#44
huzely wants to merge 1 commit intomasterfrom
codex/create-complete-movie-streaming-website

Conversation

@huzely
Copy link
Copy Markdown
Owner

@huzely huzely commented Mar 8, 2026

Motivation

  • Provide a production-ready scaffold for a full movie streaming website (GENZMOVIE) including frontend, admin, crawler and ad monetization backed by PHP 8+ and MySQL.
  • Enable automatic import of movie metadata and episodes from the OPhim API and support multiple video source types (iframe/embed, direct MP4/HLS).
  • Offer basic user features (register/login, favorites, watch history) and admin controls for crawler, ads and content management.
  • Ship a sane default architecture with security basics (prepared statements, password hashing, CSRF tokens and output escaping) and SEO-friendly routing.

Description

  • Added a complete project scaffold under genzmovie/ with MVC-style controllers, models, views, assets, includes, admin UI, API endpoint and crawler script (crawler/ophim_crawler.php).
  • Implemented frontend: Netflix-like homepage, featured slider, category sections, reusable movie cards, search + autocomplete (api route), filtering, movie detail page with episode/server selection and dual-source player (iframe and <video> with subtitles).
  • Implemented backend components: Movie, Episode, User, Ad models, HomeController, MovieController, AuthController, UserController, central router (genzmovie/index.php) and .htaccess SEO rewrite for /phim/{slug}.
  • Added admin dashboard (genzmovie/admin) to trigger OPhim import, manage ads, and inspect movies/users/comments; added ad rendering in header/sidebar/popup/footer and api/sitemap.php for sitemap generation.
  • Created genzmovie/database/schema.sql containing schema for admins, users, movies, genres, movie_genres, episodes, watch_history, favorites, comments, ads, settings and seeded a default admin account.
  • Added helpers for CSRF (includes/csrf.php), auth checks (includes/auth.php), escaping and slug utilities (includes/helpers.php), and a lightweight Database PDO wrapper (config/database.php).

Testing

  • Ran PHP lint (php -l) across all PHP files in genzmovie/ with no syntax errors detected.
  • Started a local PHP built-in server (php -S 0.0.0.0:8080 -t /workspace/Decrypt) and captured an automated Playwright screenshot of the admin login page to validate UI rendering.
  • Executed the project-level sanity checks (file generation and router boot) during development and the created SQL schema file was syntactically validated by inclusion in the repository.

Codex Task

Summary by CodeRabbit

Release Notes

  • New Features

    • Added admin dashboard for content management and crawler control to sync movie data
    • Implemented user authentication system with login and registration
    • Introduced movie search with live autocomplete suggestions
    • Added movie filtering by genre, year, country, and quality
    • Created user dashboard showing favorites and watch history
    • Added movie detail pages with episode selection and related movie recommendations
    • Implemented advertisement management system for multiple placements
    • Generated dynamic XML sitemap for improved search engine visibility
  • Documentation

    • Updated project README with GenZMovie overview and repository structure

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

A complete new web application called GenZMovie has been introduced, featuring a movie streaming platform with user authentication, movie discovery, admin controls, and content management via automated crawler integration from OPhim API.

Changes

Cohort / File(s) Summary
Documentation & Configuration
README.md, genzmovie/config/config.php, genzmovie/.htaccess
Updated README to describe GenZMovie project; added Apache rewrite rules for clean URLs; established app configuration with database credentials, API endpoints, and constants.
Database & Connection
genzmovie/database/schema.sql, genzmovie/config/database.php
Created complete database schema with 11 tables (movies, users, episodes, genres, watch history, favorites, comments, ads, etc.); implemented singleton PDO connection wrapper with exception handling.
Models
genzmovie/models/Movie.php, genzmovie/models/User.php, genzmovie/models/Episode.php, genzmovie/models/Ad.php
Added four data models with database query methods: Movie (featured, search, filter, related), User (create, auth, favorites), Episode (CRUD), Ad (active by position).
Controllers
genzmovie/controllers/HomeController.php, genzmovie/controllers/MovieController.php, genzmovie/controllers/UserController.php, genzmovie/controllers/AuthController.php
Introduced four request handlers: HomeController (index/featured feed), MovieController (show/search/filter/autocomplete), UserController (dashboard/favorites toggle), AuthController (login/register/logout).
Admin Features
genzmovie/admin/index.php, genzmovie/admin/login.php
Created admin dashboard with CSRF-protected forms for content crawler control and ad management; added admin authentication page with password verification.
Crawler & API
genzmovie/crawler/ophim_crawler.php, genzmovie/api/sitemap.php
Implemented OPhim crawler to fetch and sync movie data with local database (handling creates/updates/episodes); added dynamic XML sitemap generator.
Utilities & Router
genzmovie/includes/auth.php, genzmovie/includes/csrf.php, genzmovie/includes/helpers.php, genzmovie/index.php
Added authentication guards, CSRF token generation/validation, and helper functions (HTML escape, redirect, slugify, user ID retrieval); created front router dispatching requests to controllers.
Frontend Assets
genzmovie/assets/css/style.css, genzmovie/assets/js/app.js
Added CSS styling for movie cards, overlays, autocomplete box, and responsive layout; implemented jQuery-based autocomplete search with AJAX.
Views - Authentication
genzmovie/views/auth/login.php, genzmovie/views/auth/register.php
Created user login and registration pages with CSRF protection and Bootstrap styling in Vietnamese.
Views - Content Display
genzmovie/views/home/index.php, genzmovie/views/movie/list.php, genzmovie/views/movie/show.php
Added home page with featured carousel and category sections; created movie list with search/filter UI; implemented movie detail page with episodes, related movies, and favorites toggle.
Views - Layouts & Partials
genzmovie/views/layouts/header.php, genzmovie/views/layouts/footer.php, genzmovie/views/partials/movie-card.php
Built reusable header with ad placements and navbar; created footer with ads and popup modal support; added movie card component for consistent display.
Views - User
genzmovie/views/user/dashboard.php
Created user dashboard displaying favorites and watch history.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 twitches whiskers with delight
GenZMovie burrows deep and wide,
With crawlers, auth, and films inside,
Controllers choreograph the show,
While models make the data flow! 🎬✨
munches code carrots 🥕

🚥 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 PR title accurately summarizes the main change: a complete scaffold for a full-stack PHP movie streaming platform named GENZMOVIE with MVC architecture, controllers, models, views, admin dashboard, crawler, and API endpoints.

✏️ 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/create-complete-movie-streaming-website

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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: bbfab458b5

ℹ️ 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".

if ($existing) {
$movieId = (int) $existing['id'];
$update = $pdo->prepare('UPDATE movies SET title=:title, original_title=:original_title, description=:description, poster=:poster, year=:year, country=:country, director=:director, actors=:actors, quality=:quality, language=:language, tags=:tags, type=:type, updated_at=NOW() WHERE id=:id');
$update->execute(compact('title', 'origin', 'description', 'poster', 'year', 'country', 'director', 'actors', 'quality', 'language', 'tags', 'type') + ['id' => $movieId, 'original_title' => $origin]);
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 Remove extra bound parameter in crawler update query

In the existing-movie branch, execute() receives compact('title', 'origin', ...) plus original_title, but the SQL only defines :original_title and does not have an :origin placeholder. PDO treats extra named params as a binding error, so as soon as the crawler hits a movie that already exists, this update path can throw and stop the import run.

Useful? React with 👍 / 👎.

$.getJSON('/genzmovie/?route=autocomplete&q=' + encodeURIComponent(q), function (items) {
$box.empty();
items.forEach(item => {
$box.append(`<a href="/genzmovie/?route=movie&slug=${item.slug}">${item.title}</a>`);
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 Escape autocomplete titles before inserting into DOM

The autocomplete renderer appends item.title directly into an HTML template string. Because titles come from database content (which is populated from external API data), any title containing HTML/JS will be interpreted by the browser and can execute script in users' sessions when they type in search.

Useful? React with 👍 / 👎.

Comment on lines +76 to +77
$stmt = Database::connection()->prepare('SELECT * FROM movies WHERE id != :id AND (FIND_IN_SET(:genres, tags) OR year = :year) ORDER BY updated_at DESC LIMIT 8');
$stmt->execute(['id' => $movieId, 'genres' => $genresCsv, 'year' => $year]);
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 Match related movies using individual tags

related() passes the full comma-separated tag list into FIND_IN_SET(:genres, tags), but FIND_IN_SET matches a single token, not a CSV blob. For movies with multiple tags, this condition almost never matches, so related results are effectively reduced to the year = :year fallback and relevance drops sharply.

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: 20

🧹 Nitpick comments (16)
genzmovie/views/layouts/header.php (1)

22-22: Escape APP_NAME for defense in depth.

While APP_NAME is a server-defined constant, escaping it provides defense against future configuration mistakes or injection through environment variables.

♻️ Proposed fix
-        <a class="navbar-brand fw-bold" href="/genzmovie/"><?= APP_NAME ?></a>
+        <a class="navbar-brand fw-bold" href="/genzmovie/"><?= e(APP_NAME) ?></a>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/views/layouts/header.php` at line 22, Escape the APP_NAME output in
the header anchor to prevent injection if the constant ever contains unsafe
characters: replace the raw echo of APP_NAME in the layout header (the <?=
APP_NAME ?> usage inside the <a class="navbar-brand fw-bold">) with a call to
PHP's escaping function (htmlspecialchars) using ENT_QUOTES and UTF-8 so both
quotes and special HTML chars are encoded.
genzmovie/views/movie/list.php (2)

3-10: Filter inputs don't preserve submitted values.

After filtering, the form inputs reset to empty/default values. Users lose context of their active filters.

♻️ Proposed fix to preserve filter state
 <form class="row g-2 mb-3" method="GET" action="/genzmovie/">
     <input type="hidden" name="route" value="filter">
-    <div class="col"><input class="form-control" name="genre" placeholder="Thể loại"></div>
-    <div class="col"><input class="form-control" name="year" placeholder="Năm"></div>
-    <div class="col"><input class="form-control" name="country" placeholder="Quốc gia"></div>
-    <div class="col"><select class="form-select" name="quality"><option value="">Chất lượng</option><option>FHD</option><option>HD</option></select></div>
+    <div class="col"><input class="form-control" name="genre" placeholder="Thể loại" value="<?= e($_GET['genre'] ?? '') ?>"></div>
+    <div class="col"><input class="form-control" name="year" placeholder="Năm" value="<?= e($_GET['year'] ?? '') ?>"></div>
+    <div class="col"><input class="form-control" name="country" placeholder="Quốc gia" value="<?= e($_GET['country'] ?? '') ?>"></div>
+    <div class="col"><select class="form-select" name="quality">
+        <option value="">Chất lượng</option>
+        <option <?= ($_GET['quality'] ?? '') === 'FHD' ? 'selected' : '' ?>>FHD</option>
+        <option <?= ($_GET['quality'] ?? '') === 'HD' ? 'selected' : '' ?>>HD</option>
+    </select></div>
     <div class="col"><button class="btn btn-danger w-100">Lọc phim</button></div>
 </form>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/views/movie/list.php` around lines 3 - 10, The filter form inputs
(name="genre", "year", "country", and select name="quality") currently lose
their values after submission; update each input/select in
genzmovie/views/movie/list.php to populate its value/selected state from the
incoming GET params (e.g. use PHP echo of htmlspecialchars($_GET['genre'] ?? '')
for the genre/year/country inputs and mark the matching <option> as selected for
quality) so the form preserves submitted filter values after filtering.

11-11: Consider adding pagination for large result sets.

Rendering all movies in a single page load could cause performance issues with large datasets. Consider implementing pagination or infinite scroll.

Would you like me to help design a pagination component for this view?

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

In `@genzmovie/views/movie/list.php` at line 11, The view currently renders all
results in one go using the foreach over $movies and include of
../partials/movie-card.php which will not scale; implement pagination by
changing the controller to return a paginated subset (e.g., page and per_page
params) and update this view to loop only over the paginated $movies and render
pagination controls (next/prev/page links) alongside the <div class="row">, or
implement an infinite-scroll mechanism that fetches subsequent pages via AJAX
and appends included movie-card markup. Ensure the pagination state (page,
per_page, total_pages) is provided to the view and use those symbols ($movies,
movie-card.php, and the view file) to render navigation UI and clients to
request subsequent pages.
genzmovie/views/partials/movie-card.php (1)

4-4: Consider adding a fallback for missing poster images.

If $movie['poster'] is empty or the URL fails to load, users will see a broken image. Consider adding an onerror fallback or a default placeholder.

♻️ Proposed enhancement
-            <img src="<?= e($movie['poster']) ?>" class="card-img-top" alt="<?= e($movie['title']) ?>">
+            <img src="<?= e($movie['poster'] ?: '/genzmovie/assets/img/no-poster.jpg') ?>" class="card-img-top" alt="<?= e($movie['title']) ?>" onerror="this.src='/genzmovie/assets/img/no-poster.jpg'">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/views/partials/movie-card.php` at line 4, The img element in
movie-card.php uses <?= e($movie['poster']) ?> directly and can render a broken
image when $movie['poster'] is empty or the URL fails; update the <img> in
movie-card.php (the element using class "card-img-top" and alt="<?=
e($movie['title']) ?>") to provide a fallback by using a default placeholder
when $movie['poster'] is empty and add an onerror attribute that swaps the src
to the same placeholder if the remote image fails to load; ensure escaping still
uses e(...) for the title and poster variable before applying the fallback
logic.
genzmovie/includes/auth.php (1)

5-17: Inconsistent redirect patterns and hardcoded path.

Two issues:

  1. require_login() uses the redirect() helper (which respects BASE_URL), while require_admin() uses a hardcoded absolute path /genzmovie/admin/login.php.
  2. The hardcoded path in require_admin() will break if the application is deployed to a different directory.
♻️ Proposed fix for consistency
 function require_login(): void
 {
     if (empty($_SESSION['user'])) {
         redirect('/?route=login');
     }
 }

 function require_admin(): void
 {
     if (empty($_SESSION['admin'])) {
-        header('Location: /genzmovie/admin/login.php');
+        header('Location: ' . BASE_URL . '/admin/login.php');
         exit;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/includes/auth.php` around lines 5 - 17, require_admin() uses a
hardcoded header('Location: /genzmovie/admin/login.php') which is inconsistent
with require_login() and will break when BASE_URL or deployment path changes;
replace the header/exit pattern in require_admin() with the same redirect()
helper used by require_login() (e.g. call redirect('/admin/login.php') or the
equivalent admin login route) so the BASE_URL is respected and remove the
hardcoded path and manual header/exit.
genzmovie/includes/csrf.php (1)

18-21: Consider rotating CSRF token after successful validation.

The current implementation uses a per-session CSRF token that persists indefinitely. While this is a common approach, consider regenerating the token after successful validation to reduce the window for token leakage or replay.

♻️ Optional enhancement for token rotation
 function validate_csrf(?string $token): bool
 {
-    return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
+    $valid = !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
+    if ($valid) {
+        unset($_SESSION['csrf_token']); // Force regeneration on next csrf_token() call
+    }
+    return $valid;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/includes/csrf.php` around lines 18 - 21, validate_csrf currently
verifies the session CSRF token but never rotates it; after a successful
hash_equals check in validate_csrf you should generate a new token (e.g., using
secure random_bytes/bin2hex), replace $_SESSION['csrf_token'] with the new
value, and then return true; ensure you perform the timing-safe comparison using
hash_equals($_SESSION['csrf_token'], $token) before rotating to avoid
introducing a race or leaking validation behavior from failed comparisons.
genzmovie/.htaccess (1)

1-5: Missing catch-all rewrite rule for other routes.

The RewriteCond directives on lines 2-3 only apply to the immediately following RewriteRule (line 4). Routes like ?route=search, ?route=filter, etc. work because they use query parameters, but if you add clean URL routes beyond /phim/{slug}, they won't be handled.

Consider adding a catch-all rule to route all non-file/non-directory requests to index.php:

♻️ Proposed enhancement
 RewriteEngine On
 RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-d
 RewriteRule ^phim/([a-zA-Z0-9-]+)/?$ index.php?route=movie&slug=$1 [QSA,L]
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^(.*)$ index.php [QSA,L]
+
 RewriteRule ^$ index.php?route=home [QSA,L]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/.htaccess` around lines 1 - 5, The current .htaccess has
RewriteCond directives that only apply to the next RewriteRule (the phim slug
rule), so other clean URLs won't be routed; add a catch-all rule that also
checks the same conditions and forwards all non-file/non-directory requests to
index.php (repeat the two RewriteCond lines above the final RewriteRule) — e.g.,
after the existing RewriteRule ^phim/([a-zA-Z0-9-]+)/?$
index.php?route=movie&slug=$1 [QSA,L] add the same RewriteCond
%{REQUEST_FILENAME} !-f and RewriteCond %{REQUEST_FILENAME} !-d followed by a
generic RewriteRule (e.g., RewriteRule ^ index.php [QSA,L]) so any other path is
handled by index.php.
genzmovie/views/layouts/footer.php (1)

3-5: Make the footer self-contained.

genzmovie/views/layouts/header.php:1-6 currently seeds $sidebarAds / $popupAds and loads Ad.php, but this template consumes those side effects directly. Passing all ad collections into the layout once would remove the hidden include-order dependency and the extra model construction here.

Also applies to: 11-13, 17-19, 26-27

genzmovie/database/schema.sql (2)

60-71: Consider adding index on episodes.movie_id.

While the foreign key helps with referential integrity, an explicit index on movie_id would optimize the byMovie() queries in the Episode model. MySQL/InnoDB typically creates an index for foreign keys, but being explicit improves readability.

📊 Proposed index addition
   FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE
+  ,INDEX idx_movie_id (movie_id)
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/database/schema.sql` around lines 60 - 71, Add an explicit index on
the episodes.movie_id column to optimize Episode.byMovie() queries and improve
schema readability: update the episodes table definition by adding an INDEX
(movie_id) or create a separate CREATE INDEX statement for the episodes table
(referencing the episodes table and movie_id column) so the foreign key's
implicit index is made explicit in the schema.

23-44: Add index on movies.slug for slug lookups.

The slug column is used for lookups in findBySlug() queries. While it has a UNIQUE constraint (which implicitly creates an index in MySQL), this is good. However, consider adding an explicit index comment for clarity or adding indexes on other frequently queried columns like is_featured.

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

In `@genzmovie/database/schema.sql` around lines 23 - 44, Add an explicit index
for slug (used by findBySlug()) and consider adding an index for is_featured to
speed filters; update the CREATE TABLE movies statement to include KEY/INDEX
entries such as idx_movies_slug on the slug column and idx_movies_is_featured on
is_featured (or create separate CREATE INDEX statements after the table),
ensuring the names reference movies.slug and movies.is_featured so queries like
findBySlug() and featured lookups use the new indexes.
genzmovie/models/Episode.php (1)

24-28: save() method has implicit data contract and no success indication.

The method expects $data to contain specific keys (movie_id, server_name, etc.) but doesn't validate this. Missing keys will cause a PDOException. Consider validating required keys or documenting the expected structure. Additionally, returning a boolean or the inserted ID would help callers verify success.

💡 Proposed improvement
-    public function save(array $data): void
+    public function save(array $data): int
     {
+        $required = ['movie_id', 'server_name', 'episode_number', 'embed_link', 'mp4_link', 'subtitle_link'];
+        foreach ($required as $key) {
+            if (!array_key_exists($key, $data)) {
+                throw new InvalidArgumentException("Missing required key: $key");
+            }
+        }
         $stmt = Database::connection()->prepare('INSERT INTO episodes (movie_id, server_name, episode_number, embed_link, mp4_link, subtitle_link, created_at, updated_at) VALUES (:movie_id, :server_name, :episode_number, :embed_link, :mp4_link, :subtitle_link, NOW(), NOW())');
         $stmt->execute($data);
+        return (int) Database::connection()->lastInsertId();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/models/Episode.php` around lines 24 - 28, The save() method
currently assumes the $data array contains keys movie_id, server_name,
episode_number, embed_link, mp4_link, subtitle_link and just calls
Database::connection()->prepare(...) and $stmt->execute($data); modify save() to
validate that those required keys exist (e.g., check array_key_exists or similar
for movie_id, server_name, episode_number, embed_link, mp4_link, subtitle_link)
and throw a clear InvalidArgumentException if any are missing; after executing
the prepared statement, return a success indicator (boolean) or the inserted ID
(use Database::connection()->lastInsertId() or return (bool)$stmt->rowCount())
so callers can verify success, and ensure the method signature and any callers
are updated to handle the new return value.
genzmovie/controllers/AuthController.php (2)

35-40: No duplicate email check before registration.

If a user registers with an existing email, the database will throw a PDOException due to the UNIQUE constraint. This results in a generic "Không thể tạo tài khoản" error rather than a helpful "Email already exists" message.

💡 Proposed improvement
             } else {
                 $userModel = new User();
+                if ($userModel->findByEmail(trim($_POST['email']))) {
+                    $error = 'Email đã tồn tại.';
+                } else {
                     $ok = $userModel->create(trim($_POST['name']), trim($_POST['email']), $_POST['password']);
                     if ($ok) {
                         redirect('/?route=login');
                     }
                     $error = 'Không thể tạo tài khoản.';
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/controllers/AuthController.php` around lines 35 - 40, Add a
duplicate-email check and surface a specific "Email already exists" message
instead of the generic failure: before calling User->create(...) in
AuthController (the block using $userModel = new User(); $ok =
$userModel->create(...)), query the User model for an existing record by the
provided email (e.g., a User::findByEmail or similar lookup) and if found set
$error = 'Email already exists.' and do not call create; alternatively wrap the
$userModel->create(...) call in a try/catch for PDOException and detect
UNIQUE-constraint duplicate-key errors to set the same specific message; ensure
the controller uses the specific error message when rendering the response.

46-50: Incomplete session destruction on logout.

Using unset($_SESSION['user']) only removes the user key. For complete logout, consider destroying the session entirely to prevent session fixation attacks.

🔒 Proposed secure logout
     public function logout(): void
     {
-        unset($_SESSION['user']);
+        $_SESSION = [];
+        if (ini_get('session.use_cookies')) {
+            $params = session_get_cookie_params();
+            setcookie(session_name(), '', time() - 42000,
+                $params['path'], $params['domain'],
+                $params['secure'], $params['httponly']
+            );
+        }
+        session_destroy();
         redirect('/');
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/controllers/AuthController.php` around lines 46 - 50, The logout
method currently only unsets $_SESSION['user']; update the
AuthController::logout to fully terminate the session by unsetting all session
variables, destroying the session, and removing the session cookie (and
optionally regenerating the session id) before calling redirect('/'); ensure you
replace the single unset($_SESSION['user']) with calls that perform
session_unset(), session_destroy(), and cookie removal/regeneration to prevent
session fixation.
genzmovie/api/sitemap.php (1)

5-5: Unbounded query may cause memory issues at scale.

The query fetches all movies without pagination. For large movie catalogs, this could exhaust memory and cause timeouts. Consider implementing sitemap index with chunked sitemaps (50,000 URLs per sitemap is the standard limit).

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

In `@genzmovie/api/sitemap.php` at line 5, The current unbounded fetchAll() in
sitemap.php (the $movies = Database::connection()->query(...)->fetchAll() line)
can exhaust memory; change to stream/chunk the results and emit a sitemap index
with multiple sitemap files each capped at 50,000 URLs. Replace the single
query/fetchAll with a paginated loop using Database::connection()->query(...)
with LIMIT and OFFSET (or a server-side cursor) to fetch batches, write each
batch into a separate sitemap file (or stream a sitemap response part), and then
generate a sitemap index that references those chunked sitemap files; ensure you
remove fetchAll() and use incremental fetching to avoid loading the entire
movies table into memory.
genzmovie/admin/index.php (1)

14-14: Synchronous crawler execution may timeout.

Including the crawler script directly blocks the request until completion. For large imports, this could exceed PHP's max_execution_time. Consider running the crawler asynchronously via a job queue or background process.

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

In `@genzmovie/admin/index.php` at line 14, The current synchronous include
statement include __DIR__ . '/../crawler/ophim_crawler.php' blocks the request
and can exceed max_execution_time; instead, invoke the crawler
asynchronously—either dispatch a job to your existing queue worker (e.g., push a
task that runs ophim_crawler.php) or spawn a background process (e.g., exec or
shell_exec with PHP CLI and nohup/ampersand) so the web request returns
immediately; replace the direct include in index.php with a non-blocking
enqueue/exec call and ensure the crawler script can run standalone via CLI.
genzmovie/models/Movie.php (1)

31-35: Push autocomplete limits into SQL, not PHP.

genzmovie/controllers/MovieController.php:72 trims this result to 8 items only after calling search(). On a large catalog, every keystroke still pulls the full match set from MySQL. Add an optional limit here and apply LIMIT in the query for the autocomplete path.

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

In `@genzmovie/models/Movie.php` around lines 31 - 35, The Movie::search(string
$keyword) method currently returns all matches; change its signature to accept
an optional int $limit = null and, when $limit is provided, add a SQL LIMIT
clause to the prepared statement (use a parameterized/bound integer to avoid
injection) so the database returns only up to that many rows; keep the current
behavior when $limit is null (no LIMIT). Update the caller in the autocomplete
path (where search() is invoked) to pass 8 as the limit. Ensure the prepared
statement and execute/bind calls reference the new limit parameter (e.g.,
:limit) and bind it as an integer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@genzmovie/admin/index.php`:
- Around line 12-27: The POST handling block should explicitly abort or surface
an error when CSRF fails and protect DB operations from uncaught exceptions:
ensure validate_csrf($_POST['csrf_token'] ?? null) is checked first and when it
returns false set an error message and skip further action handling (do not fall
through), and wrap the ads insertion ($stmt = $pdo->prepare(...) and
$stmt->execute(...)) in a try/catch that catches PDOException to set a safe
error message (and/or log the exception) instead of letting it crash; also
ensure the action branch for 'import_ophim' still runs only when CSRF passed
(refer to $_POST['action'] and the include of ../crawler/ophim_crawler.php) so
no action executes when CSRF validation fails.

In `@genzmovie/admin/login.php`:
- Around line 15-17: On successful authentication in login.php (the block using
password_verify and setting $_SESSION['admin']), rotate the session ID to
prevent fixation (call session_regenerate_id(true)) and then store only minimal
admin claims in $_SESSION['admin'] such as admin id and username/email (do NOT
include the full DB row or the password hash); ensure any code that checks admin
presence (require_admin()) continues to look only for those minimal claims.

In `@genzmovie/api/sitemap.php`:
- Around line 10-14: The sitemap loop currently calls date('c',
strtotime($movie['updated_at'])) which yields an incorrect epoch when updated_at
is null; modify the foreach ($movies as $movie) block to compute a safe
timestamp: pick $movie['updated_at'] if truthy, otherwise fall back to
$movie['created_at'] (or null), and only render the <lastmod> element when a
valid date exists (i.e., after checking strtotime returned a timestamp),
otherwise omit <lastmod> or use a sensible default; update the code around the
use of $movie['updated_at'], strtotime(...) and date('c', ...) accordingly.

In `@genzmovie/assets/js/app.js`:
- Around line 11-16: The autocomplete code directly injects item.slug and
item.title into HTML and fires on every keystroke; fix by escaping/encoding
values and debouncing requests: replace the string-template append with creation
of a safe link via jQuery/DOM APIs (e.g., create an <a>, set its text with
.text(item.title) and set href using '/genzmovie/?route=movie&slug=' +
encodeURIComponent(item.slug) so item.title and item.slug are not inserted raw),
and wrap the function that calls $.getJSON(...) in a debounce (e.g., 200–300ms)
so the AJAX call is only made after typing pauses; keep using $box and $.getJSON
but change $box.append(...) to the safe element construction and add a debounced
handler for the input event that calls the $.getJSON-based lookup.

In `@genzmovie/config/config.php`:
- Around line 5-11: Replace the hardcoded credentials and deployment URL
constants (DB_HOST, DB_NAME, DB_USER, DB_PASS, BASE_URL and optionally
OPHIM_API_BASE) so they are read from environment variables or an untracked
local config; if any required env value is missing, throw a clear, fast-failing
exception (or exit) during bootstrap so deployments fail early instead of using
insecure defaults, and keep no secret values in source control. Use the existing
constant names (DB_HOST, DB_NAME, DB_USER, DB_PASS, BASE_URL, OPHIM_API_BASE) as
the targets to update so other code continues to reference them.

In `@genzmovie/controllers/HomeController.php`:
- Around line 13-15: The mapping for 'Phim mới cập nhật' currently points to the
same feed key 'series', causing duplicate datasets; update the feeds definition
in HomeController (the associative array that includes 'Phim mới cập nhật' =>
'series', 'Phim chiếu rạp' => 'theater', 'Phim bộ' => 'series') so that 'Phim
mới cập nhật' uses a distinct key (e.g., 'latest' or 'recent_updates') and
ensure the controller loads a dedicated query or service for that key (create or
call the new "latest updates" query/handler) so the homepage renders a separate
dataset instead of duplicating 'series'.

In `@genzmovie/controllers/UserController.php`:
- Around line 36-44: The current read-then-write with $check/prepare on the
favorites table races; add a unique constraint on (user_id, movie_id) and make
the toggle atomic by wrapping the logic in a transaction using
$pdo->beginTransaction(), re-checking the row with SELECT id FROM favorites
WHERE user_id = :uid AND movie_id = :mid FOR UPDATE (replace the current
$check/$found flow), then either DELETE FROM favorites WHERE id = :id or INSERT
INTO favorites (...) VALUES (...) as appropriate, and finally $pdo->commit();
ensure you rollback on error and rely on the unique constraint to prevent
duplicate inserts under concurrency.
- Around line 32-33: Validate incoming movie_id before using it: ensure the
parsed $movieId (currently from (int) ($_POST['movie_id'] ?? 0)) is a positive
integer and return a 400 response if not; then check the movies datastore for
existence (e.g., via MovieModel::find/getById or $movieRepository->findById) and
return 404 if the movie is missing; perform these checks in the same controller
action (where $userId and $movieId are set) and abort before any favorite/insert
logic to avoid creating rows with invalid FK values or causing 500 errors.

In `@genzmovie/crawler/ophim_crawler.php`:
- Around line 90-104: The episode replacement currently deletes episodes then
inserts new rows using $pdo->prepare(...)->execute(...) which can leave the DB
in an inconsistent state if an insert fails; wrap the entire movie upsert and
this episode deletion+inserts in a single transaction using
$pdo->beginTransaction(), perform the DELETE and all INSERTs, then
$pdo->commit(); on any failure catch the exception, call $pdo->rollBack() and
rethrow or log the error; make sure to reference the existing $pdo usage and the
prepare/execute calls for DELETE and INSERT so the transaction scope includes
those operations.
- Around line 12-17: The curl options currently disable TLS verification on the
curl handle $ch via curl_setopt_array; update the options in the
curl_setopt_array call (the array passed for $ch) to set CURLOPT_SSL_VERIFYPEER
=> true and add CURLOPT_SSL_VERIFYHOST => 2 to enforce certificate and hostname
verification for all crawler requests, ensuring you still keep other options
like CURLOPT_RETURNTRANSFER and CURLOPT_FOLLOWLOCATION as needed.
- Around line 65-66: The execute call for $update->execute is passing an
incorrect parameter array via compact('title', 'origin', ...) which supplies an
unused "origin" key while the SQL expects "original_title", causing
SQLSTATE[HY093]; replace the compact call with an explicit associative array
matching the query placeholders (keys: title, original_title => $origin,
description, poster, year, country, director, actors, quality, language, tags,
type, id => $movieId) so that every placeholder has a corresponding value and no
extra keys are sent.

In `@genzmovie/database/schema.sql`:
- Around line 121-122: The schema currently seeds an admin via the literal
INSERT INTO admins ... VALUES (...) which embeds a fixed password hash in the
repo; remove that hardcoded INSERT from the schema and replace it with a runtime
initialization that reads admin credentials from environment variables (e.g.,
ADMIN_EMAIL, ADMIN_PASSWORD) and creates the admin by hashing the provided
password (using your project's password hashing utility or bcrypt) in a
startup/migration script (e.g., an initAdmin or seedAdmin function) so no
plaintext or fixed hashes are committed; keep the admins table definition but
move seeding to an env-driven initializer and ensure the initializer skips
creation if an admin already exists.

In `@genzmovie/models/Movie.php`:
- Around line 74-77: The related() method is using FIND_IN_SET(:genres, tags)
with a comma-delimited $genresCsv which fails when the first argument contains
commas; change related() to split $genresCsv into individual tags and build the
WHERE clause to check each tag separately (e.g., multiple FIND_IN_SET(:tagN,
tags) ORed) or, better, rewrite the query to join the normalized movie_genres
and genres tables (as byCategory() and filter() do) to match movies by genre
properly while still excluding the current movieId and limiting results.

In `@genzmovie/models/User.php`:
- Around line 19-24: The findByEmail method currently uses SELECT * which
exposes the password hash (and any future sensitive columns); change the query
in findByEmail to select explicit columns only (e.g. id, email, name, password)
instead of *, and then ensure the controller (AuthController) strips the
password/hash before persisting to session by unsetting the password key from
the returned array prior to assigning $_SESSION['user'] so only non-sensitive
fields are stored.

In `@genzmovie/views/auth/login.php`:
- Line 4: The template uses a bare variable check if ($error) which can emit a
PHP notice when $error is not defined; modify the conditional that renders the
alert in login.php to first guard for the variable (e.g., use isset($error)
and/or !empty($error)) so the block only runs when $error exists and is
non-empty—locate the conditional around the alert div in login.php and replace
the bare if ($error) check with a safe existence check.

In `@genzmovie/views/auth/register.php`:
- Line 4: The conditional in the register view uses if ($error) which can raise
a notice when $error is undefined; change the check to use isset($error) (or
check ($error ?? null)) before rendering so the alert only prints when the
variable exists and is non-empty—update the conditional around the $error usage
in genzmovie/views/auth/register.php to use isset($error) or the null coalescing
operator with e($error) to avoid undefined variable notices.

In `@genzmovie/views/home/index.php`:
- Around line 18-23: The CTA uses the display label instead of the section key;
update the template to use the section key used by HomeController. Change the
foreach to expose both the key and the display label (e.g. foreach
($moviesBySection as $sectionKey => $section) or extract $sectionKey and
$sectionLabel from each entry), and build the anchor href using
urlencode($sectionKey) (the variable feeding q=) instead of urlencode($label);
keep the visible text as the human label. Ensure you reference $moviesBySection,
the foreach, and the anchor href when making the change.

In `@genzmovie/views/layouts/header.php`:
- Around line 40-42: The ad rendering loop exposing raw HTML via $ad['ad_code']
in the header.php foreach over $headerAds is an XSS risk; either (a) treat
ad_code as untrusted by escaping output (use an HTML-escaping helper) or (b)
ensure ad_code is sanitized/validated at input time (strip disallowed
tags/attributes or store only safe embed patterns) or (c) render third-party ads
inside an isolated iframe/nonceed script. Also add or document a
Content-Security-Policy (CSP) header (report-only at first) in your
response/boot pipeline to restrict allowed script/src/frame origins and include
this trust assumption in admin docs. Target the foreach that outputs
$ad['ad_code'] and the request/response layer where headers are set.

In `@genzmovie/views/movie/show.php`:
- Around line 28-33: The view currently outputs unvalidated external URLs
(currentEpisode['embed_link'], currentEpisode['mp4_link'],
currentEpisode['subtitle_link']) which can allow non-HTTP(S) schemes; add a
helper safe_media_url(?string $url): ?string that returns the URL only if
filter_var($url, FILTER_VALIDATE_URL) passes and parse_url($url, PHP_URL_SCHEME)
is 'http' or 'https', then call safe_media_url on each of
embed_link/mp4_link/subtitle_link and only render the <iframe>, <source> or
<track> when the validated value is non-null (still escape with e() when
outputting).

In `@README.md`:
- Around line 50-52: Remove the publicly documented default credentials by
deleting or replacing the "Default admin account" section (the lines referencing
admin@genzmovie.local and admin123) and update the installation/setup
instructions to require creating an admin account or rotating credentials during
first-run; also remove or change any seeded admin with hardcoded password in
your database seed/migration so the app forces interactive admin creation or a
one-time generated password and documents secure rotation steps instead.

---

Nitpick comments:
In `@genzmovie/.htaccess`:
- Around line 1-5: The current .htaccess has RewriteCond directives that only
apply to the next RewriteRule (the phim slug rule), so other clean URLs won't be
routed; add a catch-all rule that also checks the same conditions and forwards
all non-file/non-directory requests to index.php (repeat the two RewriteCond
lines above the final RewriteRule) — e.g., after the existing RewriteRule
^phim/([a-zA-Z0-9-]+)/?$ index.php?route=movie&slug=$1 [QSA,L] add the same
RewriteCond %{REQUEST_FILENAME} !-f and RewriteCond %{REQUEST_FILENAME} !-d
followed by a generic RewriteRule (e.g., RewriteRule ^ index.php [QSA,L]) so any
other path is handled by index.php.

In `@genzmovie/admin/index.php`:
- Line 14: The current synchronous include statement include __DIR__ .
'/../crawler/ophim_crawler.php' blocks the request and can exceed
max_execution_time; instead, invoke the crawler asynchronously—either dispatch a
job to your existing queue worker (e.g., push a task that runs
ophim_crawler.php) or spawn a background process (e.g., exec or shell_exec with
PHP CLI and nohup/ampersand) so the web request returns immediately; replace the
direct include in index.php with a non-blocking enqueue/exec call and ensure the
crawler script can run standalone via CLI.

In `@genzmovie/api/sitemap.php`:
- Line 5: The current unbounded fetchAll() in sitemap.php (the $movies =
Database::connection()->query(...)->fetchAll() line) can exhaust memory; change
to stream/chunk the results and emit a sitemap index with multiple sitemap files
each capped at 50,000 URLs. Replace the single query/fetchAll with a paginated
loop using Database::connection()->query(...) with LIMIT and OFFSET (or a
server-side cursor) to fetch batches, write each batch into a separate sitemap
file (or stream a sitemap response part), and then generate a sitemap index that
references those chunked sitemap files; ensure you remove fetchAll() and use
incremental fetching to avoid loading the entire movies table into memory.

In `@genzmovie/controllers/AuthController.php`:
- Around line 35-40: Add a duplicate-email check and surface a specific "Email
already exists" message instead of the generic failure: before calling
User->create(...) in AuthController (the block using $userModel = new User();
$ok = $userModel->create(...)), query the User model for an existing record by
the provided email (e.g., a User::findByEmail or similar lookup) and if found
set $error = 'Email already exists.' and do not call create; alternatively wrap
the $userModel->create(...) call in a try/catch for PDOException and detect
UNIQUE-constraint duplicate-key errors to set the same specific message; ensure
the controller uses the specific error message when rendering the response.
- Around line 46-50: The logout method currently only unsets $_SESSION['user'];
update the AuthController::logout to fully terminate the session by unsetting
all session variables, destroying the session, and removing the session cookie
(and optionally regenerating the session id) before calling redirect('/');
ensure you replace the single unset($_SESSION['user']) with calls that perform
session_unset(), session_destroy(), and cookie removal/regeneration to prevent
session fixation.

In `@genzmovie/database/schema.sql`:
- Around line 60-71: Add an explicit index on the episodes.movie_id column to
optimize Episode.byMovie() queries and improve schema readability: update the
episodes table definition by adding an INDEX (movie_id) or create a separate
CREATE INDEX statement for the episodes table (referencing the episodes table
and movie_id column) so the foreign key's implicit index is made explicit in the
schema.
- Around line 23-44: Add an explicit index for slug (used by findBySlug()) and
consider adding an index for is_featured to speed filters; update the CREATE
TABLE movies statement to include KEY/INDEX entries such as idx_movies_slug on
the slug column and idx_movies_is_featured on is_featured (or create separate
CREATE INDEX statements after the table), ensuring the names reference
movies.slug and movies.is_featured so queries like findBySlug() and featured
lookups use the new indexes.

In `@genzmovie/includes/auth.php`:
- Around line 5-17: require_admin() uses a hardcoded header('Location:
/genzmovie/admin/login.php') which is inconsistent with require_login() and will
break when BASE_URL or deployment path changes; replace the header/exit pattern
in require_admin() with the same redirect() helper used by require_login() (e.g.
call redirect('/admin/login.php') or the equivalent admin login route) so the
BASE_URL is respected and remove the hardcoded path and manual header/exit.

In `@genzmovie/includes/csrf.php`:
- Around line 18-21: validate_csrf currently verifies the session CSRF token but
never rotates it; after a successful hash_equals check in validate_csrf you
should generate a new token (e.g., using secure random_bytes/bin2hex), replace
$_SESSION['csrf_token'] with the new value, and then return true; ensure you
perform the timing-safe comparison using hash_equals($_SESSION['csrf_token'],
$token) before rotating to avoid introducing a race or leaking validation
behavior from failed comparisons.

In `@genzmovie/models/Episode.php`:
- Around line 24-28: The save() method currently assumes the $data array
contains keys movie_id, server_name, episode_number, embed_link, mp4_link,
subtitle_link and just calls Database::connection()->prepare(...) and
$stmt->execute($data); modify save() to validate that those required keys exist
(e.g., check array_key_exists or similar for movie_id, server_name,
episode_number, embed_link, mp4_link, subtitle_link) and throw a clear
InvalidArgumentException if any are missing; after executing the prepared
statement, return a success indicator (boolean) or the inserted ID (use
Database::connection()->lastInsertId() or return (bool)$stmt->rowCount()) so
callers can verify success, and ensure the method signature and any callers are
updated to handle the new return value.

In `@genzmovie/models/Movie.php`:
- Around line 31-35: The Movie::search(string $keyword) method currently returns
all matches; change its signature to accept an optional int $limit = null and,
when $limit is provided, add a SQL LIMIT clause to the prepared statement (use a
parameterized/bound integer to avoid injection) so the database returns only up
to that many rows; keep the current behavior when $limit is null (no LIMIT).
Update the caller in the autocomplete path (where search() is invoked) to pass 8
as the limit. Ensure the prepared statement and execute/bind calls reference the
new limit parameter (e.g., :limit) and bind it as an integer.

In `@genzmovie/views/layouts/header.php`:
- Line 22: Escape the APP_NAME output in the header anchor to prevent injection
if the constant ever contains unsafe characters: replace the raw echo of
APP_NAME in the layout header (the <?= APP_NAME ?> usage inside the <a
class="navbar-brand fw-bold">) with a call to PHP's escaping function
(htmlspecialchars) using ENT_QUOTES and UTF-8 so both quotes and special HTML
chars are encoded.

In `@genzmovie/views/movie/list.php`:
- Around line 3-10: The filter form inputs (name="genre", "year", "country", and
select name="quality") currently lose their values after submission; update each
input/select in genzmovie/views/movie/list.php to populate its value/selected
state from the incoming GET params (e.g. use PHP echo of
htmlspecialchars($_GET['genre'] ?? '') for the genre/year/country inputs and
mark the matching <option> as selected for quality) so the form preserves
submitted filter values after filtering.
- Line 11: The view currently renders all results in one go using the foreach
over $movies and include of ../partials/movie-card.php which will not scale;
implement pagination by changing the controller to return a paginated subset
(e.g., page and per_page params) and update this view to loop only over the
paginated $movies and render pagination controls (next/prev/page links)
alongside the <div class="row">, or implement an infinite-scroll mechanism that
fetches subsequent pages via AJAX and appends included movie-card markup. Ensure
the pagination state (page, per_page, total_pages) is provided to the view and
use those symbols ($movies, movie-card.php, and the view file) to render
navigation UI and clients to request subsequent pages.

In `@genzmovie/views/partials/movie-card.php`:
- Line 4: The img element in movie-card.php uses <?= e($movie['poster']) ?>
directly and can render a broken image when $movie['poster'] is empty or the URL
fails; update the <img> in movie-card.php (the element using class
"card-img-top" and alt="<?= e($movie['title']) ?>") to provide a fallback by
using a default placeholder when $movie['poster'] is empty and add an onerror
attribute that swaps the src to the same placeholder if the remote image fails
to load; ensure escaping still uses e(...) for the title and poster variable
before applying the fallback logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 836895a3-e96e-4476-a542-72b6f879df0c

📥 Commits

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

📒 Files selected for processing (32)
  • README.md
  • genzmovie/.htaccess
  • genzmovie/admin/index.php
  • genzmovie/admin/login.php
  • genzmovie/api/sitemap.php
  • genzmovie/assets/css/style.css
  • genzmovie/assets/js/app.js
  • genzmovie/config/config.php
  • genzmovie/config/database.php
  • genzmovie/controllers/AuthController.php
  • genzmovie/controllers/HomeController.php
  • genzmovie/controllers/MovieController.php
  • genzmovie/controllers/UserController.php
  • genzmovie/crawler/ophim_crawler.php
  • genzmovie/database/schema.sql
  • genzmovie/includes/auth.php
  • genzmovie/includes/csrf.php
  • genzmovie/includes/helpers.php
  • genzmovie/index.php
  • genzmovie/models/Ad.php
  • genzmovie/models/Episode.php
  • genzmovie/models/Movie.php
  • genzmovie/models/User.php
  • genzmovie/views/auth/login.php
  • genzmovie/views/auth/register.php
  • genzmovie/views/home/index.php
  • genzmovie/views/layouts/footer.php
  • genzmovie/views/layouts/header.php
  • genzmovie/views/movie/list.php
  • genzmovie/views/movie/show.php
  • genzmovie/views/partials/movie-card.php
  • genzmovie/views/user/dashboard.php

Comment thread genzmovie/admin/index.php
Comment on lines +12 to +27
if ($_SERVER['REQUEST_METHOD'] === 'POST' && validate_csrf($_POST['csrf_token'] ?? null)) {
if (($_POST['action'] ?? '') === 'import_ophim') {
include __DIR__ . '/../crawler/ophim_crawler.php';
$message = 'Đã chạy crawler OPhim.';
}
if (($_POST['action'] ?? '') === 'add_ad') {
$stmt = $pdo->prepare('INSERT INTO ads (name, position, ad_type, ad_code, status, created_at, updated_at) VALUES (:name,:position,:ad_type,:ad_code,1,NOW(),NOW())');
$stmt->execute([
'name' => $_POST['name'],
'position' => $_POST['position'],
'ad_type' => $_POST['ad_type'],
'ad_code' => $_POST['ad_code'],
]);
$message = 'Đã thêm quảng cáo';
}
}
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

CSRF validation doesn't prevent action execution on failure.

The current logic validates CSRF but doesn't prevent code from continuing when validation fails. If CSRF fails, the if block is skipped, but no error is shown and the page renders normally. More critically, if a request has valid CSRF but encounters a database error (e.g., invalid enum value), the unhandled PDOException will crash the page.

🔒 Proposed fix with proper validation flow
 if ($_SERVER['REQUEST_METHOD'] === 'POST' && validate_csrf($_POST['csrf_token'] ?? null)) {
-    if (($_POST['action'] ?? '') === 'import_ophim') {
-        include __DIR__ . '/../crawler/ophim_crawler.php';
-        $message = 'Đã chạy crawler OPhim.';
-    }
-    if (($_POST['action'] ?? '') === 'add_ad') {
-        $stmt = $pdo->prepare('INSERT INTO ads (name, position, ad_type, ad_code, status, created_at, updated_at) VALUES (:name,:position,:ad_type,:ad_code,1,NOW(),NOW())');
-        $stmt->execute([
-            'name' => $_POST['name'],
-            'position' => $_POST['position'],
-            'ad_type' => $_POST['ad_type'],
-            'ad_code' => $_POST['ad_code'],
-        ]);
-        $message = 'Đã thêm quảng cáo';
+    $action = $_POST['action'] ?? '';
+    try {
+        if ($action === 'import_ophim') {
+            include __DIR__ . '/../crawler/ophim_crawler.php';
+            $message = 'Đã chạy crawler OPhim.';
+        } elseif ($action === 'add_ad') {
+            $allowedPositions = ['header', 'sidebar', 'popup', 'video_preroll', 'footer'];
+            $allowedTypes = ['adsense', 'custom_html', 'script'];
+            if (!in_array($_POST['position'], $allowedPositions, true) || 
+                !in_array($_POST['ad_type'], $allowedTypes, true)) {
+                $message = 'Giá trị không hợp lệ.';
+            } else {
+                $stmt = $pdo->prepare('INSERT INTO ads (name, position, ad_type, ad_code, status, created_at, updated_at) VALUES (:name,:position,:ad_type,:ad_code,1,NOW(),NOW())');
+                $stmt->execute([
+                    'name' => $_POST['name'],
+                    'position' => $_POST['position'],
+                    'ad_type' => $_POST['ad_type'],
+                    'ad_code' => $_POST['ad_code'],
+                ]);
+                $message = 'Đã thêm quảng cáo';
+            }
+        }
+    } catch (PDOException $e) {
+        $message = 'Lỗi cơ sở dữ liệu.';
     }
+} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    $message = 'CSRF token không hợp lệ.';
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/admin/index.php` around lines 12 - 27, The POST handling block
should explicitly abort or surface an error when CSRF fails and protect DB
operations from uncaught exceptions: ensure validate_csrf($_POST['csrf_token']
?? null) is checked first and when it returns false set an error message and
skip further action handling (do not fall through), and wrap the ads insertion
($stmt = $pdo->prepare(...) and $stmt->execute(...)) in a try/catch that catches
PDOException to set a safe error message (and/or log the exception) instead of
letting it crash; also ensure the action branch for 'import_ophim' still runs
only when CSRF passed (refer to $_POST['action'] and the include of
../crawler/ophim_crawler.php) so no action executes when CSRF validation fails.

Comment thread genzmovie/admin/login.php
Comment on lines +15 to +17
if ($admin && password_verify($_POST['password'], $admin['password'])) {
$_SESSION['admin'] = $admin;
header('Location: /genzmovie/admin/index.php');
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every consumer of the admin session payload.
rg -n -C2 -g '!vendor/**' "\$_SESSION\['admin'\]|\$_SESSION\[\"admin\"\]"

# Check whether any auth flow already rotates the session ID.
rg -n -C2 -g '!vendor/**' 'session_regenerate_id\s*\('

Repository: huzely/Decrypt

Length of output: 40


🏁 Script executed:

# List the repository structure
git ls-files | head -30

# Check if genzmovie directory exists and explore it
find . -type d -name "genzmovie" -o -type d -name "admin" | head -20

Repository: huzely/Decrypt

Length of output: 959


🏁 Script executed:

# Read the login.php file to see lines 15-17 and context
cat -n genzmovie/admin/login.php

# Search with broader patterns for session usage
rg -n "SESSION.*admin|admin.*SESSION" -g '!vendor/**'

# Check for session_regenerate_id anywhere
rg -n "session_regenerate_id" -g '!vendor/**'

# Search in auth.php as well
cat -n genzmovie/includes/auth.php

Repository: huzely/Decrypt

Length of output: 2293


🏁 Script executed:

# Check database schema to see what fields the admins table has
cat -n genzmovie/database/schema.sql | grep -A 20 "CREATE TABLE.*admins"

# Check how admin/index.php uses the session data
cat -n genzmovie/admin/index.php

Repository: huzely/Decrypt

Length of output: 5131


Rotate the session and store only minimal admin claims.

Successful login writes the full admin row into the existing session, keeping the password hash in session state and leaving the pre-auth session ID valid after privilege escalation.

The admins table includes a password field (256-byte hash), which is unnecessarily exposed in $_SESSION['admin']. The session payload is checked only for existence in require_admin(), not for its contents, making the full row redundant.

🔒 Minimal hardening change
         if ($admin && password_verify($_POST['password'], $admin['password'])) {
+            session_regenerate_id(true);
-            $_SESSION['admin'] = $admin;
+            $_SESSION['admin'] = [
+                'id' => (int) $admin['id'],
+                'email' => $admin['email'],
+            ];
             header('Location: /genzmovie/admin/index.php');
             exit;
📝 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 ($admin && password_verify($_POST['password'], $admin['password'])) {
$_SESSION['admin'] = $admin;
header('Location: /genzmovie/admin/index.php');
if ($admin && password_verify($_POST['password'], $admin['password'])) {
session_regenerate_id(true);
$_SESSION['admin'] = [
'id' => (int) $admin['id'],
'email' => $admin['email'],
];
header('Location: /genzmovie/admin/index.php');
exit;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/admin/login.php` around lines 15 - 17, On successful authentication
in login.php (the block using password_verify and setting $_SESSION['admin']),
rotate the session ID to prevent fixation (call session_regenerate_id(true)) and
then store only minimal admin claims in $_SESSION['admin'] such as admin id and
username/email (do NOT include the full DB row or the password hash); ensure any
code that checks admin presence (require_admin()) continues to look only for
those minimal claims.

Comment thread genzmovie/api/sitemap.php
Comment on lines +10 to +14
<?php foreach ($movies as $movie): ?>
<url>
<loc><?= BASE_URL ?>/phim/<?= htmlspecialchars($movie['slug']) ?></loc>
<lastmod><?= date('c', strtotime($movie['updated_at'])) ?></lastmod>
</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 | 🟡 Minor

Handle null updated_at gracefully.

If updated_at is null (as allowed by the schema), strtotime(null) returns false, causing date('c', false) to produce an incorrect timestamp (epoch). Either ensure updated_at has a default or add a fallback.

🛠️ Proposed fix
     <?php foreach ($movies as $movie): ?>
     <url>
         <loc><?= BASE_URL ?>/phim/<?= htmlspecialchars($movie['slug']) ?></loc>
-        <lastmod><?= date('c', strtotime($movie['updated_at'])) ?></lastmod>
+        <lastmod><?= $movie['updated_at'] ? date('c', strtotime($movie['updated_at'])) : date('c') ?></lastmod>
     </url>
     <?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 ($movies as $movie): ?>
<url>
<loc><?= BASE_URL ?>/phim/<?= htmlspecialchars($movie['slug']) ?></loc>
<lastmod><?= date('c', strtotime($movie['updated_at'])) ?></lastmod>
</url>
<?php foreach ($movies as $movie): ?>
<url>
<loc><?= BASE_URL ?>/phim/<?= htmlspecialchars($movie['slug']) ?></loc>
<lastmod><?= $movie['updated_at'] ? date('c', strtotime($movie['updated_at'])) : date('c') ?></lastmod>
</url>
<?php endforeach; ?>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/api/sitemap.php` around lines 10 - 14, The sitemap loop currently
calls date('c', strtotime($movie['updated_at'])) which yields an incorrect epoch
when updated_at is null; modify the foreach ($movies as $movie) block to compute
a safe timestamp: pick $movie['updated_at'] if truthy, otherwise fall back to
$movie['created_at'] (or null), and only render the <lastmod> element when a
valid date exists (i.e., after checking strtotime returned a timestamp),
otherwise omit <lastmod> or use a sensible default; update the code around the
use of $movie['updated_at'], strtotime(...) and date('c', ...) accordingly.

Comment on lines +11 to +16
$.getJSON('/genzmovie/?route=autocomplete&q=' + encodeURIComponent(q), function (items) {
$box.empty();
items.forEach(item => {
$box.append(`<a href="/genzmovie/?route=movie&slug=${item.slug}">${item.title}</a>`);
});
});
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

XSS vulnerability and missing debounce.

The response data (item.slug and item.title) is inserted directly into the DOM without escaping, which could allow XSS if the server returns malicious content. Additionally, no debounce is applied, causing an API request on every keystroke.

🔒 Proposed fix with escaping and debounce
 $(function () {
     const $search = $('#movie-search');
     const $box = $('#autocomplete-box');
+    let debounceTimer;
+
+    function escapeHtml(str) {
+        const div = document.createElement('div');
+        div.textContent = str;
+        return div.innerHTML;
+    }

     $search.on('input', function () {
+        clearTimeout(debounceTimer);
         const q = $(this).val().trim();
         if (q.length < 2) {
             $box.empty();
             return;
         }
-        $.getJSON('/genzmovie/?route=autocomplete&q=' + encodeURIComponent(q), function (items) {
-            $box.empty();
-            items.forEach(item => {
-                $box.append(`<a href="/genzmovie/?route=movie&slug=${item.slug}">${item.title}</a>`);
+        debounceTimer = setTimeout(function () {
+            $.getJSON('/genzmovie/?route=autocomplete&q=' + encodeURIComponent(q), function (items) {
+                $box.empty();
+                items.forEach(item => {
+                    $box.append(`<a href="/genzmovie/?route=movie&slug=${encodeURIComponent(item.slug)}">${escapeHtml(item.title)}</a>`);
+                });
+            }).fail(function () {
+                $box.empty();
             });
-        });
+        }, 300);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/assets/js/app.js` around lines 11 - 16, The autocomplete code
directly injects item.slug and item.title into HTML and fires on every
keystroke; fix by escaping/encoding values and debouncing requests: replace the
string-template append with creation of a safe link via jQuery/DOM APIs (e.g.,
create an <a>, set its text with .text(item.title) and set href using
'/genzmovie/?route=movie&slug=' + encodeURIComponent(item.slug) so item.title
and item.slug are not inserted raw), and wrap the function that calls
$.getJSON(...) in a debounce (e.g., 200–300ms) so the AJAX call is only made
after typing pauses; keep using $box and $.getJSON but change $box.append(...)
to the safe element construction and add a debounced handler for the input event
that calls the $.getJSON-based lookup.

Comment on lines +5 to +11
const DB_HOST = '127.0.0.1';
const DB_NAME = 'genzmovie';
const DB_USER = 'root';
const DB_PASS = '';
const BASE_URL = 'http://localhost/genzmovie';
const APP_NAME = 'GENZMOVIE';
const OPHIM_API_BASE = 'https://ophim1.com/v1/api';
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

Move credentials and deployment URLs out of source control.

The scaffold ships root/empty-password database credentials and a localhost-only BASE_URL. That is unsafe outside a dev box and will break every non-local deployment. Load these from environment or an untracked local config and fail fast when they are missing.

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

In `@genzmovie/config/config.php` around lines 5 - 11, Replace the hardcoded
credentials and deployment URL constants (DB_HOST, DB_NAME, DB_USER, DB_PASS,
BASE_URL and optionally OPHIM_API_BASE) so they are read from environment
variables or an untracked local config; if any required env value is missing,
throw a clear, fast-failing exception (or exit) during bootstrap so deployments
fail early instead of using insecure defaults, and keep no secret values in
source control. Use the existing constant names (DB_HOST, DB_NAME, DB_USER,
DB_PASS, BASE_URL, OPHIM_API_BASE) as the targets to update so other code
continues to reference them.

<?php $metaTitle='Đăng ký'; include __DIR__ . '/../layouts/header.php'; ?>
<div class="row justify-content-center"><div class="col-md-5">
<h3>Đăng ký</h3>
<?php if ($error): ?><div class="alert alert-danger"><?= e($error) ?></div><?php endif; ?>
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

Potential PHP notice if $error is undefined.

Using if ($error) will trigger a PHP notice when $error is not set. Use isset() or the null coalescing operator to avoid warnings in strict error reporting environments.

🐛 Proposed fix
-<?php if ($error): ?><div class="alert alert-danger"><?= e($error) ?></div><?php endif; ?>
+<?php if (!empty($error)): ?><div class="alert alert-danger"><?= e($error) ?></div><?php endif; ?>
📝 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 if ($error): ?><div class="alert alert-danger"><?= e($error) ?></div><?php endif; ?>
<?php if (!empty($error)): ?><div class="alert alert-danger"><?= e($error) ?></div><?php endif; ?>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/views/auth/register.php` at line 4, The conditional in the register
view uses if ($error) which can raise a notice when $error is undefined; change
the check to use isset($error) (or check ($error ?? null)) before rendering so
the alert only prints when the variable exists and is non-empty—update the
conditional around the $error usage in genzmovie/views/auth/register.php to use
isset($error) or the null coalescing operator with e($error) to avoid undefined
variable notices.

Comment on lines +18 to +23
<?php foreach ($moviesBySection as $label => $items): ?>
<section class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h4><?= e($label) ?></h4>
<a class="btn btn-outline-light btn-sm" href="/genzmovie/?route=search&q=<?= urlencode($label) ?>">Xem tất cả</a>
</div>
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

Xem tất cả should use the section key, not the display label.

genzmovie/controllers/HomeController.php:12-21 fetches each row with a category key, but this template builds the CTA from the human-readable label instead. For sections like theater, single, or netflix, users will land on results that do not match the cards shown here.

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

In `@genzmovie/views/home/index.php` around lines 18 - 23, The CTA uses the
display label instead of the section key; update the template to use the section
key used by HomeController. Change the foreach to expose both the key and the
display label (e.g. foreach ($moviesBySection as $sectionKey => $section) or
extract $sectionKey and $sectionLabel from each entry), and build the anchor
href using urlencode($sectionKey) (the variable feeding q=) instead of
urlencode($label); keep the visible text as the human label. Ensure you
reference $moviesBySection, the foreach, and the anchor href when making the
change.

Comment on lines +40 to +42
<?php foreach ($headerAds as $ad): ?>
<div class="container py-2 ad-slot"><?= $ad['ad_code'] ?></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 | 🟡 Minor

Raw HTML output in ad rendering poses XSS risk.

The $ad['ad_code'] is rendered without escaping, which is likely intentional to allow HTML/JavaScript ad embeds. However, this creates an XSS vector if the admin panel is compromised or if ad content isn't properly validated on input.

Consider documenting this trust assumption and/or adding Content Security Policy headers to mitigate potential abuse.

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

In `@genzmovie/views/layouts/header.php` around lines 40 - 42, The ad rendering
loop exposing raw HTML via $ad['ad_code'] in the header.php foreach over
$headerAds is an XSS risk; either (a) treat ad_code as untrusted by escaping
output (use an HTML-escaping helper) or (b) ensure ad_code is
sanitized/validated at input time (strip disallowed tags/attributes or store
only safe embed patterns) or (c) render third-party ads inside an isolated
iframe/nonceed script. Also add or document a Content-Security-Policy (CSP)
header (report-only at first) in your response/boot pipeline to restrict allowed
script/src/frame origins and include this trust assumption in admin docs. Target
the foreach that outputs $ad['ad_code'] and the request/response layer where
headers are set.

Comment on lines +28 to +33
<?php if (!empty($currentEpisode['embed_link'])): ?>
<div class="ratio ratio-16x9"><iframe src="<?= e($currentEpisode['embed_link']) ?>" allowfullscreen></iframe></div>
<?php else: ?>
<video controls class="w-100" poster="<?= e($movie['poster']) ?>">
<source src="<?= e($currentEpisode['mp4_link']) ?>" type="video/mp4">
<?php if (!empty($currentEpisode['subtitle_link'])): ?><track kind="subtitles" src="<?= e($currentEpisode['subtitle_link']) ?>" srclang="vi" label="Vietnamese"><?php endif; ?>
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Trace where these media fields are written and read.
rg -n -C3 -g '!vendor/**' '\b(embed_link|mp4_link|subtitle_link)\b'

# Look for existing URL validation / allowlisting helpers.
rg -n -C3 -g '!vendor/**' 'FILTER_VALIDATE_URL|parse_url\s*\(|safe_media_url|allowed_hosts|allowed_domains'

Repository: huzely/Decrypt

Length of output: 3745


Add URL scheme/host validation before rendering media URLs.

embed_link and mp4_link are fetched from an external crawler (ophim_crawler.php) and inserted directly into the database with no validation. The view escapes them with e(), which prevents HTML injection but not URL scheme injection—an attacker controlling the external API can inject javascript:, data:, or arbitrary hosts into <iframe src> and <source src> attributes.

Add a validation function to allowlist HTTP/HTTPS URLs:

Example implementation
function safe_media_url(?string $url): ?string
{
    if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) {
        return null;
    }

    $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
    return in_array($scheme, ['http', 'https'], true) ? $url : null;
}

Then update lines 28–35 to validate before rendering:

-        <?php if (!empty($currentEpisode['embed_link'])): ?>
-            <div class="ratio ratio-16x9"><iframe src="<?= e($currentEpisode['embed_link']) ?>" allowfullscreen></iframe></div>
+        <?php if ($embedUrl = safe_media_url($currentEpisode['embed_link'] ?? null)): ?>
+            <div class="ratio ratio-16x9"><iframe src="<?= e($embedUrl) ?>" allowfullscreen></iframe></div>
         <?php else: ?>
-            <video controls class="w-100" poster="<?= e($movie['poster']) ?>">
-                <source src="<?= e($currentEpisode['mp4_link']) ?>" type="video/mp4">
-                <?php if (!empty($currentEpisode['subtitle_link'])): ?><track kind="subtitles" src="<?= e($currentEpisode['subtitle_link']) ?>" srclang="vi" label="Vietnamese"><?php endif; ?>
+            <?php if ($videoUrl = safe_media_url($currentEpisode['mp4_link'] ?? null)): ?>
+            <video controls class="w-100" poster="<?= e($movie['poster']) ?>">
+                <source src="<?= e($videoUrl) ?>" type="video/mp4">
+                <?php if ($subtitleUrl = safe_media_url($currentEpisode['subtitle_link'] ?? null)): ?><track kind="subtitles" src="<?= e($subtitleUrl) ?>" srclang="vi" label="Vietnamese"><?php endif; ?>
             </video>
+            <?php endif; ?>
         <?php endif; ?>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@genzmovie/views/movie/show.php` around lines 28 - 33, The view currently
outputs unvalidated external URLs (currentEpisode['embed_link'],
currentEpisode['mp4_link'], currentEpisode['subtitle_link']) which can allow
non-HTTP(S) schemes; add a helper safe_media_url(?string $url): ?string that
returns the URL only if filter_var($url, FILTER_VALIDATE_URL) passes and
parse_url($url, PHP_URL_SCHEME) is 'http' or 'https', then call safe_media_url
on each of embed_link/mp4_link/subtitle_link and only render the <iframe>,
<source> or <track> when the validated value is non-null (still escape with e()
when outputting).

Comment thread README.md
Comment on lines +50 to +52
6. **Default admin account**:
- Email: `admin@genzmovie.local`
- Password: `admin123`
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

Do not ship publicly documented seeded admin credentials.

Documenting a known default admin account/password here makes accidental insecure deployments much more likely, especially since the PR also seeds that admin in the schema. Please switch installation to require creating/rotating admin credentials during setup, and avoid publishing a reusable password in the README.

Suggested doc change
-6. **Default admin account**:
-   - Email: `admin@genzmovie.local`
-   - Password: `admin123`
+6. **Admin bootstrap**:
+   - Create the initial admin account during installation, or
+   - Immediately rotate any seeded admin credentials before exposing the site publicly.
+   - Never keep development/default credentials in non-local environments.
📝 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
6. **Default admin account**:
- Email: `admin@genzmovie.local`
- Password: `admin123`
6. **Admin bootstrap**:
- Create the initial admin account during installation, or
- Immediately rotate any seeded admin credentials before exposing the site publicly.
- Never keep development/default credentials in non-local environments.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 50 - 52, Remove the publicly documented default
credentials by deleting or replacing the "Default admin account" section (the
lines referencing admin@genzmovie.local and admin123) and update the
installation/setup instructions to require creating an admin account or rotating
credentials during first-run; also remove or change any seeded admin with
hardcoded password in your database seed/migration so the app forces interactive
admin creation or a one-time generated password and documents secure rotation
steps instead.

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