112 lines
6.4 KiB
PHP
Executable file
112 lines
6.4 KiB
PHP
Executable file
<?php
|
|
if (!defined('PORTAL')) { http_response_code(403); exit; }
|
|
require_once __DIR__ . '/common.php';
|
|
|
|
function files_handle(bool $admin, string $token): ?array {
|
|
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST' || !isset($_POST['files_action'])) return null;
|
|
$operation = files_string($_POST['files_action']);
|
|
if (!in_array($operation, ['upload', 'delete'], true)) throw new RuntimeException('Invalid file action.');
|
|
if ($operation === 'delete' && (!$admin || $token === '' || !hash_equals($token, files_string($_POST['csrf'] ?? null)))) {
|
|
throw new RuntimeException('Deletion denied: administrator access and a valid CSRF token are required.');
|
|
}
|
|
$lock = files_lock(LOCK_EX);
|
|
try {
|
|
$store = files_read();
|
|
if ($operation === 'delete') {
|
|
$key = files_string($_POST['files_id'] ?? null);
|
|
if (!preg_match('/^[a-f0-9]{32}$/D', $key) || !isset($store['items'][$key])) throw new RuntimeException('Unknown file.');
|
|
// Remove the actual file first; retain metadata on an unlink failure.
|
|
$path = __DIR__ . '/data/uploads/' . $key . '.bin';
|
|
if (is_file($path) && !@unlink($path)) throw new RuntimeException('Cannot delete stored file.');
|
|
unset($store['items'][$key]);
|
|
files_write($store);
|
|
return ['success', 'File deleted.'];
|
|
}
|
|
if (time() - $store['last_upload'] < FILES_INTERVAL) throw new RuntimeException('Please wait 10 seconds between uploads (shared by all visitors).');
|
|
$upload = $_FILES['files_upload'] ?? null;
|
|
if (!is_array($upload) || !isset($upload['error']) || !is_int($upload['error'])) throw new RuntimeException('Choose one file to upload.');
|
|
if ($upload['error'] !== UPLOAD_ERR_OK) throw new RuntimeException('Upload failed (PHP error ' . $upload['error'] . '). Check upload limits and temporary storage.');
|
|
$temp = files_string($upload['tmp_name'] ?? null);
|
|
if (!is_uploaded_file($temp)) throw new RuntimeException('Invalid uploaded file.');
|
|
$size = filesize($temp);
|
|
if ($size === false || $size < 1 || $size > FILES_MAX_BYTES) throw new RuntimeException('Files must be between 1 byte and 50 MiB.');
|
|
if (count($store['items']) >= FILES_MAX_COUNT) throw new RuntimeException('The 500-file limit has been reached.');
|
|
$used = array_sum(array_column($store['items'], 'size'));
|
|
// Include orphaned files left by interrupted requests in the storage budget.
|
|
$actual = 0;
|
|
foreach (glob(__DIR__ . '/data/uploads/*.bin') ?: [] as $stored) {
|
|
$bytes = filesize($stored);
|
|
if ($bytes === false) throw new RuntimeException('Cannot measure stored files.');
|
|
$actual += $bytes;
|
|
}
|
|
if (max($used, $actual) + $size > FILES_TOTAL_BYTES) throw new RuntimeException('The 1 GiB storage budget would be exceeded.');
|
|
$free = disk_free_space(__DIR__ . '/data');
|
|
if ($free === false || $free < $size + 10 * 1024 * 1024) throw new RuntimeException('Not enough free disk space.');
|
|
$key = bin2hex(random_bytes(16));
|
|
$destination = __DIR__ . '/data/uploads/' . $key . '.bin';
|
|
if (!move_uploaded_file($temp, $destination)) throw new RuntimeException('Cannot store uploaded file.');
|
|
try {
|
|
if (!chmod($destination, 0640)) throw new RuntimeException('Cannot set uploaded file permissions.');
|
|
$store['items'][$key] = ['name' => files_name(files_string($upload['name'] ?? null)), 'size' => $size, 'time' => time()];
|
|
$store['last_upload'] = time();
|
|
files_write($store);
|
|
} catch (Throwable $ex) {
|
|
if (!@unlink($destination)) throw new RuntimeException('Save failed and cleanup also failed. Check data/uploads for an orphan file.');
|
|
throw $ex;
|
|
}
|
|
return ['success', 'Uploaded: ' . $store['items'][$key]['name']];
|
|
} finally { flock($lock, LOCK_UN); fclose($lock); }
|
|
}
|
|
|
|
$filesNotice = null;
|
|
$filesItems = [];
|
|
$filesReady = true;
|
|
try {
|
|
$filesNotice = files_handle((bool)$isAdmin, (string)($csrf ?? ''));
|
|
} catch (Throwable $filesException) {
|
|
$filesNotice = ['danger', $filesException->getMessage()];
|
|
}
|
|
try {
|
|
$filesLock = files_lock(LOCK_SH);
|
|
try { $filesStore = files_read(); $filesItems = $filesStore['items']; }
|
|
finally { flock($filesLock, LOCK_UN); fclose($filesLock); }
|
|
uasort($filesItems, fn($filesLeft, $filesRight) => $filesRight['time'] <=> $filesLeft['time']);
|
|
} catch (Throwable $filesException) {
|
|
$filesReady = false;
|
|
$filesNotice = ['danger', $filesException->getMessage()];
|
|
}
|
|
?>
|
|
<section class="mb-4" aria-labelledby="files-heading">
|
|
<h2 class="h4" id="files-heading">File sharing</h2>
|
|
<?php if ($filesNotice): ?>
|
|
<div class="alert alert-<?= e($filesNotice[0]) ?>" role="alert"><?= e($filesNotice[1]) ?></div>
|
|
<?php endif; ?>
|
|
<p class="text-body-secondary small">Public uploads and downloads. Maximum 50 MiB per file, 500 files, 1 GiB total. One upload every 10 seconds across the site. Downloads are not scanned for malware.</p>
|
|
<?php if ($filesReady): ?>
|
|
<form method="post" enctype="multipart/form-data" class="card card-body mb-3">
|
|
<label for="files-upload" class="form-label">Share a file</label>
|
|
<input type="file" class="form-control mb-2" id="files-upload" name="files_upload" required>
|
|
<div><button type="submit" class="btn btn-accent" name="files_action" value="upload">Upload</button></div>
|
|
</form>
|
|
<?php if (!$filesItems): ?><p class="text-body-secondary">No shared files yet.</p><?php endif; ?>
|
|
<ul class="list-group">
|
|
<?php foreach ($filesItems as $filesKey => $filesItem): ?>
|
|
<li class="list-group-item">
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
|
<div class="text-break">
|
|
<a href="<?= e('files/download.php?files_id=' . rawurlencode($filesKey)) ?>"><?= e($filesItem['name']) ?></a>
|
|
<div class="small text-body-secondary"><?= e(number_format($filesItem['size'] / 1048576, 2)) ?> MiB · <?= e(gmdate('Y-m-d H:i', $filesItem['time'])) ?> UTC</div>
|
|
</div>
|
|
<?php if ($isAdmin): ?>
|
|
<form method="post">
|
|
<input type="hidden" name="csrf" value="<?= e($csrf) ?>">
|
|
<input type="hidden" name="files_id" value="<?= e($filesKey) ?>">
|
|
<button class="btn btn-outline-danger btn-sm" name="files_action" value="delete">Delete</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
</section>
|