Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 60 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,69 @@
# Decrypt
### [+] Created By HTR-TECH (@***tahmid.rayat***)
### [+] Disclaimer :
***Decrypter is a tool to decrypt Encrypted Bash Scripts into a Readable Format.This Tool is created for Educational Purpose only.I am not responsible for any misuse of this tool.***
# GENZMOVIE

<img src="https://raw.githubusercontent.com/htr-tech/release-download/master/images/decrypter.png" alt="" border="0" />
GENZMOVIE is a PHP + MySQL movie streaming website with OPhim auto crawler, admin dashboard, ad monetization, user accounts, watchlist/history, SEO routes, and responsive UI.

### [+] Installation
```apt update```
## Project Structure

```apt install git python2 -y```
```text
genzmovie/
├── config/
├── controllers/
├── models/
├── views/
├── assets/
├── uploads/
├── admin/
├── crawler/
├── api/
├── includes/
├── database/
├── index.php
└── .htaccess
```

```git clone https://github.com/hax0rtahm1d/decrypt```
## Features

```cd decrypt```
- Netflix-style homepage with featured slider and category blocks.
- Search + autocomplete + filter by genre/year/country/quality.
- Movie details with metadata, episodes, and related movies.
- Player supports iframe embed and direct MP4/HLS source.
- Multiple server episode links.
- User register/login/logout with dashboard (favorites + watch history).
- Ad monetization: header/sidebar/popup/footer/video pre-roll placements.
- Admin dashboard for crawler trigger, ad management, overview of movies/users/comments.
- OPhim crawler imports movie metadata + episodes + links.
- SEO: friendly route `/phim/{slug}` and XML sitemap endpoint.
- Security basics: password hashing, prepared statements, CSRF token, output escaping.

```python2 dec.py```
## Installation Guide

### Or, Use Single Command
1. **Copy files** into web root (Apache/Nginx + PHP 8+).
2. **Create database**:
```bash
mysql -u root -p < genzmovie/database/schema.sql
```
3. **Update DB credentials** in `genzmovie/config/config.php`.
4. **Set document root** to project root or `/genzmovie` and enable mod_rewrite.
5. **Open website**:
- Frontend: `http://localhost/genzmovie/`
- Admin: `http://localhost/genzmovie/admin/login.php`
6. **Default admin account**:
- Email: `admin@genzmovie.local`
- Password: `admin123`
Comment on lines +50 to +52

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.


```
apt update && apt install git python2 -y && git clone https://github.com/hax0rtahm1d/decrypt && cd decrypt && python2 dec.py
```
## Crawler

- Manual import: use **Import từ OPhim** button in `/admin`.
- Cronjob:
```bash
*/30 * * * * php /path/to/genzmovie/crawler/ophim_crawler.php
```

## Sitemap

- XML sitemap endpoint: `http://localhost/genzmovie/api/sitemap.php`

## Notes

## [+] Find Me on :
[![Github](https://img.shields.io/badge/Github-HTR--TECH-green?style=for-the-badge&logo=github)](https://github.com/htr-tech)
[![Instagram](https://img.shields.io/badge/IG-%40tahmid.rayat-red?style=for-the-badge&logo=instagram)](https://www.instagram.com/tahmid.rayat)
[![Messenger](https://img.shields.io/badge/Chat-Messenger-blue?style=for-the-badge&logo=messenger)](https://m.me/tahmid.rayat.official)
- Add your Google AdSense / ad network code at admin ads form.
- For production, configure HTTPS, strict CSP, and hardened session settings.
5 changes: 5 additions & 0 deletions genzmovie/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^phim/([a-zA-Z0-9-]+)/?$ index.php?route=movie&slug=$1 [QSA,L]
RewriteRule ^$ index.php?route=home [QSA,L]
47 changes: 47 additions & 0 deletions genzmovie/admin/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/helpers.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/csrf.php';
require_admin();

$pdo = Database::connection();
$message = null;

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';
}
}
Comment on lines +12 to +27

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.


$movies = $pdo->query('SELECT * FROM movies ORDER BY id DESC LIMIT 20')->fetchAll();
$users = $pdo->query('SELECT id,name,email,status FROM users ORDER BY id DESC LIMIT 20')->fetchAll();
$comments = $pdo->query('SELECT c.id,c.content,m.title FROM comments c JOIN movies m ON m.id=c.movie_id ORDER BY c.id DESC LIMIT 20')->fetchAll();
$ads = $pdo->query('SELECT * FROM ads ORDER BY id DESC')->fetchAll();
?>
<!doctype html><html><head><meta charset="utf-8"><title>Admin - GENZMOVIE</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></head>
<body><div class="container py-4">
<h2>Admin Dashboard</h2>
<?php if ($message): ?><div class="alert alert-success"><?= e($message) ?></div><?php endif; ?>
<div class="row g-3">
<div class="col-md-6"><div class="card"><div class="card-body"><h5>Crawler Control</h5><form method="POST"><?= csrf_field() ?><input type="hidden" name="action" value="import_ophim"><button class="btn btn-primary">Import từ OPhim</button></form><p class="mt-2 small">Cronjob: <code>*/30 * * * * php /path/genzmovie/crawler/ophim_crawler.php</code></p></div></div></div>
<div class="col-md-6"><div class="card"><div class="card-body"><h5>Ads Management</h5><form method="POST"><?= csrf_field() ?><input type="hidden" name="action" value="add_ad"><input class="form-control mb-2" name="name" placeholder="Tên quảng cáo" required><select class="form-select mb-2" name="position"><option>header</option><option>sidebar</option><option>popup</option><option>video_preroll</option><option>footer</option></select><select class="form-select mb-2" name="ad_type"><option>adsense</option><option>custom_html</option><option>script</option></select><textarea class="form-control mb-2" name="ad_code" rows="3" required></textarea><button class="btn btn-danger">Lưu quảng cáo</button></form></div></div></div>
</div>
<hr>
<h5>Movie Management</h5><table class="table table-striped"><tr><th>ID</th><th>Title</th><th>Year</th></tr><?php foreach($movies as $m): ?><tr><td><?= $m['id'] ?></td><td><?= e($m['title']) ?></td><td><?= e((string)$m['year']) ?></td></tr><?php endforeach; ?></table>
<h5>User Management</h5><table class="table table-striped"><tr><th>ID</th><th>Name</th><th>Email</th><th>Status</th></tr><?php foreach($users as $u): ?><tr><td><?= $u['id'] ?></td><td><?= e($u['name']) ?></td><td><?= e($u['email']) ?></td><td><?= e($u['status']) ?></td></tr><?php endforeach; ?></table>
<h5>Comment Management</h5><table class="table table-striped"><tr><th>ID</th><th>Movie</th><th>Comment</th></tr><?php foreach($comments as $c): ?><tr><td><?= $c['id'] ?></td><td><?= e($c['title']) ?></td><td><?= e($c['content']) ?></td></tr><?php endforeach; ?></table>
<h5>Ads</h5><table class="table table-striped"><tr><th>Name</th><th>Position</th><th>Type</th></tr><?php foreach($ads as $ad): ?><tr><td><?= e($ad['name']) ?></td><td><?= e($ad['position']) ?></td><td><?= e($ad['ad_type']) ?></td></tr><?php endforeach; ?></table>
</div></body></html>
28 changes: 28 additions & 0 deletions genzmovie/admin/login.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/helpers.php';
require_once __DIR__ . '/../includes/csrf.php';

$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validate_csrf($_POST['csrf_token'] ?? null)) {
$error = 'CSRF token invalid.';
} else {
$stmt = Database::connection()->prepare('SELECT * FROM admins WHERE email = :email LIMIT 1');
$stmt->execute(['email' => trim($_POST['email'])]);
$admin = $stmt->fetch();
if ($admin && password_verify($_POST['password'], $admin['password'])) {
$_SESSION['admin'] = $admin;
header('Location: /genzmovie/admin/index.php');
Comment on lines +15 to +17

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.

exit;
}
$error = 'Sai thông tin đăng nhập';
}
}
?>
<!doctype html><html><head><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></head>
<body class="bg-dark text-light d-flex align-items-center" style="height:100vh"><div class="container"><div class="col-md-4 mx-auto">
<h3>Admin Login</h3><?php if ($error): ?><div class="alert alert-danger"><?= e($error) ?></div><?php endif; ?>
<form method="POST"><?= csrf_field() ?><input class="form-control mb-2" name="email" type="email" required><input class="form-control mb-2" name="password" type="password" required><button class="btn btn-danger w-100">Login</button></form>
</div></div></body></html>
16 changes: 16 additions & 0 deletions genzmovie/api/sitemap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/xml; charset=utf-8');
$movies = Database::connection()->query('SELECT slug, updated_at FROM movies ORDER BY updated_at DESC')->fetchAll();
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc><?= BASE_URL ?>/</loc></url>
<?php foreach ($movies as $movie): ?>
<url>
<loc><?= BASE_URL ?>/phim/<?= htmlspecialchars($movie['slug']) ?></loc>
<lastmod><?= date('c', strtotime($movie['updated_at'])) ?></lastmod>
</url>
Comment on lines +10 to +14

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.

<?php endforeach; ?>
</urlset>
9 changes: 9 additions & 0 deletions genzmovie/assets/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
body { font-family: 'Segoe UI', sans-serif; }
.movie-card img { height: 260px; object-fit: cover; }
.movie-card:hover { transform: translateY(-4px); transition: .2s; box-shadow: 0 0 12px rgba(255,255,255,.12); }
.featured-img { max-height: 420px; object-fit: cover; filter: brightness(.6); }
.bg-overlay { background: rgba(0,0,0,.55); }
.autocomplete-box { position:absolute; top:100%; left:0; right:0; background:#111; z-index:1000; border:1px solid #444; }
.autocomplete-box a { display:block; color:#fff; padding:6px 10px; text-decoration:none; }
.autocomplete-box a:hover { background:#222; }
.ad-slot iframe, .ad-slot img { max-width: 100%; }
24 changes: 24 additions & 0 deletions genzmovie/assets/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
$(function () {
const $search = $('#movie-search');
const $box = $('#autocomplete-box');

$search.on('input', function () {
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>`);

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 +11 to +16

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.

});

$(document).on('click', function (e) {
if (!$(e.target).closest('#movie-search').length) {
$box.empty();
}
});
});
15 changes: 15 additions & 0 deletions genzmovie/config/config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

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';
Comment on lines +5 to +11

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.


if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
24 changes: 24 additions & 0 deletions genzmovie/config/database.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/config.php';

class Database
{
private static ?PDO $instance = null;

public static function connection(): PDO
{
if (self::$instance === null) {
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', DB_HOST, DB_NAME);
self::$instance = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}

return self::$instance;
}
}
51 changes: 51 additions & 0 deletions genzmovie/controllers/AuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/../models/User.php';

class AuthController
{
public function login(): void
{
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validate_csrf($_POST['csrf_token'] ?? null)) {
$error = 'CSRF token invalid.';
} else {
$userModel = new User();
$user = $userModel->findByEmail(trim($_POST['email']));
if ($user && password_verify($_POST['password'], $user['password'])) {
$_SESSION['user'] = $user;
redirect('/');
}
$error = 'Email hoặc mật khẩu không đúng.';
}
}
include __DIR__ . '/../views/auth/login.php';
}

public function register(): void
{
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validate_csrf($_POST['csrf_token'] ?? null)) {
$error = 'CSRF token invalid.';
} else {
$userModel = new User();
$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.';
}
}
include __DIR__ . '/../views/auth/register.php';
}

public function logout(): void
{
unset($_SESSION['user']);
redirect('/');
}
}
31 changes: 31 additions & 0 deletions genzmovie/controllers/HomeController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/../models/Movie.php';

class HomeController
{
public function index(): void
{
$movieModel = new Movie();
$sections = [
'Phim mới cập nhật' => 'series',
'Phim chiếu rạp' => 'theater',
'Phim bộ' => 'series',
Comment on lines +13 to +15

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

Phim mới cập nhật currently duplicates the series feed.

Both labels map to 'series', so the homepage will render the same dataset twice under different headings. This row needs a distinct key or a dedicated “latest updates” query instead.

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

In `@genzmovie/controllers/HomeController.php` around lines 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'.

'Phim lẻ' => 'single',
'Phim Netflix đề cử' => 'netflix',
'Phim hành động' => 'Hành Động',
'Phim kinh dị' => 'Kinh Dị',
'Phim hoạt hình' => 'Hoạt Hình',
];

$featured = $movieModel->featured();
$moviesBySection = [];
foreach ($sections as $label => $key) {
$moviesBySection[$label] = $movieModel->byCategory($key);
}

include __DIR__ . '/../views/home/index.php';
}
}
Loading