ytkns/public/index.php

1044 lines
35 KiB
PHP
Raw Normal View History

2020-06-10 16:03:13 +00:00
<?php
namespace YTKNS;
use Exception;
require_once __DIR__ . '/../startup.php';
2024-09-05 22:49:42 +00:00
if(!function_exists('base64uri_encode')) {
function base64uri_encode(string $string): string {
return rtrim(strtr(base64_encode($string), '+/', '-_'), '=');
}
}
if(!function_exists('base64uri_decode')) {
function base64uri_decode(string $string, bool $strict = false): string|false {
return base64_decode(str_pad(strtr($string, '-_', '+/'), strlen($string) % 4, '=', STR_PAD_RIGHT));
}
}
2020-06-10 16:03:13 +00:00
function page_url(string $path, array $params = []): string {
if(isset($params['p']))
unset($params['p']);
$mainDomain = Config::get('domain.main');
$url = '';
if($_SERVER['HTTP_HOST'] !== $mainDomain)
$url .= 'https://' . $mainDomain;
$url .= $path;
if(!empty($params))
$url .= '?' . http_build_query($params);
return $url;
}
function html_header(array $vars = []): string {
$vars['title'] ??= 'You\'re the kid now, squid';
$vars['head'] ??= '';
if(!empty($vars['redirect'])) {
$timeout = (int)($vars['redirect_timeout'] ?? 0);
$vars['head'] .= '<meta http-equiv="refresh" content="' . $timeout . '; url=' . $vars['redirect'] . '">';
}
if(!empty($vars['styles']) && is_array($vars['styles'])) {
foreach($vars['styles'] as $style)
$vars['head'] .= sprintf('<link href="%s" type="text/css" rel="stylesheet"/>', $style);
}
$vars['menu_site'] = Template::renderSet('menu-site-item', [
['text' => 'Home', 'link' => page_url('/')],
['text' => 'Create a Zone', 'link' => page_url('/zones/create')],
['text' => 'Zones', 'link' => page_url('/zones')],
2020-10-16 19:24:40 +00:00
['text' => 'Random', 'link' => page_url('/zones/random')],
2020-06-10 16:03:13 +00:00
]);
$userMenu = [];
if(UserSession::hasInstance()) {
$userName = UserSession::instance()->getUser()->getUsername();
$userMenu = [
['text' => "@{$userName}", 'link' => page_url("/@{$userName}")],
['text' => 'My Zones', 'link' => page_url('/zones?f=my')],
['text' => 'Settings', 'link' => page_url('/settings')],
];
$userMenu[] = ['text' => 'Log out', 'link' => page_url('/auth/logout', ['s' => UserSession::instance()->getSmallToken()])];
} else {
$userMenu = [
['text' => 'Log in', 'link' => page_url('/auth/login')],
];
}
$vars['menu_user'] = Template::renderSet('menu-user-item', $userMenu);
2021-07-28 20:52:46 +00:00
$vars['maintenance'] = YTKNS_MAINTENANCE ? Template::renderRaw('maintenance') : '';
2020-06-10 16:03:13 +00:00
return Template::renderRaw('header', $vars);
}
function html_footer(array $vars = []): string {
$vars['footer_took'] = number_format(microtime(true) - YTKNS_STARTUP, 5);
$vars['footer_year'] = date('Y');
$scripts = $vars['scripts'] ?? null;
$vars['scripts'] = '';
if(!empty($scripts) && is_array($scripts)) {
foreach($scripts as $script)
$vars['scripts'] .= sprintf('<script type="text/javascript" charset="utf-8" src="%s"></script>', $script);
}
return Template::renderRaw('footer', $vars);
}
function html_information(string $message, string $title = 'Information', ?string $redirect = null, int $redirectTimeout = 2): string {
$html = html_header([
'title' => $title . ' - YTKNS',
'redirect' => $redirect,
'redirect_timeout' => $redirectTimeout,
]);
if(!empty($redirect))
$message .= Template::renderRaw('information-redirect', [
'info_redirect' => $redirect,
]);
$html .= Template::renderRaw('information', [
'info_title' => $title,
'info_content' => $message,
]);
$html .= html_footer();
return $html;
}
function html_pagination(int $pages, int $current, string $urlFormat): string {
$html = '<div class="pagination">';
for($i = 1; $i <= $pages; $i++)
$html .= sprintf('<a href="%s" class="pagination-page%s">%d</a>', sprintf($urlFormat, $i), ($current === $i ? ' pagination-page-current' : ''), $i);
return $html . '</div>';
}
2021-07-28 20:52:46 +00:00
if(!YTKNS_MAINTENANCE && !empty($_COOKIE['ytkns_login']) && is_string($_COOKIE['ytkns_login'])) {
2020-06-10 16:03:13 +00:00
try {
$session = UserSession::byToken($_COOKIE['ytkns_login']);
$session->update();
$session->setInstance();
if($session->getBump())
setcookie('ytkns_login', $session->getToken(), $session->getExpires(), '/', '.' . Config::get('domain.main'), false, true);
unset($session);
} catch(UserSessionNotFoundException $ex) {}
}
$zoneName = strtolower(substr($_SERVER['HTTP_HOST'], 0, -strlen(Config::get('domain.main')) - 1));
if(!empty($zoneName)) {
$redirect = ZoneRedirect::find($zoneName);
if($redirect !== null) {
$redirect->execute();
return;
}
try {
$zoneInfo = Zone::byName($zoneName);
} catch(ZoneNotFoundException $ex) {
http_response_code(404);
echo html_header(['title' => 'Zone not found!']);
Template::render('zones/none', [
'zone_create_url' => page_url('/zones/create', ['name' => $zoneName]),
]);
echo html_footer();
return;
}
2021-07-28 20:52:46 +00:00
if(!YTKNS_MAINTENANCE) {
if(!empty($_GET['_refresh_screenshot'])) {
header('Location: /');
$zoneInfo->takeScreenshot();
return;
}
2020-06-10 16:03:13 +00:00
2021-07-28 20:52:46 +00:00
ZoneView::increment($zoneInfo, $_SERVER['REMOTE_ADDR']);
}
2020-06-10 16:03:13 +00:00
echo (string)$zoneInfo->getPageBuilder(true);
return;
}
$reqMethod = $_SERVER['REQUEST_METHOD'];
$reqPath = '/' . trim(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
if($reqPath === '/') {
echo html_header();
Template::render('home');
echo html_footer();
return;
}
if(substr($reqPath, 0, 4) === '/ss/') {
header('Content-Type: image/jpeg');
2020-06-10 16:03:13 +00:00
header('X-Accel-Redirect: /assets/no-screenshot.jpg');
return;
}
if(preg_match('#^/@([A-Za-z0-9-_]+)$#', $reqPath, $matches)) {
try {
$profile = User::forProfile($matches[1]);
} catch(Exception $ex) {
http_response_code(404);
echo html_header(['title' => 'User not found - YTKNS']);
Template::render('profile/notfound');
echo html_footer();
return;
}
$zones = Zone::byUser($profile, 'zone_views', false);
if(count($zones) < 1) {
$profileZones = Template::renderRaw('profile/zone-none');
} else {
$profileZones = [];
foreach($zones as $zone)
$profileZones[] = [
'zone_id' => $zone->getId(),
'zone_name' => $zone->getName(),
'zone_title' => $zone->getTitle(),
'zone_views' => number_format($zone->getViews()),
'zone_url' => $zone->getUrl(),
'zone_screenshot' => $zone->getScreenshotUrl(),
];
$profileZones = Template::renderRaw('profile/zone-list', [
'zone_items' => Template::renderSet('profile/zone-item', $profileZones),
]);
}
echo html_header(['title' => $profile->username . ' @ YTKNS']);
Template::render('profile/index', [
'profile_username' => $profile->username,
'profile_zones' => $profileZones,
]);
echo html_footer();
return;
}
if($reqPath === '/zones') {
$zoneFilter = filter_input(INPUT_GET, 'f');
if($zoneFilter === 'my' && !UserSession::hasInstance()) {
echo html_information('You must be logged in to do this.');
return;
}
$zoneTake = 20;
$zonePage = max(filter_input(INPUT_GET, 'page', FILTER_SANITIZE_NUMBER_INT) - 1, 0);
$zoneOffset = $zonePage * $zoneTake;
$zoneSort = filter_input(INPUT_GET, 'sort');
$zoneSortDesc = filter_input(INPUT_GET, 'asc', FILTER_SANITIZE_NUMBER_INT) < 1;
$zoneOrderings = [
'creation' => 'zone_created',
'updated' => 'zone_updated',
'views' => 'zone_views',
'user' => 'user_id',
];
if(!array_key_exists($zoneSort, $zoneOrderings)) {
switch($zoneFilter) {
case 'my':
$zoneSort = 'creation';
break;
default:
$zoneSort = 'views';
$zoneSortDesc = false;
break;
}
}
switch($zoneFilter) {
case 'my':
$zones = Zone::byUser(UserSession::instance()->getUser(), $zoneOrderings[$zoneSort], $zoneSortDesc, $zoneTake, $zoneOffset);
$zoneListTemplate = 'zones/my-item';
$zoneListTitle = 'My Zones';
break;
default:
$zones = Zone::all($zoneOrderings[$zoneSort], $zoneSortDesc, $zoneTake, $zoneOffset);
$zoneListTemplate = 'zones/item';
$zoneListTitle = 'Zones';
$zoneFilter = '';
break;
}
$zoneCount = Zone::count();
$zonePages = ceil($zoneCount / $zoneTake);
$zoneList = [];
foreach($zones as $zone)
$zoneList[] = [
'zone_id' => $zone->getId(),
'zone_name' => $zone->getName(),
'zone_title' => $zone->getTitle(),
'zone_views' => number_format($zone->getViews()),
'zone_url' => $zone->getUrl(),
'zone_edit_url' => page_url(sprintf('/zones/%d', $zone->getId())),
'zone_delete_url' => page_url(sprintf('/zones/%d/delete', $zone->getId())),
'zone_screenshot' => $zone->getScreenshotUrl(),
'zone_user_url' => page_url('/@' . $zone->getUser()->getUsername()),
'zone_user_name' => $zone->getUser()->getUsername(),
];
$zoneSortings = '';
foreach(array_keys($zoneOrderings) as $order) {
$orderCurrent = $zoneSort === $order;
$zoneSortings .= sprintf(
'<a href="?f=%6$s&sort=%1$s&asc=%2$d" class="zones-sorts-sort%4$s">%3$s%5$s</a>',
$order, $orderCurrent ? $zoneSortDesc : !$zoneSortDesc, ucfirst($order),
$orderCurrent ? ' zones-sorts-current' : '',
$orderCurrent ? sprintf(
' <img src="/assets/arrow_%s.png" alt="%s"/>',
$zoneSortDesc ? 'down' : 'up',
$zoneSortDesc ? 'Descending' : 'Ascending'
) : '', $zoneFilter
);
}
echo html_header(['title' => $zoneListTitle . ' - YTKNS']);
Template::render('zones/list', [
'zone_list_title' => $zoneListTitle,
'zone_list' => Template::renderSet($zoneListTemplate, $zoneList),
'zone_sortings' => $zoneSortings,
]);
echo html_pagination($zonePages, $zonePage + 1, sprintf('/zones?f=%s&sort=%s&asc=%d&page=%%s', $zoneFilter, $zoneSort, !$zoneSortDesc));
echo html_footer();
return;
}
2020-10-16 19:24:40 +00:00
if($reqPath === '/zones/random') {
$zoneInfo = Zone::byRandom();
header('Location: ' . $zoneInfo->getUrl());
return;
}
2020-06-10 16:03:13 +00:00
if($reqPath === '/zones/create') {
if(!UserSession::hasInstance()) {
http_response_code(403);
echo html_information('You must be logged in to do this.');
return;
}
$createToken = UserSession::instance()->getSmallToken(4);
if($reqMethod === 'POST') {
$zoneToken = filter_input(INPUT_POST, 'zone_token');
$zoneName = filter_input(INPUT_POST, 'zone_subdomain');
$zoneTitle = filter_input(INPUT_POST, 'zone_title');
if($zoneToken !== $createToken) {
$createError = 'Invalid request.';
} else {
if(empty($zoneName) || empty($zoneTitle)) {
$createError = 'Please fill in all fields.';
} else {
if(!Zone::validName($zoneName)) {
$createError = 'Name contains invalid characters or is too long.';
} elseif(ZoneRedirect::exists($zoneName) || Zone::exists($zoneName)) {
$createError = 'A Zone with this name already exists.';
} elseif(!ctype_alpha($zoneName)
|| mb_strlen($zoneTitle) > 255) {
$createError = 'Invalid data.';
} else {
$createZone = Zone::create(UserSession::instance()->getUser(), $zoneName, $zoneTitle);
$createZone->addEffect(new \YTKNS\Effects\NewlyCreatedPageEffect);
echo html_information('Zone created!', 'Information - YTKNS', "/zones/{$createZone->getId()}");
return;
}
}
}
} elseif($reqMethod === 'GET') {
$zoneName = filter_input(INPUT_GET, 'name');
}
if(isset($createError)) {
$createError = Template::renderRaw('error', [
'error_text' => $createError,
]);
}
echo html_header(['title' => 'Create a Zone - YTKNS']);
Template::render('zones/create', [
'create_action' => page_url('/zones/create'),
'create_domain' => Config::get('domain.main'),
'create_token' => $createToken,
'create_error' => $createError ?? '',
'create_subdomain' => $zoneName ?? '',
'create_title' => $zoneTitle ?? '',
]);
echo html_footer();
return;
}
if($reqPath === '/zones/_effects') {
header('Content-Type: application/json; charset=utf-8');
if(!UserSession::hasInstance()) {
http_response_code(403);
echo json_encode([
'err' => 'You must be logged in.',
]);
return;
}
$effects = [];
foreach(SITE_EFFECTS as $effectClass) {
$instance = new $effectClass;
$effects[] = [
'type' => trim(substr($effectClass, 14, -6), '\\'),
'name' => $instance->getEffectName(),
'props' => $instance->getEffectProperties(),
];
}
echo json_encode($effects);
return;
}
if($reqPath === '/zones/_preview') {
if(!UserSession::hasInstance()) {
http_response_code(403);
echo html_information('You must be logged in to do this.');
return;
}
if($reqMethod !== 'POST') {
http_response_code(405);
echo html_information('Must be a POST request.');
return;
}
$zoneToken = filter_input(INPUT_POST, 'zone_token');
if($zoneToken !== UserSession::instance()->getSmallToken(10)) {
http_response_code(403);
echo html_information('Invalid token.');
return;
}
$zoneTitle = filter_input(INPUT_POST, 'zone_title');
$zoneEffects = filter_input(INPUT_POST, 'zone_effect', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
try {
$zoneInfo = new Zone;
$zoneInfo->setTitle($zoneTitle);
$zoneInfo->setPassiveMode();
foreach($zoneEffects as $effectName => $effectValues) {
$effectInfo = ZoneEffect::effectClass($effectName);
$effectInfo->setEffectParams($effectValues);
$zoneInfo->addEffect($effectInfo);
}
} catch(ZoneInvalidTitleException $ex) {
http_response_code(400);
echo html_information('Invalid title.');
return;
} catch(ZoneEffectClassNotFoundException $ex) {
http_response_code(400);
echo html_information(sprintf('Invalid effect name: %s.', $ex->getMessage()));
return;
} catch(PageEffectException $ex) {
http_response_code(400);
echo html_information(sprintf('%s: %s', $effectName ?? '', $ex->getMessage()));
return;
} catch(Exception $ex) {
http_response_code(500);
echo html_information(sprintf('Failed to generate preview.<br/>%s: %s', get_class($ex), $ex->getMessage()));
return;
}
echo (string)$zoneInfo->getPageBuilder();
return;
}
if(preg_match('#^/zones/([0-9]+)/delete$#', $reqPath, $matches)) {
if(!UserSession::hasInstance()) {
http_response_code(403);
echo html_information('You must be logged in to do this.');
return;
}
try {
$zoneInfo = Zone::byId($matches[1]);
} catch(ZoneNotFoundException $ex) {
http_response_code(404);
echo html_information('This zone doesn\'t exist.');
return;
}
if($zoneInfo->getUserId() !== UserSession::instance()->getUserId()) {
http_response_code(403);
echo html_information('You aren\'t allowed to touch this zone.');
return;
}
$deleteToken = UserSession::instance()->getSmallToken(20);
if($reqMethod === 'POST') {
if(filter_input(INPUT_POST, 'zone_token') !== $deleteToken) {
http_response_code(403);
echo html_information('Invalid token.');
return;
}
header('Location: /zones?f=my');
$zoneInfo->delete();
return;
}
echo html_header(['title' => sprintf('Deleting zone %s - YTKNS', $zoneInfo->getName())]);
Template::render('zones/delete', [
'delete_zone_id' => $zoneInfo->getId(),
'delete_zone_name' => $zoneInfo->getName(),
'delete_zone_token' => $deleteToken,
]);
echo html_footer();
return;
}
2020-10-16 19:24:40 +00:00
if(preg_match('#^/zones/([0-9]+)/sbs$#', $reqPath, $matches)) {
echo '<!doctype html>';
echo '<meta charset="utf-8"/><title>YTKNS Side By Side Editor</title>';
echo '<iframe name="preview" style="position: absolute; top: 0; left: 0; bottom: 0; height: 100%; width: 70%; border-width: 0;"></iframe>';
echo '<iframe name="editor" src="/zones/' . $matches[1] . '" style="position: absolute; top: 0; right: 0; bottom: 0; height: 100%; width: 30%; border-width: 0;"></iframe>';
return;
}
2020-06-10 16:03:13 +00:00
if(preg_match('#^/zones/([0-9]+)$#', $reqPath, $matches)) {
if(!UserSession::hasInstance()) {
http_response_code(403);
echo html_information('You must be logged in to do this.');
return;
}
try {
$zoneInfo = Zone::byId($matches[1]);
} catch(ZoneNotFoundException $ex) {
http_response_code(404);
echo html_information('This zone doesn\'t exist.');
return;
}
if(UserSession::instance()->getUserId() !== $zoneInfo->getUserId()
&& UserSession::instance()->getUserId() !== 1) {
http_response_code(403);
echo html_information('You aren\'t allowed to touch this zone.');
return;
}
2020-10-16 19:24:40 +00:00
$isSBS = !empty($_GET['sbs']);
2020-06-10 16:03:13 +00:00
$cssHash = hash_file('sha256', YTKNS_PUB . '/assets/editor.css');
$jsHash = hash_file('sha256', YTKNS_PUB . '/assets/editor.js');
2020-10-16 19:24:40 +00:00
if($isSBS) {
echo '<!doctype html>';
echo '<link href="/assets/style.css" type="text/css" rel="stylesheet"/>';
echo '<link href="/assets/editor.css?v=' . $cssHash . '" type="text/css" rel="stylesheet"/>';
echo '<style>.ye { height: 100%; width: 100%; } .ye-sidebar { min-width: 200px; }</style>';
} else {
echo html_header([
'title' => 'Editing Zone - YTKNS',
'styles' => [
page_url('/assets/editor.css', ['v' => $cssHash]),
],
]);
}
2020-06-10 16:03:13 +00:00
Template::render('zones/edit', [
'edit_id' => $zoneInfo->getId(),
'edit_token' => UserSession::instance()->getSmallToken(10),
'edit_css_ver' => substr($cssHash, 0, 16),
'edit_js_ver' => substr($jsHash, 0, 16),
'upload_token' => UserSession::instance()->getSmallToken(6),
]);
2020-10-16 19:24:40 +00:00
if($isSBS) {
echo '<script type="text/javascript" charset="utf-8" src="/assets/editor.js?v=' . $jsHash . '"></script>';
} else {
echo <<<HTML
<script>
window.addEventListener('DOMContentLoaded', function() {
if(window.location !== window.parent.location)
location.assign(location.toString() + '?sbs=1');
});
</script>
HTML;
echo html_footer([
'scripts' => [
page_url('/assets/editor.js', ['v' => $jsHash]),
],
]);
}
2020-06-10 16:03:13 +00:00
return;
}
if(preg_match('#^/zones/([0-9]+).json$#', $reqPath, $matches)) {
header('Content-Type: application/json; charset=utf-8');
if(!UserSession::hasInstance()) {
http_response_code(403);
echo json_encode([
'err' => 'You must be logged in to do this.',
]);
return;
}
try {
$zoneInfo = Zone::byId($matches[1]);
} catch(ZoneNotFoundException $ex) {
http_response_code(404);
echo json_encode([
'err' => 'This zone doesn\'t exist',
]);
return;
}
if(UserSession::instance()->getUserId() !== $zoneInfo->getUserId()
&& UserSession::instance()->getUserId() !== 1) {
http_response_code(403);
echo json_encode([
'err' => 'You aren\'t allowed to touch this zone.',
]);
return;
}
if($reqMethod === 'GET') {
echo $zoneInfo->toJson(true);
return;
}
if($reqMethod !== 'POST') {
http_response_code(405);
echo json_encode([
'err' => 'Invalid request method.',
]);
return;
}
$zoneToken = filter_input(INPUT_POST, 'zone_token');
if($zoneToken !== UserSession::instance()->getSmallToken(10)) {
http_response_code(403);
echo json_encode([
'err' => 'Invalid token.',
]);
return;
}
$zoneId = filter_input(INPUT_POST, 'zone_id', FILTER_VALIDATE_INT);
if($zoneId !== $zoneInfo->getId()) {
http_response_code(400);
echo json_encode([
'err' => 'Zone ID in POST data does not match the target zone ID.',
]);
return;
}
$zoneTitle = filter_input(INPUT_POST, 'zone_title');
$zoneEffects = filter_input(INPUT_POST, 'zone_effect', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
try {
$zoneInfo->setTitle($zoneTitle);
$zoneInfo->setPassiveMode();
if(is_array($zoneEffects)) {
foreach($zoneEffects as $effectName => $effectValues) {
$effectInfo = ZoneEffect::effectClass($effectName);
$effectInfo->setEffectParams($effectValues);
$zoneInfo->addEffect($effectInfo);
}
}
$zoneInfo->update(['zone_title']);
// Schedule a screenshot to be taken
$zoneInfo->queueTask('screenshot');
} catch(ZoneInvalidTitleException $ex) {
http_response_code(400);
echo json_encode([
'err' => 'Invalid title.',
]);
return;
} catch(ZoneEffectClassNotFoundException $ex) {
http_response_code(400);
echo json_encode([
'err' => sprintf('Invalid effect name: %s.', $ex->getMessage()),
]);
return;
} catch(PageEffectException $ex) {
http_response_code(400);
echo json_encode([
'err' => sprintf('%s: %s', $effectName ?? '', $ex->getMessage()),
]);
return;
} catch(Exception $ex) {
http_response_code(500);
echo json_encode([
'err' => sprintf("An unexpected error occurred.\r\n%s: %s", get_class($ex), $ex->getMessage()),
]);
return;
}
echo json_encode([
'msg' => 'Saved!',
]);
return;
}
if($reqPath === '/uploads') {
header('Content-Type: application/json; charset=utf-8');
if($reqMethod !== 'POST') {
http_response_code(405);
echo json_encode([
'err' => 'Unsupported request method.',
]);
return;
}
if(!UserSession::hasInstance()) {
http_response_code(403);
echo json_encode([
'err' => 'You must be logged in to upload files.',
]);
return;
}
if(filter_input(INPUT_POST, 'upload_token') !== UserSession::instance()->getSmallToken(6)) {
http_response_code(403);
echo json_encode([
'err' => 'Invalid upload token.',
]);
return;
}
if(empty($_FILES['upload_file']['tmp_name'])) {
http_response_code(400);
echo json_encode([
'err' => 'Missing file.',
]);
return;
}
$hash = hash_file('sha256', $_FILES['upload_file']['tmp_name']);
$existing = Upload::byHash($hash);
if($existing !== null) {
if($existing->getDMCA() > 0) {
http_response_code(451);
echo json_encode([
'err' => 'This file has been removed in response to a DMCA takedown request. It may not be used.',
]);
return;
}
if($existing->getDeleted() > 0) {
http_response_code(404);
echo json_encode([
'err' => 'This file has been flagged for deletion, it cannot be reuploaded at this time.',
]);
return;
}
http_response_code(200);
echo json_encode([
'err' => 'File has been uploaded already.',
'file' => $existing->getId(),
]);
return;
}
if($_FILES['upload_file']['size'] > 5242880) {
http_response_code(413);
echo json_encode([
'err' => 'Upload is too large.',
]);
return;
}
$contentType = mime_content_type($_FILES['upload_file']['tmp_name']);
if(!in_array($contentType, ALLOWED_UPLOADS)) {
http_response_code(400);
echo json_encode([
'err' => sprintf('File type not allowed. (Must be %s)', implode(', ', ALLOWED_UPLOADS)),
]);
return;
}
try {
$upload = Upload::create(UserSession::instance()->getUser(), $_FILES['upload_file']['name'], $contentType, $hash);
} catch(UploadCreationFailedException $ex) {
http_response_code(500);
echo json_encode([
'err' => 'Failed to create upload record.',
]);
return;
}
if(!move_uploaded_file($_FILES['upload_file']['tmp_name'], $upload->getPath())) {
http_response_code(500);
echo json_encode([
'err' => 'Upload failed.',
]);
return;
}
http_response_code(201);
echo json_encode([
'file' => $upload->getId(),
]);
return;
}
if(preg_match('#^/uploads/([a-zA-Z0-9-_]{16})$#', $reqPath, $matches)) {
try {
$uploadInfo = Upload::byId($matches[1]);
} catch(UploadNotFoundException $ex) {
http_response_code(404);
return;
}
if(!empty($_SERVER['HTTP_ORIGIN'])) {
$zoneDomain = sprintf(Config::get('domain.zone'), '');
$originPart = substr($_SERVER['HTTP_ORIGIN'], strlen($_SERVER['HTTP_ORIGIN']) - strlen($zoneDomain));
if($originPart === $zoneDomain) {
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
}
}
if($_SERVER['REQUEST_METHOD'] !== 'HEAD' && $_SERVER['REQUEST_METHOD'] !== 'OPTIONS')
header('X-Accel-Redirect: /raw-uploads/' . $uploadInfo->getId());
header('Content-Type: ' . $uploadInfo->getType());
header('Content-Disposition: inline; filename="' . $uploadInfo->getName() . '"');
return;
}
if(preg_match('#^/uploads/([a-zA-Z0-9-_]{16}).json$#', $reqPath, $matches)) {
header('Content-Type: application/json; charset=utf-8');
if(!UserSession::hasInstance()) {
http_response_code(403);
echo json_encode([
'err' => 'You must be logged in to do this.',
]);
return;
}
try {
$uploadInfo = Upload::byId($matches[1]);
} catch(UploadNotFoundException $ex) {
http_response_code(404);
echo json_encode([
'err' => 'Upload not found.',
]);
return;
}
echo $uploadInfo->toJson(true);
return;
}
if($reqPath === '/settings') {
if(!UserSession::hasInstance()) {
2024-09-05 22:49:42 +00:00
http_response_code(403);
2020-06-10 16:03:13 +00:00
echo html_information('You must be logged in to access this page.');
return;
}
echo html_header(['title' => 'Settings - YTKNS']);
Template::render('settings/index');
echo html_footer();
return;
}
if($reqPath === '/auth/login') {
2021-07-28 20:52:46 +00:00
if(YTKNS_MAINTENANCE) {
http_response_code(503);
echo html_information('You cannot log in during maintenance.');
return;
}
2020-06-10 16:03:13 +00:00
if(UserSession::hasInstance()) {
http_response_code(404);
echo html_information('You are logged in already.');
return;
}
2024-09-05 22:49:42 +00:00
if(filter_has_var(INPUT_GET, 'state')) {
$state = base64uri_decode((string)filter_input(INPUT_GET, 'state'));
if($state === false || strlen($state) !== 60) {
http_response_code(400);
echo html_information('Provided state is invalid.');
return;
2020-06-10 16:03:13 +00:00
}
2024-09-05 22:49:42 +00:00
$signature = hash_hmac('sha256', substr($state, 32), YTKNS_OA2_STATE_SECRET, true);
if(!hash_equals($signature, substr($state, 0, 32))) {
http_response_code(403);
echo html_information('Request verification failed.');
return;
}
2021-07-28 20:52:46 +00:00
2024-09-05 22:49:42 +00:00
$state = unpack('a32hash/Jtime/a20verifier', $state);
if($state === false) {
http_response_code(500);
echo html_information('State unpack failed.');
return;
}
2020-06-10 16:03:13 +00:00
2024-09-05 22:49:42 +00:00
if($state['time'] < strtotime('-15 minutes') || $state['time'] > strtotime('+15 minutes')) {
http_response_code(403);
echo html_information('Authorisation request timestamp is no longer valid.');
return;
}
2020-06-10 16:03:13 +00:00
2024-09-05 22:49:42 +00:00
if(filter_has_var(INPUT_GET, 'error')) {
$error = (string)filter_input(INPUT_GET, 'error');
$text = (string)filter_input(INPUT_GET, 'error_description');
if($text === '')
$text = match($error) {
'access_denied' => 'You rejected the authorisation request.',
'invalid_request' => 'YTKNS sent an invalid request to the authorisation server, please report this!',
'invalid_scope' => 'YTKNS sent an invalid request to the authorisation server, please report this!',
'server_error' => 'Something went wrong on the authorisation server, please try again later.',
default => sprintf('An unexpected error occurred: %s', $error),
};
http_response_code(400);
echo html_information(htmlspecialchars($text));
return;
}
2020-06-10 16:03:13 +00:00
2024-09-05 22:49:42 +00:00
if(filter_has_var(INPUT_GET, 'code')) {
try {
$postData = sprintf(
'grant_type=authorization_code&code=%s&code_verifier=%s',
rawurlencode((string)filter_input(INPUT_GET, 'code')),
rawurldecode($state['verifier'])
);
$authz = sprintf('Basic %s', base64_encode(sprintf('%s:%s', YTKNS_OA2_CLIENT_ID, YTKNS_OA2_CLIENT_SECRET)));
$tokenInfo = json_decode(file_get_contents('https://api.flashii.net/oauth2/token', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => implode("\r\n", [
sprintf('Authorization: %s', $authz),
'Content-Type: application/x-www-form-urlencoded',
sprintf('Content-Length: %d', strlen($postData)),
'User-Agent: YTKNS',
]),
'content' => $postData,
],
])));
if(isset($tokenInfo->access_token)) {
$fUserInfo = json_decode(file_get_contents('https://api.flashii.net/v1/me', false, stream_context_create([
'http' => [
'method' => 'GET',
'header' => implode("\r\n", [
sprintf('Authorization: Bearer %s', $tokenInfo->access_token),
'User-Agent: YTKNS',
]),
],
])));
if(empty($fUserInfo->id)) {
http_response_code(500);
echo html_information('Authentication failed.');
return;
}
2020-06-10 16:03:13 +00:00
2024-09-05 22:49:42 +00:00
try {
$userInfo = User::byRemoteId($fUserInfo->id);
$loginMessage = 'You are now logged in!';
} catch(UserNotFoundException) {
try {
$userInfo = User::create($fUserInfo->id, $fUserInfo->name);
} catch(\PDOException) {
$userInfo = User::create($fUserInfo->id, sprintf('%s_%04d', $fUserInfo->name, random_int(0, 9999)));
}
$loginMessage = 'Your account been created!';
2020-06-10 16:03:13 +00:00
}
2024-09-05 22:49:42 +00:00
// leaving session bumping off for now, the implementation needs to be better for that
$session = UserSession::create($userInfo, false);
$session->setInstance();
setcookie('ytkns_login', $session->getToken(), $session->getExpires(), '/', '.' . Config::get('domain.main'), false, true);
echo html_information($loginMessage, 'Welcome', '/');
return;
2020-06-10 16:03:13 +00:00
}
2024-09-05 22:49:42 +00:00
} catch(\Exception $ex) {
http_response_code(500);
echo html_information('Authorisation request failed, please try again.');
2020-06-10 16:03:13 +00:00
}
2024-09-05 22:49:42 +00:00
return;
2020-06-10 16:03:13 +00:00
}
}
2024-09-05 22:49:42 +00:00
$verifier = random_bytes(20);
$time = pack('J', time());
$signature = hash_hmac('sha256', $time . $verifier, YTKNS_OA2_STATE_SECRET, true);
$state = base64uri_encode($signature . $time . $verifier);
2020-06-10 16:03:13 +00:00
2024-09-05 22:49:42 +00:00
header(sprintf(
'Location: https://id.flashii.net/oauth2/authorise?response_type=code&scope=identify&code_challenge_method=S256&client_id=%s&state=%s&code_challenge=%s&redirect_uri=%s',
rawurlencode(YTKNS_OA2_CLIENT_ID),
rawurlencode($state),
rawurlencode(base64uri_encode(hash('sha256', $verifier, true))),
rawurlencode('https://ytkns.com/auth/login'),
));
2020-06-10 16:03:13 +00:00
return;
}
if($reqPath === '/auth/logout') {
if(!UserSession::hasInstance()) {
http_response_code(404);
echo html_information('You are not logged in.');
return;
}
if(filter_input(INPUT_GET, 's') !== UserSession::instance()->getSmallToken()) {
http_response_code(403);
echo html_information('Log out verification failed, please try again.');
return;
}
UserSession::instance()->destroy();
UserSession::unsetInstance();
echo html_information('You have been logged out.', 'Log out', '/');
return;
}
http_response_code(404);
echo html_information('The requested page does not exist.', 'Page not found');