1043 lines
35 KiB
PHP
1043 lines
35 KiB
PHP
<?php
|
|
namespace YTKNS;
|
|
|
|
use Exception;
|
|
|
|
require_once __DIR__ . '/../startup.php';
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
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')],
|
|
['text' => 'Random', 'link' => page_url('/zones/random')],
|
|
]);
|
|
|
|
$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);
|
|
|
|
$vars['maintenance'] = YTKNS_MAINTENANCE ? Template::renderRaw('maintenance') : '';
|
|
|
|
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>';
|
|
}
|
|
|
|
if(!YTKNS_MAINTENANCE && !empty($_COOKIE['ytkns_login']) && is_string($_COOKIE['ytkns_login'])) {
|
|
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;
|
|
}
|
|
|
|
if(!YTKNS_MAINTENANCE) {
|
|
if(!empty($_GET['_refresh_screenshot'])) {
|
|
header('Location: /');
|
|
$zoneInfo->takeScreenshot();
|
|
return;
|
|
}
|
|
|
|
ZoneView::increment($zoneInfo, $_SERVER['REMOTE_ADDR']);
|
|
}
|
|
|
|
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');
|
|
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;
|
|
}
|
|
|
|
if($reqPath === '/zones/random') {
|
|
$zoneInfo = Zone::byRandom();
|
|
header('Location: ' . $zoneInfo->getUrl());
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
$isSBS = !empty($_GET['sbs']);
|
|
$cssHash = hash_file('sha256', YTKNS_PUB . '/assets/editor.css');
|
|
$jsHash = hash_file('sha256', YTKNS_PUB . '/assets/editor.js');
|
|
|
|
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]),
|
|
],
|
|
]);
|
|
}
|
|
|
|
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),
|
|
]);
|
|
|
|
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]),
|
|
],
|
|
]);
|
|
}
|
|
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()) {
|
|
http_response_code(403);
|
|
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') {
|
|
if(YTKNS_MAINTENANCE) {
|
|
http_response_code(503);
|
|
echo html_information('You cannot log in during maintenance.');
|
|
return;
|
|
}
|
|
|
|
if(UserSession::hasInstance()) {
|
|
http_response_code(404);
|
|
echo html_information('You are logged in already.');
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
$state = unpack('a32hash/Jtime/a20verifier', $state);
|
|
if($state === false) {
|
|
http_response_code(500);
|
|
echo html_information('State unpack failed.');
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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!';
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
} catch(\Exception $ex) {
|
|
http_response_code(500);
|
|
echo html_information('Authorisation request failed, please try again.');
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
$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);
|
|
|
|
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'),
|
|
));
|
|
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');
|