123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
<?php
class URLRouter {
private $scriptDir;
private $webPath;
private $baseDir;
public function __construct($scriptDir, $webPath, $baseDir) {
$this->scriptDir = $scriptDir;
$this->webPath = $webPath;
$this->baseDir = $baseDir;
}
public function parseCleanURL() {
$requestUri = $_SERVER['REQUEST_URI'];
$scriptName = $_SERVER['SCRIPT_NAME'];
$scriptPath = dirname($scriptName);
if ($scriptPath !== '/' && !empty($scriptPath)) {
$requestUri = str_replace($scriptPath, '', $requestUri);
}
$requestUri = ltrim($requestUri, '/');
$parts = explode('?', $requestUri, 2);
$cleanPath = $parts[0];
if (isset($parts[1])) {
parse_str($parts[1], $queryParams);
$_GET = array_merge($_GET, $queryParams);
}
if (!empty($cleanPath) && substr($cleanPath, -1) === '/') {
$cleanPath = rtrim($cleanPath, '/');
}
$decodedPath = $this->decodePathFromURL($cleanPath);
if ($decodedPath === false) {
http_response_code(400);
exit('Invalid path');
}
return $decodedPath;
}
public function generateFileURL($path, $filename, $action = 'view', $viewType = 'default') {
$encodedPath = $this->encodePathForURL($path);
$encodedFilename = rawurlencode($filename);
$fullPath = $encodedPath ? $encodedPath . '/' . $encodedFilename : $encodedFilename;
switch ($action) {
case 'download':
return $this->webPath . '/' . $fullPath . '/?download=file';
case 'view':
default:
return $this->webPath . '/' . $fullPath . '/?view=' . $viewType;
}
}
public function generateFolderURL($path, $foldername = '', $action = 'view') {
$encodedPath = $this->encodePathForURL($path);
$encodedFoldername = $foldername ? rawurlencode($foldername) : '';
if ($action === 'download') {
$fullPath = $encodedPath ? $encodedPath . '/' . $encodedFoldername : $encodedFoldername;
return $this->webPath . '/' . $fullPath . '/?download=archive';
} else {
$fullPath = $encodedPath;
if ($encodedFoldername) {
$fullPath = $fullPath ? $fullPath . '/' . $encodedFoldername : $encodedFoldername;
}
$url = $this->webPath . '/' . $fullPath . '/';
$url = preg_replace('#/+#', '/', $url);
$url = str_replace(':/', '://', $url);
return $url;
}
}
public function generateSortURL($sortBy, $currentSort, $currentDir, $currentPath) {
$newDir = ($sortBy === $currentSort && $currentDir === 'asc') ? 'desc' : 'asc';
$params = http_build_query([
'sort' => $sortBy,
'dir' => $newDir
]);
if (empty($currentPath)) {
$baseUrl = $this->webPath . '/';
} else {
$encodedPath = $this->encodePathForURL($currentPath);
$baseUrl = $this->webPath . '/' . $encodedPath . '/';
}
return $baseUrl . '?' . $params;
}
public function generateBreadcrumbs($currentPath) {
$parts = array_filter(explode('/', $currentPath));
$breadcrumbs = '<a href="' . $this->webPath . '/">root</a>';
$path = '';
foreach ($parts as $part) {
$path .= '/' . $part;
$encodedPath = $this->encodePathForURL($path);
$breadcrumbs .= ' / <a href="' . $this->webPath . '/' . $encodedPath . '/">' . htmlspecialchars($part) . '</a>';
}
return $breadcrumbs;
}
public function isFileRequest($currentPath) {
$fullPath = $this->baseDir . '/' . $currentPath;
return is_file($fullPath);
}
public function isFolderRequest($currentPath) {
$fullPath = $this->baseDir . '/' . $currentPath;
return is_dir($fullPath);
}
private function encodePathForURL($path) {
if (empty($path)) return '';
$segments = explode('/', $path);
$encodedSegments = array_map('rawurlencode', $segments);
return implode('/', $encodedSegments);
}
private function decodePathFromURL($encodedPath) {
if (empty($encodedPath)) return '';
$decodedPath = rawurldecode($encodedPath);
if (strpos($decodedPath, '../') !== false || strpos($decodedPath, '..\\') !== false) {
return false;
}
$decodedPath = str_replace("\0", '', $decodedPath);
return $decodedPath;
}
public function handleFileRequest($currentPath) {
$fullPath = $this->baseDir . '/' . $currentPath;
if (strpos(realpath($fullPath), realpath($this->baseDir)) !== 0) {
http_response_code(403);
exit('Access denied');
}
if (!is_file($fullPath)) {
http_response_code(404);
exit('File not found');
}
$filename = basename($currentPath);
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$parentPath = dirname($currentPath);
if ($parentPath === '.') $parentPath = '';
if (!isFileAccessible($fullPath, $parentPath, $extension)) {
http_response_code(403);
exit('File not accessible');
}
if (isset($_GET['download'])) {
$fileSize = filesize($fullPath);
$maxSize = getMaxDownloadSize('file');
if ($fileSize > $maxSize) {
http_response_code(413);
$fileSizeFormatted = formatSizeForError($fileSize);
$maxSizeFormatted = formatSizeForError($maxSize);
exit("File too large for download. File size: {$fileSizeFormatted}, Maximum allowed: {$maxSizeFormatted}");
}
$this->handleFileDownload($fullPath, $filename);
}
$viewType = isset($_GET['view']) ? $_GET['view'] : 'raw';
switch ($viewType) {
case 'default':
$this->handleFileView($fullPath, $filename, $extension, 'default');
break;
case 'code':
$this->handleFileView($fullPath, $filename, $extension, 'code');
break;
case 'markdown':
if (in_array($extension, ['md', 'markdown'])) {
$this->handleFileView($fullPath, $filename, $extension, 'markdown');
} else {
$newUrl = $_SERVER['REQUEST_URI'];
$newUrl = preg_replace('/[?&]view=markdown/', '?view=default', $newUrl);
if (!strpos($newUrl, '?')) {
$newUrl .= '?view=default';
}
header('Location: ' . $newUrl);
exit;
}
break;
case 'raw':
default:
$this->handleRawFileView($fullPath, $filename, $extension);
break;
}
}
private function handleRawFileView($fullPath, $filename, $extension) {
$mimeTypes = [
'txt' => 'text/plain',
'md' => 'text/plain',
'markdown' => 'text/plain',
'js' => 'text/plain',
'css' => 'text/plain',
'html' => 'text/plain',
'htm' => 'text/plain',
'json' => 'application/json',
'xml' => 'text/xml',
'php' => 'text/plain',
'py' => 'text/plain',
'sql' => 'text/plain',
'log' => 'text/plain',
'yml' => 'text/plain',
'yaml' => 'text/plain',
'conf' => 'text/plain',
'config' => 'text/plain',
'ini' => 'text/plain',
'env' => 'text/plain',
'sh' => 'text/plain',
'bat' => 'text/plain',
'ps1' => 'text/plain',
];
$directServeExtensions = [
'pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'jfif', 'avif', 'ico',
'cur', 'tiff', 'bmp', 'heic', 'svg', 'mp4', 'mkv', 'mp3', 'aac',
'flac', 'm4a', 'ogg', 'opus', 'wma', 'mov', 'webm', 'wmv', '3gp',
'flv', 'm4v', 'docx', 'xlsx'
];
if (in_array($extension, $directServeExtensions)) {
$this->serveFileDirect($fullPath, $extension, $filename);
} else {
$mimeType = isset($mimeTypes[$extension]) ? $mimeTypes[$extension] : 'text/plain';
header('Content-Type: ' . $mimeType . '; charset=utf-8');
header('Content-Disposition: inline; filename="' . $filename . '"');
readfile($fullPath);
exit;
}
}
public function handleFolderRequest($currentPath) {
$fullPath = $this->baseDir . '/' . $currentPath;
if (strpos(realpath($fullPath), realpath($this->baseDir)) !== 0) {
http_response_code(403);
exit('Access denied');
}
if (!is_dir($fullPath)) {
http_response_code(404);
exit('Directory not found');
}
if (!isFolderAccessible($currentPath)) {
http_response_code(403);
exit('Folder not accessible');
}
if (isset($_GET['download']) && $_GET['download'] === 'archive') {
$folderSize = getDirectorySize($fullPath);
$maxSize = getMaxDownloadSize('folder');
if ($folderSize > $maxSize) {
http_response_code(413);
$folderSizeFormatted = formatSizeForError($folderSize);
$maxSizeFormatted = formatSizeForError($maxSize);
exit("Folder too large for download. Folder size: {$folderSizeFormatted}, Maximum allowed: {$maxSizeFormatted}");
}
$folderName = basename($currentPath);
if (empty($folderName)) {
$folderName = 'files';
}
$this->handleFolderDownload($fullPath, $folderName);
exit;
}
return true;
}
private function handleFileDownload($fullPath, $filename) {
global $disableFileDownloads;
if ($disableFileDownloads) {
http_response_code(403);
exit('File downloads disabled');
}
$fileSize = filesize($fullPath);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . $fileSize);
header('Cache-Control: no-cache');
header('Accept-Ranges: bytes');
if (ob_get_level()) {
ob_end_clean();
}
$handle = fopen($fullPath, 'rb');
if ($handle === false) {
http_response_code(500);
exit('Cannot read file');
}
$chunkSize = 8192;
while (!feof($handle)) {
$chunk = fread($handle, $chunkSize);
if ($chunk === false) {
break;
}
echo $chunk;
flush();
if (connection_aborted()) {
break;
}
}
fclose($handle);
exit;
}
private function handleFileView($fullPath, $filename, $extension, $viewMode = 'default') {
global $currentPath;
$relativePath = $currentPath ? $currentPath . '/' . $filename : $filename;
$fileModTime = filemtime($fullPath);
$cacheKey = 'fileview_' . md5($relativePath . '_' . $viewMode . '_' . $fileModTime);
$cache = initializeCache();
$cachedOutput = $cache->get($cacheKey, 'fileview');
if ($cachedOutput !== null) {
echo $cachedOutput;
exit;
}
$fileContent = file_get_contents($fullPath);
$fileName = htmlspecialchars($filename);
$directServeExtensions = [
'pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'jfif', 'avif', 'ico',
'cur', 'tiff', 'bmp', 'heic', 'svg', 'mp4', 'mkv', 'mp3', 'aac',
'flac', 'm4a', 'ogg', 'opus', 'wma', 'mov', 'webm', 'wmv', '3gp',
'flv', 'm4v', 'docx', 'xlsx'
];
if (in_array($extension, $directServeExtensions)) {
$this->serveFileDirect($fullPath, $extension, $filename);
} else {
ob_start();
$this->serveFileAsText($fileContent, $filename, $extension, $viewMode);
$output = ob_get_clean();
$cache->set($cacheKey, 'fileview', $output, 3600);
echo $output;
exit;
}
}
private function serveFileDirect($fullPath, $extension, $filename) {
$mimeTypes = [
'pdf' => 'application/pdf',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
'jfif' => 'image/jpeg',
'avif' => 'image/avif',
'ico' => 'image/vnd.microsoft.icon',
'cur' => 'image/vnd.microsoft.icon',
'tiff' => 'image/tiff',
'bmp' => 'image/bmp',
'heic' => 'image/heic',
'svg' => 'image/svg+xml',
'mp4' => 'video/mp4',
'mkv' => 'video/webm',
'mp3' => 'audio/mpeg',
'aac' => 'audio/aac',
'flac' => 'audio/flac',
'm4a' => 'audio/mp4',
'ogg' => 'audio/ogg',
'opus' => 'audio/ogg',
'wma' => 'audio/x-ms-wma',
'mov' => 'video/quicktime',
'webm' => 'video/webm',
'wmv' => 'video/x-ms-wmv',
'3gp' => 'video/3gpp',
'flv' => 'video/x-flv',
'm4v' => 'video/mp4',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
$mimeType = $mimeTypes[$extension] ?? 'application/octet-stream';
header('Content-Type: ' . $mimeType);
header('Content-Disposition: inline; filename="' . $filename . '"');
header('Content-Length: ' . filesize($fullPath));
readfile($fullPath);
exit;
}
private function serveFileAsText($fileContent, $filename, $extension, $viewMode = 'default') {
$markdownExtensions = ['md', 'markdown'];
$isMarkdown = in_array($extension, $markdownExtensions);
$showMarkdown = false;
$showCode = false;
if ($viewMode === 'default' && $isMarkdown) {
$showMarkdown = true;
} elseif ($viewMode === 'markdown' && $isMarkdown) {
$showMarkdown = true;
} else {
$showCode = true;
}
$showRaw = isset($_GET['raw']) && $_GET['raw'] === '1';
if ($showRaw) {
$showMarkdown = false;
$showCode = false;
}
$currentUrl = $_SERVER['REQUEST_URI'];
$baseUrlParts = parse_url($currentUrl);
$baseUrl = $baseUrlParts['path'];
$currentParams = [];
if (isset($baseUrlParts['query'])) {
parse_str($baseUrlParts['query'], $currentParams);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $filename; ?></title>
<link rel="icon" type="image/x-icon" href="<?php echo $this->webPath; ?>/.indexer_files/favicon/icon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="<?php echo $this->webPath; ?>/.indexer_files/favicon/16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="<?php echo $this->webPath; ?>/.indexer_files/favicon/32x32.png">
<link rel="icon" type="image/png" sizes="48x48" href="<?php echo $this->webPath; ?>/.indexer_files/favicon/48x48.png">
<link rel="icon" type="image/png" sizes="96x96" href="<?php echo $this->webPath; ?>/.indexer_files/favicon/96x96.png">
<link rel="icon" type="image/png" sizes="144x144" href="<?php echo $this->webPath; ?>/.indexer_files/favicon/144x144.png">
<link rel="icon" type="image/png" sizes="192x192" href="<?php echo $this->webPath; ?>/.indexer_files/favicon/192x192.png">
<link rel="apple-touch-icon" sizes="180x180" href="<?php echo $this->webPath; ?>/.indexer_files/favicon/180x180.png">
<link rel="stylesheet" href="<?php echo $this->webPath; ?>/.indexer_files/local_api/style/base-1.2.1.min.css">
<?php if ($showMarkdown): ?>
<link rel="stylesheet" href="<?php echo $this->webPath; ?>/.indexer_files/local_api/style/viewer-markdown-1.2.0.min.css">
<?php else: ?>
<link rel="stylesheet" href="<?php echo $this->webPath; ?>/.indexer_files/local_api/style/viewer-code-1.2.1.min.css">
<link rel="stylesheet" href="<?php echo $this->webPath; ?>/.indexer_files/local_api/style/atom-one-dark.min.css">
<?php endif; ?>
</head>
<body>
<?php
$securityStatus = getSecurityStatus();
$lockIcon = $securityStatus['secure']
? $this->webPath . '/.indexer_files/icons/app/green.png'
: $this->webPath . '/.indexer_files/icons/app/red.png';
?>
<div class="security-bar">
<span class="security-lock" data-tooltip="<?php echo $securityStatus['secure'] ? 'Connection is secure (HTTPS)' : 'Connection is not secure - Consider using HTTPS'; ?>">
<img src="<?php echo htmlspecialchars($lockIcon); ?>" alt="<?php echo $securityStatus['secure'] ? 'Secure' : 'Not Secure'; ?>">
</span>
<div class="security-bar-filename"><?php echo htmlspecialchars($filename); ?></div>
<div class="security-bar-buttons">
<?php if ($isMarkdown): ?>
<?php
$rawParams = array_merge($currentParams, ['view' => 'raw']);
unset($rawParams['raw']);
$rawUrl = $baseUrl . '?' . http_build_query($rawParams);
$isRawActive = ($viewMode === 'raw');
?>
<a href="<?php echo $rawUrl; ?>" class="security-bar-btn<?php echo $isRawActive ? ' active' : ''; ?>">Raw</a>
<?php
$codeParams = array_merge($currentParams, ['view' => 'code']);
unset($codeParams['raw']);
$codeUrl = $baseUrl . '?' . http_build_query($codeParams);
$isCodeActive = ($viewMode === 'code');
?>
<a href="<?php echo $codeUrl; ?>" class="security-bar-btn<?php echo $isCodeActive ? ' active' : ''; ?>">Code</a>
<?php
$markdownParams = array_merge($currentParams, ['view' => 'default']);
unset($markdownParams['raw']);
$markdownUrl = $baseUrl . '?' . http_build_query($markdownParams);
$isMarkdownActive = ($viewMode === 'default' || $viewMode === 'markdown');
?>
<a href="<?php echo $markdownUrl; ?>" class="security-bar-btn<?php echo $isMarkdownActive ? ' active' : ''; ?>">Markdown</a>
<?php else: ?>
<?php
$rawParams = array_merge($currentParams, ['view' => 'raw']);
unset($rawParams['raw']);
$rawUrl = $baseUrl . '?' . http_build_query($rawParams);
$isRawActive = ($viewMode === 'raw');
?>
<a href="<?php echo $rawUrl; ?>" class="security-bar-btn<?php echo $isRawActive ? ' active' : ''; ?>">Raw</a>
<?php
$codeParams = array_merge($currentParams, ['view' => 'code']);
unset($codeParams['raw']);
$codeUrl = $baseUrl . '?' . http_build_query($codeParams);
$isCodeActive = ($viewMode === 'code' || $viewMode === 'default');
?>
<a href="<?php echo $codeUrl; ?>" class="security-bar-btn<?php echo $isCodeActive ? ' active' : ''; ?>">Code</a>
<?php endif; ?>
</div>
</div>
<?php if ($isMarkdown): ?>
<?php if ($showRaw): ?>
<pre><?php echo htmlspecialchars($fileContent); ?></pre>
<?php elseif ($showMarkdown): ?>
<div class="markdown-content">
<?php echo Markdown::parse($fileContent); ?>
</div>
<?php else: ?>
<?php echo CodeHighlight::render($fileContent, $extension, $filename); ?>
<?php endif; ?>
<?php elseif ($showCode): ?>
<?php echo CodeHighlight::render($fileContent, $extension, $filename); ?>
<?php else: ?>
<pre><?php echo htmlspecialchars($fileContent); ?></pre>
<?php endif; ?>
</body>
</html>
<?php
}
private function handleFolderDownload($fullPath, $folderName) {
global $disableFolderDownloads, $zipCacheDir;
if ($disableFolderDownloads) {
http_response_code(403);
exit('Folder downloads disabled');
}
if (!is_dir($zipCacheDir)) {
mkdir($zipCacheDir, 0755, true);
}
cleanupOldTempFiles();
$tempHash = bin2hex(random_bytes(16));
$tempDir = $zipCacheDir . '/' . $tempHash;
if (copyDirectoryExcludePhp($fullPath, $tempDir)) {
$zipName = $folderName . '.zip';
$zipPath = $zipCacheDir . '/' . $tempHash . '.zip';
$zip = new ZipArchive();
if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
addDirectoryToZip($zip, $tempDir, $folderName);
$zip->close();
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $zipName . '"');
header('Content-Length: ' . filesize($zipPath));
if (ob_get_level()) {
ob_end_clean();
}
readfile($zipPath);
deleteDirectory($tempDir);
unlink($zipPath);
exit;
} else {
deleteDirectory($tempDir);
http_response_code(500);
exit('Cannot create ZIP file');
}
} else {
http_response_code(500);
exit('Cannot copy directory');
}
}
}
?>