0% found this document useful (0 votes)
3 views6 pages

School Tracking Real Code Explained

The document provides a detailed explanation of a School Tracking System's source code, including database connection, user authentication, and file upload validation. It highlights key functionalities such as role-based access control, password hashing, and bug fixes for attendance records and file size validation. The explanations are accompanied by code snippets that illustrate how each component operates and the rationale behind specific coding choices.

Uploaded by

sqwala72
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views6 pages

School Tracking Real Code Explained

The document provides a detailed explanation of a School Tracking System's source code, including database connection, user authentication, and file upload validation. It highlights key functionalities such as role-based access control, password hashing, and bug fixes for attendance records and file size validation. The explanations are accompanied by code snippets that illustrate how each component operates and the rationale behind specific coding choices.

Uploaded by

sqwala72
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

School Tracking System — Real Code, Explained

Actual source code with line-by-line explanation — core files + both fixed bugs

1 Database Connection — config/[Link]

config/[Link] — full file

1<?php
2$host = '[Link]';
3$db = 'school_tracking_db';
4$user = 'root';
5$pass = '';
6$dsn = "mysql:host=$host;port=3306;dbname=$db;charset=utf8mb4";
7
8$options = [
9 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
10 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
11 PDO::ATTR_EMULATE_PREPARES => false,
12];
13
14try {
15 $pdo = new PDO($dsn, $user, $pass, $options);
16} catch (PDOException $e) {
17 die('Database connection failed: ' . $e->getMessage());
18}
19
20return $pdo;

Lines 2–6 Build the connection string (DSN). Host, database name, and charset ( utf8mb4 ) get combined into one string PDO needs to know
where and how to connect.

Line 9 ERRMODE_EXCEPTION — if a query fails, PDO throws a real PHP exception instead of silently returning false . This is why every database
call in this project can be wrapped in try/catch .

Line 10 FETCH_ASSOC — every row comes back as an associative array like ['student_id' => 4, 'full_name' => 'Ali'] instead of a
numbered array. That's why code everywhere uses $row['student_id'] .

Line 11 — the most important line in this file EMULATE_PREPARES = false tells PHP: don't fake the prepared statement yourself — send the
actual query template and the actual data to MySQL as two separate things, and let MySQL do the substitution. This is what makes SQL injection
essentially impossible here, because user input is never glued into the SQL string at all.

Lines 14–18 If the connection itself fails (wrong password, MySQL not running), the script stops immediately with die() rather than continuing
and breaking in a confusing way later.

Line 20 The file returns the $pdo object. Every page connects to the database with $pdo = require __DIR__ . '/config/[Link]'; — calling
it like a function that hands back the connection.
2 Login & Role Checks — includes/[Link]

includes/[Link] — full file

1<?php
2
3date_default_timezone_set('Asia/Muscat');
4
5if (session_status() === PHP_SESSION_NONE) {
6 session_start();
7}
8
9function isLoggedIn(): bool {
10 return isset($_SESSION['user_id']);
11}
12
13function requireLogin(): void {
14 if (!isLoggedIn()) {
15 header('Location: /school_tracking/[Link]');
16 exit;
17 }
18}
19
20function requireRole(string $role): void {
21 requireLogin();
22 if ($_SESSION['role'] !== $role) {
23 header('Location: /school_tracking/[Link]');
24 exit;
25 }
26}
27
28function getCurrentUser(): array {
29 return [
30 'user_id' => $_SESSION['user_id'] ?? null,
31 'username' => $_SESSION['username'] ?? null,
32 'role' => $_SESSION['role'] ?? null,
33 'full_name' => $_SESSION['full_name'] ?? null,
34 ];
35}
36
37function loginUser(array $user): void {
38 $_SESSION['user_id'] = $user['user_id'];
39 $_SESSION['username'] = $user['username'];
40 $_SESSION['role'] = $user['role'];
41 $_SESSION['full_name'] = $user['full_name'];
42}
43
44function logoutUser(): void {
45 session_unset();
46 session_destroy();
47 header('Location: /school_tracking/[Link]');
48 exit;
49}

Lines 9–11 isLoggedIn() just checks whether $_SESSION['user_id'] exists. PHP sessions persist data per-browser across page loads, so once
this is set at login, every later page request can read it.

Lines 13–18 — the access gate requireLogin() is called at the top of every protected page. If isLoggedIn() is false, it sends an HTTP redirect
header to the login page and calls exit immediately — exit is critical here, because without it PHP would keep running the rest of the page even
after sending the redirect.

Lines 20–26 — role-based access control, in one function requireRole('teacher') first calls requireLogin() (so it covers both checks), then
compares $_SESSION['role'] against the role the page expects. A logged-in student hitting a teacher page fails this check and gets redirected —
this single function is the entire role-based access control mechanism for the whole app.

Lines 37–42 loginUser() is called right after a successful password check in [Link]. It copies four fields from the database row into the
session, so they're available on every later page without querying the database again each time.
3 Shared Helpers — includes/[Link]

Only the two functions relevant to passwords and file uploads are shown here, since those are the ones examiners are most likely to ask
about.

includes/[Link] — password hashing

59function hashPassword(string $password): string {


60 return password_hash($password, PASSWORD_DEFAULT);
61}

Line 60 password_hash() is a built-in PHP function. PASSWORD_DEFAULT currently maps to the bcrypt algorithm — it takes the plain password
and produces a long scrambled string that includes a random "salt" baked in, so even two users with the same password get different hashes. The
original password is never recoverable from the hash; checking a login is done by re-hashing the typed password the same way and comparing,
which is what password_verify() does.

includes/[Link] — file upload validation

63function isValidFileUpload(array $file, array $allowedTypes = ['pdf', 'docx', 'zip'], float $maxSizeMB = 10): array {
64 if ($file['error'] !== UPLOAD_ERR_OK) {
65 return ['valid' => false, 'error' => 'File upload error (code ' . $file['error'] . ').'];
66 }
67
68 $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

69 if (!in_array($ext, $allowedTypes, true)) {


70 return ['valid' => false, 'error' => 'Invalid file type...'];
71 }
72
73 $mimeMap = [ /* extension => allowed real MIME types */ ];

82 $mime = mime_content_type($file['tmp_name']);
83 if (isset($mimeMap[$ext]) && !in_array($mime, $mimeMap[$ext], true)) {
84 return ['valid' => false, 'error' => 'File content does not match its extension.'];
85 }
86
87 if ($file['size'] > $maxSizeMB * 1024 * 1024) {
88 return ['valid' => false, 'error' => "File exceeds {$maxSizeMB} MB limit."];
89 }
90
91 return ['valid' => true, 'error' => ''];
92}

Line 68 Pulls the file extension from its original name (e.g. [Link] → pdf ), lower-cased so .PDF and .pdf are treated the same.

Lines 82–85 — extension alone isn't trusted mime_content_type() looks at the file's actual binary content to detect its real type, regardless of
what the filename claims. So renaming a script to [Link] doesn't fool this check — the real content still won't match application/pdf .

Line 87 — the size check pattern used everywhere in this project $maxSizeMB * 1024 * 1024 converts a human number like 10 into the actual
byte count PHP works with (1024×1024 bytes = 1MB). $file['size'] is already in bytes, so both sides of the comparison are now in the same
unit. This exact pattern — multiplying by 1024 twice — is the fix that was missing in the original buggy version (see Bug 2 below).
4 Login Logic — [Link]

[Link] — PHP logic (HTML form omitted)

20const MAX_ATTEMPTS = 5;
21const LOCKOUT_MINUTES = 15;
...
41$stmt = $pdo->prepare(
42 'SELECT user_id, username, password, role, full_name,
43 failed_attempts, login_timestamp
44 FROM users WHERE username = ? LIMIT 1'
45);
46$stmt->execute([$username]);
47$user = $stmt->fetch();
...
52$locked = false;
53if ($user['role'] !== 'admin'
54 && (int)$user['failed_attempts'] >= MAX_ATTEMPTS
55 && $user['login_timestamp'] !== null) {
56 $lastAttempt = strtotime($user['login_timestamp']);
57 $lockoutEnds = $lastAttempt + (LOCKOUT_MINUTES * 60);
58 if (time() < $lockoutEnds) {
59 $minutesLeft = ceil(($lockoutEnds - time()) / 60);
60 $error = "Account locked after " . MAX_ATTEMPTS . " failed attempts. "
61 . "Try again in {$minutesLeft} minute(s).";
62 $locked = true;
63 } else {
64 $pdo->prepare('UPDATE users SET failed_attempts = 0 WHERE user_id = ?')
65 ->execute([$user['user_id']]);
66 $user['failed_attempts'] = 0;
67 }
68}
...
70if (!$locked) {
71 if (password_verify($password, $user['password'])) {
72 $pdo->prepare(
73 'UPDATE users SET failed_attempts=0, login_timestamp=NOW(), last_login=NOW() WHERE user_id=?'
74 )->execute([$user['user_id']]);
75 loginUser($user);
76 redirectToDashboard($user['role']);
77 } else {
78 $newAttempts = (int)$user['failed_attempts'] + 1;
79 $pdo->prepare(
80 'UPDATE users SET failed_attempts=?, login_timestamp=NOW() WHERE user_id=?'
81 )->execute([$newAttempts, $user['user_id']]);
82 $remaining = MAX_ATTEMPTS - $newAttempts;
... }
89}

Lines 41–47 ? in the SQL is a placeholder. execute([$username]) sends the typed username as data, never as part of the query text — same
protection mechanism from [Link] in action.

Lines 53–55 — the lockout condition, three parts All three must be true to even consider locking: the account isn't admin, failed attempts has
reached 5, and there's an actual previous attempt timestamp to measure from.

Lines 56–58 strtotime() converts the stored timestamp into a Unix time number. Adding 15 minutes (900 seconds) gives the moment the
lockout ends. If "now" is still before that moment, the account stays locked.

Lines 63–67 If the 15 minutes have already passed, the code resets failed_attempts back to 0 in the database right then — so the very next
password check (right below) starts fresh.

Line 71 password_verify() takes the plain password the user just typed and the bcrypt hash stored in the database, and returns true/false —
this is the only place a password comparison happens, and the plain password is never stored or logged anywhere.

Lines 78–82 On a wrong password, the attempt count goes up by one and gets saved immediately, and $remaining tells the user exactly how
many tries are left before lockout.
5 Bug Fix #1 — Duplicate Attendance Rows
teacher/[Link]

THE BUG

Saving attendance for the same student, same class, same date more than once created a second row in the attendance table instead
of replacing the first one — so one student could end up with two (or more) conflicting attendance records for the same day.

school_tracking_db.sql — the table rule that makes the fix possible

138CREATE TABLE IF NOT EXISTS attendance (


139 attendance_id INT NOT NULL AUTO_INCREMENT,
140 student_id INT NOT NULL,
141 teacher_id INT NOT NULL,
142 class_id INT NOT NULL,
143 date DATE NOT NULL,
144 status ENUM('present','absent','late') NOT NULL,
...
148 UNIQUE KEY uq_attendance (student_id, class_id, date),
... -- foreign keys follow
158);

Line 148 — the actual fix, at the database level UNIQUE KEY uq_attendance (student_id, class_id, date) tells MySQL: no two rows are
allowed to have the same combination of these three values together. It's not one column being unique — it's the combination of all three. Try to
insert a second row with the exact same student+class+date, and MySQL itself rejects it as a duplicate key error.

teacher/[Link] — the save logic

78try {
80 $stmtUpsert = $pdo->prepare(
81 'INSERT INTO attendance (student_id, teacher_id, class_id, date, status)
82 VALUES (?, ?, ?, ?, ?)
83 ON DUPLICATE KEY UPDATE
84 status = VALUES(status),
85 teacher_id = VALUES(teacher_id)'
86 );
87
88 $summary = ['present' => 0, 'absent' => 0, 'late' => 0];
89
90 foreach ($attendance as $studentId => $status) {
91 $studentId = (int) $studentId;
92 if (!in_array($status, $validStatuses, true)) continue;
93
94 $stmtUpsert->execute([$studentId, $teacherId, $postClassId, $postDate, $status]);
95 $summary[$status]++;
96 }

HOW THE TWO PIECES WORK TOGETHER

For each student in the class, the code tries to INSERT a new attendance row. If that exact student+class+date combo doesn't exist
yet, it's a normal insert. If it does already exist (because the teacher already saved attendance for that day), the unique key from the
table makes MySQL trigger the ON DUPLICATE KEY UPDATE branch instead — which overwrites the existing row's status and
teacher_id rather than creating a new row. VALUES(status) just means "use the status value I tried to insert." The same student on
the same day for the same class can now only ever have one row, no matter how many times attendance gets saved.
6 Bug Fix #2 — Oversized Files Not Rejected
student/[Link]

THE BUG

The file size check compared the file's byte size directly against the plain number 10 , instead of converting 10 megabytes into its real
byte value first. Since a file's size in bytes is always a large number (millions), comparing it to a tiny number like 10 meant the check
almost never actually blocked anything — files well over the intended 10MB limit were accepted.

student/[Link] — submission handling

40$file = $_FILES['submission_file'];
41$origName = $file['name'];
42$tmpPath = $file['tmp_name'];
43$fileSize = $file['size'];
44
45$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
46$allowed = ['pdf', 'doc', 'docx', 'zip'];
...
54$mime = mime_content_type($tmpPath);
55
56if (!in_array($ext, $allowed, true) || !in_array($mime, $allowedMimes, true)) {
57 $error = 'Invalid file type. Allowed: PDF, DOC, DOCX, ZIP.';
58} elseif ($fileSize > 10 * 1024 * 1024) {
59 $error = 'File exceeds 10 MB limit.';
60} else {
... // proceed to save the file
96}

THE FIX, EXPLAINED DIRECTLY

$fileSize on line 43 is the file's size in raw bytes, exactly as PHP measures it. The corrected check on line 58 is $fileSize > 10 *
1024 * 1024 — that expression equals 10,485,760, the real number of bytes in 10 megabytes. Now both sides of the comparison are in
the same unit (bytes vs bytes), so the check actually does what it's supposed to: any file genuinely larger than 10MB gets rejected with
a clear error message, and the upload stops before move_uploaded_file() ever runs.

Why the order of checks matters File type is checked before size. If someone uploads a disallowed file type that also happens to be huge, they get
the type error first — the size check on line 58 only runs if the type check on line 56 already passed ( elseif ).

School Tracking System Web Application · Real code reference for final-year defense preparation

You might also like