Skip to content

Instantly share code, notes, and snippets.

@dough10
Created January 16, 2025 17:48
a php script for downloading files from web server
<?php
/**
* get basic auth from headers
*
* @return string|null
*/
function getAuthorizationHeader() {
$headers = null;
if (isset($_SERVER['Authorization'])) {
$headers = trim($_SERVER['Authorization']);
} elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) {
$headers = trim($_SERVER['HTTP_AUTHORIZATION']);
} elseif (function_exists('apache_request_headers')) {
$requestHeaders = apache_request_headers();
$headers = isset($requestHeaders['Authorization']) ? trim($requestHeaders['Authorization']) : null;
}
return $headers;
}
/**
* makes bytes readable by humans
*
* @param mixed $bytes
*
* @return string
*/
function formatFileSize($bytes) {
if ($bytes >= 1073741824) {
$bytes = number_format($bytes / 1073741824, 2) . ' GB';
} elseif ($bytes >= 1048576) {
$bytes = number_format($bytes / 1048576, 2) . ' MB';
} elseif ($bytes >= 1024) {
$bytes = number_format($bytes / 1024, 2) . ' KB';
} else {
$bytes = $bytes . ' B';
}
return $bytes;
}
/**
* updates a pending downloads status
*
* @param array $array
* @param string $searchName
* @param string $newStatus
*
* @return bool
*/
function updateObjectStatus(&$array, $searchName, $newStatus) {
foreach ($array as $index => $object) {
if ($object['name'] === $searchName && $object['status'] === 'pending') {
$array[$index]['status'] = $newStatus;
return true;
}
}
return false;
}
// decode basic auth header for username
if (preg_match('/^Basic\s(.+)$/i', getAuthorizationHeader(), $matches)) {
$base64Credentials = $matches[1];
$credentials = base64_decode($base64Credentials);
list($username, $password) = explode(":", $credentials, 2);
} else {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit();
}
session_start();
// session download data
if (!isset($_SESSION['downloads']) || !is_array($_SESSION['downloads'])) {
$_SESSION['downloads'] = [];
}
//store username in session
$_SESSION['username'] = $username;
// clear basic auth login variables
unset($username);
unset($password);
$dir = __DIR__;
$currentFile = basename(__FILE__);
$files = [];
$allowedExtensions = [
// 'txt',
// 'pdf',
// 'jpg',
// 'png',
// 'zip',
// 'mp3',
// 'flac',
// 'log',
'json'
];
// walk folder structure for files
if ($handle = opendir($dir)) {
while (false !== ($entry = readdir($handle))) {
if ($entry != "." && $entry != ".." && $entry != $currentFile) {
// file is not a child of the directory containing this file
$filePath = realpath($dir . DIRECTORY_SEPARATOR . $entry);
if (strpos($filePath, $dir) !== 0) {
continue;
}
// file is not an allowed type
$fileExtension = pathinfo($entry, PATHINFO_EXTENSION);
if (!in_array($fileExtension, $allowedExtensions)) {
continue;
}
// file is hidden
if (strpos($entry, '.') === 0) {
continue;
}
// build list of files in this directory, that are not hidden and of the correct file extension
if (is_file($filePath)) {
$files[] = [
'name' => $entry,
'path' => basename($filePath),
'size' => filesize($filePath),
'modified' => filemtime($filePath)
];
}
}
}
closedir($handle);
// newest file to the top of list
usort($files, function($a, $b) {
return $b['modified'] - $a['modified'];
});
}
// log status of a download
if (isset($_GET['download'])) {
// get request required
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method Not Allowed']);
exit();
}
// POST request has a filename
if (isset($_POST['file']) && !empty($_POST['file'])) {
// Completed: marked as completed
if (isset($_GET['complete']) && $_GET['complete'] === 'true') {
$updated = updateObjectStatus($_SESSION['downloads'], basename($_POST['file']), 'completed');
if (!$updated) {
http_response_code(500);
echo json_encode(['error' => 'Failed to update file status.']);
exit();
}
echo json_encode($_SESSION['downloads']);
exit();
}
// Completed: user stopped
if (isset($_GET['complete']) && $_GET['complete'] === 'canceled') {
$updated = updateObjectStatus($_SESSION['downloads'], basename($_POST['file']), 'canceled');
if (!$updated) {
http_response_code(500);
echo json_encode(['error' => 'Failed to update file status.']);
exit();
}
echo json_encode($_SESSION['downloads']);
exit();
}
// Completed: failed
if (isset($_GET['complete']) && $_GET['complete'] === 'failed') {
$updated = updateObjectStatus($_SESSION['downloads'], basename($_POST['file']), 'failed');
if (!$updated) {
http_response_code(500);
echo json_encode(['error' => 'Failed to update file status.']);
exit();
}
echo json_encode($_SESSION['downloads']);
exit();
}
// Completed: invalid completed status
if (isset($_GET['complete'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid completed status.']);
exit();
}
$fileNameToCheck = basename($_POST['file']);
$fileExists = !empty(array_filter($files, function($file) use ($fileNameToCheck) {
return strcasecmp($file['name'], $fileNameToCheck) === 0;
}));
// log download attempt **(doesn't matter if file exist or not)**
error_log("Download request: " . $_POST['file'] . " by user: " . $_SESSION['username'] . "@" . $_SERVER['REMOTE_ADDR'] . " User-Agent: " . $_SERVER['HTTP_USER_AGENT']);
// files isn't in array of files
if (!$fileExists) {
http_response_code(400);
echo json_encode(['error' => 'Invalid file parameter.']);
exit();
}
// log file as pending
$file = basename($_POST['file']);
array_push($_SESSION['downloads'], ['name' => $file, 'status' => 'pending']);
echo json_encode($_SESSION['downloads']);
exit();
} else {
// no file given
http_response_code(400);
echo json_encode(['error' => 'File parameter required.']);
exit();
}
}
// reset session
if (isset($_GET['reset'])) {
// post request required
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method Not Allowed']);
exit();
}
// session managment
error_log('User ' . $_SESSION['username'] . ' reset their session at ' . date('Y-m-d H:i:s'));
session_regenerate_id(true);
session_unset();
session_destroy();
echo json_encode(['message' => 'Action completed successfully.']);
exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="data:image/x-icon;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8C////QP///5H///+l////pf///6X///+l////pf///6X///+l////pf///6X///+l////pf///6X///+l////pf///6X///+l////kf///0D///8C////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP///0T////m////////////////////////////////////////////////////////////////////////////////////////////////////5v///0T///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8A////kP/////////8////5f///+L////i////4v///+L////i////4v///+L////i////4v///+L////i////4v///+L////i////5f////z/////////kP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///+l/////////+X///83////G////x3///8d////Hf///x3///8d////Hf///x3///8d////Hf///x3///8d////Hf///xv///83////5f////////+l////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhoaAP///6X/////////4v///x3///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP///x3////i/////////6UBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaGhoA////pf/////////i////Hf///wAAAAAAAAAAAAAAAAAAAAAA////AP///xr///8a////AAAAAAAAAAAAAAAAAAAAAAD///8A////Hf///+L/////////pQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcXFwD///+S////6P///8j///8a////AAAAAAAAAAAAAAAAAP///wD///8h////tv///7b///8h////AAAAAAAAAAAAAAAAAP///wD///8a////yP///+j///+SAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAP///xP///8e////Gv///wP///8AAAAAAAAAAAD///8A////IP///7b//////////////7b///8g////AAAAAAAAAAAA////AP///wP///8a////Hv///xMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP///yD///+2/////////////////////////7b///8g////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8g////tv///////////////////////////////////7b///8g////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8A////IP///7b//////////v////P////+/////v////P////+/////////7b///8g////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP///yL///+2///////////////D////iv////3////9////iv///8P//////////////7b///8i////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8A////XP////b/////////xP///yn///9Z//////////////9Z////Kf///8T/////////9v///1z///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8K////h////7z///8q////AP///1r//////////////1r///8A////Kv///7z///+H////Cv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8N////Gf///wD///8A////Wv//////////////Wv///wD///8A////Gf///w3///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///9a//////////////9a////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP///1r//////////////1r///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8A////Wv//////////////Wv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///9a//////////////9a////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP///1r//////////////1r///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8A////Wv//////////////Wv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///86////pf///6X///86////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///////////////////////////4AAAf+AAAH/gAAB/4AAAf+H/+H/h+fh/4fD4f+HgeH//wD///4Af//8AD//+AAf//gAH//4Qh///MM////D////w////8P////D////w////8P////D////////////////////////////8=">
<title><?php echo htmlspecialchars($_SERVER['HTTP_HOST']) ?></title>
<meta name="description" content="File downloads">
<style>
:root {
--text-color: #333333;
--body-color:rgb(185, 185, 185);
--file-border: 1px solid rgba(161, 161, 161, 0.3);
--card-color: rgb(255, 255, 255);
--focus-border: 1px solid var(--text-color);
}
body {
font-family: 'Roboto', 'Noto', sans-serif;
margin: 0;
padding: 24px;
display: flex;
align-items: center;
flex-direction: column;
height: 100vh;
background-color: var(--body-color);
color: var(--text-color);
user-select: none;
-webkit-user-select: none;
}
h1 {
text-align: center;
font-size: 2em;
display: flex;
align-items: center;
justify-content: center;
}
h1>svg {
height: 72px;
width: 72px;
margin: -24px 8px -24px -22px;
}
svg {
height: 24px;
width: 24px;
}
.file {
border-top: var(--file-border);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
color: inherit;
font-size: inherit;
cursor: pointer;
}
a {
display: inline;
text-decoration: none;
font-size: 1em;
color: inherit;
transition: color 0.3s ease;
text-decoration: underline;
}
a:hover {
color: inherit;
}
.card {
min-width: 360px;
max-width: 600px;
width: 100%;
color: inherit;
background: var(--card-color);
position: relative;
border-radius: 1.25em;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
font-size: 1em;
padding: 0 0 24px 0;
}
.flex-row {
border-top: var(--file-border);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 16px 0 0 0;
}
.margin-right-8 {
margin-right: 8px;
}
.margin-left-4 {
margin-left: 4px;
}
section {
display: flex;
justify-content: center;
}
.button {
appearance: none;
display: inline-flex;
min-width: 5.14em;
margin: 0.29em 0.29em;
color: var(--contrast-color, #ffffff);
background-color: var(--pop-color, #333333);
text-align: center;
text-transform: uppercase;
outline-width: 0;
border-radius: 5px;
padding: 0.7em 0.57em;
cursor: pointer;
position: relative;
box-sizing: border-box;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
pointer-events: all;
justify-content: center;
align-items: center;
transition: background-color var(--animate-150, 150ms) linear;
transform: translate3d(0, 0, 0);
flex-direction: row;
border: none;
overflow: visible;
}
.button:after {
display: inline-block;
width: 100%;
height: 100%;
border-radius: 5px;
opacity: 0;
transition: opacity var(--animate-150, 150ms) cubic-bezier(.33, .17, .85, 1.1);
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12), 0 5px 5px -3px rgba(0, 0, 0, .4);
content: ' ';
position: absolute;
top: 0;
left: 0;
}
.button:hover:after {
opacity: 1;
}
.button:hover:active:after {
opacity: 0;
}
.button[disabled] {
background: rgba(84, 84, 84, 0.4);
color: #ffffff;
box-shadow: none;
cursor: none;
pointer-events: none;
}
.button[disabled]:active,
.button[disabled]:hover,
.button[disabled]:active:hover {
box-shadow: none;
}
.button[noshadow],
.button[noshadow]:active,
.button[noshadow]:hover,
.button[noshadow]:hover:after,
.button[noshadow]:after {
box-shadow: none;
}
.button>* {
pointer-events: none;
}
.button :first-child {
margin-right: 1em;
}
.button :nth-child(2) {
display: flex;
align-items: center;
margin-right: 1em;
}
.small-button {
padding: 0.5em;
cursor: pointer;
position: relative;
border-radius: 50%;
transform: translate3d(0, 0, 0);
border: none;
background: none;
color: inherit;
width: 24px;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.small-button[disabled] {
color: var(--disabled-color);
cursor: not-allowed;
}
.small-button>* {
pointer-events: none;
}
ul {
list-style-type: none;
list-style: none;
list-style-image: none;
padding-left: 0;
margin: 0;
}
li:focus {
outline: var(--focus-border);
}
.to-top {
position: fixed;
bottom: 10px;
left: 50%;
width: auto;
transform: translateX(-50%);
animation: show-it var(--animate-150, 150ms) linear forwards;
transition: transform var(--animate-150, 150ms) linear;
}
.to-top[disabled] {
animation: hide-it var(--animate-150, 150ms) linear forwards;
}
.toast {
background: #323232;
padding: 16px;
display: inline-block;
font-size: 14px;
text-align: left;
position: fixed;
border-radius: 3px;
bottom: 8px;
left: 8px;
color: #fff;
z-index: 10;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
min-width: 200px;
cursor: pointer;
overflow: visible;
opacity: 0;
transition: all var(--animate-150, 150ms) cubic-bezier(.33, .17, .85, 1.1);
transform: translateY(80px) translate3d(0, 0, 0);
}
.toast[opened] {
transform: translateY(0);
opacity: 1;
}
.toast:after {
display: inline-block;
width: 100%;
height: 100%;
border-radius: 5px;
opacity: 0;
transition: opacity var(--animate-150, 150ms) cubic-bezier(.33, .17, .85, 1.1);
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12), 0 5px 5px -3px rgba(0, 0, 0, .4);
content: ' ';
position: absolute;
top: 0;
left: 0;
}
.toast:hover:after {
opacity: 1;
}
.toast>* {
pointer-events: none;
}
strong {
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
}
.toast-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
}
.toast-wrapper>div:first-child {
max-width: 280px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.toast .yellow-text {
color: yellow;
margin-left: 24px;
}
.dl-bg {
background: #323232;
padding: 16px;
display: flex;
flex-direction: column;
font-size: 14px;
text-align: left;
position: fixed;
border-radius: 8px;
top: 8px;
color: #fff;
z-index: 4;
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12), 0 5px 5px -3px rgba(0, 0, 0, .4);
width: 100%;
min-width: 330px;
max-width: 570px;
margin: auto;
left: 50%;
opacity: 0;
transition-property: transform, opacity;
transition-duration: 300ms;
transition-timing-function: linear;
transform: translateY(-120%) translateX(-50%);
}
.dl-bg[open] {
opacity: 1;
transform: translateY(0) translateX(-50%);
}
.dl-wrapper {
width: 100%;
margin: 0 0 16px 0;
}
.dl-info {
display: flex;
justify-content: space-between;
font-size: 0.75em;
}
.bar-wrapper {
position: relative;
height: 12px;
background-color: rgb(185, 185, 185);
margin-bottom: 4px;
border: 1px solid rgb(51 51 51 / 10%);
overflow: hidden;
}
.bar {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #333333;
transform: translateX(-100%);
}
.row {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.margin-t-minus {
margin-top: -20px;
}
footer>nav {
margin-bottom: 16px;
}
dialog {
overflow: hidden;
background: var(--card-color);
color: inherit;
border: none;
padding: 0;
border-radius: 1.25em;
width:100%;
min-width: 360px;
max-width: 600px;
height: 522px;
position: fixed;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
transform: translate3d(0, -300%, 0);
transition: all 250ms linear allow-discrete;
opacity: 0;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
}
dialog[open] {
z-index: 2;
opacity: 1;
transform: translate3d(0, 0, 0);
pointer-events: all;
}
dialog::backdrop {
z-index: 1;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
animation: fade-out 250ms linear forwards;
}
dialog[open]::backdrop {
animation: fade-in 250ms linear forwards;
}
dialog>.small-button.close {
position: absolute;
top: 12px;
right: 24px;
}
dialog>ul {
overflow-y: auto;
overflow-x: hidden;
margin: 0;
width: 100%;
text-align: center;
min-height: 454px;
}
dialog>ul>li {
padding: 8px 24px;
border-top: var(--file-border);
display: flex;
justify-content: space-between;
align-items: center;
}
dialog>ul>li:last-of-type {
border-bottom: var(--file-border);
}
dialog>.button {
position: absolute;
top: 10px;
left: 10px;
background-color: var(--card-color);
color: var(--text-color);
}
.attention {
animation: get-attention 250ms ease-in-out 2;
}
.dialog-attention {
animation: get-dialog-attention 500ms ease-in-out;
}
#hist_but {
position: absolute;
top: 24px;
right: 44px;
}
.spin {
animation: spin 1.5s linear infinite;
}
#uname {
position: absolute;
top: 56px;
left: 157px;
font-size: 0.8em;
}
@keyframes hide-it {
from {
display: block;
opacity: 1
}
to {
display: none;
opacity: 0;
}
}
@keyframes show-it {
from {
display: none;
opacity: 0;
}
to {
display: block;
opacity: 1
}
}
@keyframes fade-out {
from {
opacity: 1
}
to {
opacity: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1
}
}
@keyframes get-attention {
0% {
transform: scale(1);
color: inherit;
}
50% {
transform: scale(1.25);
color: red;
}
100% {
transform: scale(1);
color: inherit;
}
}
@keyframes get-dialog-attention {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
@keyframes spin {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}
@media (prefers-color-scheme: light) {
:root {
--text-color: #333333;
--body-color:rgb(185, 185, 185);
--file-border: 1px solid rgba(161, 161, 161, 0.3);
--card-color: rgb(255, 255, 255);
}
}
@media (prefers-color-scheme: dark) {
:root {
--text-color: #dfdfdf;
--body-color:rgb(22, 22, 22);
--file-border:1px solid hsla(0, 0%, 88%, .1);
--card-color: rgb(58, 58, 58);
}
}
@media (max-width: 600px) {
h1 {
font-size: 1.6em;
}
h1>svg {
height: 36px;
width: 36px;
}
.card {
font-size: 0.8em;
}
.dl-bg {
width: initial;
}
#hist_but {
top: 8px;
right: 28px;
}
dialog {
width: 0;
}
dialog>.button {
top: 15px;
}
strong {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
#uname {
display: none;
}
}
</style>
</head>
<body>
<div
id="dls"
class="dl-bg"></div>
<div
class="card">
<header aria-label="Site header">
<h1>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
fill="currentColor"
aria-hidden="true">
<path
d="M480-313 287-506l43-43 120 120v-371h60v371l120-120 43 43-193 193ZM220-160q-24 0-42-18t-18-42v-143h60v143h520v-143h60v143q0 24-18 42t-42 18H220Z"/>
</svg>
<?php echo $_SERVER['HTTP_HOST'] . "\n"; ?>
</h1>
<span id="uname"><?php echo $_SESSION['username']; ?></span>
<button
aria-label="Open download history"
title="Open download history"
id="hist_but"
class="small-button">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
fill="currentColor">
<path
d="M480-120q-138 0-240.5-91.5T122-440h82q14 104 92.5 172T480-200q117 0 198.5-81.5T760-480q0-117-81.5-198.5T480-760q-69 0-129 32t-101 88h110v80H120v-240h80v94q51-64 124.5-99T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Zm112-192L440-464v-216h80v184l128 128-56 56Z"/>
</svg>
</button>
</header>
<main>
<ul>
<?php
foreach ($files as $file) {
$formattedSize = formatFileSize($file['size']);
$escaped_name = htmlspecialchars($file['name'], ENT_QUOTES, 'UTF-8');
$escaped_size = htmlspecialchars($formattedSize, ENT_QUOTES, 'UTF-8');
$escaped_path = htmlspecialchars($file['path'], ENT_QUOTES, 'UTF-8');
?>
<li
class='file'
tabindex="0"
title='File: <?php echo $escaped_name . ', ' . $escaped_size; ?>'
aria-label='File: <?php echo $escaped_name . ', ' . $escaped_size; ?>'
data-path='<?php echo $escaped_path; ?>'
data-name='<?php echo $escaped_name; ?>'>
<strong><?php echo $escaped_name; ?></strong>
<span>Size: <?php echo $escaped_size; ?></span>
</li>
<?php
}
if (count($files) < 1) {
?>
<li>No files</li>
<?php
}
?>
</ul>
</main>
<footer>
<nav
class="flex-row"
title="Social: github.com/dough10">
<svg
aria-hidden="true"
class="margin-right-8"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100">
<title>GitHub Logo</title>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="currentColor"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/>
</svg>
<a
href="https://github.com/dough10"
rel="noopener noreferrer"
target="_blank"
aria-label="Visit my GitHub profile">
github.com/dough10
</a>
</nav>
<section title="Security contact">
Security contact:
<a
href=".well-known/security.txt"
class="margin-left-4"
target="_blank"
aria-label="Security contact details">
security.txt
</a>
</section>
</footer>
</div>
<dialog
id="history"
aria-label="A log of files downloaded">
<h2>History</h2>
<button
autofocus
aria-label="Close"
title="Close"
class="close small-button">
<svg viewBox="0 0 24 24">
<path
fill="currentColor"
d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
<button
title="Clear download history"
aria-label="Clear download history"
class="button clear"
noshadow>
clear
</button>
<!--
acess denied buzz by Jacco18
https://freesound.org/s/419023/
License: Creative Commons 0
-->
<audio
id="error"
src="data:audio/wav;base64, SUQzAwAAAAAAbVRYWFgAAAAgAAAARW5jb2RlZCBieQBMQU1FIGluIEZMIFN0dWRpbyAxMlRYWFgAAAAbAAAAQlBNIChiZWF0cyBwZXIgbWludXRlKQAxMzBUWUVSAAAABQAAADIwMThURFJDAAAABQAAADIwMTj/+5BEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYaW5nAAAADwAAAA0AADG/ABUVFRUVFRUrKysrKysrKzw8PDw8PDw8Tk5OTk5OTmNjY2NjY2NjdXV1dXV1dXWKioqKioqKnJycnJycnJyxsbGxsbGxscfHx8fHx8fd3d3d3d3d3fLy8vLy8vLy/////////wAAAGRMQU1FMy45OXIE3QAAAAAAAAAANSAkAvZNAAH0AAAxv9ze3U8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/++BEAAAAawDKHQAACAAAD/CgAAEhYgcx+b0SBGLCpb8zokAAAAcD//////////+CDQkQjzUMrwzsX0zFpbTbRm83/CwkTBBiZr8XCAQxIENBGzRBzDzBwNgBpSWdCuxuXtq0Ix48yKHeGDLGcSo4psWElUp97DCBFqNzddNQFQEA8AW8PsbXE1h5InEy7Q8IBwMAizGg895/L7eU5q1IqtGig4ksQ3jff7+/3I7f0jzwxFHfTAWEWo7ambB//X///+Ne3Y5nzuEQXI0x/3/h+5GN/zf/v///1SUkLqWLGH7zoYvKI3R3n0YY6jaPv/////97vW9b7T5//f7vvLvM9PfDkxyJz/yyhtdhiUP3D/4a///v565qxe7zlXDPU1znd5/3WfM/qb1vDvd5f9TWFi7reNQFwQQcB/r9oSzw4UwTATCum4lEjaTSQn+Ak2nAUM0Y87aggCPOscKRf5uthzZzCAov/MoYHjhwRIMV//gYOPAACZNiLNEMWA/D/Axc1KEx4FHNAQagMGBy4n7zt4GCEGIAIKApOBSLOGftDdd4//+5+GB0wGuOIzd4HzjTXNrHk++/3+7fidjy6AwGqdt4Zj6TjBHvglp2Gv///mmJz0s/8JRY038BSuMTlOy99HU//33Du/3+GGbySuXxiMWLGb+QxB0Jryi5Sxtq0N//8/9f/eZdtc+nt////77+9/A9iVfF/wsUlJdjdvOb/mv3jlhZ/9UmqmeeWs7dfdnV/XPpLN3PVar9/lft/dTt23e5T1e48ypeyOxhTYHG1IMhJ2m5e3eZt0ZGMhGNIGkxvR+16wnYcsqWERiiplRh6mRzTDEUvjoFgVVMEFMgIcU1RQlKGXmhwgiyxUrxCUwhMBAsNE4LSS6LcheB3LGUmVlpOeuNyGSO+EIXu2CWqxJIw0tIuQxNnEuTxEJG63V8zzXJfHm9hb6St/Z5v4OiFC3NwIXAzv2IWkCzZu7osrgjr3PrH6Wim3AZdWuu7blT9Woc1OVIafS81OpEX1lsxFbtE/VlrDXbUdfuldxZkMQHDr/Ur8w/Br61XtkzdIEhqXt9Kd2LEJtNFldSXOtuUTOTzulO1Ju/jajGEm79eVwS1+rSxHKQW47clsm79O57lUMU5PU0bjriTdNcvZ2//oV/+Lyb0kPWsUiZgBE0GBGNRrWmfpIIc0lQsGFQOChEdaumnAg8Nqcl414F+GWmhhpjhGBBwx8YEVCVK2mdiBQVS15AyIhEUh1StApXCYHIXbYumQ2V/U8oNj7uufPq308EridtIR9mYolRJCOXKypOUTp9wg+27axIehymk8KZ8/sopbUYl8os6nOO8yLleHqRrfM6ufbfJyAp1+b1PHJXEqZnUzBcn19O9uqn4Z/9bGv/++Bk34AIhWZT/msAAPeMir/N4JAlUaM13a0ABKc0paO1oABZp4VMzT5zFWchihnXYlsWo4aqyu1Tf/cef3+/uKsEfCQSntPfpaaIcvcsNVlUAXpZLpLHrb208B91Lc+1OVr2tWsbpNKF//uGDAABGAAAAAAUNjJwjYhRDOWDMmsA5UtUrGLLA5SgiEYQdBo7MrcFOlWUaNgwwvOB0LENwWINOTKEYcKDhyRKcwVNg1cWnGhgJBAIOGKBEWTtMeOMOMmmJDJkYAo+FgEWBpEGYOiKIAjWVqCMobQKYkYYwatAZBLoJjwYqARUmJLxXWzoLn0zBZExuQQc15lTtrylkUHhC0nBEACH33UvTER9Xe4N+OM5a0TDCwAYm1ta7zPa26vWpNTWw/rRIna7QSJ1W4ue+sIYM5qszyQU+rDZc796L13jZssqCb8zFo23CAk8XYdx3H/fbvI80p+XQyn6eIyqMKdQ1F+wDZp3UtTMxhKsYv9HMu3co5ZqYoXwh+MUM3K5/4Iu1aaZ+ju1KWblcWiFMMAewPmDIAADxio4AqRHsHQgRzBoovIYM2IT6EAYfl0vVVEgSx2UJ3IJqodDRDMWKLdywCAgMyMQ0AwMxBQv8X2GBYa/MsSLUl3TKkA4sbE6aIQNjwseHQCcYJAFnEqzdAnxYwWBgkhSfCgocRjQ0KjTUiSqdFiy7Un0HRgCQMBoO2Z+1qLQSmL1hQ0tqSuWmGWBgqNEI9y2XrXdUeXRplawzyV4yYIM563ET2tqcMkTGiLgwhKSUrEqX4KhSURfa/nv+M4WDWel+mm/jntcT4+/r8XTdyeqNO1PKbspgdLSC2yrIa9Cpts7tVpI/07jDkRkjdbMgbWgZLEX+l8hhy/dpI1G6bfXqw19Z2nrmHXfvuNLA9LDsmjWX/++OpBU5+MzQWj3//b3dM93fXGqalmJlzAEACUnSYMYbD1jFSeQQQHMIAWS2o4aNCkxrdEkU32dSlHKlb9mcsqWoCe1hsOvpPS+mcmNoDqsPnJETnQqK6WlPWHxv0OnvqzgYlM2xWx5BwuEyzhCaPaPzfNaAZODpekDd0Hp12oR0+oWk9eSTKsNYJXpwrvstBkvJJ+v+19xx8re3W6syk3LMldB5krqWxT1YxFrZJR24MkLXuPMXJEVTYTBqTVt51qV2Hrvpi3Fmr5R5dnimQAAAKalImwBORggwYGgG8kGkpllxSJQmBUBJQhEJAzH48w6A3OWHdveUCOa5MlsyetlMUzFpuvYYKzMJxxXcofPbGHQ3NWkw0Hh2W2pYXCeyhwxhMmMqESed50ZkyRrOHbGBE3WECj2xWibP0vMuexjmB1lb125rGtbkgyzX9fe82Sx2mE9oyKj6mYq6xFAP7fe1pyspOb/+9BEqQAFtmhT+yxNoLjNCn9libQYBaFL7LE2gt6zKXmXpbjDlYwZIc90gIJ00bShITzUO07rwV2HrIdq3Ijq+UWVZZljAAAAEnBoYlUA4gVjayZCAOeAxAwGAg5CHTtKAgzlqsgBOmAWnrCOLKK7gYrYn2sS+WTDoU6Vj+zEIbEwRQ0CQtSSkzD4erWYCPZKJigrDrx+hKCoZkweqCjYfRFZwtngg4Tx1Mm7NP9HZNtOGiKKDF0DSSq7AvBWXlCLzJOdJ5i6rs2KUWmxcYXQU7F+/hREo9lOlh1BJKbaadimCG9mleUoUIZYtSnpcYmYWHabTxFik43V5TMrtOUpTKs6Oz0xgAABWPLiG5DMlSgsxygEgKsGCSnqTAF+iERgS2STIeGCmSXDgL1BgocgBTrIE60a8P5ciZQXTIbTilirRCzAUzxOuZ3tGGptnTphMygfTK2EyHIzHKfrAORhlQbnnYD6BYfjp1j5J/pvUUKmiZY+q5Pp0/J7tQ6zjYfjBxEehsUl3tNia6SQ5TDLJIpbU95a1vC9TgpkkN7OF5UDmyzwnClVPBI/lp3XdOPq8lFYeNGACmVqNSJngwAAAAEmPHWzEBFF3oNUIbHjQcMfa4c0ZgUvZyBjHVvrugOD1/vfHH0U2QrBVjYJgjR60QfBCBAS8p03gsDWFLEUVBquI51ScTmA1M6kP9JMRgA7hfleUxyHMzjxE+LZGJPEUxVLSWYCYStEVihHYhRelxOc6OhRk6tXgIx+l2Z2MDJglKQmDayxVxILFOt5WH3FkDaNZhdRuUDiZpojuEAbkou9cPwwyvdojzWJYwhnMnWRR2DF7V7uePzfZ2zCpP5cznU208z0Wx8IFGO1LPNhu2T9ry22pAAAFk2SqmTZF4mzEKx0lGKCgBNlFTxQLACPKhrBG6pfHaa5lmIyMwbSWBZqAcBjG2pC7khAMD+Z0gNRLENLyG+p2wz3yndiLMLU/K6Gew0gu1WRKGpyQ2ifIwupx0hIdpC4hyaaMZqcQhAoa0VmVtG5bAw2BRYSrk6Ti0Q/KDSsyzCD3twLlywwPqGU4lNljCY0sYxFAfWuZ5ES90G7siuOLW5NtK4tXsGL2uh2//vQRMSCJsNsUHMvTODJTQosZel+GpWfQ4yw3oMZs+i5l5p4fjTN9ZDZhV61Hbj1PaeZ+0l4MqBYDzTfS9Vb4QAGIgwswGjrUMkUwGgeOnKaAgk2PDpltLAAqPy/3jR9fFxmSuxekrPBodBLij0/DSJU0WJBxUQfhmC4W4sEjBfFoeMZgdpThIGz8edx1bTcWeIHS1SDoSeGYDZnBLSmb130gKKurFHL+Bb1mPNJHkfF1xpVVWJXtiHZDVrzU4LKNW1CvIzKyFxc6rnO3T19SXIIUTzDEzE8ylXqIK6xryG4QAoPpcWLIF5fWwEYkei2lI538mG7bgd3LRhYMfEN6O56bu9G2SKRHwdIsY7UopUAFhDV3QQABg6IQVG4AI+3xNgwW6SxAQJsltuDl3KCAGXLdpExWwu+ySHYhE2IKvEYOoaCFFgXkJRQLRyVhPjaQBugzEJYpGBTLRlByubCyvoJ2kCDTOhPq1Rp0yShc0ya22JF1S7KadVjLmkkSn48PmGy+NNN5lU1v8qNhaG7d46/HlngVYPH+bz0cMBDjLDJLzOpKWNLYt1ukeQIb7lcbbB/eKwwCSPjdac7sTKvbcL28TfRenx9+pZdu/0rYZE+Sayx2pRTVWpCZoUAAABLJkRi4Jg0wEvNcMMTEi7Mi5wACCwNTJ8QcEW2waCUhkm5IclWtYN0awg05CkAiWJDTvEWXDMfCVLsXUaYuCkgnssnIqxnNFcM/SDEE8XZzXlyp1WpWk9EHijPEZ3igjOu+2lHypg1RO7UUvxMnZaPnORgrvM0rli0WzZuLe0K0NgxnDLK6g1/xWHV7Z3S/i3rDdPnUSj2N4sGA3alrCxlgtqN4EB/b5vCebpeZgzhqh+DKsaiYljUxA1eb/MDd+7a4hJzPqVmJZdBAAAEsWOHXzEDHCyAQbbDq3aLlDQ4d2FBoulQXcoZEwJFrZTH23JxHhdiZHoMJtOddHwTxHIvJ8HScxrDYJgYsVVKliLmGlaKr1iEiEgM4pyDMl1OxphcJ4pojVlXOlhQWdZkXByR4jqGkZNwmqvqpWOWRiiNk0aDhTu8Uw2PKxb/FLSRIj1rldV3m2q1y9iPqUzrVYbpVsUW0f/74ES1gAZkaNH1aeAAzY0aPqy8ACa5mUv5rQAExDMpPzWgANzvlhfwNTY892zFIk8kB/b5tAgbpd48zaE+xDleU8BnjeDa9tfFaw6wIz2hJzPqhXiI7MpWMlQCSbVijls23MGqNeTMiXMOnMBCAHk4rYyIMw2AHCQPbAXg4kc5C8L7zGPzahD/BTZQzCpDAATIhBo0Z8uyAuO7QJDgIDTGZPhY0YQgiqCgyJoWBqTbijrPBwywBh7A5aNAViLBsHeIoHvsz0AglgigZBIXBlANLpFJIpkbdnDXUlWlgwx3n0EhjKAuFWMjgtWieFeLnYvM+DDIowqCYFhDJm5KbqD1WWLsSsZXE5Fef6tcgulpF8t2gGG4Bg19FTSRG9l8rfaQwAzxKxvoZiT5PA7amk3LnZkLryFZTLYjKGsxWIxCbf+bWHW6/SwDNnSZnAMQaRAdqFZUlnV2XtwlsM08GvU3GfjDR5Bcp21ikZgCWUsOwGlCu+JtjhuXPOmo+2LLrcqYmQ//kf/4idWdVrbpFcUMCRMKqtbrv1ANZRYzakxxAdLGRbgNOJJzVplMzdgE+w6aY9Sc1kf9UblGDoxvMJzeYIHw2kUKgzOwQcaM2ARVAxFpIFEgwYjSYsUXWBxAiSpqqqszW4gmQEF0kyXALsuGw8wINdgkCsxkuM/yNKxGRruBRtCMFCkNkJjg1mUtAa7GaB9Va0T2sMtbxvkgC7L7svL1xVYW4qlBsZgxTZmEMJluTHGCM0QGLnVGigzqRSxcjuShxY1nSvrKrTLnDbHA1+BqXbaNMvv9L2CN60tIh/Gfw7JKamZVDucojs/LYcnpZTs9X7DDdGQSqA2i0qUkHvs5z/wxlBr8NNR2ZTALzuO8TCYalDGZBE4ddmRyt0KeUu7HnLft+4/K5uUPxlxwp+s/7P/6P/5Brcze+nNWo3JNwgoyxSomIjzalMxsEUICjABAaAiHGhgYKQglwHlrNKScpzF88rKqEJespqJ3DYwLanjNTPMsHzHZsK1qXVyugxZ2isVMzsjC+iViM0kht0gMdnrCuZt4jw3FVPoDe2vLQNuO40aJCjra4ixH9YLfSBLWO+bN6xnEsRxiR9yPY/fVjYxXNGamcOGdYvAbW6rLDpBYYaxa/cmx9drxI9xWLG8uKfedX1GxDvJeFA3XUbHxWldfNdT/d2vV/E3uUpkSyk28ZMAZ9yAlQWjLFMYRIjQhBKZGMCBAcskvlWhhLWqdMZcvj+PGWKcs6Yy+cG3D1rSVozVlqeny4SZbMJByK6LbbnD1Kjlw/hVzPLiAlm6EzxX7i44z546VfVzVc4tRr1uFDrA25Lzg9tK6UsaBt7HbIUPXziVkkdwoNlTLml32I+ntHKl5K+LlxhfGnvx23//70EStAAYVaNf/ZeAIu+z63+08AVnxoTmMMN6C1bRqvYel/AMfOMO8TS2zjV7bg+B951ubeID9qvZwg5xbGGKur6+c0l3uMOkKe7AAAAENbPCBPwoWQGa4mwv8UHWUyoaKX1QVVWeaSpix9+WAsliUNLJSwBplqqUscWGkS5YIMImnv6z5NaNQfLVfLFf16ouyqXoDYdlFR7og15jKKMEIbT0JjUZfdxWRLil3Mo2/VPELdbOy0llo4mqEfCkW7ACxoubJMRKXmhSPfeofDyWbI0x8crG9TfWrKR4pnqY+SvwXW9Zo5YMaW5+F1oyMCqolssWvclwmN6TkX9Jb/xQVVOaR7HoHaRGmY+d3U2s8lLLx2NO0Phm/nb3adUTikjnH1BsAfkK2EYjG81cDtGuwRASMYApCqzMDfLSmKTTpCUddFHWqHJhTqyrtyQ1jekU02Rxgw1DvcqkbySq1livos8sOKoo2vaFBXBJtvkynVUwsTFfxUSoDN40CrsRLOWaUmgJkADYdch0rspHGyVEitLYprSRJw13al8lSy6RmoyfcvCJM1J8Y1fTy1SVLEWWhz7SL+2RVKVxa2TcFtPIVMWf7tycauLJVjbpWKq6czf2FTOuSy8MOCJjDUMo+GgUOGomUGqUmGDjCsNd4XAbkilVROP1GGcU0KMrk0fLkuWVlgmiTEQBPIhTOatYkUm5/ieReJtBgMyugZbUJUqzBmzbsymQXsnsSef08FcqmMy0w1uNLJ7W/i8K1106jzSbYVS9qxPo2sQ6bfZVtX1o1o0LVfeLBiwavt29tQp30Nlg6i/4ln/k99PdWxG9a7rvcVhi4tbvsfNcbgbtjNf22b///edZg2zsnf7ZVLJJZLx8Y7QDAJAEYMFNmciweUo4ElUMDNKbqXVVanyypFY7U0I8GFhvbUahs5+o1levmRcKtiO5N2TCwxqqytkfNibjUY3U7ipVzFPGDFdQI9kgl6KZuVzi9Q2BG8FSsKedxqrnVnx/RcQZX0lJVdBq9pi+60hPo0a9cYhMLC9qysL3Fe9xChbkfO87trdYMurwL/MXGN0fbtaFGnYtVxuCy4rSvxG8LcF7nHriddM0b7zq7DNn/++BEuoAFzmhSfWXgCMHs+j+svAFohZNF+b2SBTKzKH85sgDXr95+MwRtq0s/u7siAZgLJYJJZUS0gwlmVYhlKuY3gBcVMrHTeF4xMPMofzITgz4SGQgx01MtJTGDs08xMDGyYlM0FjCUUwmXMoFDAQtE4wsFMPFxoXMcKjDghgYEBgMBgUGYIHB4iCAUEGYiKm5h4wEFokII8lsGgtYUhAJaxUBjAcBRVcaEYEH1ElQOsrMJCrhpWMTVG+7KwgDBICCgRExDFK9CNR5fLNm4OXfTDgfGfgB3B0CLJAgAVPAyKTMV7tZavBMGts8D5yjFYjiYLomZG2dDdtW7MNiah6ndPWZO1x0qSkdyUZtmcuB37ld1TeL2IpMDwLQJoSF2lyNidBpa9WnPbStbelsTpwDLoNlEpYIyeclj+SyWMsmYu4l6678bf1YJgfHSZq4bYWU06oGZvFEnUdJtm7wxC9QK7bsQ/dhkVOf//9+qOZpXse6SWI0CM2h42ud7wxKLTBolMtNw4yvTG4/MYA0WKiCc1aXzNxOMWioaEgGNBlMGjKfNBhkwUAwoKzYC4wpLMgIjutwFAQCACsCOHgTOQA1RAHgsw8RMLAjHwZMIuoXUTEacZqTg0dDiIMFBGEOAWxHgBIhBRU9PFgYBF/S/YFDzHRVCQhc7LBY6wSLKbottcL1olg0MMoFi/A6CjQOztv2ItnZ9N2ki36kEMP5MEAAHAxhQ8mSEAj9oOKVNNYG1mEMOZUt+WSd1JdLIYZQ4kMFu01w4AsyctuFwEu2xB2cGHROle19HJdxucDqXvfdderDjuUiltIFAYHFaTxeBQxYR0VqogNNcVubbwY+0pbm/rmOVAE0/FO48xOMsitt5YvP09xJBhDAV7pCQI2VwUv2nuDKOyZk8eibvz0SvOxL8HZf2MQjN+P21vHm72WNEmim5wxEZIUThguZh8cJg5mNBjKBzFi0KR4A1scAEARlLWVYDtcx35hPTZQRTI48U8r+oDuKPDxdOLfAP5Kwtv2VFtRzLD/Kggsr8vbOoLOXdqSMp57ZX7MEKW1MTSNbc41jQmWaK40/h6gacKoa9hSx124SN76dvux03NByhskfcJqVtaW1/HisCqVEaHJTcaRWtUWsHU1WFkqwxtVniuGWh7Pm8PVPmCpX0+NwX7163PYrEvN+Xr5wl0vUzv4+X08UMCMzJveuESqNuScorAboBSAxQEbOMsS/b4QMyAOpDiZE4JECoS11FQz24XMwZW+UxjXTydpIuigKACVMhcNUeyvU7131tkjqA1XkFvYIbCiXNbVTZP3asYmKHuVTsTyFPE3ZqfNrNWDDa43g1mr4ULuLg2MNWSHBb2aNPSM/lbLRqQdQWS7E2QlTFf4k3WmX/+9BErAAGUGbW/2ngCsMNGv/svAEYmaNFzKTcgzGz6LmmI5DzkyQoWb3tIqWaPNmlty2cIddWme07jFpnPvj/bW9fWvaXWKekjcw18j28TETP+PfMPeZI9Fc1OHMQAAAGEnjjQNsZtXeMtEBTBclOcHFslBJqiqlcCMnmWVtjm2UMl+MLTyEQLAKBv20lDdIiJHtygtZVqBVctiWAgObXjSv3Ay5Yjx9nZp5U9KXdluMYrySNMqqM7lw0YFYVRA4WkRE4LCgUIy0VByZ8HK9Mqkion4o1CQXEdnJKm4Vt41pAmPIlirKHv9bNMWF1bL4GcsFHF5CrwQGO/whKzUTlNqhr/18T17RaLJG+hzVwyGVn26p5O/RNsm1xlf+pYQ1Z1EAAABxo+aUkEiQCtTUMgLBzdgICBhBNPAaBszCgVmSuW4JrvDKGGNX1EEqadAS4i8Yu4sQf9oheOKSdwmrspplHVhn8izMa731VPy2GI4zWNwEwvb1qghydlUPMqjrcnFh+eH4Haj7dUuTn4tJBuXKFVXh+KrfhnBZhOXFDzvYl920GLn3/3nrnou4lFWNKOnabcoeH7HZz0QaacTVGJJA4rTYg9RMVMa7+xB549yQ9h3FEyguMcgwiCS+y2OJD4tlNDSWuMr/1KlZFSYQgAAAGDAjklIkSVAtOaj5vUDiAiEDE0Tx1YUBEQYGDYk/iYg53AWst6/HE1CUAuEoMYl5lvzNM1DJEOFcMsgxL0WLikoxfELlU4j0JESlOuUyH2tF+H0oF02KgXdEqdSRFwfEdBs6feOW5GJEJZNvnkBGx7KZ36Lp5FaGC0zhlqxpxpWkR08fY9deBEiPI0aPPTP1NbUadkteHCifDuJM32n3By3TNfg22/Ye/hx8eeXc2ocODb2pbGLwcxIElNSVvTOvrVo2JMbpnO8+AVaL+041oSHmWMAAAJacSKOhzmsQ3ecqVBzjWwMKfSqAhyGEloBkDQ4aRwKOEXM4IE5P0oRpVJ9+tSIljG2wwEwl1cjVQabC/Xr9lOi8kj+ifPk5ElDrAbIp65XSs3g/pU+/ZrRruS+uGtatR7Hl3bfoyYftquSDcwbXWlK/jRJ4igYoG/Fmh//vgRKwABrBqUXVl4ADFDSpvrDwAJ6nXUfm8Egydsyl/N5AAqSE4wXdqxZs6piuWyzBi93mY0LUJ032j1/YHBt1u8bOsxYM+N39Na21x7fMV9abUOHNFZo/gavAm/1q0bFKXpm0fKmNmOJu0KABQBAaFEcj0bJze0owt/CxeYajmJgxgwiasPhhMZQIIJTQBAICzPAcwBOMSuTEM8WGjLA0zFQ2JBgpQ5BhJcUHWS/Bay97ZgQMdExQyLUwLIqRB6kuyU6UBcFEFexQlEFMX1VUp0KzChUw7ENY0gLLYAoqrhxkaFWOk6cDIvMwUNQNQpiz/jQV9L4gJj76q2O08LBmQNKR3UpUxf9ryga/G736VR11nHUfTAbBDjuUtp+26UkCyRntqittUh9rLM2QTL+P/IlMnlYS+VaDIHpX2XfEYAikoophqrlOE8UkuQBehiP9hzKBJfG448EJkcjbm/dO1Wmfx+5e28quQiPYq7gimhqck7Y26zTOofgWBIs6D4MOm4tqNRWGbVLCZU8lybt444Yc5vff///////////9VbNW5plZlrm4ZjJCMkSom1E5LtTh2wEh4AIQRAmPgQgQTExUyUIOcBjPykyYTDB8IUTQSQwgfMbHQhnBKybFZjKvUF0QbIKgggMBDQGAm1SIUGQODhS2g0KgEmUWi166wYQJCgUJu5dpxVKVY2GKLtcgdMR/090eI0suSrtUtoC9DAWltxvR9VdsTOS872s2Y4xyCpdClS1m4PHKtyx91pxp07qm8KWBo2qvDGZK/sarO8+0DVo2yjkH3XHxoaeQNjjbL5HG5FFq7WoefWQSiTO5H59y52UP1ORyILvg52pA2leah2MS+ffjTeSpy32ZlEo09MCu/cppE/MGS+R2m7Tk1jbWa5TyxiMTMdbJALdYxYiz6TbgSi5vO9BOMDTlaWalY87d/4u7d/01mIWGGjOF4iRqYUOb4gDgRnhxZgnSgISw5HBGhxGBOwY4MBkjLwQDCDQOMmVgmycAZSRIjTDygSdQUaQQZU0btGZpIZNmYsIZB0Y0aZAqRCjEpzKDzIRX8AAUy5gGFQqXNUwMcbIErRwElJpoYTMeqiAhBnAIl5xVAHQUUjCBGrsrEA0BG2WEgh1TCDRoQZEIHDwgc1gt2YICIxkOseQkI4AQWtMQiUJsMJILiAhhGlJBI9VRCepamq3Ry1mq7ftmZQDQMLSNykTH04pMz5eb3JyoYLrbk5cvSVnkMndiynmrLjZ6rlndDQUNG3q7lbF0QwsPBcdcV/XfZCsDF4bVWTqXlDj6R9pD1ReQ2JZIqZpLgSqGoEk+EM2LEIjlK8cYnIZl1NZr07tLtgp3YMgSZcSXONLpmWTshh+BquVvm96/DX/////////vgRKQAC06ITQZrQAFxUKnszWgAZKGZTfmtAASMMym/NZAA///xd5JRLKa3/OdpO8/////+////36sLltDLJTfprdurvk1rtiaKJJpqqq6/MNTNH/OXCBRkZSmKEGqsGfFG4kGNGAfINMwoJMEEVoIQghDgQIYwtDYQGHShhwokCNITNQma0DjRiiKwRqkQBJix4zhEIEGDXDQQxZwiLixQSFGUGEBUskGIAQBMoILyg0WElTOCzIhzFCE9wMkR3JTiVYJFmaKM5GFhhgaChiAijxMaM4LHjQgFBcMsQRE2rGSCMjIAjgI+A5KFhCVKgpaBQtAE0EMFuA6jHG6F7AYCLUvWoYn+hPYE3ieTPFM32XAgcpimRiq+FpdcUWZS5amLOi6bGWtytuadaSzNHOFQa9mLL7YcifLpW6dO9MpZerhjLdX7fFq78MmXPHE+nFU6T6eiGKZojJIZwgWAX0dqB1itIfuUP498GPyxG+6lmGZ+fj0ZfLlnN+GfLNgxgq+IYgNrDqs2dp9YEltHWy33Pm/1/P//////////+3YqWMs/5/4f//////////96cn7N3AWFl3eHZsqlWkKCQ2im45JLLdzSnDMFRCDEDg4NAxOwXQBYCZwATKDKHA4QXyMyCKhI0bUDLwCxL2uwnOGGzCxjYukjlg1NkJhgQrPjYADZuDABHAQCKBkQEaIoXgwaPCg4Unui2LBoCHAqpI8mDAKKTPYHHhjus9ybROhe7J3NgOCW0ctIxGuOKZxNnKz2TNbdtylAnUXNKbb3v/FH0X2/8ZZ0vRMqH29bm01xXfj7sSP3+xijzvI6rSGXtxgORQe0hy1iRyBo3Krj81KSKROTvNDj7xui64vYxOvcy6IuI5teS1N3qZ2YTJFoNPo7rkwDYkE1IZXVuWYtB0Irw5SV1iQmCoi9NeWe/j7yinu5zlKg4/X35x6ILa5IGl4vrNuOn/+oCf/vdWVlvMVqM2IyacTbtktu2NmcIW5AfMGZMyaDigs5umuIBBkzQqUjU0xzwEqwp9B0I7DkGBq3DqoY8YFgFraUXaUvDHjPHUpMgU1TBYEkDRMVyLCiogsMXPFiE20l4Ce1gZATPLVLosjUOpW7hxqkRkFTDFSxpzdS/KBStqfEOpGKIxNhslU3YGo5jDrV1DoQo43aed6VrncpTOCpezpdrmPmwVt5fAj3zzjNSdtiU5aZu9s8zBocecuahlmTJ26Sx76ZxrMAQHQQ9XidPD7X4xNYz8azeVDK0+sCslkLhyyC3ioHte2RNIgKpE4J1ZhykgWmj8fjD6OrEY/GJZeZ45MohmNztyPTsTfyfi/ZmQM0k8UlNV9r8UqRuo/9eHk//wEbWZcwAAO+Cl4NlMDEy1DxqO48wAzB//vgZAqCCAxmTqdnIAKuDEr/7TwBGjGjR8y9jcImr2v9pg48fOIZCIenbCZZAKKn6ixFssdQyWEa5BBipCSYNZGBhgxLgcDeFBVeijauGrIela6ExVoghAoQsNDjfwCpmlGZLk8/zT39cpC4cAJyETTLBVucldKSUtS/L7osQDDKn27Oyr1OR32SVm4LBLlQyZfJ4YUaVHGYJanRTUvTtfWHGaMEbCxF/YZltSClxReKyOdnY1D8tpOU8kh6A6Tr9zinDy0UR+M3I1g7b3tLjMVlNe1F4MbVwoxd+XSeU5x+q+GF+rcxpKd/Y7XwtyrPD93pZTfhvKNWu15fQXpuM2aSZ+9y9Uu6x3rVLdCwtD++7a/+5zWKyOS9YdywkqZQAYUsCrJc9b5qmpM/L8kAd+EiRGBBw5wEfupeXq3fuZ1H8xqmtVOoRGExaDqC8ama/STKultndzN66gRW59EQmWkCAu2V1CvMuWZkxqebMa7x/O+xv+sGuq7xFo1QoESPS0f+0GO3Uti+v4ngruLn+1t/0zGtN74rfVL6atzw7VrSm/J4mZ93xn/x6bzTftbOK/+sGLiHfeN/w66xE8FlpGWJYQAWybIA9D9w0CIxRXo5LSqkrGFBEwDWEToJpiYVLsABWmTFJC4tjsYgjZHQzMMQg0qwxkDKRbNkeSOItcgrx+LpctkM+wLt9sDPCQkjQ6DsJ4iEqrkgpl0PcGFmyeZj+RR3QFPLSsRR9exeJwb+ynRY+5q1EJZdIq6zj6wZu0T4V3KvbEkTPLGDtRYfEzKV5fu2Kbw8Vi1r4k1tVt3RUmSHcv8xyo5ed+M0ldsDkz0ctfFzc10sJlJxSt4TRrrucs/reWktPaOKk5kl6em+dM/KdUibcTlaquQxI8hgS5LKFdBwciTCQEuNHE/HpZJBH4TkvbSYn1YmuH+bceSEA1pUvxxkelz0v1c7aQUNbywfmhtd2x7FZ2DLrFdaNZu9pdqeN1v9I2Ky2c2dgZlrMV6kMlv/tiwoNqn/sYilrij6OYCxqXCp7SsyN/0m/n/D9z/HVTivdLpXRViGEAABWxo4czMEAw2GkmQ4doQ02ZRYGeQJFQNVBA+GUNYirRMvYzJzp1kwU8H8vGsXkhLWoC/CJnTHLq4j5KgCsZZPYEGVoNIHM5zUR0M7iRDzejhYXDTpMaKMvL3opiarJ11H7NDRr5DWGZcF6j1bJs5XTE/esC5Chcp1SaUovIqhkYWsTWwm0k3Z/zhusiG0bKGJmNYTTU8pR8lyaV1ZSTeJSbqSJOG3itfLSnnt60dDKH18xM9KVX6VVZuBY9Up3t1GzslSwAAACMlmInRNlphjyGdQFnxkkwi1Kg4Y0wVDm3KA1Nq6PkRcVpTXqaUMmXE6zhRZ//vgRH0CBjNoUPMvTHDFLRoOZYbkF62fO/WXgAL3M+h+tPAAvWsyl9WAo4uRRNZd5kLTG4L+p8HSp60DIBoVMROgtSpuUeclctJIakNQy6K0Q2WmLJ0pNXEbtD67IlFp5oeii6crYpfbZOVKxmLlrCEZFpd6VuBqPtc1MStUaa6iIfvDN7WaibATL1lCrakvZds7OqNLak2oktu8qb52r/HpuRNwuc5aFU35pE1Fxp1Z4h5V2Qf4SYqFAEVIBwgIsCrQImZBQXWNEKH0HwU2HIiMAQhJurEZvK2XQy0R4VUORbVqRa0SgVdK1MbidL43VmAXEmRzIV40kKGPTlte4bVa1H2ynE/mcs7fL6pp2JidWYrX1FlYFmPaymPK2V1S23dH72DGktnOIPraks0Z9n/Gm2aZivCd1pA3W2WKNvFdQt5rWrMhsSG4T63KzbhXpFfbhOLF4W9Rs/4heDXyQtbzFp32cYhR9dhi43mvxauI0VF4zz90qCQS1JscEkCLhkxoVdMOMeSA0ZMMHE0+hkGYIEpkWWUdXqxpEZSo0gJS5fEqPM5at7IotvWE0XF9ezdBO5CoVr2bo506t3sNlaoa6QTa6fSSTqXTrELb1qnsw6itqE/eaKZytuFrH1l7qK3KmNfNGJ3W1JVayxa/yajsUbEbNZKSbgxfGYo77M0Lcs1rPWWkOaem1bh7CviLCtD3nyb1Gz/WtZa/Nf5H0NlanKFiXOurYv3mu7Wzi0VFKXSHzKS6GIBIdovX70cbQMdCzABoAthzY0X1MKFTKSIIITNgQQMZjQ+FQAxM8NrNDb3MOFgMRGAigyPMABMMYOhsABEAAAcdBANhp+N5xBgKZmRRBcIECi6qPqVSTJhyhYBIbDQWLPsyoGgkVDLhhwLOLvBRIwwtUBEYISjoyZwniVNL1FVutFxkgcTMWUQiTNS9U0RSYCyV0vlST7Ok5nWbPEWHA4MXeEYBAakkCBYGCu03FiL+1oaa1MRVorWpnjKHmcJ9VmyyC2fSpW5yWqVbOP819amneM+kjvKdzs/H4dj0ZuUMeeyHJbUicWq75/1qb/3vmUOy2f3IZVF5NLqabt/d5h9N3GrvPu8aDeP/////////97PePf/+8/PmX/hl+9a7VaRMi4wqwJh6bgMKgIW8tCT/lkmSm5u0qhdhdTIc1u9JI2QxFjAIMopox6OguDDD4cEiVGDhJCSHAIlBgHLKmJAaEHQx4DUDUBUFjh4j0A0zhCoBagUFXgdBQysDUgSIyFfqD6kRU4OZPDAsoLUBmBeaU0jWmFDw7RUegMaAQFcQOEKAxClbsidHkvmBId0gHfiseOUYAJBDJkCqkCoyQ2D7Naq0tE5CRThO9G2JGKQq8VDYCnM8ymqmNqAV//uwRPGACchySf5vQAEv7kk/zmQAASgDP9wAACApgGZ7gAAEMZp9lzLmdB02t0dLAjntARuWo9sdcmWNiYekVVwpt441rW3dsJVNeYcxN1pRFodnpDUWBj0CNrGpjlp6cqaVVcJVVx/XxhwZbboMZq3Ul0xTW7lelvfLs8blfuFPcoMe/////vHH/1/87+/7/75zPvM/w3z8udsOCbotWl7GgiXGnhN/+14AAeAAAAAAAcqAAAAAAHgAAH/5ZUxBTUUzLjk5LjNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="
type="audio/wav">
</audio>
<ul>
<?php
if (isset($_SESSION['downloads'])) {
foreach (array_reverse($_SESSION['downloads']) as $dl) {
$escaped_dl = htmlspecialchars($dl['name'], ENT_QUOTES, 'UTF-8');
$escaped_status = htmlspecialchars($dl['status'], ENT_QUOTES, 'UTF-8');
?>
<li>
<strong><?php echo $escaped_dl; ?></strong>
<span><?php echo $escaped_status; ?></span>
</li>
<?php
}
}
?>
</ul>
</dialog>
<button
class="to-top small-button"
disabled title="Scroll to top"
aria-disabled="true">
<svg viewBox="0 0 24 24">
<path
fill="currentColor"
d="M15,20H9V12H4.16L12,4.16L19.84,12H15V20Z"/>
</svg>
</button>
<script>
const lsState = Number(localStorage.getItem('sound'));
let licenseDisplayed = false;
const soundLicense = '<--\nacess denied buzz by Jacco18\nhttps://freesound.org/s/419023/\nLicense: Creative Commons 0\n-->';
let sound = Boolean(lsState);
function toggleSoundFX() {
sound = !sound;
if (sound && !licenseDisplayed) {
licenseDisplayed = true;
console.log(soundLicense);
}
localStorage.setItem('sound', Number(sound));
return `SoundFX: ${sound ? 'On':'Off'}`;
}
(_ => {
/**
* List of files actively being downloaded
*/
let activedownloads = [];
/**
* wait an ammout of time
*
*
* @param {ms} milliseconds
*
* @returns {Promise<Void>} Nothing
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* check if a string appears to be a URL
*
* @param {String} str
*
* @returns {Boolean}
*/
function looksLikeAUrl(str) {
return str.startsWith('http://') || str.startsWith('https://');
}
/**
* toast overflow
*/
const _toastCache = [];
/**
* display a toast message
*
* @param {String} message - text to be displayed in the toast
* @param {Number} _timeout - in seconds || defualt 3.5 seconds ** optional
* @param {String} link - url to go to when toast is clicked
* @param {String} linkText - yellow text
*/
class Toast {
constructor(message, _timeout, link, linkText) {
// push toast to cache if currently displaying a toast
if (document.querySelector('#toast')) {
_toastCache.push([
message,
_timeout,
link,
linkText
]);
return;
}
// bind this to internal functions
this._transitionEnd = this._transitionEnd.bind(this);
this._cleanUp = this._cleanUp.bind(this);
this._clicked = this._clicked.bind(this);
// create the toast
this._timer = false;
this._timeout = _timeout * 1000 || 3500;
this.toast = this._createToast();
if (link && linkText) {
this.toast.append(this._withLink(message, link, linkText));
} else {
this.toast.textContent = message;
}
console.log(message);
document.querySelector('body').append(this.toast);
sleep(25).then(_ => requestAnimationFrame(_ => {
this.toast.toggleAttribute('opened');
}));
}
/**
* returns a new toast html element
*
* @returns {HTMLElement} hot toast
*/
_createToast() {
const toast = document.createElement('div');
toast.id = 'toast';
toast.classList.add('toast');
toast.addEventListener('transitionend', this._transitionEnd, true);
toast.addEventListener('click', this._clicked, true);
return toast;
}
/**
* butter in the toast with some link info
* @param {String} message - text string
* @param {String} link - URL
* @param {String} linkText - text string
*
* @returns {HTMLElement} link wrapper
*/
_withLink(message, link, linkText) {
const mText = document.createElement('div');
mText.textContent = message;
if (typeof link === 'string' && !looksLikeAUrl(link)) {
return mText;
}
const lText = document.createElement('div');
lText.textContent = linkText;
lText.classList.add('yellow-text');
const wrapper = document.createElement('div');
wrapper.classList.add('toast-wrapper');
wrapper.append(mText, lText);
this.link = link;
return wrapper;
}
/**
* event handler for toast click
*/
_clicked(e) {
if (this.link && typeof this.link === 'string' && isValidURL(this.link)) {
window.open(this.link, "_blank");
} else if (this.link && typeof this.link === 'function') {
this.link();
} else if (this.link) {
console.error(`Toast "link" paramater must be a valid URL or function: Value=${this.link}, type=${typeof this.link}`);
}
this._cleanUp();
}
/**
* play closing animation and remove element from document
*/
_cleanUp() {
if (this._timer) {
clearTimeout(this._timer);
this._timer = false;
}
this.toast.addEventListener('transitionend', _ => {
if (this.toast) this.toast.remove();
});
requestAnimationFrame(_ => {
this.toast.removeAttribute('opened');
});
}
/**
* called after opening animation
* sets up closing animation
*/
_transitionEnd() {
this._timer = setTimeout(this._cleanUp, this._timeout);
this.toast.removeEventListener('transitionend', this._transitionEnd);
}
}
/**
* infinite loop to look if cached toast messages to be displayed
*/
setInterval(_ => {
if (!_toastCache.length) {
return;
}
if (document.querySelector('#toast')) {
return;
}
new Toast(
_toastCache[0][0],
_toastCache[0][1],
_toastCache[0][2],
_toastCache[0][3]
);
_toastCache.splice(0, 1);
}, 500);
/**
* creates html entry for download log
*
* @param {Object} dl
* @param {String} dl.name
* @param {String} dl.path
*
* @returns {HTMLElement}
*/
function createLogEntry(dl) {
const name = document.createElement('strong');
name.textContent = dl.name;
const status = document.createElement('span');
status.textContent = dl.status;
const li = document.createElement('li');
li.append(name, status);
return li;
}
/**
* format bytes to be readable by humans
*
* @param {Number} bytes
*
* @returns {String}
*/
function formatBytes(bytes) {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 B';
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
}
/**
* makes a pending file as complete in download logs
*
* @param {String} name
*/
async function logCompleted(name, status) {
const postBody = new FormData();
postBody.append('file', name);
const res = await fetch(`index.php?download=true&complete=${status}`, {
method: 'POST',
body: postBody
});
if (!res.ok) {
new Toast(`Failed updating ${name} completed status`);
return;
}
const updates = await res.json();
updates.reverse();
const html = updates.map(createLogEntry);
document.querySelector('#history>ul').replaceChildren(...html);
}
/**
* opens file save dialog
*
* @param {Bytes} chunks
* @param {String} name
*/
async function cueFileSave(chunks, name) {
const fileBlob = new Blob(chunks);
const link = document.createElement('a');
link.href = URL.createObjectURL(fileBlob);
link.download = name;
link.click();
URL.revokeObjectURL(link.href);
logCompleted(name, true);
}
/**
* cleans up finished download
*
* @param {String} name filename
* @param {HTMLElement} dls download list ui
* @param {HTMLElement} row the current download
*/
function cleanupDownload(name, dls, row) {
if ((dls.querySelectorAll('.row').length) <= 1) {
dls.removeAttribute('open');
}
row.remove();
// remove from active list
const ndx = activedownloads.indexOf(name);
if (ndx !== -1) activedownloads.splice(ndx, 1);
}
/**
* download a file
*
* @param {Object} obj
* @param {String} obj.path
* @param {String} obj.name
*
* @returns {Boolean}
*/
async function download({path, name}) {
const clearButton = document.querySelector('#history>.clear');
clearButton.setAttribute('disabled', true);
const abortController = new AbortController();
const signal = abortController.signal;
new Toast(`Downloading: ${name}`, 1);
let row; // to access it when a download fails
try {
const res = await fetch(path, { signal });
const contentLength = res.headers.get('Content-Length');
if (!res.ok) {
new Toast(`Failed to fetch ${path}`);
return;
}
const startTime = Date.now();
let lastLoadedBytes = 0;
let lastTime = startTime;
const reader = res.body.getReader();
const totalBytes = parseInt(contentLength, 10);
let loadedBytes = 0;
const chunks = [];
const bar = document.createElement('div');
bar.classList.add('bar');
const barWapper = document.createElement('div');
barWapper.classList.add('bar-wrapper');
barWapper.append(bar);
const filename = document.createElement('div');
filename.textContent = name;
const dlSpeed = document.createElement('div');
dlSpeed.textContent = '0B/s';
const dlInfo = document.createElement('div');
dlInfo.classList.add('dl-info');
dlInfo.append(filename, dlSpeed);
const dlWrapper = document.createElement('div');
dlWrapper.title = `downloading: ${name}`;
dlWrapper.classList.add('dl-wrapper');
dlWrapper.append(barWapper, dlInfo);
const svgPath = document.createElementNS("http://www.w3.org/2000/svg", 'path');
svgPath.setAttribute("d", "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z");
svgPath.setAttribute('fill', 'red');
const svg = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
svg.append(svgPath);
svg.setAttribute('viewBox', "0 0 24 24");
const cancelButton = document.createElement('button');
cancelButton.title = 'cancel download';
cancelButton.classList.add('small-button', 'margin-t-minus');
cancelButton.append(svg);
row = document.createElement('div');
row.classList.add('row');
row.append(dlWrapper, cancelButton);
const dls = document.querySelector('#dls');
dls.append(row);
dls.setAttribute('open', true);
cancelButton.addEventListener('click', _ => {
abortController.abort();
document.querySelector('#hist_but>svg').classList.remove('spin');
bar.style.transform = `translateX(-100%)`;
cleanupDownload(name, dls, row);
});
while (true) {
const { done, value } = await reader.read();
const currentTime = Date.now();
const timeElapsed = currentTime - lastTime;
if (done) {
new Toast('Download Complete.', 2);
bar.style.transform = `translateX(-0%)`;
const downloadSpeed = loadedBytes / (timeElapsed / 1000);
const speed = formatBytes(downloadSpeed);
dlSpeed.textContent = `100% @ ${speed}/s`;
await sleep(500);
cleanupDownload(name, dls, row);
break;
}
loadedBytes += value.length;
chunks.push(value);
const progress = (loadedBytes / totalBytes) * 100;
bar.style.transform = `translateX(-${100 - progress}%)`;
if (timeElapsed >= 1000) {
const bytesDownloaded = loadedBytes - lastLoadedBytes;
const downloadSpeed = bytesDownloaded / (timeElapsed / 1000);
const speed = formatBytes(downloadSpeed);
dlSpeed.textContent = `${progress.toFixed(1)}% @ ${speed}/s`;
lastTime = currentTime;
lastLoadedBytes = loadedBytes;
}
}
cueFileSave(chunks, name);
} catch(error) {
if (error.name === 'AbortError') {
new Toast('Download canceled.');
logCompleted(name, 'canceled');
} else {
new Toast(`Failed to fetch ${path}`);
console.error('An error occurred during the fetch:', error);
logCompleted(name, 'failed');
}
await sleep(1000);
cleanupDownload(name, document.querySelector('#dls'), row);
}
clearButton.removeAttribute('disabled');
}
/**
* records a download to the php session
*
* @param {String} file
*
* @returns {Boolean}
*/
async function recordDownload(file) {
const postBody = new FormData();
postBody.append('file', file);
const res = await fetch(`index.php?download=true`, {
method: 'POST',
body: postBody
});
if (!res.ok) return false;
const downloaded = await res.json();
const liList = downloaded.map(createLogEntry);
liList.reverse();
const list = document.querySelector('#history>ul');
list.replaceChildren(...liList);
console.log(`${downloaded.length} download(s) logged for this session`);
return true;
}
window.onload = () => {
const user = '<?php echo "User: " . $_SESSION['username'] ?>';
const session = '<?php echo "Session ID: " . session_id(); ?>';
const previousDownloads = <?php echo json_encode($_SESSION['downloads']); ?>.length;
console.log(`${user}\n${session}\nPrevious downloads: ${previousDownloads}\nTo ${sound ? 'disable':'enable'} soundfx type: toggleSoundFX()`);
if (sound && !licenseDisplayed) {
licenseDisplayed = true;
console.log(soundLicense);
}
// file listing clicked
const files = document.querySelectorAll('.file');
files.forEach(file => {
file.addEventListener('click', async _ => {
document.querySelector('#hist_but>svg').classList.add('spin');
const exists = await recordDownload(file.dataset.name);
if (exists) {
activedownloads.push(file.dataset.name);
await download({...file.dataset});
}
document.querySelector('#hist_but>svg').classList.remove('spin');
});
file.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === 'Space') {
file.click();
}
});
});
// clear button clicked
const clearButton = document.querySelector('#history>.clear');
clearButton.addEventListener('click', async _ => {
const none = document.querySelectorAll('#history>ul>li').length < 1;
if (none) {
new Toast('Nothing to clear.');
return;
}
clearButton.setAttribute('disabled', true);
const res = await fetch('index.php?reset=true', {method: 'POST'});
clearButton.removeAttribute('disabled');
if (!res.ok) {
new Toast('Error: resetting history');
return;
}
const data = await res.json();
new Toast('History cleared.');
const list = document.querySelector('#history>ul');
list.innerHTML = '';
});
// history icon clicked
document.querySelector('#hist_but').addEventListener('click', _ => {
document.querySelector('#history').showModal();
});
// arrow clicked
const toTop = document.querySelector('.to-top');
toTop.addEventListener('click', _ => document.documentElement.scrollTo({
top: 0,
behavior: 'smooth'
}));
// scroll main document
let lastTop = 0;
document.onscroll = () => {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
if (scrollTop < lastTop) {
toTop.setAttribute('disabled', true);
} else if (scrollTop > 0) {
toTop.removeAttribute('disabled');
} else {
toTop.setAttribute('disabled', true);
}
lastTop = scrollTop;
};
// clicked dialog close
document.querySelectorAll('dialog>.close').forEach(button => {
button.addEventListener('click', _ => {
const dialog = button.parentElement;
dialog.close();
});
});
// clickd outsde dialog
const dialogs = document.querySelectorAll('dialog');
dialogs.forEach(dialog => {
dialog.addEventListener('click', event => {
const closeButton = dialog.querySelector('.small-button.close');
const aniend = _ => {
dialog.removeEventListener('animationend', aniend);
closeButton.classList.remove('attention');
dialog.classList.remove('dialog-attention');
};
var rect = dialog.getBoundingClientRect();
var isInDialog = (rect.top <= event.clientY && event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX && event.clientX <= rect.left + rect.width);
if (!isInDialog) {
if (sound) document.querySelector('#error').play();
dialog.addEventListener('animationend', aniend);
closeButton.classList.add('attention');
dialog.classList.add('dialog-attention');
}
});
});
// navigating away from site
window.addEventListener('beforeunload', event => {
if (!activedownloads.length) {
return;
}
// user has active downloads
activedownloads.forEach(dl => logCompleted(dl, 'canceled'));
const message = 'Download(s) active. Are you sure you want to leave?';
event.returnValue = message;
return message;
});
};
})()
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment