Created
January 16, 2025 17:48
a php script for downloading files from web server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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