🐘
siteEditor.php
Back
📝 Php ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
<?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,.25);border:1px solid rgba(255,224,102,.4)} .match_marker_current{position:absolute;background:rgba(96,165,250,.4);border:1px solid rgba(96,165,250,.9)} .ace_selection{background:rgba(34,211,238,.35)!important} .ace_selected-word{background:rgba(148,163,184,.2)!important} /* Remove Ace's default sprite background */ .ace_fold-widget{background:none!important;border:none!important;font-size:16px!important;width:18px!important;height:18px!important;text-align:center!important;line-height:18px!important;cursor:pointer!important} /* Closed arrow (folded) */ .ace_fold-widget.ace_closed::before{content:"▶";color:#ef4444!important;font-weight:bold} /* Open arrow (unfolded) */ .ace_fold-widget.ace_open::before{content:"▼";color:#ef4444!important;font-weight:bold} /* Hover effect */ .ace_fold-widget:hover::before{color:#dc2626!important;background:rgba(239,68,68,.15)!important;border-radius:3px} .overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:none;align-items:flex-start;justify-content:center;z-index:1000;padding-top:20px} .overlay.open{display:flex} .modal{background:#1e293b;border:1px solid #475569;border-radius:8px;width:90%;max-width:500px;max-height:40vh;display:flex;flex-direction:column} .modal.compact{max-height:auto} .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.hidden{display:none} .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;flex-wrap:wrap} .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="findPasteBtn" onclick="openFindPaste()">Find</button> <select id="modeSwitch" onchange="switchMode()" style="padding:6px;background:#0f172a;color:#e5e7eb;border:1px solid #475569;border-radius:4px;font-size:13px"> <option value="php">PHP</option> <option value="html">HTML</option> <option value="javascript">JavaScript</option> <option value="css">CSS</option> <option value="json">JSON</option> <option value="python">Python</option> <option value="markdown">Markdown</option> <option value="text">Plain Text</option> </select> <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 id="findBtn" onclick="findPasted()">Find</button> <button id="replaceBtn" onclick="replacePasted()" style="display:none">Replace</button> <button id="addBtn" onclick="addPasted()" style="display:none">Add</button> <button onclick="closeFindPaste()">Cancel</button> </div> </div> </div> <script> const ed = ace.edit('editor'); ed.setTheme('ace/theme/monokai'); ed.setOptions({ fontSize:14, showPrintMargin:false, wrap:true, showFoldWidgets:true, foldStyle:'markbegin', enableAutoIndent: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); // Set the mode selector to match the file document.getElementById('modeSwitch').value = mode; function switchMode(){ const selectedMode = document.getElementById('modeSwitch').value; if(selectedMode !== 'text'){ ed.getSession().setMode('ace/mode/' + selectedMode); } else { ed.getSession().setMode('ace/mode/text'); } } // Disable folding entirely - we only want the widgets for selection ed.getSession().setUseWrapMode(true); ed.commands.removeCommand('toggleFoldWidget'); ed.commands.removeCommand('toggleParentFoldWidget'); ed.commands.removeCommand('foldall'); ed.commands.removeCommand('unfoldall'); // Override the fold method to prevent folding const originalAddFold = ed.getSession().addFold; ed.getSession().addFold = function() { return false; }; // --- Intercept fold-widget clicks to select instead of folding --- ed.on('guttermousedown', function(e) { const target = e.domEvent.target; const session = ed.getSession(); const Range = ace.require('ace/range').Range; // Only act on fold widgets (triangles) if (target.classList.contains('ace_fold-widget')) { const row = e.getDocumentPosition().row; const range = session.getFoldWidgetRange(row); // Stop Ace from toggling fold state e.stop(); e.stopPropagation(); e.domEvent.stopPropagation(); e.domEvent.preventDefault(); if (range) { // Select the whole block const extended = new Range( range.start.row, 0, range.end.row, session.getLine(range.end.row).length ); ed.selection.setRange(extended, false); ed.scrollToLine(range.start.row, true, true, () => {}); ed.focus(); } // ✋ Important: return true stops Ace's internal handler return true; } }); 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 let foundFoldRange = null; function openFindPaste(){ document.getElementById('overlay').classList.add('open'); document.getElementById('pasteArea').focus(); document.getElementById('findStatus').textContent = 'Paste code and click Find to locate it'; document.getElementById('replaceBtn').style.display = 'none'; document.getElementById('addBtn').style.display = 'none'; document.getElementById('findBtn').style.display = 'inline-block'; document.querySelector('.modal-body').classList.remove('hidden'); document.querySelector('.modal').classList.remove('compact'); foundFoldRange = null; } function closeFindPaste(){ document.getElementById('overlay').classList.remove('open'); document.querySelector('.modal-body').classList.remove('hidden'); document.querySelector('.modal').classList.remove('compact'); foundFoldRange = null; } 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'; document.getElementById('replaceBtn').style.display = 'none'; document.getElementById('addBtn').style.display = 'inline-block'; document.getElementById('findBtn').style.display = 'none'; foundFoldRange = null; 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, ()=>{}); // Store the range for replacement foundFoldRange = range; // Hide the textarea so user can see the selected code document.querySelector('.modal-body').classList.add('hidden'); document.querySelector('.modal').classList.add('compact'); document.getElementById('findStatus').textContent = `Found at line ${startRow + 1} (${Math.round(fold.matchRatio * 100)}% match, ${lang})`; document.getElementById('replaceBtn').style.display = 'inline-block'; document.getElementById('addBtn').style.display = 'none'; document.getElementById('findBtn').style.display = 'none'; } function replacePasted(){ if(!foundFoldRange){ document.getElementById('findStatus').textContent = 'No code block selected to replace'; return; } const pastedText = document.getElementById('pasteArea').value.trim(); if(!pastedText){ document.getElementById('findStatus').textContent = 'Nothing to replace with'; return; } // Replace the selected range with the pasted text ed.session.replace(foundFoldRange, pastedText); document.getElementById('findStatus').textContent = 'Replaced successfully!'; setTimeout(() => closeFindPaste(), 1500); } function addPasted(){ const pastedText = document.getElementById('pasteArea').value.trim(); if(!pastedText){ document.getElementById('findStatus').textContent = 'Nothing to add'; return; } // Insert at cursor position const pos = ed.getCursorPosition(); ed.session.insert(pos, '\n' + pastedText + '\n'); document.getElementById('findStatus').textContent = 'Added at cursor position!'; setTimeout(() => closeFindPaste(), 1500); } // === TOKEN-AWARE FOLD SELECTION === let lastCursorPos = null; let lastFoldIndex = -1; let cachedFolds = []; // Reset cache when file changes so we never use stale folds ed.getSession().on('change', () => { cachedFolds = []; lastCursorPos = null; lastFoldIndex = -1; }); function selectFold() { const pos = ed.getCursorPosition(); const session = ed.getSession(); const R = ace.require('ace/range').Range; // Recompute folds if cursor row changed OR no cache if (!lastCursorPos || lastCursorPos.row !== pos.row) { cachedFolds = getAllFoldsForRowTokenAware(pos.row); lastFoldIndex = -1; // Reset to start at line selection lastCursorPos = { row: pos.row, column: pos.column }; } // Level -1: First click always selects current line if (lastFoldIndex === -1) { const line = session.getLine(pos.row); const range = new R(pos.row, 0, pos.row, line.length); ed.selection.setRange(range, false); ed.focus(); lastFoldIndex = 0; // Move to first fold next time return; } // Levels 0+: Cycle through folds (innermost to outermost) if (lastFoldIndex < cachedFolds.length) { const fold = cachedFolds[lastFoldIndex]; const range = new R( fold.start, 0, fold.end, session.getLine(fold.end).length ); ed.selection.setRange(range, false); ed.scrollToLine(fold.start, true, true, () => {}); ed.focus(); lastFoldIndex++; // After last fold, prepare to wrap back to line if (lastFoldIndex >= cachedFolds.length) { lastFoldIndex = -1; } } else { // Exhausted all folds, wrap back to line selection const line = session.getLine(pos.row); const range = new R(pos.row, 0, pos.row, line.length); ed.selection.setRange(range, false); ed.focus(); lastFoldIndex = -1; // Reset so next click starts at line again } } /** * Build all {…} folds that actually ENCLOSE the given row, * ignoring braces that occur inside strings/comments/regex. * Uses Ace tokenization for reliability and precise cursor containment. */ function getAllFoldsForRowTokenAware(targetRow) { const session = ed.getSession(); const lineCount = session.getLength(); const stack = []; const allPairs = []; // Helper: ignore comment/string/regex tokens for brace detection const isCodeToken = (type) => !/comment|string|regex/i.test(type); // Build brace pairs with row+col for (let row = 0; row < lineCount; row++) { const tokens = session.getTokens(row); let col = 0; for (const tok of tokens) { const { type, value } = tok; if (isCodeToken(type)) { for (let i = 0; i < value.length; i++) { const ch = value[i]; if (ch === '{') { stack.push({ row, col: col + i }); } else if (ch === '}') { const open = stack.pop(); if (open) { allPairs.push({ startRow: open.row, startCol: open.col, endRow: row, endCol: col + i }); } } } } col += value.length; } } // Cursor precise position const cursor = ed.getCursorPosition(); // {row, column} // Strict containment by (row,col): // - If cursor on start row, must be strictly after '{' // - If cursor on end row, must be strictly before '}' // - If between rows, OK const containsCursor = (p) => { if (cursor.row < p.startRow || cursor.row > p.endRow) return false; if (cursor.row === p.startRow && cursor.column <= p.startCol) return false; if (cursor.row === p.endRow && cursor.column >= p.endCol) return false; return true; }; // Filter: keep only folds that contain the cursor, and are real multi-line folds // (or at least have some room; this avoids "{}" and brace-adjacent noise) const filtered = allPairs.filter((p) => { // Must enclose the cursor by position if (!containsCursor(p)) return false; // Size sanity: prefer multi-line, but allow same-row only if width > 1 const isMultiLine = p.endRow > p.startRow; const sameLineWidth = p.endRow === p.startRow ? (p.endCol - p.startCol) : 0; // Reject tiny/no-content folds if (!isMultiLine && sameLineWidth <= 1) return false; if (isMultiLine && (p.endRow - p.startRow) <= 1) { // For adjacent rows, make sure there's actual content between them const textBetween = session.getTextRange({ start: { row: p.startRow, column: p.startCol + 1 }, end: { row: p.endRow, column: p.endCol } }).trim(); if (!textBetween) return false; } return true; }); // Deduplicate and sort inner → outer by span (rows first, then cols) const key = (p) => `${p.startRow}:${p.startCol}-${p.endRow}:${p.endCol}`; const uniqMap = new Map(); for (const p of filtered) uniqMap.set(key(p), p); const uniq = Array.from(uniqMap.values()); uniq.sort((a, b) => { const spanA = (a.endRow - a.startRow) || 0.0001; // prefer smaller row spans first const spanB = (b.endRow - b.startRow) || 0.0001; if (spanA !== spanB) return spanA - spanB; // tie-break by column span (smaller first) const colSpanA = (a.endRow === a.startRow) ? (a.endCol - a.startCol) : 0; const colSpanB = (b.endRow === b.startRow) ? (b.endCol - b.startCol) : 0; return colSpanA - colSpanB; }); // Return in the shape your selectFold expects: {start, end} rows only // (but they're now guaranteed to be real, enclosing folds) return uniq.map(p => ({ start: p.startRow, end: p.endRow })); } // Search functionality let state={matches:[],idx:-1,markers:[]}; function clear(){ // Clear all markers properly const session = ed.getSession(); state.markers.forEach(id=>{ try{ session.removeMarker(id); }catch(e){} }); state.markers=[]; } function mark(){ clear(); const session = ed.getSession(); 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'; const markerId = session.addMarker(r,cls,'text'); state.markers.push(markerId); }); } 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>