mirror of
https://github.com/myronblair/parkerslingshotrentals
synced 2026-07-28 09:02:29 -05:00
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
This commit is contained in:
+13
-8
@@ -14,13 +14,6 @@ function _createToken(): string {
|
|||||||
db()->exec("DELETE FROM admin_tokens WHERE expires_at < NOW()");
|
db()->exec("DELETE FROM admin_tokens WHERE expires_at < NOW()");
|
||||||
return $token;
|
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');
|
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (($_SERVER['HTTP_ACCEPT'] ?? '') === 'application/json');
|
||||||
|
|
||||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||||
@@ -38,7 +31,7 @@ if (($_GET['action'] ?? '') === 'logout') {
|
|||||||
if ($rawToken) db()->prepare("DELETE FROM admin_tokens WHERE token=?")->execute([$rawToken]);
|
if ($rawToken) db()->prepare("DELETE FROM admin_tokens WHERE token=?")->execute([$rawToken]);
|
||||||
header('Location: /admin/'); exit;
|
header('Location: /admin/'); exit;
|
||||||
}
|
}
|
||||||
$authed = $rawToken !== '' && _verifyToken($rawToken);
|
$authed = $rawToken !== '' && verifyAdminToken($rawToken);
|
||||||
$token = $authed ? $rawToken : '';
|
$token = $authed ? $rawToken : '';
|
||||||
|
|
||||||
// ── AJAX handlers ─────────────────────────────────────────────────────────────
|
// ── AJAX handlers ─────────────────────────────────────────────────────────────
|
||||||
@@ -696,6 +689,9 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
onclick="toggleReq(<?= $bid ?>,'insurance_verified',this)">
|
onclick="toggleReq(<?= $bid ?>,'insurance_verified',this)">
|
||||||
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
|
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -763,6 +759,9 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
|
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
|
||||||
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
|
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -1060,6 +1059,9 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
onclick="cvToggleReq(<?= $bid ?>,'insurance_verified',this)">
|
onclick="cvToggleReq(<?= $bid ?>,'insurance_verified',this)">
|
||||||
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
|
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -1118,6 +1120,9 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
onclick="cvToggleReq(<?= $bid ?>,'license_verified',this)">
|
onclick="cvToggleReq(<?= $bid ?>,'license_verified',this)">
|
||||||
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
|
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+6
-12
@@ -2,9 +2,7 @@
|
|||||||
require_once dirname(__DIR__) . '/db.php';
|
require_once dirname(__DIR__) . '/db.php';
|
||||||
|
|
||||||
$token = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? '');
|
$token = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? '');
|
||||||
$stmt = db()->prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()");
|
if (!verifyAdminToken($token)) {
|
||||||
$stmt->execute([$token]);
|
|
||||||
if (!$stmt->fetch()) {
|
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
header('Content-Type: text/plain');
|
header('Content-Type: text/plain');
|
||||||
exit('Unauthorized — please log in to the admin panel first.');
|
exit('Unauthorized — please log in to the admin panel first.');
|
||||||
@@ -29,25 +27,21 @@ if (!$row || !$row['file_path']) {
|
|||||||
exit('No document on file.');
|
exit('No document on file.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$root = dirname(__DIR__);
|
$path = resolveUploadPath(dirname(__DIR__), $row['file_path']);
|
||||||
$base = realpath($root . '/uploads');
|
if (!$path) {
|
||||||
$path = realpath($root . '/' . $row['file_path']);
|
|
||||||
|
|
||||||
if (!$path || !$base || !str_starts_with($path, $base . DIRECTORY_SEPARATOR)) {
|
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
header('Content-Type: text/plain');
|
header('Content-Type: text/plain');
|
||||||
exit('File not found.');
|
exit('File not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$mime = mime_content_type($path);
|
$mime = detectAllowedMime($path);
|
||||||
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];
|
if (!$mime) {
|
||||||
if (!isset($allowed[$mime])) {
|
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
header('Content-Type: text/plain');
|
header('Content-Type: text/plain');
|
||||||
exit('Invalid file type.');
|
exit('Invalid file type.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$fname = $type . '-' . $ref . '.' . $allowed[$mime];
|
$fname = $type . '-' . $ref . '.' . ALLOWED_DOC_MIMES[$mime];
|
||||||
header('Content-Type: ' . $mime);
|
header('Content-Type: ' . $mime);
|
||||||
header('Content-Disposition: inline; filename="' . $fname . '"');
|
header('Content-Disposition: inline; filename="' . $fname . '"');
|
||||||
header('Content-Length: ' . filesize($path));
|
header('Content-Length: ' . filesize($path));
|
||||||
|
|||||||
+3
-2
@@ -111,7 +111,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'>
|
<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 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>
|
<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 →</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 →</a>
|
||||||
</div>
|
</div>
|
||||||
<p style='color:#374151'>Questions? Call or text <strong>(817) 266-2022</strong> or reply to this email.</p>
|
<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>
|
<p style='color:#374151'>Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
|
||||||
@@ -123,6 +123,7 @@ $confirmHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans
|
|||||||
|
|
||||||
// Square deposit authorization (delayed capture — hold only, not charged yet)
|
// Square deposit authorization (delayed capture — hold only, not charged yet)
|
||||||
$depositStatus = null;
|
$depositStatus = null;
|
||||||
|
$sqId = null;
|
||||||
if ($squareToken) {
|
if ($squareToken) {
|
||||||
$sqResp = squareApi('POST', '/payments', [
|
$sqResp = squareApi('POST', '/payments', [
|
||||||
'source_id' => $squareToken,
|
'source_id' => $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.";
|
$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.";
|
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]);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ define('PARKER_DB_PASS', '4@rxg*8kovxCr7w6');
|
|||||||
|
|
||||||
define('ADMIN_USER', 'admin');
|
define('ADMIN_USER', 'admin');
|
||||||
define('ADMIN_PHONE', '(817) 266-2022');
|
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('ADMIN_SESSION_KEY', 'parker_admin_auth');
|
||||||
|
|
||||||
define('CYBERMAIL_API_KEY', 'sk_live_7f9b0f9a29f6de31a0d229d4af75d56b094ad724fc58a57d');
|
define('CYBERMAIL_API_KEY', 'sk_live_7f9b0f9a29f6de31a0d229d4af75d56b094ad724fc58a57d');
|
||||||
@@ -47,7 +47,7 @@ function squareApi(string $method, string $path, array $body = []): array {
|
|||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
'Square-Version: ' . SQUARE_VERSION,
|
'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') {
|
if ($method === 'POST') {
|
||||||
$opts[CURLOPT_POST] = true;
|
$opts[CURLOPT_POST] = true;
|
||||||
$opts[CURLOPT_POSTFIELDS] = $body ? json_encode($body) : '{}';
|
$opts[CURLOPT_POSTFIELDS] = $body ? json_encode($body) : '{}';
|
||||||
@@ -59,7 +59,34 @@ function squareApi(string $method, string $path, array $body = []): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateRef(): string {
|
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 {
|
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',
|
'Content-Type: application/json',
|
||||||
],
|
],
|
||||||
CURLOPT_TIMEOUT => 15,
|
CURLOPT_TIMEOUT => 15,
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
|
||||||
]);
|
]);
|
||||||
$resp = curl_exec($ch);
|
$resp = curl_exec($ch);
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
|||||||
@@ -56,11 +56,22 @@ CREATE TABLE `bookings` (
|
|||||||
`waiver_name` varchar(160) DEFAULT NULL,
|
`waiver_name` varchar(160) DEFAULT NULL,
|
||||||
`waiver_sig` mediumtext DEFAULT NULL,
|
`waiver_sig` mediumtext DEFAULT NULL,
|
||||||
`insurance_verified` tinyint(1) NOT NULL DEFAULT 0,
|
`insurance_verified` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`insurance_file` varchar(255) DEFAULT NULL,
|
||||||
`deposit_received` tinyint(1) NOT NULL DEFAULT 0,
|
`deposit_received` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`license_verified` 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_id` varchar(64) DEFAULT NULL,
|
||||||
`square_payment_status` varchar(20) DEFAULT NULL,
|
`square_payment_status` varchar(20) DEFAULT NULL,
|
||||||
`square_refund_id` varchar(64) 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(),
|
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
|||||||
+3
-5
@@ -23,15 +23,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$error) {
|
|||||||
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
|
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
|
||||||
$error = 'Upload failed — please try again or check file size.';
|
$error = 'Upload failed — please try again or check file size.';
|
||||||
} else {
|
} else {
|
||||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
$mime = detectAllowedMime($file['tmp_name']);
|
||||||
$mime = $finfo->file($file['tmp_name']);
|
if (!$mime) {
|
||||||
$allowed = ['image/jpeg','image/png','application/pdf'];
|
|
||||||
if (!in_array($mime, $allowed)) {
|
|
||||||
$error = 'Only JPG, PNG, or PDF files are accepted.';
|
$error = 'Only JPG, PNG, or PDF files are accepted.';
|
||||||
} elseif ($file['size'] > 10 * 1024 * 1024) {
|
} elseif ($file['size'] > 10 * 1024 * 1024) {
|
||||||
$error = 'File must be under 10 MB.';
|
$error = 'File must be under 10 MB.';
|
||||||
} else {
|
} else {
|
||||||
$ext = ['image/jpeg'=>'jpg','image/png'=>'png','application/pdf'=>'pdf'][$mime];
|
$ext = ALLOWED_DOC_MIMES[$mime];
|
||||||
$dir = __DIR__ . '/uploads/' . $ref;
|
$dir = __DIR__ . '/uploads/' . $ref;
|
||||||
if (!is_dir($dir)) mkdir($dir, 0750, true);
|
if (!is_dir($dir)) mkdir($dir, 0750, true);
|
||||||
$fname = $type . '_' . date('YmdHis') . '.' . $ext;
|
$fname = $type . '_' . date('YmdHis') . '.' . $ext;
|
||||||
|
|||||||
+6
-17
@@ -1,15 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/db.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'] ?? '');
|
$token = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? '');
|
||||||
if (!_verifyToken($token)) {
|
if (!verifyAdminToken($token)) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
header('Content-Type: text/plain');
|
header('Content-Type: text/plain');
|
||||||
exit('Unauthorized — please log in to the admin panel first.');
|
exit('Unauthorized — please log in to the admin panel first.');
|
||||||
@@ -34,25 +27,21 @@ if (!$row || !$row['file_path']) {
|
|||||||
exit('Document not found.');
|
exit('Document not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$base = realpath(__DIR__ . '/uploads');
|
$path = resolveUploadPath(__DIR__, $row['file_path']);
|
||||||
$path = realpath(__DIR__ . '/' . $row['file_path']);
|
if (!$path) {
|
||||||
|
|
||||||
if (!$path || !$base || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0) {
|
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
header('Content-Type: text/plain');
|
header('Content-Type: text/plain');
|
||||||
exit('File not found.');
|
exit('File not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
$mime = detectAllowedMime($path);
|
||||||
$mime = $finfo->file($path);
|
if (!$mime) {
|
||||||
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];
|
|
||||||
if (!isset($allowed[$mime])) {
|
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
header('Content-Type: text/plain');
|
header('Content-Type: text/plain');
|
||||||
exit('Invalid file type.');
|
exit('Invalid file type.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$fname = $type . '-' . $ref . '.' . $allowed[$mime];
|
$fname = $type . '-' . $ref . '.' . ALLOWED_DOC_MIMES[$mime];
|
||||||
header('Content-Type: ' . $mime);
|
header('Content-Type: ' . $mime);
|
||||||
header('Content-Disposition: inline; filename="' . $fname . '"');
|
header('Content-Disposition: inline; filename="' . $fname . '"');
|
||||||
header('Content-Length: ' . filesize($path));
|
header('Content-Length: ' . filesize($path));
|
||||||
|
|||||||
+3
-3
@@ -34,7 +34,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$signed) {
|
|||||||
$error = 'Please type your full name to sign.';
|
$error = 'Please type your full name to sign.';
|
||||||
} elseif ($missing) {
|
} elseif ($missing) {
|
||||||
$error = 'Please check all required boxes before signing.';
|
$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.';
|
$error = 'Please draw your signature in the box above.';
|
||||||
} else {
|
} else {
|
||||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
|
$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'>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>
|
<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>
|
</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>
|
||||||
</div>";
|
</div>";
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$signed) {
|
|||||||
<li>Valid driver's license</li>
|
<li>Valid driver's license</li>
|
||||||
<li>Proof of personal auto insurance</li>
|
<li>Proof of personal auto insurance</li>
|
||||||
</ul>
|
</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>
|
<p style='color:#374151'>Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
|
||||||
</div>
|
</div>
|
||||||
<div style='background:#f3f4f6;padding:16px;text-align:center'>
|
<div style='background:#f3f4f6;padding:16px;text-align:center'>
|
||||||
|
|||||||
Reference in New Issue
Block a user