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(= $bid ?>,'insurance_verified',this)">
= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
+
+ View Document ↗
+
@@ -763,6 +759,9 @@ textarea.notes-ta:focus{border-color:#f97316}
onclick="toggleReq(= $bid ?>,'license_verified',this)">
= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
+
+ View Document ↗
+
@@ -1060,6 +1059,9 @@ textarea.notes-ta:focus{border-color:#f97316}
onclick="cvToggleReq(= $bid ?>,'insurance_verified',this)">
= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
+
+ View Document ↗
+
@@ -1118,6 +1120,9 @@ textarea.notes-ta:focus{border-color:#f97316}
onclick="cvToggleReq(= $bid ?>,'license_verified',this)">
= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
+
+ 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 = "
$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 |
-

+
";
@@ -75,7 +75,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$signed) {