Spaces:
Running
Running
File size: 9,404 Bytes
e31284f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | <?php
/**
* ApiValidator Class
*
* Handles API request validation, authentication, and input sanitization
*/
class ApiValidator
{
private $pdo;
private $apiKeys;
public function __construct($pdo, $apiKeys = [])
{
$this->pdo = $pdo;
$this->apiKeys = $apiKeys;
}
/**
* Validate API request
* Checks Content-Type and optionally validates API key
*
* @param array $allowedMethods Array of allowed HTTP methods (e.g. ['POST', 'GET'])
* @return array Validation result
*/
public function validateRequest($allowedMethods = ['POST'])
{
// Check request method
$method = $_SERVER['REQUEST_METHOD'];
if (!in_array($method, $allowedMethods)) {
return [
'valid' => false,
'error' => "Invalid request method. Allowed: " . implode(', ', $allowedMethods),
'http_code' => 405
];
}
// Check Content-Type header (only for methods with body)
if (in_array($method, ['POST', 'PUT', 'PATCH'])) {
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (stripos($contentType, 'application/json') === false) {
return [
'valid' => false,
'error' => 'Invalid Content-Type. Expected application/json',
'http_code' => 400
];
}
}
// Validate API key if configured
if (!empty($this->apiKeys)) {
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (empty($authHeader)) {
return [
'valid' => false,
'error' => 'Missing Authorization header',
'http_code' => 401
];
}
// Extract Bearer token
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
return [
'valid' => false,
'error' => 'Invalid Authorization header format. Expected: Bearer <API_KEY>',
'http_code' => 401
];
}
$apiKey = $matches[1];
if (!in_array($apiKey, $this->apiKeys)) {
return [
'valid' => false,
'error' => 'Invalid API key',
'http_code' => 401
];
}
}
return ['valid' => true];
}
/**
* Parse and validate JSON payload
*
* @return array Parsed data or error
*/
public function parseJsonPayload()
{
$rawInput = file_get_contents('php://input');
if (empty($rawInput)) {
return [
'valid' => false,
'error' => 'Empty request body',
'http_code' => 400
];
}
$data = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [
'valid' => false,
'error' => 'Invalid JSON: ' . json_last_error_msg(),
'http_code' => 400
];
}
return [
'valid' => true,
'data' => $data
];
}
/**
* Validate payment request fields
*
* @param array $data Request data
* @return array Validation result with errors
*/
public function validatePaymentRequest($data)
{
$errors = [];
// Validate student_id
if (empty($data['student_id'])) {
$errors['student_id'] = 'Student ID is required';
} elseif (!is_string($data['student_id']) && !is_numeric($data['student_id'])) {
$errors['student_id'] = 'Student ID must be a string or number';
}
// Validate teller_no
if (empty($data['teller_no'])) {
$errors['teller_no'] = 'Teller number is required';
} elseif (!is_string($data['teller_no']) && !is_numeric($data['teller_no'])) {
$errors['teller_no'] = 'Teller number must be a string or number';
}
// Validate amount
if (!isset($data['amount'])) {
$errors['amount'] = 'Amount is required';
} elseif (!is_numeric($data['amount'])) {
$errors['amount'] = 'Amount must be a number';
} elseif (floatval($data['amount']) <= 0) {
$errors['amount'] = 'Amount must be greater than zero';
}
// Validate payment_date
if (empty($data['payment_date'])) {
$errors['payment_date'] = 'Payment date is required';
} elseif (!strtotime($data['payment_date'])) {
$errors['payment_date'] = 'Invalid payment date format';
}
if (!empty($errors)) {
return [
'valid' => false,
'errors' => $errors,
'http_code' => 400
];
}
return ['valid' => true];
}
/**
* Validate student exists in database
*
* @param string $studentId Student ID
* @return array Validation result with student data
*/
public function validateStudentExists($studentId)
{
$sql = "SELECT sr.id, sr.student_code,
CONCAT(sr.last_name, ' ', sr.first_name, ' ', COALESCE(sr.other_name, '')) AS full_name,
sr.admission_status,
al.level_name
FROM tb_student_registrations sr
LEFT JOIN tb_academic_levels al ON sr.level_id = al.id
WHERE sr.id = :student_id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['student_id' => $studentId]);
$student = $stmt->fetch();
if (!$student) {
return [
'valid' => false,
'error' => "Student not found (ID searched: '$studentId')",
'http_code' => 404
];
}
if ($student['admission_status'] !== 'Active') {
return [
'valid' => false,
'error' => 'Student is not active',
'http_code' => 400
];
}
return [
'valid' => true,
'student' => $student
];
}
/**
* Validate teller number exists and has unreconciled amount
*
* @param string $tellerNo Teller number
* @return array Validation result with teller data
*/
public function validateTellerNumber($tellerNo)
{
$sql = "SELECT
bs.id,
bs.description,
bs.amount_paid,
bs.payment_date,
COALESCE(fp.total_registered_fee, 0.00) AS registered_amount,
(bs.amount_paid - COALESCE(fp.total_registered_fee, 0.00)) AS unreconciled_amount
FROM tb_account_bank_statements bs
LEFT JOIN (
SELECT teller_no, SUM(amount_paid) AS total_registered_fee
FROM tb_account_school_fee_payments
GROUP BY teller_no
) fp ON SUBSTRING_INDEX(bs.description, ' ', -1) = fp.teller_no
WHERE SUBSTRING_INDEX(bs.description, ' ', -1) = :teller_number
LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['teller_number' => $tellerNo]);
$teller = $stmt->fetch();
if (!$teller) {
return [
'valid' => false,
'error' => 'Teller number not found',
'http_code' => 404
];
}
if ($teller['unreconciled_amount'] <= 0) {
return [
'valid' => false,
'error' => 'Teller number has no unreconciled amount',
'http_code' => 400
];
}
return [
'valid' => true,
'teller' => $teller
];
}
/**
* Check for duplicate teller number usage
* Prevents the same teller number from being used multiple times
*
* @param string $tellerNo Teller number
* @param string $studentId Student ID (optional, for better error message)
* @return array Validation result
*/
public function checkTellerDuplicate($tellerNo, $studentId = null)
{
// Note: The system allows the same teller to be used for different students
// This check is to ensure the teller has available unreconciled amount
// The actual duplicate check is handled by the validateTellerNumber method
return ['valid' => true];
}
/**
* Sanitize input data
*
* @param array $data Input data
* @return array Sanitized data
*/
public function sanitizeInput($data)
{
return [
'student_id' => trim($data['student_id'] ?? ''),
'teller_no' => trim($data['teller_no'] ?? ''),
'amount' => floatval($data['amount'] ?? 0),
'payment_date' => trim($data['payment_date'] ?? '')
];
}
}
|