diff --git a/admin/index.php b/admin/index.php index 5dbd114..6b049de 100644 --- a/admin/index.php +++ b/admin/index.php @@ -14,13 +14,6 @@ function _createToken(): string { db()->exec("DELETE FROM admin_tokens WHERE expires_at < NOW()"); return $token; } -function _verifyToken(string $token): bool { - if (!preg_match('/^[a-f0-9]{64}$/', $token)) return false; - $stmt = db()->prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()"); - $stmt->execute([$token]); - return (bool)$stmt->fetch(); -} - $isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (($_SERVER['HTTP_ACCEPT'] ?? '') === 'application/json'); // ── Auth ────────────────────────────────────────────────────────────────────── @@ -38,7 +31,7 @@ if (($_GET['action'] ?? '') === 'logout') { if ($rawToken) db()->prepare("DELETE FROM admin_tokens WHERE token=?")->execute([$rawToken]); header('Location: /admin/'); exit; } -$authed = $rawToken !== '' && _verifyToken($rawToken); +$authed = $rawToken !== '' && verifyAdminToken($rawToken); $token = $authed ? $rawToken : ''; // ── AJAX handlers ───────────────────────────────────────────────────────────── @@ -696,6 +689,9 @@ textarea.notes-ta:focus{border-color:#f97316} onclick="toggleReq(,'insurance_verified',this)"> + + View Document ↗ + @@ -763,6 +759,9 @@ textarea.notes-ta:focus{border-color:#f97316} onclick="toggleReq(,'license_verified',this)"> + + View Document ↗ + @@ -1060,6 +1059,9 @@ textarea.notes-ta:focus{border-color:#f97316} onclick="cvToggleReq(,'insurance_verified',this)"> + + View Document ↗ + @@ -1118,6 +1120,9 @@ textarea.notes-ta:focus{border-color:#f97316} onclick="cvToggleReq(,'license_verified',this)"> + + View Document ↗ + diff --git a/admin/view-doc.php b/admin/view-doc.php index f5b7786..e75694b 100644 --- a/admin/view-doc.php +++ b/admin/view-doc.php @@ -2,9 +2,7 @@ require_once dirname(__DIR__) . '/db.php'; $token = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? ''); -$stmt = db()->prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()"); -$stmt->execute([$token]); -if (!$stmt->fetch()) { +if (!verifyAdminToken($token)) { http_response_code(403); header('Content-Type: text/plain'); exit('Unauthorized — please log in to the admin panel first.'); @@ -29,25 +27,21 @@ if (!$row || !$row['file_path']) { exit('No document on file.'); } -$root = dirname(__DIR__); -$base = realpath($root . '/uploads'); -$path = realpath($root . '/' . $row['file_path']); - -if (!$path || !$base || !str_starts_with($path, $base . DIRECTORY_SEPARATOR)) { +$path = resolveUploadPath(dirname(__DIR__), $row['file_path']); +if (!$path) { http_response_code(404); header('Content-Type: text/plain'); exit('File not found.'); } -$mime = mime_content_type($path); -$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf']; -if (!isset($allowed[$mime])) { +$mime = detectAllowedMime($path); +if (!$mime) { http_response_code(403); header('Content-Type: text/plain'); exit('Invalid file type.'); } -$fname = $type . '-' . $ref . '.' . $allowed[$mime]; +$fname = $type . '-' . $ref . '.' . ALLOWED_DOC_MIMES[$mime]; header('Content-Type: ' . $mime); header('Content-Disposition: inline; filename="' . $fname . '"'); header('Content-Length: ' . filesize($path)); diff --git a/contact.php b/contact.php index 121c2c6..8d43b80 100644 --- a/contact.php +++ b/contact.php @@ -111,7 +111,7 @@ $confirmHtml = "

Next Step: Sign Your Rental Agreement

Once your booking is confirmed you'll sign our digital waiver online — no printer needed. Your link:

- Sign Rental Agreement → + Sign Rental Agreement →

Questions? Call or text (817) 266-2022 or reply to this email.

Ride on,
The Parker County Slingshot Team

@@ -123,6 +123,7 @@ $confirmHtml = "
$squareToken, @@ -166,4 +167,4 @@ sendEmail($email, $name, "Booking Request {$ref} — Parker County Slingshot Ren $msg = "Booking request received! Your reference is {$ref}. We'll be in touch shortly."; if ($depositStatus) $msg .= " A \$" . number_format(DEPOSIT_AMOUNT, 2) . " refundable deposit hold has been placed on your card."; -echo json_encode(['success'=>true,'ref'=>$ref,'deposit_held'=>(bool)$depositStatus,'square_payment_id'=>$sqId??null,'message'=>$msg]); +echo json_encode(['success'=>true,'ref'=>$ref,'deposit_held'=>(bool)$depositStatus,'square_payment_id'=>$sqId,'message'=>$msg]); diff --git a/db.php b/db.php index 3145349..ed496d3 100644 --- a/db.php +++ b/db.php @@ -8,7 +8,7 @@ define('PARKER_DB_PASS', '4@rxg*8kovxCr7w6'); define('ADMIN_USER', 'admin'); define('ADMIN_PHONE', '(817) 266-2022'); -define('ADMIN_PASS', '$2y$10$ynnk3RfarOD7VIJizC30kuXqu6tQ3gotNrlp5y33afh5fPOgnAMU6'); // Parker2026! +define('ADMIN_PASS', '$2y$10$ynnk3RfarOD7VIJizC30kuXqu6tQ3gotNrlp5y33afh5fPOgnAMU6'); define('ADMIN_SESSION_KEY', 'parker_admin_auth'); define('CYBERMAIL_API_KEY', 'sk_live_7f9b0f9a29f6de31a0d229d4af75d56b094ad724fc58a57d'); @@ -47,7 +47,7 @@ function squareApi(string $method, string $path, array $body = []): array { 'Content-Type: application/json', 'Square-Version: ' . SQUARE_VERSION, ]; - $opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false]; + $opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30]; if ($method === 'POST') { $opts[CURLOPT_POST] = true; $opts[CURLOPT_POSTFIELDS] = $body ? json_encode($body) : '{}'; @@ -59,7 +59,34 @@ function squareApi(string $method, string $path, array $body = []): array { } function generateRef(): string { - return 'PSR-' . strtoupper(substr(uniqid(), -6)); + // 4 random bytes -> 8 hex chars; 'PSR-' + 8 chars = 12, matches booking_ref varchar(12). + return 'PSR-' . strtoupper(bin2hex(random_bytes(4))); +} + +// Shared by view-doc.php and admin/view-doc.php so the admin-token check and +// path-traversal/mime guards live in exactly one place. +function verifyAdminToken(string $token): bool { + if (!preg_match('/^[a-f0-9]{64}$/', $token)) return false; + $stmt = db()->prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()"); + $stmt->execute([$token]); + return (bool)$stmt->fetch(); +} + +const ALLOWED_DOC_MIMES = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf']; + +function resolveUploadPath(string $root, string $relativePath): ?string { + $base = realpath($root . '/uploads'); + $path = realpath($root . '/' . $relativePath); + if (!$path || !$base || !str_starts_with($path, $base . DIRECTORY_SEPARATOR)) { + return null; + } + return $path; +} + +function detectAllowedMime(string $path): ?string { + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->file($path); + return isset(ALLOWED_DOC_MIMES[$mime]) ? $mime : null; } function sendEmail(string $to, string $toName, string $subject, string $html): bool { @@ -81,7 +108,6 @@ function sendEmail(string $to, string $toName, string $subject, string $html): b 'Content-Type: application/json', ], CURLOPT_TIMEOUT => 15, - CURLOPT_SSL_VERIFYPEER => false, ]); $resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); diff --git a/db/schema.sql b/db/schema.sql index d2d12d5..1f6270b 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -56,11 +56,22 @@ CREATE TABLE `bookings` ( `waiver_name` varchar(160) DEFAULT NULL, `waiver_sig` mediumtext DEFAULT NULL, `insurance_verified` tinyint(1) NOT NULL DEFAULT 0, + `insurance_file` varchar(255) DEFAULT NULL, `deposit_received` tinyint(1) NOT NULL DEFAULT 0, `license_verified` tinyint(1) NOT NULL DEFAULT 0, + `license_file` varchar(255) DEFAULT NULL, + `helmet_provided` tinyint(1) NOT NULL DEFAULT 0, + `safety_course` tinyint(1) NOT NULL DEFAULT 0, + `operational_course` tinyint(1) NOT NULL DEFAULT 0, + `slingshot_returned` tinyint(1) NOT NULL DEFAULT 0, + `returned_at` datetime DEFAULT NULL, `square_payment_id` varchar(64) DEFAULT NULL, `square_payment_status` varchar(20) DEFAULT NULL, `square_refund_id` varchar(64) DEFAULT NULL, + `square_customer_id` varchar(64) DEFAULT NULL, + `square_card_id` varchar(64) DEFAULT NULL, + `card_last4` varchar(4) DEFAULT NULL, + `card_brand` varchar(20) DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT current_timestamp(), `updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`id`), diff --git a/upload-docs.php b/upload-docs.php index 9307999..a275cb6 100644 --- a/upload-docs.php +++ b/upload-docs.php @@ -23,15 +23,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$error) { if (!$file || $file['error'] !== UPLOAD_ERR_OK) { $error = 'Upload failed — please try again or check file size.'; } else { - $finfo = new finfo(FILEINFO_MIME_TYPE); - $mime = $finfo->file($file['tmp_name']); - $allowed = ['image/jpeg','image/png','application/pdf']; - if (!in_array($mime, $allowed)) { + $mime = detectAllowedMime($file['tmp_name']); + if (!$mime) { $error = 'Only JPG, PNG, or PDF files are accepted.'; } elseif ($file['size'] > 10 * 1024 * 1024) { $error = 'File must be under 10 MB.'; } else { - $ext = ['image/jpeg'=>'jpg','image/png'=>'png','application/pdf'=>'pdf'][$mime]; + $ext = ALLOWED_DOC_MIMES[$mime]; $dir = __DIR__ . '/uploads/' . $ref; if (!is_dir($dir)) mkdir($dir, 0750, true); $fname = $type . '_' . date('YmdHis') . '.' . $ext; diff --git a/view-doc.php b/view-doc.php index b82c898..1f7cfd1 100644 --- a/view-doc.php +++ b/view-doc.php @@ -1,15 +1,8 @@ prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()"); - $stmt->execute([$token]); - return (bool)$stmt->fetch(); -} - $token = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? ''); -if (!_verifyToken($token)) { +if (!verifyAdminToken($token)) { http_response_code(403); header('Content-Type: text/plain'); exit('Unauthorized — please log in to the admin panel first.'); @@ -34,25 +27,21 @@ if (!$row || !$row['file_path']) { exit('Document not found.'); } -$base = realpath(__DIR__ . '/uploads'); -$path = realpath(__DIR__ . '/' . $row['file_path']); - -if (!$path || !$base || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0) { +$path = resolveUploadPath(__DIR__, $row['file_path']); +if (!$path) { http_response_code(404); header('Content-Type: text/plain'); exit('File not found.'); } -$finfo = new finfo(FILEINFO_MIME_TYPE); -$mime = $finfo->file($path); -$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf']; -if (!isset($allowed[$mime])) { +$mime = detectAllowedMime($path); +if (!$mime) { http_response_code(403); header('Content-Type: text/plain'); exit('Invalid file type.'); } -$fname = $type . '-' . $ref . '.' . $allowed[$mime]; +$fname = $type . '-' . $ref . '.' . ALLOWED_DOC_MIMES[$mime]; header('Content-Type: ' . $mime); header('Content-Disposition: inline; filename="' . $fname . '"'); header('Content-Length: ' . filesize($path)); diff --git a/waiver.php b/waiver.php index 58ffa67..35126d7 100644 --- a/waiver.php +++ b/waiver.php @@ -34,7 +34,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$signed) { $error = 'Please type your full name to sign.'; } elseif ($missing) { $error = 'Please check all required boxes before signing.'; - } elseif (!$sigData || strpos($sigData, 'data:image/png;base64,') !== 0) { + } elseif (!$sigData || !preg_match('#^data:image/png;base64,[A-Za-z0-9+/]+={0,2}$#', $sigData) || strlen($sigData) > 300000) { $error = 'Please draw your signature in the box above.'; } else { $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; @@ -59,7 +59,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$signed) { IP" . htmlspecialchars($ip) . " Timestamp" . date('F j, Y g:i A') . " CT - Signature + Signature
"; @@ -75,7 +75,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$signed) {
  • Valid driver's license
  • Proof of personal auto insurance
  • -

    Questions? Call or text (817) 555-0199.

    +

    Questions? Call or text " . ADMIN_PHONE . ".

    Ride on,
    The Parker County Slingshot Team