Compare commits

...

6 Commits

Author SHA1 Message Date
myron a8f3a9085b Add copyright notice 2026-07-27 04:55:22 +00:00
myron 671df18039 CRITICAL: block direct access to customer license/insurance uploads (was fully downloadable with no auth); fix double-booking race condition and unstable deposit-hold idempotency key 2026-07-06 07:53:11 +00:00
Myron Blair 3bf7fe7620 Move db.php secrets out of webroot; switch to universal Square account
db.php: replace with a require shim pointing to db-secrets.php outside
public_html (DB password, admin hash, CyberMail key, Square token were all
sitting in a web-reachable file, only protected by .htaccess).

index.html: update the Web Payments SDK App ID/Location ID to the new
unified Square account shared with tomtomgames.com and tomsjavajive.com.
2026-07-05 15:46:25 +00:00
myron ffebe87210 Block public access to .git directory (leaked GitHub PAT, same issue found on sibling sites) 2026-07-04 16:04:05 -05:00
myron 13c0fd6e4c Add rewrite-based deny rules for db/ and config/ (htaccess Deny directives are ignored on this LiteSpeed vhost) 2026-07-04 19:06:17 +00:00
myron d12baebcbc Fix security issues from code review: remove plaintext password comment, enable TLS verification, harden booking ref entropy, fix XSS/injection in signature emails, fix broken waiver link, consolidate duplicated auth/file-serving logic, add missing document view links, sync schema.sql with live DB 2026-07-04 14:01:33 -05:00
14 changed files with 113 additions and 167 deletions
+17
View File
@@ -2,3 +2,20 @@
Order deny,allow
Deny from all
</Files>
RewriteEngine On
RewriteRule ^db\.php$ - [F,L]
RewriteCond %{REQUEST_URI} ^/db\.php$
RewriteRule .* - [F,L]
# .git/ was found directly downloadable (leaked GitHub PAT) - same class of
# issue found and fixed on sibling sites this session.
RewriteCond %{REQUEST_URI} ^/\.git
RewriteRule .* - [F,L]
# uploads/ (customer license/insurance docs) must never be directly downloadable -
# same Order/Deny-doesn't-work-on-OpenLiteSpeed gotcha as .git above. Docs are
# served only through view-doc.php / admin/view-doc.php, which read by filesystem
# path via readfile() - blocking direct URL access here doesn't break that flow.
RewriteCond %{REQUEST_URI} ^/uploads/
RewriteRule .* - [F,L]
+1
View File
@@ -0,0 +1 @@
Property of TomTom Enterprises.
+13 -8
View File
@@ -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' ?>
</button>
<?php if (!empty($b['insurance_file'])): ?>
<a class="flow-link" href="/admin/view-doc.php?ref=<?= urlencode($b['booking_ref']) ?>&type=insurance&_t=<?= urlencode($token) ?>" target="_blank">View Document ↗</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
@@ -763,6 +759,9 @@ textarea.notes-ta:focus{border-color:#f97316}
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
</button>
<?php if (!empty($b['license_file'])): ?>
<a class="flow-link" href="/admin/view-doc.php?ref=<?= urlencode($b['booking_ref']) ?>&type=license&_t=<?= urlencode($token) ?>" target="_blank">View Document ↗</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
@@ -1060,6 +1059,9 @@ textarea.notes-ta:focus{border-color:#f97316}
onclick="cvToggleReq(<?= $bid ?>,'insurance_verified',this)">
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
</button>
<?php if (!empty($cb['insurance_file'])): ?>
<a class="flow-link" href="/admin/view-doc.php?ref=<?= urlencode($cb['booking_ref']) ?>&type=insurance&_t=<?= urlencode($token) ?>" target="_blank">View Document ↗</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
@@ -1118,6 +1120,9 @@ textarea.notes-ta:focus{border-color:#f97316}
onclick="cvToggleReq(<?= $bid ?>,'license_verified',this)">
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
</button>
<?php if (!empty($cb['license_file'])): ?>
<a class="flow-link" href="/admin/view-doc.php?ref=<?= urlencode($cb['booking_ref']) ?>&type=license&_t=<?= urlencode($token) ?>" target="_blank">View Document ↗</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
+6 -12
View File
@@ -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));
+2
View File
@@ -0,0 +1,2 @@
RewriteEngine On
RewriteRule .* - [F,L]
+36 -18
View File
@@ -39,29 +39,43 @@ $pkg = PACKAGES[$package];
$rentalDate = $date;
$endDate = date('Y-m-d', strtotime($date . ' +' . $pkg['days'] . ' days'));
// Check availability
$conflict = db()->prepare(
// Check availability + create booking atomically. A named advisory lock (scoped to the
// requested date range) prevents two concurrent submissions from both passing the
// conflict check before either has inserted, which would otherwise double-book the
// vehicle for overlapping dates.
$lockName = 'psr_booking_' . $rentalDate . '_' . $endDate;
$gotLock = (int) db()->query("SELECT GET_LOCK('" . addslashes($lockName) . "', 5)")->fetchColumn();
if (!$gotLock) {
http_response_code(503);
echo json_encode(['success'=>false,'error'=>'Please try again in a moment.']); exit;
}
try {
$conflict = db()->prepare(
"SELECT id FROM bookings
WHERE status IN ('pending','confirmed')
AND rental_date <= ? AND end_date >= ?"
);
$conflict->execute([$endDate, $rentalDate]);
if ($conflict->fetch()) {
);
$conflict->execute([$endDate, $rentalDate]);
if ($conflict->fetch()) {
echo json_encode(['success'=>false,'error'=>'Sorry, that date is already booked. Please choose another date.']); exit;
}
$blockedCheck = db()->prepare("SELECT id FROM blocked_dates WHERE block_date BETWEEN ? AND ?");
$blockedCheck->execute([$rentalDate, $endDate]);
if ($blockedCheck->fetch()) {
}
$blockedCheck = db()->prepare("SELECT id FROM blocked_dates WHERE block_date BETWEEN ? AND ?");
$blockedCheck->execute([$rentalDate, $endDate]);
if ($blockedCheck->fetch()) {
echo json_encode(['success'=>false,'error'=>'That date is unavailable. Please choose another date.']); exit;
}
}
// Create booking
$ref = generateRef();
$stmt = db()->prepare(
// Create booking
$ref = generateRef();
$stmt = db()->prepare(
"INSERT INTO bookings (booking_ref, name, email, phone, package, rental_date, end_date, amount, notes)
VALUES (?,?,?,?,?,?,?,?,?)"
);
$stmt->execute([$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message]);
);
$stmt->execute([$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message]);
} finally {
db()->query("SELECT RELEASE_LOCK('" . addslashes($lockName) . "')");
}
$dateLabel = date('F j, Y', strtotime($rentalDate));
$pkgLabel = $pkg['label'];
@@ -111,7 +125,7 @@ $confirmHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans
<div style='margin:20px 0;padding:16px;background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;text-align:center'>
<p style='margin:0 0 10px;font-size:14px;font-weight:700;color:#111'>Next Step: Sign Your Rental Agreement</p>
<p style='margin:0 0 14px;font-size:13px;color:#6b7280'>Once your booking is confirmed you'll sign our digital waiver online — no printer needed. Your link:</p>
<a href='' . SITE_URL . '/waiver.php?ref={$ref}' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-weight:700;font-size:14px'>Sign Rental Agreement &rarr;</a>
<a href='" . SITE_URL . "/waiver.php?ref={$ref}' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-weight:700;font-size:14px'>Sign Rental Agreement &rarr;</a>
</div>
<p style='color:#374151'>Questions? Call or text <strong>(817) 266-2022</strong> or reply to this email.</p>
<p style='color:#374151'>Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
@@ -123,10 +137,14 @@ $confirmHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans
// Square deposit authorization (delayed capture — hold only, not charged yet)
$depositStatus = null;
$sqId = null;
if ($squareToken) {
$sqResp = squareApi('POST', '/payments', [
'source_id' => $squareToken,
'idempotency_key' => $ref . '-dep-' . time(),
// Stable per booking ref (not time()-suffixed) so a network retry or
// accidental double-submit of this same deposit hold is recognized by
// Square as a duplicate instead of placing a second hold on the card.
'idempotency_key' => $ref . '-dep',
'amount_money' => ['amount' => (int)(DEPOSIT_AMOUNT * 100), 'currency' => 'USD'],
'autocomplete' => false, // hold only — capture when confirmed
'location_id' => SQUARE_LOCATION_ID,
@@ -166,4 +184,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]);
+1 -89
View File
@@ -1,90 +1,2 @@
<?php
define('SITE_URL', 'https://www.parkerslingshotrentals.com');
define('PARKER_DB_HOST', 'localhost');
define('PARKER_DB_NAME', 'park_slingshot');
define('PARKER_DB_USER', 'park_slingshotuser');
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_SESSION_KEY', 'parker_admin_auth');
define('CYBERMAIL_API_KEY', 'sk_live_7f9b0f9a29f6de31a0d229d4af75d56b094ad724fc58a57d');
define('MAIL_FROM', 'noreply@orbishosting.com');
define('MAIL_FROM_NAME', 'Parker County Slingshot Rentals');
define('ADMIN_EMAIL', 'info@parkerslingshotrentals.com');
define('SQUARE_ACCESS_TOKEN', 'EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v');
define('SQUARE_APP_ID', 'sq0idp-YSM7BU9IVyOWSzpeP-0nzQ');
define('SQUARE_LOCATION_ID', 'L8GZYHYKE95CE');
define('SQUARE_VERSION', '2024-01-18');
define('DEPOSIT_AMOUNT', 45.00); // $45 deposit hold — balance due at pickup
define('PACKAGES', [
'half-day' => ['label' => 'Half Day (4 hrs)', 'amount' => 99.00, 'days' => 0],
'full-day' => ['label' => 'Full Day (8 hrs)', 'amount' => 169.00, 'days' => 0],
'weekend' => ['label' => 'Weekend (48 hrs)', 'amount' => 299.00, 'days' => 1],
]);
function db(): PDO {
static $pdo;
if (!$pdo) {
$pdo = new PDO(
'mysql:host=' . PARKER_DB_HOST . ';dbname=' . PARKER_DB_NAME . ';charset=utf8mb4',
PARKER_DB_USER, PARKER_DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
}
return $pdo;
}
function squareApi(string $method, string $path, array $body = []): array {
$ch = curl_init('https://connect.squareup.com/v2' . $path);
$headers = [
'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
'Content-Type: application/json',
'Square-Version: ' . SQUARE_VERSION,
];
$opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false];
if ($method === 'POST') {
$opts[CURLOPT_POST] = true;
$opts[CURLOPT_POSTFIELDS] = $body ? json_encode($body) : '{}';
}
curl_setopt_array($ch, $opts);
$resp = curl_exec($ch);
curl_close($ch);
return json_decode($resp ?: '{}', true);
}
function generateRef(): string {
return 'PSR-' . strtoupper(substr(uniqid(), -6));
}
function sendEmail(string $to, string $toName, string $subject, string $html): bool {
$apiKey = defined('CYBERMAIL_API_KEY') ? CYBERMAIL_API_KEY : '';
if (!$apiKey || strpos($apiKey, 'YOUR_KEY') !== false) return false;
$payload = json_encode([
'from' => MAIL_FROM,
'to' => $to,
'subject' => $subject,
'html' => $html,
]);
$ch = curl_init('https://platform.cyberpersons.com/email/v1/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => false,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $code === 202;
}
require_once dirname(__DIR__) . "/db-secrets.php";
+2
View File
@@ -0,0 +1,2 @@
RewriteEngine On
RewriteRule .* - [F,L]
+11
View File
@@ -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`),
+1 -1
View File
@@ -1104,7 +1104,7 @@
const cardEl = document.getElementById('card-container');
if (cardEl) cardEl.style.display = '';
try {
const payments = Square.payments('sq0idp-YSM7BU9IVyOWSzpeP-0nzQ', 'L8GZYHYKE95CE');
const payments = Square.payments('sq0idp-UnXYsN5U7ba8F8C9LfY5ZA', 'LW7MNGFP44QZZ');
squareCard = await payments.card({
style: {
'.input-container': { borderColor: 'rgba(255,255,255,0.12)', borderRadius: '6px' },
+3 -5
View File
@@ -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;
+5 -8
View File
@@ -1,8 +1,5 @@
# Block direct web access documents served only through admin/view-doc.php
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
# Block direct web access - documents served only through view-doc.php / admin/view-doc.php.
# Require all denied / Order deny,allow do NOT work on this OpenLiteSpeed setup -
# only RewriteRule [F,L] actually blocks requests here (confirmed this session).
RewriteEngine On
RewriteRule .* - [F,L]
+6 -17
View File
@@ -1,15 +1,8 @@
<?php
require_once __DIR__ . '/db.php';
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();
}
$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));
+3 -3
View File
@@ -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) {
<tr><td style='color:#6b7280;padding:6px 0'>IP</td><td style='padding:6px 0'>" . htmlspecialchars($ip) . "</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Timestamp</td><td style='padding:6px 0'>" . date('F j, Y g:i A') . " CT</td></tr>
</table>
<img src='{$sigData}' style='margin-top:16px;border:1px solid #e5e7eb;border-radius:6px;max-width:100%;height:auto' alt='Signature' />
<img src='" . htmlspecialchars($sigData) . "' style='margin-top:16px;border:1px solid #e5e7eb;border-radius:6px;max-width:100%;height:auto' alt='Signature' />
</div>
</div>";
@@ -75,7 +75,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$signed) {
<li>Valid driver's license</li>
<li>Proof of personal auto insurance</li>
</ul>
<p style='color:#374151'>Questions? Call or text <strong>(817) 555-0199</strong>.</p>
<p style='color:#374151'>Questions? Call or text <strong>" . ADMIN_PHONE . "</strong>.</p>
<p style='color:#374151'>Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
</div>
<div style='background:#f3f4f6;padding:16px;text-align:center'>