Siteβ―Builder
Editing:
site-builder.php
writable 0666
<?php /* SiteβBuilder 1.6 β breadcrumb, modal folder, blankβname guard */ //require_once $_SERVER['DOCUMENT_ROOT'] . '/members/lib/auth.php'; //require_login(); // must be loggedβin //if (current_user()['role'] !== 'admin') { // forbidden_page(); // only admins allowed //} define('SB_TITLE','Siteβ―Builder'); define('SB_MAX_SIZE',4*1024*1024); define('SB_DEF_DIRMODE',0755); define('SB_DEF_FILEMODE',0644); define('SB_BASE',__DIR__); define('MENU_DIR',SB_BASE.'/parts/modules/nav'); if(!function_exists('str_starts_with')){ function str_starts_with($h,$n){return strpos($h,$n)===0;} } ini_set('display_errors',1); error_reporting(E_ALL); /* CSRF ----------------------------------------------------------------- */ /* If auth.php already started the session, the check below is harmless; if not, it starts one for our CSRF logic. */ if (session_status() === PHP_SESSION_NONE) { session_start(); } function tok(){if(!isset($_SESSION['tok']))$_SESSION['tok']=bin2hex(random_bytes(16)); return '<input type="hidden" name="tok" value="'.$_SESSION['tok'].'">';} function tok_ok(){if(($_POST['tok']??'')!==($_SESSION['tok']??'')) exit('Bad CSRF');} /* helpers -------------------------------------------------------------- */ function clean($p){return trim(str_replace(['..','\\'],['','/'],$p),'/');} function full($rel){return rtrim(realpath(SB_BASE),'/').'/'.clean($rel);} function safe($rel){return str_starts_with(full($rel),realpath(SB_BASE));} function menu_list():array{ $out=[]; foreach(glob(MENU_DIR.'/menu-*.php')?:[] as $f) $out[]=preg_replace('/^menu-|\.php$/i','',basename($f)); sort($out); return $out; } function include_line($m){ $p=str_replace(realpath($_SERVER['DOCUMENT_ROOT']),'',realpath(MENU_DIR."/menu-$m.php")); return "<?php include \$_SERVER['DOCUMENT_ROOT'] . '$p'; ?>"; } function detect_ext($t){ if (stripos($t,'<?php')!==false) return 'php'; if (preg_match('/^\s*<\s*!doctype|<html/i',$t)) return 'html'; if (json_decode($t) !== null || trim($t)==='[]') return 'json'; if (preg_match('/^#+\s+\w+/m',$t)) return 'md'; return 'txt'; } /* router ----------------------------------------------------------------*/ $act=$_POST['act']??$_GET['act']??'home'; $rel=clean($_POST['rel']??$_GET['rel']??''); if(!safe($rel)) exit('Path outside base'); switch($act){ case 'newDir': tok_ok(); $d=full($rel); // $rel already includes new folder name if(!is_dir($d)){ mkdir($d,SB_DEF_DIRMODE,true); if(!empty($_POST['writable'])) chmod($d,0777); } break; case 'rmDir': tok_ok(); if(is_dir(full($rel))) @rmdir(full($rel)); break; case 'newFile': case 'saveFile': tok_ok(); $name=$act==='newFile'?trim($_POST['filename']??''):trim($_POST['fname']??''); if($name==='') exit('Filename required'); $data=$_POST['content']??''; if(strlen($data)>SB_MAX_SIZE) exit('Too big'); $ext=strtolower(pathinfo($name,PATHINFO_EXTENSION)); if(!$ext){ $ext=detect_ext($data); $name.='.'.$ext; } if($ext==='php' && ($sel=trim($_POST['menuinclude']??''))){ $inc=include_line($sel); if(strpos($data,$inc)===false){ if(preg_match('/<\/head\s*>/i',$data,$m,PREG_OFFSET_CAPTURE)){ $pos=$m[0][1]+strlen($m[0][0]); $data=substr_replace($data,"\n$inc",$pos,0); }elseif(preg_match('/<body[^>]*>/i',$data,$m,PREG_OFFSET_CAPTURE)){ $pos=$m[0][1]+strlen($m[0][0]); $data=substr_replace($data,"\n$inc",$pos,0); }else $data=$inc.$data; } } $file=full("$rel/$name"); if($act==='newFile'&&file_exists($file)) exit('File exists'); file_put_contents($file,$data); chmod($file,empty($_POST['writable'])?SB_DEF_FILEMODE:0666); if($act==='newFile'){header("Location:?rel=$rel&edit=$name");exit;} break; case 'rmFile': tok_ok(); if(is_file(full($rel)))@unlink(full($rel)); break; } /* ui data ---------------------------------------------------------------*/ $edit=$_GET['edit']??null; $abs =full($rel); $menus=menu_list(); $isPhp=$edit?strtolower(pathinfo($edit,PATHINFO_EXTENSION))==='php':false; $sub =str_replace(realpath(SB_BASE),'',$abs) ?: '/'; $scheme=(!empty($_SERVER['HTTPS'])&&$_SERVER['HTTPS']!=='off')?'https':'http'; $host =$_SERVER['HTTP_HOST']??'localhost'; ?> <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"> <title><?=SB_TITLE?></title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet"> <style> :root{--bg:#101119;--panel:#1c1d27;--accent:#ff4ecd;--accent2:#37e6ff;--fg:#e9e9f2; --radius:12px;--blur:14px;font-family:Inter,system-ui,sans-serif;color:var(--fg);} *{box-sizing:border-box} body{margin:0;background:var(--bg);min-height:100vh;display:flex;flex-direction:column} h1{font-size:1.5rem;margin:0;background:var(--panel);padding:1rem 2rem} main{flex:1;display:grid;grid-template-columns:320px 1fr;gap:2rem;padding:2rem} aside,section{background:var(--panel);border-radius:var(--radius);padding:1.2rem;backdrop-filter:blur(var(--blur))} input,button,select,textarea{font:inherit;border:none;border-radius:6px} input,select{padding:.45rem .6rem;background:#2b2c3b;color:var(--fg)} input::placeholder,textarea::placeholder{color:#8a8aa0} textarea{width:100%;height:64vh;background:#1b1c24;color:var(--fg);padding:1rem; font-family:monospace;border-radius:var(--radius);border:1px solid var(--accent2)} button{cursor:pointer;background:var(--accent);color:#fff;padding:.55rem 1.2rem;margin-top:.6rem} .label-mini{font-size:.85rem;opacity:.75} .disabled{opacity:.5} .dir a{color:var(--accent2);text-decoration:none} breadcrumb a{color:var(--accent2);text-decoration:none;margin-right:.2rem} dialog{border:none;border-radius:var(--radius);background:var(--panel);padding:1.5rem;color:var(--fg); width:300px} dialog::backdrop{background:rgba(0,0,0,.6)} @media(max-width:600px){main{grid-template-columns:1fr}} a { color: aqua; text-decoration: none; transition: color 0.3s ease, text-shadow 0.3s ease; } a:hover { color: #00ffff; /* Brighter aqua on hover */ text-shadow: 0 0 6px rgba(0, 255, 255, 0.7); } </style> <script> function toggleMenuPicker(extId,boxId){ const ext=document.getElementById(extId); const box=document.getElementById(boxId); if(ext&&box) box.style.display = ext.value==='php' ? 'block':'none'; } function openFolderDlg(){ const dlg=document.getElementById('folderDlg'); dlg.showModal(); } function closeFolderDlg(){document.getElementById('folderDlg').close();} function syncRel(inp){ const hidden=document.getElementById('newDirRel'); hidden.value='<?=htmlspecialchars($rel==='.'?'':$rel)?>/'+inp.value; } function checkFilename(e,fieldId){ const v=document.getElementById(fieldId).value.trim(); if(v===''){ alert('Please enter a filename'); e.preventDefault(); } } </script> </head><body onload="toggleMenuPicker('extsel','menuBox')"> <h1><?=SB_TITLE?></h1> <main> <!-- TOOLBOX ------------------------------------------------------------------> <aside> <!-- breadcrumb --> <?php echo '<breadcrumb class="label-mini"><a href="?rel=">current</a>'; if($sub!=='/'){ $accum=''; foreach(array_filter(explode('/',$sub)) as $seg){ $accum .= '/'.$seg; echo '/ <a href="?rel='.rawurlencode(ltrim($accum,'/')).'">'.$seg.'</a>'; } } echo '</breadcrumb>'; ?> <hr style="border:none;border-top:1px solid #333;margin:.8rem 0 1rem"> <h3 style="margin:.3rem 0 1rem">Directory</h3> <div style="max-height:40vh;overflow:auto;font-size:.93rem;margin-bottom:1rem"> <?php foreach(scandir($abs) as $it){ if($it==='.') continue; $p=$rel? "$rel/$it":$it; echo is_dir($abs.'/'.$it) ?"π <a class=\"dir\" href=\"?rel=".rawurlencode($p)."\">$it/</a><br>" :"π <a class=\"dir\" href=\"?rel=".rawurlencode($rel)."&edit=".rawurlencode($it)."\">$it</a><br>"; } ?> </div> <button type="button" onclick="openFolderDlg()">New folder</button><br><br> <a href="/mai-site/parts/nav/nav-builder.php" title="Builder">[Menus]</a> </aside> <!-- WORKSPACE ----------------------------------------------------------------> <section> <?php if(!$edit): ?> <h2 style="margin-top:0">Create a new file</h2> <form method="post" onsubmit="checkFilename(event,'fnfield')"> <?=tok()?> <input type="hidden" name="act" value="newFile"> <input type="hidden" name="rel" value="<?=$rel?>"> <input id="fnfield" name="filename" placeholder="filename (no ext)"> <select id="extsel" name="extsel" onchange="toggleMenuPicker('extsel','menuBox')"> <option>html</option><option selected>php</option><option>txt</option><option>md</option><option>json</option> </select> <label class="label-mini"><input type="checkbox" name="writable"> writable 0666</label><br> <?php if($menus): ?> <div id="menuBox" style="margin-top:.5rem"> <select name="menuinclude"> <option value="">β insert menu β</option> <?php foreach($menus as $m) echo "<option value=\"$m\">$m</option>"; ?> </select> </div> <?php endif; ?> <textarea name="content" placeholder="Paste your pageβ¦"></textarea> <button>Save file</button> </form> <?php else: $file=full("$rel/$edit"); $code=file_get_contents($file); $url=$scheme.'://'.$host.'/'.ltrim(clean($rel.'/'.$edit),'/'); ?> <h2 style="margin-top:0"> Editing: <a href="<?=$edit?>" target="_blank" style="color:var(--accent2)"><?=$edit?></a> </h2> <form method="post"> <?=tok()?> <input type="hidden" name="act" value="saveFile"> <input type="hidden" name="rel" value="<?=$rel?>"> <input type="hidden" name="fname" value="<?=$edit?>"> <label class="label-mini"><input type="checkbox" name="writable" <?=is_writable($file)?'checked':''?>> writable 0666</label> <?php if($menus && $isPhp): ?> <select name="menuinclude" style="margin-left:1rem"> <option value="">β insert menu β</option> <?php foreach($menus as $m){ $inc=include_line($m); $d=strpos($code,$inc)!==false?'disabled class="disabled"':''; echo "<option value=\"$m\" $d>$m".($d?' (added)':'')."</option>"; } ?> </select> <?php endif; ?> <textarea name="content"><?=htmlspecialchars($code)?></textarea> <button>Save changes</button> </form> <?php endif; ?> </section> </main> <!-- FOLDER MODAL -------------------------------------------------------------> <dialog id="folderDlg"> <form method="post"> <?=tok()?> <input type="hidden" name="act" value="newDir"> <input type="hidden" id="newDirRel" name="rel" value="<?=$rel?>/"> <h3 style="margin-top:0">Create folder</h3> <input id="dirName" name="dirname" placeholder="folder-name" oninput="syncRel(this)"> <label class="label-mini" style="display:block;margin-top:.6rem"> <input type="checkbox" name="writable"> writable 0777 </label> <div style="margin-top:1rem;display:flex;gap:.7rem"> <button>Create</button> <button type="button" onclick="closeFolderDlg()" style="background:#666">Cancel</button> </div> </form> </dialog> </body></html>
Save changes
Create folder
writable 0777
Create
Cancel