1072 lines
35 KiB
PHP
1072 lines
35 KiB
PHP
|
<?php
|
||
|
namespace YTKNS;
|
||
|
|
||
|
use Exception;
|
||
|
|
||
|
require_once __DIR__ . '/../startup.php';
|
||
|
|
||
|
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')],
|
||
|
]);
|
||
|
|
||
|
$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')],
|
||
|
];
|
||
|
|
||
|
if(Config::get('user.invite_only', Config::TYPE_BOOL))
|
||
|
$userMenu[] = ['text' => 'Invites', 'link' => page_url('/settings/invites')];
|
||
|
|
||
|
$userMenu[] = ['text' => 'Log out', 'link' => page_url('/auth/logout', ['s' => UserSession::instance()->getSmallToken()])];
|
||
|
} else {
|
||
|
$userMenu = [
|
||
|
['text' => 'Log in', 'link' => page_url('/auth/login')],
|
||
|
['text' => 'Register', 'link' => page_url('/auth/register')],
|
||
|
];
|
||
|
}
|
||
|
|
||
|
$vars['menu_user'] = Template::renderSet('menu-user-item', $userMenu);
|
||
|
|
||
|
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(!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(!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('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/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]+)$#', $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;
|
||
|
}
|
||
|
|
||
|
$cssHash = hash_file('sha256', YTKNS_PUB . '/assets/editor.css');
|
||
|
$jsHash = hash_file('sha256', YTKNS_PUB . '/assets/editor.js');
|
||
|
|
||
|
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),
|
||
|
]);
|
||
|
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(404);
|
||
|
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 === '/settings/invites') {
|
||
|
if(!UserSession::hasInstance()) {
|
||
|
http_response_code(404);
|
||
|
echo html_information('You must be logged in to access this page.');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$currentUser = UserSession::instance()->getUser();
|
||
|
$createdInvites = UserInvite::fromCreator($currentUser);
|
||
|
$createToken = $currentUser->getId() !== 1 /*&& count($createdInvites) >= 5*/ ? '' : UserSession::instance()->getSmallToken(11);
|
||
|
|
||
|
if($reqMethod === 'POST') {
|
||
|
if(empty($createToken)) {
|
||
|
$inviteError = 'You\'ve reached the maximum amount of invites you can generate.';
|
||
|
} elseif($createToken !== filter_input(INPUT_POST, 'invite_token')) {
|
||
|
$inviteError = 'Cannot create invite.';
|
||
|
} else {
|
||
|
try {
|
||
|
$createdInvites[] = $createdInvite = UserInvite::create($currentUser);
|
||
|
$inviteError = $createdInvite->getToken();
|
||
|
} catch(UserInviteCreationFailedException $ex) {
|
||
|
$inviteError = 'Invite creation failed.';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(isset($inviteError))
|
||
|
$inviteError = Template::renderRaw('error', [
|
||
|
'error_text' => $inviteError,
|
||
|
]);
|
||
|
|
||
|
if(!empty($createToken))
|
||
|
$inviteCreate = Template::renderRaw('settings/invites-create', [
|
||
|
'create_token' => $createToken,
|
||
|
]);
|
||
|
|
||
|
$invitesItems = [];
|
||
|
foreach($createdInvites as $inviteItem) {
|
||
|
$invitesItems[] = [
|
||
|
'invite_created' => date('c', $inviteItem->getCreated()),
|
||
|
'invite_used' => (($_used = $inviteItem->getUsed()) === null ? 'Unused' : date('c', $_used)),
|
||
|
'invite_user' => (($_user = $inviteItem->getUser()) === null ? 'Unused' : sprintf('<a href="/@%1$s">%1$s</a>', $_user->getUsername())),
|
||
|
'invite_token' => $inviteItem->getToken(),
|
||
|
];
|
||
|
}
|
||
|
|
||
|
echo html_header(['title' => 'Invites - YTKNS']);
|
||
|
Template::render('settings/invites', [
|
||
|
'invite_error' => $inviteError ?? '',
|
||
|
'invite_create' => $inviteCreate ?? '',
|
||
|
'invite_list' => Template::renderSet('settings/invites-item', $invitesItems),
|
||
|
]);
|
||
|
echo html_footer();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($reqPath === '/auth/login') {
|
||
|
if(UserSession::hasInstance()) {
|
||
|
http_response_code(404);
|
||
|
echo html_information('You are logged in already.');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($reqMethod === 'POST') {
|
||
|
$loginUsername = filter_input(INPUT_POST, 'username');
|
||
|
$loginPassword = filter_input(INPUT_POST, 'password');
|
||
|
$loginRemember = !empty(filter_input(INPUT_POST, 'remember'));
|
||
|
|
||
|
if(empty($loginUsername) || empty($loginPassword)) {
|
||
|
$authError = 'Username or password missing.';
|
||
|
} else {
|
||
|
try {
|
||
|
$loginUser = User::forLogin($loginUsername);
|
||
|
} catch(\Exception $ex) {}
|
||
|
|
||
|
if(empty($loginUser) || empty($loginUser->password) || !password_verify($loginPassword, $loginUser->password)) {
|
||
|
$authError = 'Username or password was invalid.';
|
||
|
} else {
|
||
|
$session = UserSession::create($loginUser, $loginRemember);
|
||
|
$session->setInstance();
|
||
|
setcookie('ytkns_login', $session->getToken(), $session->getExpires(), '/', '.' . Config::get('domain.main'), false, true);
|
||
|
echo html_information('You are now logged in!', 'Welcome', '/');
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(isset($authError))
|
||
|
$authError = Template::renderRaw('error', [
|
||
|
'error_text' => $authError,
|
||
|
]);
|
||
|
|
||
|
$authFields = [
|
||
|
[
|
||
|
'field_title' => 'Username or E-Mail Address',
|
||
|
'field_type' => 'text',
|
||
|
'field_name' => 'username',
|
||
|
'field_value' => ($loginUsername ?? ''),
|
||
|
],
|
||
|
[
|
||
|
'field_title' => 'Password',
|
||
|
'field_type' => 'password',
|
||
|
'field_name' => 'password',
|
||
|
'field_value' => '',
|
||
|
],
|
||
|
];
|
||
|
|
||
|
echo html_header(['title' => 'Log in - YTKNS']);
|
||
|
Template::render('auth/login' . (isset($_GET['new']) ? '2' : ''), [
|
||
|
'auth_error' => $authError ?? '',
|
||
|
'auth_fields' => Template::renderSet('auth/field', $authFields),
|
||
|
'auth_remember' => ($loginRemember ?? false) ? ' checked' : '',
|
||
|
]);
|
||
|
echo html_footer();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if($reqPath === '/auth/register') {
|
||
|
if(UserSession::hasInstance()) {
|
||
|
http_response_code(404);
|
||
|
echo html_information('You are logged in already.');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$inviteOnly = Config::get('user.invite_only', Config::TYPE_BOOL);
|
||
|
|
||
|
if($reqMethod === 'POST') {
|
||
|
$registerUsername = filter_input(INPUT_POST, 'username');
|
||
|
$registerPassword = filter_input(INPUT_POST, 'password');
|
||
|
$registerEMail = filter_input(INPUT_POST, 'email');
|
||
|
$registerInvite = filter_input(INPUT_POST, 'invite');
|
||
|
|
||
|
if(empty($registerUsername) || empty($registerPassword) || empty($registerEMail) || ($inviteOnly && empty($registerInvite))) {
|
||
|
$authError = 'You must fill in all fields.';
|
||
|
} else {
|
||
|
if($inviteOnly) {
|
||
|
try {
|
||
|
$userInvite = UserInvite::byToken($registerInvite);
|
||
|
|
||
|
if($userInvite->isUsed()) {
|
||
|
$authError = 'Invalid invite token.';
|
||
|
}
|
||
|
} catch(UserInviteNotFoundException $ex) {
|
||
|
$authError = 'Invalid invite token.';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(!isset($authError)) {
|
||
|
if(!preg_match('#^([a-zA-Z0-9-_]{1,20})$#', $registerUsername)) {
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$createdUser = User::create(
|
||
|
$registerUsername,
|
||
|
$registerPassword,
|
||
|
$registerEMail
|
||
|
);
|
||
|
|
||
|
if(isset($userInvite))
|
||
|
$userInvite->markUsed($createdUser);
|
||
|
} catch(UserCreationInvalidNameException $ex) {
|
||
|
$authError = 'Your username contains invalid characters or is too short or long.<br/>Must be between 1 and 20 characters and may only contains alphanumeric characters as well as - and _.';
|
||
|
} catch(UserCreationInvalidPasswordException $ex) {
|
||
|
$authError = 'Your password must have at least 6 unique characters.';
|
||
|
} catch(UserCreationInvalidMailException $ex) {
|
||
|
$authError = 'Your e-mail address isn\'t real.';
|
||
|
} catch(UserCreationFailedException $ex) {
|
||
|
$authError = 'Failed to create user.';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} elseif($reqMethod === 'GET') {
|
||
|
$registerInvite = filter_input(INPUT_GET, 'inv', FILTER_SANITIZE_STRING);
|
||
|
}
|
||
|
|
||
|
$authFields = [
|
||
|
[
|
||
|
'field_title' => 'Username',
|
||
|
'field_type' => 'text',
|
||
|
'field_name' => 'username',
|
||
|
'field_value' => ($registerUsername ?? ''),
|
||
|
],
|
||
|
[
|
||
|
'field_title' => 'Password',
|
||
|
'field_type' => 'password',
|
||
|
'field_name' => 'password',
|
||
|
'field_value' => '',
|
||
|
],
|
||
|
[
|
||
|
'field_title' => 'E-Mail Address',
|
||
|
'field_type' => 'email',
|
||
|
'field_name' => 'email',
|
||
|
'field_value' => ($registerEMail ?? ''),
|
||
|
],
|
||
|
];
|
||
|
|
||
|
if($inviteOnly)
|
||
|
$authFields[] = [
|
||
|
'field_title' => 'Invitation',
|
||
|
'field_type' => 'password',
|
||
|
'field_name' => 'invite',
|
||
|
'field_value' => ($registerInvite ?? ''),
|
||
|
];
|
||
|
|
||
|
if(isset($authError))
|
||
|
$authError = Template::renderRaw('error', [
|
||
|
'error_text' => $authError,
|
||
|
]);
|
||
|
|
||
|
echo html_header(['title' => 'Register - YTKNS']);
|
||
|
Template::render('auth/register', [
|
||
|
'auth_error' => $authError ?? '',
|
||
|
'auth_fields' => Template::renderSet('auth/field', $authFields),
|
||
|
]);
|
||
|
echo html_footer();
|
||
|
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');
|