<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
session_start();
if (empty($_SESSION['csrftoken'])) {
$_SESSION['csrftoken'] = bin2hex(random_bytes(16));
}
$CSRF = $_SESSION['csrftoken'];
$file_path = isset($_GET['file']) ? $_GET['file'] : '';
$allowed_dir = realpath('.');
if (empty($file_path)) die("No file specified.");
$file_path = realpath($file_path);
if ($file_path === false || strpos($file_path, $allowed_dir) !== 0) die("Access denied.");
if (!file_exists($file_path)) die("File not found.");
if (!is_file($file_path)) die("Not a file.");
$file_content = file_get_contents($file_path);
$file_extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (($_POST['csrf'] ?? '') !== $CSRF) die("CSRF failed");
if (isset($_POST['action']) && $_POST['action'] === 'save') {
file_put_contents($file_path, $_POST['content'] ?? '');
$_SESSION['flash'] = "Saved!";
header("Location: " . $_SERVER['REQUEST_URI']);
exit;
}
}
$file_name = basename($file_path);
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<title><?= htmlspecialchars($file_name) ?></title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.9/ace.js"></script>
<script src="fold-finder.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden}
body{display:flex;flex-direction:column;background:#0f172a;color:#e5e7eb;font:13px system-ui,sans-serif}
.bar{display:flex;gap:4px;padding:6px;background:#1e293b;border-bottom:1px solid #334155;flex-wrap:wrap}
.bar button,.bar a{padding:6px 10px;background:#0f172a;color:#e5e7eb;border:1px solid #475569;border-radius:4px;font-size:13px;text-decoration:none;cursor:pointer;white-space:nowrap}
.bar button:active{background:#334155}
.spacer{flex:1;min-width:10px}
.find{display:flex;gap:4px;padding:6px;background:#1e293b;border-bottom:1px solid #334155}
.find input{flex:1;padding:6px;background:#0f172a;color:#e5e7eb;border:1px solid #475569;border-radius:4px}
.find button{padding:6px 12px;background:#0f172a;color:#e5e7eb;border:1px solid #475569;border-radius:4px}
.find .count{padding:6px;color:#94a3b8;font-size:12px}
#editor{flex:1;width:100%;height:100%}
.msg{padding:6px;background:#166534;color:#dcfce7;font-size:12px}
.match_marker{position:absolute;background:rgba(255,224,102,.3);border:1px solid rgba(255,224,102,.5)}
.match_marker_current{position:absolute;background:rgba(96,165,250,.3);border:1px solid rgba(96,165,250,.7)}
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:none;align-items:center;justify-content:center;z-index:1000}
.overlay.open{display:flex}
.modal{background:#1e293b;border:1px solid #475569;border-radius:8px;width:90%;max-width:500px;max-height:80vh;display:flex;flex-direction:column}
.modal-head{display:flex;justify-content:space-between;align-items:center;padding:10px;border-bottom:1px solid #475569}
.modal-head h3{font-size:14px;font-weight:600}
.modal-head button{background:transparent;border:none;color:#e5e7eb;font-size:20px;cursor:pointer;padding:0;width:24px;height:24px;line-height:20px}
.modal-body{padding:10px;flex:1;overflow:auto}
.modal-body select{width:100%;padding:8px;background:#0f172a;color:#e5e7eb;border:1px solid #475569;border-radius:4px;margin-bottom:8px;font-size:13px}
.modal-body textarea{width:100%;min-height:200px;background:#0f172a;color:#e5e7eb;border:1px solid #475569;border-radius:4px;padding:8px;font:13px monospace;resize:vertical}
.modal-foot{padding:10px;border-top:1px solid #475569;display:flex;gap:6px;justify-content:flex-end}
.modal-foot button{padding:8px 16px;background:#0f172a;color:#e5e7eb;border:1px solid #475569;border-radius:4px;cursor:pointer}
.modal-foot button:active{background:#334155}
.status{margin-top:8px;padding:6px;background:#0f172a;border:1px solid #475569;border-radius:4px;font-size:12px;color:#94a3b8;min-height:32px}
</style>
</head>
<body>
<?php if (!empty($_SESSION['flash'])): ?>
<div class="msg"><?= htmlspecialchars($_SESSION['flash']); $_SESSION['flash'] = null; ?></div>
<?php endif; ?>
<div class="bar">
<button id="saveBtn" onclick="save()">Save</button>
<button id="selectBtn" onclick="selectFold()">Select Fold</button>
<button id="findPasteBtn" onclick="openFindPaste()">Find</button>
<a href="siteExplorer.php?dir=<?= urlencode(dirname($file_path)) ?>">Back</a>
<div class="spacer"></div>
<span style="font-size:12px;color:#94a3b8"><?= htmlspecialchars($file_name) ?></span>
</div>
<div class="find">
<input id="query" placeholder="Search...">
<button onclick="prev()">◀</button>
<button onclick="next()">▶</button>
<span class="count" id="count"></span>
</div>
<div id="editor"></div>
<form id="form" method="post" style="display:none">
<input type="hidden" name="csrf" value="<?= htmlspecialchars($CSRF) ?>">
<input type="hidden" name="action" value="save">
<input type="hidden" name="content" id="content">
</form>
<div id="overlay" class="overlay">
<div class="modal">
<div class="modal-head">
<h3>Find Pasted Code</h3>
<button onclick="closeFindPaste()">×</button>
</div>
<div class="modal-body">
<select id="langSelect">
<option value="auto">Auto-detect</option>
<option value="js">JavaScript</option>
<option value="php">PHP</option>
<option value="html">HTML</option>
<option value="python">Python</option>
</select>
<textarea id="pasteArea" placeholder="Paste function or code block here..."></textarea>
<div class="status" id="findStatus">Paste code and click Find to locate it</div>
</div>
<div class="modal-foot">
<button onclick="findPasted()">Find</button>
<button onclick="closeFindPaste()">Close</button>
</div>
</div>
</div>
<script>
const ed = ace.edit('editor');
ed.setTheme('ace/theme/monokai');
ed.setOptions({fontSize:14,showPrintMargin:false,wrap:true});
ed.setValue(<?= json_encode($file_content) ?>,-1);
const modes={php:'php',html:'html',css:'css',js:'javascript',json:'json',py:'python',sql:'sql',md:'markdown'};
const mode=modes['<?= $file_extension ?>']||'text';
if(mode!=='text')ed.getSession().setMode('ace/mode/'+mode);
function save(){
document.getElementById('content').value=ed.getValue();
document.getElementById('form').submit();
}
ed.commands.addCommand({
name:'save',
bindKey:{win:'Ctrl-S',mac:'Command-S'},
exec:()=>save()
});
// Find pasted overlay
function openFindPaste(){
document.getElementById('overlay').classList.add('open');
document.getElementById('pasteArea').focus();
document.getElementById('findStatus').textContent = 'Paste code and click Find to locate it';
}
function closeFindPaste(){
document.getElementById('overlay').classList.remove('open');
}
function findPasted(){
const pastedText = document.getElementById('pasteArea').value.trim();
if(!pastedText){
document.getElementById('findStatus').textContent = 'Please paste some code first';
return;
}
let lang = document.getElementById('langSelect').value;
if(lang === 'auto'){
lang = FoldFinder.detectLanguage(pastedText);
}
const fullText = ed.getValue();
const fold = FoldFinder.findFold(fullText, pastedText, lang);
if(!fold){
document.getElementById('findStatus').textContent = 'Could not find matching code block';
return;
}
// Convert character positions to row/col
const beforeStart = fullText.slice(0, fold.start);
const beforeEnd = fullText.slice(0, fold.end);
const startRow = (beforeStart.match(/\n/g) || []).length;
const startCol = fold.start - beforeStart.lastIndexOf('\n') - 1;
const endRow = (beforeEnd.match(/\n/g) || []).length;
const endCol = fold.end - beforeEnd.lastIndexOf('\n') - 1;
// Select the fold
const R = ace.require('ace/range').Range;
const range = new R(startRow, startCol, endRow, endCol);
ed.selection.setRange(range);
ed.scrollToLine(startRow, true, true, ()=>{});
document.getElementById('findStatus').textContent = `Found at line ${startRow + 1} (${Math.round(fold.matchRatio * 100)}% match, ${lang})`;
setTimeout(() => closeFindPaste(), 1500);
}
// Fold selection logic
let lastFoldLevel = -1;
let lastCursorPos = null;
function selectFold(){
const pos = ed.getCursorPosition();
const session = ed.getSession();
const R = ace.require('ace/range').Range;
if(!lastCursorPos || lastCursorPos.row !== pos.row || lastCursorPos.column !== pos.column){
lastFoldLevel = -1;
}
lastCursorPos = {row: pos.row, column: pos.column};
const folds = findFoldsAtCursor(pos.row);
if(folds.length === 0){
const line = session.getLine(pos.row);
ed.selection.setRange(new R(pos.row, 0, pos.row, line.length));
lastFoldLevel = -1;
return;
}
lastFoldLevel++;
if(lastFoldLevel >= folds.length){
lastFoldLevel = 0;
}
const fold = folds[lastFoldLevel];
ed.selection.setRange(new R(fold.start, 0, fold.end, session.getLine(fold.end).length));
ed.scrollToLine(fold.start, true, true, ()=>{});
}
function findFoldsAtCursor(row){
const session = ed.getSession();
const lines = session.getDocument().getAllLines();
const folds = [];
const opens = [
/^\s*function\s+\w+/,
/^\s*(?:const|let|var)\s+\w+\s*=\s*(?:function|\([^)]*\)\s*=>)/,
/^\s*(?:if|while|for|foreach)\s*\(/,
/^\s*class\s+\w+/,
/^\s*def\s+\w+/,
/^\s*<(\w+)[^>]*>/,
/\{\s*$/,
/^\s*switch\s*\(/,
/^\s*try\s*\{/
];
const phpOpen = /^\s*<\?php/;
const phpClose = /^\s*\?>/;
for(let i = 0; i < lines.length; i++){
const line = lines[i];
const indent = line.match(/^\s*/)[0].length;
let isOpen = false;
let tagName = null;
const tagMatch = line.match(/^\s*<(\w+)[^>]*>/);
if(tagMatch && !line.match(/<\/\1>/)){
tagName = tagMatch[1];
isOpen = true;
}
if(phpOpen.test(line)){
isOpen = true;
tagName = 'php';
}
if(!isOpen){
for(let pattern of opens){
if(pattern.test(line)){
isOpen = true;
break;
}
}
}
if(isOpen && i <= row){
let closeRow = -1;
if(tagName === 'php'){
for(let j = i + 1; j < lines.length; j++){
if(phpClose.test(lines[j])){
closeRow = j;
break;
}
}
} else if(tagName){
let depth = 1;
for(let j = i + 1; j < lines.length; j++){
if(lines[j].match(new RegExp('<' + tagName + '\\b'))){
depth++;
}
if(lines[j].match(new RegExp('</' + tagName + '>'))){
depth--;
if(depth === 0){
closeRow = j;
break;
}
}
}
} else {
let depth = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
for(let j = i + 1; j < lines.length && depth > 0; j++){
const jLine = lines[j];
depth += (jLine.match(/\{/g) || []).length;
depth -= (jLine.match(/\}/g) || []).length;
if(depth === 0){
closeRow = j;
break;
}
}
}
if(closeRow > row){
folds.push({start: i, end: closeRow, indent: indent});
}
}
}
folds.sort((a, b) => (a.end - a.start) - (b.end - b.start));
return folds;
}
// Search functionality
let state={matches:[],idx:-1,markers:[]};
function clear(){
state.markers.forEach(id=>ed.getSession().removeMarker(id));
state.markers=[];
}
function mark(){
clear();
const R=ace.require('ace/range').Range;
state.matches.forEach((m,i)=>{
const r=new R(m.r,m.s,m.r,m.e);
const cls=i===state.idx?'match_marker_current':'match_marker';
state.markers.push(ed.getSession().addMarker(r,cls,'text'));
});
}
function go(){
if(state.idx<0||!state.matches.length)return;
const m=state.matches[state.idx];
const R=ace.require('ace/range').Range;
const r=new R(m.r,m.s,m.r,m.e);
ed.selection.setRange(r,false);
ed.scrollToLine(m.r,true,true,()=>{});
mark();
document.getElementById('count').textContent=state.matches.length?`${state.idx+1}/${state.matches.length}`:'';
}
function search(){
const q=document.getElementById('query').value.trim();
if(!q){state={matches:[],idx:-1,markers:[]};clear();document.getElementById('count').textContent='';return;}
const rx=new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),'gi');
const lines=ed.getSession().getDocument().getAllLines();
const matches=[];
for(let r=0;r<lines.length;r++){
let m;rx.lastIndex=0;
while((m=rx.exec(lines[r]))){
matches.push({r:r,s:m.index,e:m.index+m[0].length});
if(m.index===rx.lastIndex)rx.lastIndex++;
}
}
state.matches=matches;
state.idx=matches.length?0:-1;
go();
}
function next(){
if(!state.matches.length)return;
state.idx=(state.idx+1)%state.matches.length;
go();
}
function prev(){
if(!state.matches.length)return;
state.idx=(state.idx-1+state.matches.length)%state.matches.length;
go();
}
document.getElementById('query').addEventListener('input',search);
document.getElementById('query').addEventListener('keydown',e=>{
if(e.key==='Enter'){
e.preventDefault();
e.shiftKey?prev():next();
}
});
// Close overlay on outside click
document.getElementById('overlay').addEventListener('click', (e) => {
if(e.target.id === 'overlay') closeFindPaste();
});
</script>
</body>
</html>