<?php
$folderPath = isset($_GET['pith']) ? $_GET['pith'] : null;
$rawFile = isset($_GET['file']) ? $_GET['file'] : null;
$rootPath = realpath('pith/source');
$filePath = ($folderPath && $rawFile) ? $folderPath . DIRECTORY_SEPARATOR . $rawFile : $rawFile;
// "hide" source/.htaccess
if ($filePath && basename($filePath) === '.htaccess') {
$filePath = null;
$rawFile = null;
}
// ?pith=
$topFolder = $folderPath ?: basename($rootPath);
if (!$folderPath && $filePath) {
$partsPath = explode(DIRECTORY_SEPARATOR, ltrim($filePath, DIRECTORY_SEPARATOR));
$topFolder = count($partsPath) > 1 ? $partsPath[0] : basename($rootPath);
}
// ?pith=[folder]?file=README.md
if ($folderPath && !$rawFile) {
$readmePath = $folderPath . DIRECTORY_SEPARATOR . 'README.md';
if (is_file($rootPath . DIRECTORY_SEPARATOR . $readmePath)) {
$filePath = $readmePath;
}
}
// download button
if (isset($_GET['download']) && $filePath) {
$safePath = getSafePath($rootPath . DIRECTORY_SEPARATOR . $filePath, $rootPath);
if ($safePath && is_file($safePath)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($safePath) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($safePath));
readfile($safePath);
exit;
}
}
function getSafePath($path, $root) {
$realPath = realpath($path);
if ($realPath && strpos($realPath . DIRECTORY_SEPARATOR, $root . DIRECTORY_SEPARATOR) === 0) {
return $realPath;
}
return null;
}
// tree build
function renderTree($dir, $root, $filePath = null, $folderPath = null, $depth = 0) {
$items = scandir($dir);
usort($items, function ($a, $b) use ($dir) {
$aDir = is_dir($dir . DIRECTORY_SEPARATOR . $a);
$bDir = is_dir($dir . DIRECTORY_SEPARATOR . $b);
return $aDir === $bDir ? strcmp($a, $b) : ($aDir ? 1 : -1);
});
echo $depth === 0 ? "<ul class='pith_tree'>" : "<ul>";
foreach ($items as $item) {
if ($item == "." || $item == ".." || strpos($item, '.') === 0) continue;
$fullPath = $dir . DIRECTORY_SEPARATOR . $item;
$relPath = str_replace($root . DIRECTORY_SEPARATOR, '', $fullPath);
if ($folderPath) {
if ($relPath !== $folderPath && strpos($relPath, $folderPath . DIRECTORY_SEPARATOR) !== 0 && strpos($folderPath, $relPath . DIRECTORY_SEPARATOR) !== 0) {
continue;
}
}
if (is_dir($fullPath)) {
$isOpen = "";
if ($folderPath && ($relPath === $folderPath || strpos($relPath, $folderPath . DIRECTORY_SEPARATOR) === 0 || strpos($folderPath, $relPath . DIRECTORY_SEPARATOR) === 0)) {
$isOpen = "open";
}
echo "<li class='folder'><details $isOpen data-path='" . htmlspecialchars($relPath) . "'><summary style='--depth:$depth'>" . htmlspecialchars($item) . "</summary>";
renderTree($fullPath, $root, $filePath, $folderPath, $depth + 1);
echo "</details></li>";
} else {
$activeClass = ($filePath === $relPath) ? "active-file" : "";
$parts = explode(DIRECTORY_SEPARATOR, $relPath);
$fileTopFolder = count($parts) > 1 ? $parts[0] : null;
if ($fileTopFolder) {
$urlFile = substr($relPath, strlen($fileTopFolder) + 1);
$url = "?pith=" . urlencode($fileTopFolder) . "&file=" . urlencode($urlFile);
} else {
$url = "?pith=&file=" . urlencode($relPath);
}
$fileTime = pith_time_since(filemtime($fullPath));
$fileSize = pith_file_size(filesize($fullPath));
echo "<li class='file $activeClass'><a class='file_row' href='$url' style='--depth:$depth'><span class='file_name'>" . htmlspecialchars($item) . "</span><span class='file_time'>" . $fileTime . "</span><span class='file_size'>" . $fileSize . "</span></a></li>";
}
}
echo "</ul>";
}
function pith_time_since($ts) {
$diff = max(0, time() - $ts);
$units = [31536000 => 'year', 2592000 => 'month', 86400 => 'day', 3600 => 'hour', 60 => 'min'];
foreach ($units as $secs => $unit) {
if ($diff >= $secs) {
$n = (int) floor($diff / $secs);
return $n . ' ' . $unit . ($n > 1 ? 's' : '') . ' ago';
}
}
return 'just now';
}
function pith_file_size($bytes) {
if ($bytes >= 1048576) return number_format($bytes / 1048576, 1) . ' mb';
return number_format($bytes / 1024, 1) . ' kb';
}
?>
<div class="tenon_header">
<div class="tenon_header_td_left">
<span class="title">Pith <?php
if ($folderPath) {
echo "- ";
$is_mobile = function_exists('isMobile') ? isMobile() : false;
$is_logged_in = ($login ?? false) === true;
if ($is_mobile) {
$pith_title_trim = $is_logged_in ? 20 : 45;
} else {
$pith_title_trim = $is_logged_in ? 35 : 45;
}
$safe_pith_title = htmlspecialchars($title ?? '', ENT_QUOTES, 'UTF-8');
echo substr($safe_pith_title, 0, $pith_title_trim) . " ";
}
?></span>
</div>
<div class="tenon_header_td_right">
</div>
</div>
<div class="pith_table">
<div class="pith_td">
<div class='pith_sidebar'>
<?php renderTree($rootPath, $rootPath, $filePath, $folderPath); ?>
</div>
<div class='main_content'>
<div class='pith_code'>
<div class='pith_viewer'>
<div class='pith_viewer_inner'>
<?php
$showCopy = false;
if ($filePath) {
$safePath = getSafePath($rootPath . DIRECTORY_SEPARATOR . $filePath, $rootPath);
if ($safePath && is_file($safePath)) {
$ext = strtolower(pathinfo($safePath, PATHINFO_EXTENSION));
$imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'];
if (in_array($ext, $imageExts)) {
echo "<div class='image_viewport'><img src='pith/source/" . htmlspecialchars($filePath) . "' alt=''></div>";
} else {
echo "<pre><code id='file_content'>" . htmlspecialchars(file_get_contents($safePath)) . "</code></pre>";
$showCopy = true;
}
} else {
echo "<div class='empty_state'>Error: File not found or access denied.</div>";
}
}
?>
</div>
</div>
<?php if ($showCopy): ?>
<button class='copy_btn' id='copyBtn' onclick='copyCode()'>Copy</button>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script src='pith/highlight.min.js'></script>
<script src='pith/highlightjs-line-numbers.js'></script>
<script>
hljs.highlightAll();
hljs.initLineNumbersOnLoad();
function copyCode() {
const codeElement = document.getElementById('file_content');
const btn = document.getElementById('copyBtn');
if (!codeElement) return;
const codeCells = codeElement.querySelectorAll('.hljs-ln-code');
let text = "";
if (codeCells.length > 0) {
text = Array.from(codeCells).map(cell => cell.textContent).join('\n');
} else {
text = codeElement.textContent;
}
navigator.clipboard.writeText(text).then(() => {
const originalText = btn.innerText;
btn.innerText = "Copied";
btn.classList.add('success');
setTimeout(() => {
btn.innerText = originalText;
btn.classList.remove('success');
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
alert("Failed to copy text.");
});
}
</script>
<script>
const detailsElements = document.querySelectorAll('details[data-path]');
detailsElements.forEach(details => {
const path = details.getAttribute('data-path');
if (details.open) {
sessionStorage.setItem('p-' + path, 'open');
}
else if (sessionStorage.getItem('p-' + path) === 'open') {
details.open = true;
}
details.addEventListener('toggle', (e) => {
const path = details.getAttribute('data-path');
if (details.open) {
sessionStorage.setItem('p-' + path, 'open');
} else {
sessionStorage.removeItem('p-' + path);
}
fitViewer();
});
const summary = details.querySelector('summary');
if (summary) {
summary.addEventListener('click', (e) => {
if (!details.open) {
e.preventDefault();
window.location.href = '?pith=' + encodeURIComponent(path);
}
});
}
});
function fitViewer() {
const viewer = document.querySelector('.pith_viewer');
if (!viewer) return;
viewer.style.height = '';
const extra = window.innerHeight - document.body.offsetHeight;
if (extra > 0) {
viewer.style.height = (viewer.offsetHeight + extra) + 'px';
}
}
window.addEventListener('load', () => {
fitViewer();
if (new URLSearchParams(location.search).has('file')) {
document.querySelector('.pith_code')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
window.addEventListener('resize', fitViewer);
</script>