| 1 |
<?php |
| 2 |
|
| 3 |
namespace BitCode\BitForm\API\Controller; |
| 4 |
|
| 5 |
use BitCode\BitForm\Core\Database\FormEntryModel; |
| 6 |
use BitCode\BitForm\Core\Util\FileHandler; |
| 7 |
use WP_Error; |
| 8 |
use WP_REST_Controller; |
| 9 |
use WP_REST_Request; |
| 10 |
|
| 11 |
class FileController extends WP_REST_Controller |
| 12 |
{ |
| 13 |
public function handleFileDownload(WP_REST_Request $request) |
| 14 |
{ |
| 15 |
$params = $request->get_params(); |
| 16 |
$formID = isset($params['formID']) ? absint($params['formID']) : 0; |
| 17 |
if (empty($formID)) { |
| 18 |
return new WP_Error('invalid_form_id', 'Invalid or missing formID parameter', ['status' => 400, 'success' => false]); |
| 19 |
} |
| 20 |
|
| 21 |
$entryID = isset($params['entryID']) ? absint($params['entryID']) : 0; |
| 22 |
if (empty($entryID)) { |
| 23 |
return new WP_Error('invalid_entry_id', 'Invalid or missing entryID parameter', ['status' => 400, 'success' => false]); |
| 24 |
} |
| 25 |
|
| 26 |
$fileName = isset($params['fileID']) ? sanitize_file_name((string) $params['fileID']) : ''; |
| 27 |
if (empty($fileName)) { |
| 28 |
return new WP_Error('invalid_file_id', 'Invalid or missing fileID parameter', ['status' => 400, 'success' => false]); |
| 29 |
} |
| 30 |
|
| 31 |
$filePath = $this->getValidatedFilePath($formID, $entryID, $fileName); |
| 32 |
if (is_wp_error($filePath)) { |
| 33 |
return $filePath; |
| 34 |
} |
| 35 |
|
| 36 |
if (!$this->isValidEntryFileRequest($formID, $entryID)) { |
| 37 |
return new WP_Error('file_not_found', 'File not found.', ['status' => 404, 'success' => false]); |
| 38 |
} |
| 39 |
|
| 40 |
$forceDownload = isset($params['download']) && 'true' === strtolower(sanitize_text_field((string) $params['download'])); |
| 41 |
|
| 42 |
$this->serveFile($filePath, $forceDownload); |
| 43 |
} |
| 44 |
|
| 45 |
private function getValidatedFilePath($formID, $entryID, $fileName) |
| 46 |
{ |
| 47 |
$baseDir = FileHandler::getEntriesFileUploadDir($formID, $entryID); |
| 48 |
$baseDirRealPath = realpath($baseDir); |
| 49 |
|
| 50 |
if (empty($baseDirRealPath) || !is_dir($baseDirRealPath)) { |
| 51 |
return new WP_Error('file_not_found', 'File not found.', ['status' => 404, 'success' => false]); |
| 52 |
} |
| 53 |
|
| 54 |
$filePath = $baseDirRealPath . DIRECTORY_SEPARATOR . $fileName; |
| 55 |
$realFilePath = realpath($filePath); |
| 56 |
|
| 57 |
if (empty($realFilePath) || !is_file($realFilePath) || !is_readable($realFilePath)) { |
| 58 |
return new WP_Error('file_not_found', 'File not found.', ['status' => 404, 'success' => false]); |
| 59 |
} |
| 60 |
|
| 61 |
$normalizedBaseDir = trailingslashit(wp_normalize_path($baseDirRealPath)); |
| 62 |
$normalizedFilePath = wp_normalize_path($realFilePath); |
| 63 |
|
| 64 |
if (0 !== strpos($normalizedFilePath, $normalizedBaseDir)) { |
| 65 |
return new WP_Error('invalid_file_path', 'Invalid file path.', ['status' => 400, 'success' => false]); |
| 66 |
} |
| 67 |
|
| 68 |
return $realFilePath; |
| 69 |
} |
| 70 |
|
| 71 |
private function isValidEntryFileRequest($formID, $entryID) |
| 72 |
{ |
| 73 |
$entryModel = new FormEntryModel(); |
| 74 |
$entry = $entryModel->get( |
| 75 |
'id', |
| 76 |
[ |
| 77 |
'id' => $entryID, |
| 78 |
'form_id' => $formID, |
| 79 |
] |
| 80 |
); |
| 81 |
|
| 82 |
return !is_wp_error($entry) && !empty($entry); |
| 83 |
} |
| 84 |
|
| 85 |
private function serveFile($filePath, $forceDownload = false) |
| 86 |
{ |
| 87 |
$fileName = wp_basename($filePath); |
| 88 |
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); |
| 89 |
$fileInfo = wp_check_filetype($fileName); |
| 90 |
$contentType = !empty($fileInfo['type']) ? $fileInfo['type'] : 'application/octet-stream'; |
| 91 |
|
| 92 |
$safeTextPreviewExtensions = [ |
| 93 |
'csv', |
| 94 |
'json', |
| 95 |
'md', |
| 96 |
'txt', |
| 97 |
]; |
| 98 |
$safeMediaPreviewExtensions = [ |
| 99 |
'pdf', |
| 100 |
'jpg', |
| 101 |
'jpeg', |
| 102 |
'png', |
| 103 |
'gif', |
| 104 |
'webp', |
| 105 |
]; |
| 106 |
$allowInlineTextPreview = !$forceDownload && in_array($extension, $safeTextPreviewExtensions, true); |
| 107 |
$allowInlineMediaPreview = !$forceDownload && in_array($extension, $safeMediaPreviewExtensions, true); |
| 108 |
$allowInlinePreview = $allowInlineTextPreview || $allowInlineMediaPreview; |
| 109 |
|
| 110 |
if ($allowInlineTextPreview) { |
| 111 |
$contentType = 'text/plain; charset=' . get_option('blog_charset', 'UTF-8'); |
| 112 |
} elseif (!$allowInlineMediaPreview) { |
| 113 |
$contentType = 'application/octet-stream'; |
| 114 |
} |
| 115 |
|
| 116 |
if (!headers_sent()) { |
| 117 |
if (function_exists('nocache_headers')) { |
| 118 |
nocache_headers(); |
| 119 |
} |
| 120 |
|
| 121 |
header('X-Content-Type-Options: nosniff'); |
| 122 |
header('X-Frame-Options: SAMEORIGIN'); |
| 123 |
header('Referrer-Policy: same-origin'); |
| 124 |
header('Content-Description: File Transfer'); |
| 125 |
header('Content-Length: ' . filesize($filePath)); |
| 126 |
header('Content-Transfer-Encoding: binary'); |
| 127 |
header('Accept-Ranges: none'); |
| 128 |
|
| 129 |
if (!$allowInlinePreview) { |
| 130 |
header('Content-Type: application/octet-stream'); |
| 131 |
header('Content-Disposition: attachment; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\'' . rawurlencode($fileName)); |
| 132 |
} else { |
| 133 |
header('Content-Type: ' . $contentType); |
| 134 |
header('Content-Disposition: inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\'' . rawurlencode($fileName)); |
| 135 |
} |
| 136 |
} |
| 137 |
|
| 138 |
while (ob_get_level() > 0) { |
| 139 |
ob_end_clean(); |
| 140 |
} |
| 141 |
|
| 142 |
flush(); |
| 143 |
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Streaming binary download; WP_Filesystem has no streaming equivalent and get_contents() would load entire file into memory. |
| 144 |
readfile($filePath); |
| 145 |
exit; |
| 146 |
} |
| 147 |
} |
| 148 |
|