Compare commits
2 Commits
main
...
2021-09-04
| Author | SHA1 | Date |
|---|---|---|
|
|
6b7d941c70 | |
|
|
dcc8e61201 |
|
|
@ -1,2 +0,0 @@
|
||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
thumbs/assets/*
|
||||||
|
thumbs/avatars/*
|
||||||
|
asset/files/*
|
||||||
|
api/private/config.php
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
*
|
||||||
|
{
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Comic Sans MS', Verdana, Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
H1
|
||||||
|
{
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT']."/api/private/core.php";
|
||||||
|
pageBuilder::buildHeader();
|
||||||
|
?>
|
||||||
|
Do not run <code>game:HttpGet("http://polygon.pizzaboxer.xyz/XD")</code> in studio
|
||||||
|
please no
|
||||||
|
Plz
|
||||||
|
<?php pageBuilder::buildFooter();
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("System");
|
||||||
|
|
||||||
|
Users::RequireAdmin();
|
||||||
|
|
||||||
|
function getSetupUsage($clients)
|
||||||
|
{
|
||||||
|
$usage = 0;
|
||||||
|
|
||||||
|
if(is_array($clients))
|
||||||
|
{
|
||||||
|
foreach ($clients as $client)
|
||||||
|
{
|
||||||
|
$usage += getSetupUsage($client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$usage = System::GetFolderSize("/var/www/pizzaboxer.xyz/setup$clients/", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roles =
|
||||||
|
[
|
||||||
|
Users::STAFF_MODERATOR => "Moderator",
|
||||||
|
Users::STAFF_ADMINISTRATOR => "Administrator",
|
||||||
|
Users::STAFF_CATALOG => "Catalog Manager"
|
||||||
|
];
|
||||||
|
|
||||||
|
$servermemory = System::GetMemoryUsage();
|
||||||
|
$usersOnline = Users::GetUsersOnline();
|
||||||
|
$pendingRenders = Polygon::GetPendingRenders();
|
||||||
|
$thumbPing = Polygon::GetServerPing(1);
|
||||||
|
|
||||||
|
$usage = (object)
|
||||||
|
[
|
||||||
|
"Memory" => (object)
|
||||||
|
[
|
||||||
|
"Total" => System::GetFileSize($servermemory->total),
|
||||||
|
"SytemUsage" => System::GetFileSize($servermemory->total-$servermemory->free),
|
||||||
|
"PHPUsage" => System::GetFileSize(memory_get_usage(true))
|
||||||
|
],
|
||||||
|
|
||||||
|
"Disk" => (object)
|
||||||
|
[
|
||||||
|
"Total" => System::GetFileSize(disk_total_space("/")),
|
||||||
|
"SystemUsage" => System::GetFileSize(disk_total_space("/")-disk_free_space("/")),
|
||||||
|
"PolygonUsage" => System::GetFolderSize("/var/www/pizzaboxer.xyz/polygon/"),
|
||||||
|
"ThumbnailUsage" => System::GetFolderSize("/var/www/pizzaboxer.xyz/polygoncdn/"),
|
||||||
|
"SetupUsage" => System::GetFileSize(getSetupUsage([2009, 2010, 2011, 2012]))
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
pageBuilder::$pageConfig["title"] = SITE_CONFIG["site"]["name"]." Administration";
|
||||||
|
pageBuilder::buildHeader();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!--h1 style="position:absolute;opacity:0.5;font-size:10rem;z-index:10000000">THIS IS NOT <br> MULTAKOS SCREENSHOT LOL</h1-->
|
||||||
|
<h2 class="font-weight-normal"><?=SITE_CONFIG["site"]["name"]?> Administration</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-7 divider-right">
|
||||||
|
<h3 class="pb-2 font-weight-normal">You are <?=vowel($roles[SESSION["adminLevel"]])?></h3>
|
||||||
|
<div class="row px-3 mb-2">
|
||||||
|
<?php if(Users::IsAdmin([Users::STAFF_MODERATOR, Users::STAFF_ADMINISTRATOR])) { ?>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-danger btn-lg btn-block px-0" href="/admin/moderate-user"><i class="fal fa-gavel"></i> Moderate user</a>
|
||||||
|
</div>
|
||||||
|
<?php } if(Users::IsAdmin()) { ?>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-danger btn-lg btn-block px-0" href="/admin/moderate-assets"><i class="fal fa-file-exclamation"></i> Moderate assets</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-primary btn-lg btn-block px-0" href="/admin/render-queue"><i class="fal fa-images"></i> Render queue</a>
|
||||||
|
</div>
|
||||||
|
<?php } if(Users::IsAdmin(Users::STAFF_ADMINISTRATOR)) { ?>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-primary btn-lg btn-block px-0" href="/admin/site-banners"><i class="fal fa-bullhorn"></i> Site banners</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-primary btn-lg btn-block px-0" href="#" onclick="polygon.buildModal({header: 'coming soon', body: 'Sample Text', buttons: [{class:'btn btn-primary px-4', dismiss:true, text:'OK'}]});"><i class="fal fa-rss-square"></i> Newsfeed</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-primary btn-lg btn-block px-0" href="/admin/staff-audit"><i class="fal fa-book"></i> Audit log</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-primary btn-lg btn-block px-0" href="/admin/error-log"><i class="fal fa-exclamation-triangle"></i> Error log</a>
|
||||||
|
</div>
|
||||||
|
<?php } if(Users::IsAdmin([Users::STAFF_CATALOG, Users::STAFF_ADMINISTRATOR])) { ?>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-success btn-lg btn-block px-0" href="/admin/create-asset"><i class="fal fa-file-plus"></i> Create asset</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-success btn-lg btn-block px-0" href="/admin/give-asset"><i class="fal fa-gift"></i> Give asset</a>
|
||||||
|
</div>
|
||||||
|
<?php } if(Users::IsAdmin(Users::STAFF_ADMINISTRATOR)) { ?>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-success btn-lg btn-block px-0" href="/admin/give-currency"><i class="fal fa-pizza-slice"></i> Give <?=SITE_CONFIG["site"]["currency"]?></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 py-2 px-1">
|
||||||
|
<a class="btn btn-outline-success btn-lg btn-block px-0" href="#" onclick="polygon.buildModal({header: 'Credentials', body: '<span>You\'ll have to enter these yourself!</span> <br> Username: <code>ProjectPolygon</code> <br> Password: <code>962e8f89341b4e5f208076b5d06fb1b6</code>', buttons: [{class:'btn btn-primary px-4', attributes: [{'attr': 'onclick', 'val': 'window.location = \'https://stats.pizzaboxer.xyz\''}], text:'Continue'}]})"><i class="fal fa-chart-pie"></i> Statistics</a>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<h3 class="pb-3 font-weight-normal">Website / Server Info</h3>
|
||||||
|
<div class="card w-100 mt-2">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h3 class="font-weight-normal"><i class="fal fa-server"></i> <?=gethostname()?></h3>
|
||||||
|
<small><?=php_uname()?></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card w-100 mt-2">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h3 class="font-weight-normal"><i class="fal fa-memory"></i> <?=$usage->Memory->SytemUsage?> / <?=$usage->Memory->Total?> In Use</h3>
|
||||||
|
<small><?=$usage->Memory->PHPUsage?> is being used by PHP</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card w-100 mt-2">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h3 class="font-weight-normal"><i class="fal fa-hdd"></i> <?=$usage->Disk->SystemUsage?> / <?=$usage->Disk->Total?> Used</h3>
|
||||||
|
<small><?=SITE_CONFIG["site"]["name"]?> is using <?=$usage->Disk->PolygonUsage?></small><br>
|
||||||
|
<small>Thumbnail CDN is using <?=$usage->Disk->ThumbnailUsage?></small><br>
|
||||||
|
<small>Client setup (2009-2012) is using <?=$usage->Disk->SetupUsage?> total</small>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card w-100 mt-2">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<?php if(SITE_CONFIG["site"]["thumbserver"]) { ?>
|
||||||
|
<h3 class="font-weight-normal"><i class="fal fa-images"></i> <?=$pendingRenders?> asset renders pending</h3>
|
||||||
|
<?php if($thumbPing+35 > time()) { ?>
|
||||||
|
<small>The thumbnail server is currently online</small>
|
||||||
|
<?php } else { ?>
|
||||||
|
<small>The thumbnail server last registered online at <?=date("j/n/Y g:i:s A", $thumbPing)?></small>
|
||||||
|
<?php } } else { ?>
|
||||||
|
<h3 class="font-weight-normal"><i class="fal fa-images"></i> Thumbserver is disabled</h3>
|
||||||
|
<small>The thumbnail server has been manually disabled. <br> Go to /api/private/config.php to re-enable it.</small>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card w-100 mt-2">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h3 class="font-weight-normal"><i class="fal fa-user"></i> <?=$usersOnline?> user<?=$usersOnline>1?'s':''?> currently online</h3>
|
||||||
|
<?php if($usersOnline == 1) { ?><small>dead much?</small><?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php pageBuilder::buildFooter(); ?>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$assetid = $_POST['assetID'] ?? false;
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
|
||||||
|
$query = $pdo->prepare("DELETE FROM ownedAssets WHERE assetId = :aid AND userId = :uid");
|
||||||
|
$query->bindParam(":aid", $assetid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
if(!$query->rowCount()) api::respond(400, false, "You do not own this asset");
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$wearing = isset($_POST["wearing"]) && $_POST["wearing"] == "true";
|
||||||
|
$userId = SESSION["userId"];
|
||||||
|
$type = $_POST["type"] ?? false;
|
||||||
|
$page = $_POST["page"] ?? 1;
|
||||||
|
$assets = [];
|
||||||
|
|
||||||
|
if($wearing)
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM ownedAssets WHERE userId = :uid AND wearing = 1");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$type_str = Catalog::GetTypeByNum($type);
|
||||||
|
if(!Catalog::GetTypeByNum($type)) api::respond(400, false, "Invalid asset type");
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM ownedAssets INNER JOIN assets ON assets.id = assetId WHERE userId = :uid AND assets.type = :type AND wearing = 0");
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
}
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$pages = ceil($query->fetchColumn()/8);
|
||||||
|
if($page > $pages) $page = $pages;
|
||||||
|
if(!is_numeric($page) || $page < 1) $page = 1;
|
||||||
|
$offset = ($page - 1)*8;
|
||||||
|
|
||||||
|
if(!$pages) api::respond(200, true, $wearing ? 'You are not currently wearing anything' : 'You don\'t have any unequipped '.($type_str.(!str_ends_with($type_str, 's') ? 's' : '').' to wear'));
|
||||||
|
|
||||||
|
if($wearing)
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("
|
||||||
|
SELECT assets.* FROM ownedAssets
|
||||||
|
INNER JOIN assets ON assets.id = assetId
|
||||||
|
WHERE userId = :uid AND wearing = 1
|
||||||
|
ORDER BY last_toggle DESC LIMIT 8 OFFSET $offset");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("
|
||||||
|
SELECT assets.* FROM ownedAssets
|
||||||
|
INNER JOIN assets ON assets.id = assetId
|
||||||
|
WHERE userId = :uid AND assets.type = :type AND wearing = 0
|
||||||
|
ORDER BY timestamp DESC LIMIT 8 OFFSET $offset");
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
}
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
while($asset = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$assets[] =
|
||||||
|
[
|
||||||
|
"url" => "/".encode_asset_name($asset->name)."-item?id=".$asset->id,
|
||||||
|
"item_id" => $asset->id,
|
||||||
|
"item_name" => htmlspecialchars($asset->name),
|
||||||
|
"item_thumbnail" => Thumbnails::GetAsset($asset, 420, 420)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $pages, "items" => $assets]));
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$bodyColors = json_decode(SESSION["userInfo"]["bodycolors"]);
|
||||||
|
$bodyPart = $_POST["BodyPart"] ?? false;
|
||||||
|
$color = $_POST["Color"] ?? false;
|
||||||
|
|
||||||
|
if(!$color || !in_array($bodyPart, ["Head", "Torso", "Left Arm", "Right Arm", "Left Leg", "Right Leg"])) api::respond(400, false, "Bad Request");
|
||||||
|
$brickcolor = Users::hex2bc(rgbtohex($color));
|
||||||
|
if(!$brickcolor) api::respond(200, false, "Invalid body color #".rgbtohex($color));
|
||||||
|
$bodyColors->{$bodyPart} = $brickcolor;
|
||||||
|
$bodyColors = json_encode($bodyColors);
|
||||||
|
|
||||||
|
$query = $pdo->prepare("UPDATE users SET bodycolors = :bodycolors WHERE id = :uid");
|
||||||
|
$query->bindParam(":bodycolors", $bodyColors, PDO::PARAM_STR);
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
Polygon::RequestRender("Avatar", SESSION["userId"]);
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$assetid = $_POST['assetID'] ?? false;
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT wearing, assets.* FROM ownedAssets INNER JOIN assets ON assets.id = assetId WHERE userId = :uid AND assetId = :aid");
|
||||||
|
$query->bindParam(":aid", $assetid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
$info = $query->fetch(PDO::FETCH_OBJ);
|
||||||
|
if(!$info) api::respond(400, false, "You do not own this asset");
|
||||||
|
|
||||||
|
$wear = !$info->wearing;
|
||||||
|
|
||||||
|
if(in_array($info->type, [2, 11, 12, 17, 18])) //asset types that can only have one worn at a time
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("UPDATE ownedAssets INNER JOIN assets ON assets.id = assetId SET wearing = 0 WHERE userId = :uid AND type = :type");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":type", $info->type, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
if($wear)
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("UPDATE ownedAssets SET wearing = 1, last_toggle = UNIX_TIMESTAMP() WHERE userId = :uid AND assetId = :aid");
|
||||||
|
$query->bindParam(":aid", $assetid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif($info->type == 8) //up to 3 hats can be worn at the same time
|
||||||
|
{
|
||||||
|
if($wear)
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM ownedAssets INNER JOIN assets ON assets.id = assetId WHERE userId = :uid AND type = 8 AND wearing");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
if($query->fetchColumn() >= 5) api::respond(400, false, "You cannot wear more than 5 hats at a time");
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $pdo->prepare("UPDATE ownedAssets SET wearing = :wear, last_toggle = UNIX_TIMESTAMP() WHERE userId = :uid AND assetId = :aid");
|
||||||
|
$query->bindParam(":wear", $wear, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":aid", $assetid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
}
|
||||||
|
elseif($info->type == 19) //no limit to how many gears can be equipped
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("UPDATE ownedAssets SET wearing = :wear, last_toggle = UNIX_TIMESTAMP() WHERE userId = :uid AND assetId = :aid");
|
||||||
|
$query->bindParam(":wear", $wear, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":aid", $assetid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
api::respond(400, false, "You cannot wear this asset!");
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "secure" => true, "logged_in" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$sessionkey = SESSION['sessionKey'];
|
||||||
|
|
||||||
|
$sesscount = $pdo->prepare("SELECT COUNT(*) FROM sessions WHERE userId = :uid AND valid AND NOT sessionKey = :key");
|
||||||
|
$sesscount->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$sesscount->bindParam(":key", $sessionkey, PDO::PARAM_STR);
|
||||||
|
$sesscount->execute();
|
||||||
|
|
||||||
|
if(!$sesscount->fetchColumn()) api::respond(400, false, "There are no other sessions to log out of");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("UPDATE sessions SET valid = 0 WHERE userId = :uid AND valid AND NOT sessionKey = :key");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":key", $sessionkey, PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true]);
|
||||||
|
|
||||||
|
$FeedResults = db::run(
|
||||||
|
"SELECT feed.*, users.username FROM feed
|
||||||
|
INNER JOIN users ON users.id = feed.userId WHERE userId = :uid
|
||||||
|
OR groupId IS NULL AND userId IN
|
||||||
|
(
|
||||||
|
SELECT (CASE WHEN requesterId = :uid THEN receiverId ELSE requesterId END) FROM friends
|
||||||
|
WHERE requesterId = :uid OR receiverId = :uid AND status = 1
|
||||||
|
)
|
||||||
|
OR groupId IN
|
||||||
|
(
|
||||||
|
SELECT groups_members.GroupID FROM groups_members
|
||||||
|
INNER JOIN groups_ranks ON groups_ranks.GroupID = groups_members.GroupID AND groups_ranks.Rank = groups_members.Rank
|
||||||
|
WHERE groups_members.UserID = :uid AND groups_ranks.permissions LIKE '%\"CanViewGroupStatus\":true%'
|
||||||
|
)
|
||||||
|
ORDER BY feed.id DESC LIMIT 15",
|
||||||
|
[":uid" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
|
||||||
|
$feed = [];
|
||||||
|
$news = [];
|
||||||
|
|
||||||
|
/*$news[] =
|
||||||
|
[
|
||||||
|
"header" => '<h4 class="font-weight-normal">lol</h4>',
|
||||||
|
"message" => 'fucked your mom'
|
||||||
|
];*/
|
||||||
|
|
||||||
|
/*$news[] =
|
||||||
|
[
|
||||||
|
"header" => '<h4 class="font-weight-normal">this isn\'t dead!!!! (probably)</h4>',
|
||||||
|
"message" => "ive been more inclined to work on polygon now after like 4 months, so i guess development has resumed <br><br> 2fa has been implemented, and next on the roadmap is the catalog system. so yeah, stay tuned for that"
|
||||||
|
];*/
|
||||||
|
|
||||||
|
/* $news[] =
|
||||||
|
[
|
||||||
|
"header" => "",
|
||||||
|
"img" => "https://media.discordapp.net/attachments/745025397749448814/835635922590629888/HDKolobok-256px-3.gif",
|
||||||
|
// "message" => "What you know about KOLONBOK. ™ "
|
||||||
|
"message" => "KOLONBOK. ™ Has fix 2009"
|
||||||
|
]; */
|
||||||
|
|
||||||
|
/* $news[] =
|
||||||
|
[
|
||||||
|
"header" => "Groups have been released!",
|
||||||
|
"img" => "/img/ProjectPolygon.png",
|
||||||
|
"message" => "Groups have now been fully released, with more functionality than you could ever imagine. Groups don't cost anything to make, and you can join up to 20 of them. <br> If you haven't yet, come join the <a href=\"/groups?gid=1\">official group</a>!"
|
||||||
|
]; */
|
||||||
|
|
||||||
|
while($row = $FeedResults->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$timestamp = timeSince($row->timestamp);
|
||||||
|
|
||||||
|
if($row->groupId == NULL)
|
||||||
|
{
|
||||||
|
$feed[] =
|
||||||
|
[
|
||||||
|
"userName" => $row->username,
|
||||||
|
"img" => Thumbnails::GetAvatar($row->userId, 100, 100),
|
||||||
|
"header" => "<p class=\"m-0\"><a href=\"/user?ID={$row->userId}\">{$row->username}</a> - <small>{$timestamp}</small></p>",
|
||||||
|
"message" => Polygon::FilterText($row->text)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$GroupInfo = Groups::GetGroupInfo($row->groupId, true, true);
|
||||||
|
|
||||||
|
$feed[] =
|
||||||
|
[
|
||||||
|
"userName" => $GroupInfo->name,
|
||||||
|
"img" => Thumbnails::GetAssetFromID($GroupInfo->emblem, 420, 420),
|
||||||
|
"header" => "<p class=\"m-0\"><a href=\"/groups?gid={$GroupInfo->id}\">{$GroupInfo->name}</a> - <small>posted by <a href=\"/user?ID={$row->userId}\">{$row->username}</a></small> - <small>{$timestamp}</small></p>",
|
||||||
|
"message" => Polygon::FilterText($row->text)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$FeedCount = $FeedResults->rowCount();
|
||||||
|
|
||||||
|
if($FeedCount < 15)
|
||||||
|
{
|
||||||
|
$feed[] =
|
||||||
|
[
|
||||||
|
"userName" => "Your feed is currently empty!",
|
||||||
|
"img" => "/img/feed/friends.png",
|
||||||
|
"header" => "<h4 class=\"font-weight-normal\">Looks like your feed's empty</h4>",
|
||||||
|
"message" => "If you haven't made any friends yet, <a href='/browse'>go make some</a>! <br> If you already have some, why don't you kick off the discussion?"
|
||||||
|
];
|
||||||
|
|
||||||
|
if($FeedCount < 14)
|
||||||
|
{
|
||||||
|
$feed[] =
|
||||||
|
[
|
||||||
|
"userName" => "Customize your character",
|
||||||
|
"img" => "/img/feed/cart.png",
|
||||||
|
"header" => "<h4 class=\"font-weight-normal\">Customize your character</h4>",
|
||||||
|
"message" => "Log in every day and earn 10 pizzas. Pizzas can be used to buy clothing in our <a href=\"/catalog\">catalog</a>. You can also create your own clothing on the <a href=\"/develop\">Build page</a>."
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "feed" => $feed, "news" => $news]);
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Games");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
$query = $pdo->prepare("
|
||||||
|
SELECT selfhosted_servers.* FROM client_sessions
|
||||||
|
INNER JOIN selfhosted_servers ON selfhosted_servers.id = serverID
|
||||||
|
WHERE uid = :uid AND used
|
||||||
|
GROUP BY serverID ORDER BY client_sessions.id DESC LIMIT 8");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
while($game = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
$items[] =
|
||||||
|
[
|
||||||
|
"game_name" => Polygon::FilterText($game->name),
|
||||||
|
"game_id" => $game->id,
|
||||||
|
"game_thumbnail" => Thumbnails::GetAvatar($game->hoster, 250, 250),
|
||||||
|
"playing" => Games::GetPlayersInServer($game->id)->rowCount()
|
||||||
|
];
|
||||||
|
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "items" => $items]);
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$type = $_POST["type"] ?? false;
|
||||||
|
$page = $_POST["page"] ?? 1;
|
||||||
|
$transactions = [];
|
||||||
|
|
||||||
|
if(!in_array($type, ["Purchases", "Sales"])) api::respond(400, false, "Bad Request");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM transactions WHERE ".($type=="Sales"?"seller":"purchaser")." = :uid");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$pages = ceil($query->fetchColumn()/15);
|
||||||
|
$offset = ($page - 1)*15;
|
||||||
|
|
||||||
|
$query = $pdo->prepare("
|
||||||
|
SELECT transactions.*, users.username, assets.name FROM transactions
|
||||||
|
INNER JOIN users ON users.id = ".($type=="Sales"?"purchaser":"seller")." INNER JOIN assets ON assets.id = transactions.assetId
|
||||||
|
WHERE ".($type=="Sales"?"seller":"purchaser")." = :uid ORDER BY id DESC LIMIT 15 OFFSET $offset");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
if(!$query->rowCount()) api::respond(200, true, "You have not ".($type=="Sales"?"sold":"purchased")." any items!");
|
||||||
|
|
||||||
|
while($transaction = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$memberID = $type == "Sales" ? $transaction->purchaser : $transaction->seller;
|
||||||
|
$transactions[] =
|
||||||
|
[
|
||||||
|
"type" => $type == "Sales" ? "Sold" : "Purchased",
|
||||||
|
"date" => date('j/n/y', $transaction->timestamp),
|
||||||
|
"member_name" => $transaction->username,
|
||||||
|
"member_id" => $memberID,
|
||||||
|
"member_avatar" => Thumbnails::GetAvatar($memberID, 48, 48),
|
||||||
|
"asset_name" => htmlspecialchars($transaction->name),
|
||||||
|
"asset_id" => $transaction->assetId,
|
||||||
|
"amount" => $transaction->amount
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "transactions" => $transactions, "pages" => $pages]);
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Auth");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST['currentpwd']) || !isset($_POST['newpwd']) || !isset($_POST['confnewpwd'])) api::respond(400, false, "Bad Request");
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$row = (object)SESSION["userInfo"];
|
||||||
|
$currentpwd = new Auth($_POST['currentpwd']);
|
||||||
|
$newpwd = new Auth($_POST['newpwd']);
|
||||||
|
|
||||||
|
if($row->lastpwdchange+1800 > time()) api::respond(429, false, "Please wait ".ceil((($row->lastpwdchange+1800)-time())/60)." minutes before attempting to change your password again");
|
||||||
|
if(!$currentpwd->VerifyPassword($row->password)) api::respond(400, false, "Your current password does not match");
|
||||||
|
if($_POST['currentpwd'] == $_POST['newpwd']) api::respond(400, false, "Your new password cannot be the same as your current one");
|
||||||
|
if(strlen(preg_replace('/[0-9]/', "", $_POST['newpwd'])) < 6) api::respond(400, false, "Your new password is too weak. Make sure it contains at least six non-numeric characters");
|
||||||
|
if(strlen(preg_replace('/[^0-9]/', "", $_POST['newpwd'])) < 2) api::respond(400, false, "Your new password is too weak. Make sure it contains at least two numbers");
|
||||||
|
if($_POST['newpwd'] != $_POST['confnewpwd']) api::respond(400, false, "Confirmation password does not match");
|
||||||
|
|
||||||
|
$newpwd->UpdatePassword($userid);
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
Users::UpdatePing();
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "friendRequests" => (int)SESSION["friendRequests"]]);
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "secure" => true, "logged_in" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST['blurb']) || !isset($_POST['theme']) || !isset($_POST['filter'])) api::respond(400, false, "Bad Request");
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$filter = (int)($_POST['filter'] == 'true');
|
||||||
|
$debugging = (int)(isset($_POST['debugging']) && $_POST['debugging'] == 'true');
|
||||||
|
|
||||||
|
if(!in_array($_POST['theme'], ["light", "dark", "hitius", "2014"])) api::respond(200, false, "Invalid theme");
|
||||||
|
|
||||||
|
if(!strlen($_POST['blurb'])) api::respond(200, false, "Your blurb can't be empty");
|
||||||
|
if(strlen($_POST['blurb']) > 1000) api::respond(200, false, "Your blurb is too large");
|
||||||
|
if(Polygon::IsExplicitlyFiltered($_POST["blurb"])) api::respond(200, false, "Your blurb contains inappropriate text");
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"UPDATE users SET blurb = :blurb, filter = :filter, theme = :theme, debugging = :debugging WHERE id = :uid",
|
||||||
|
[":uid" => $userid, ":blurb" => $_POST['blurb'], ":filter" => $filter, ":theme" => $_POST['theme'], ":debugging" => $debugging]
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "Your settings have been updated");
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
Polygon::ImportClass("Discord");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userId = SESSION["userId"];
|
||||||
|
$status = $_POST['status'] ?? false;
|
||||||
|
|
||||||
|
if(!strlen($status)) api::respond(200, false, "Your status cannot be empty");
|
||||||
|
if(strlen($status) > 140) api::respond(200, false, "Your status cannot be more than 140 characters");
|
||||||
|
|
||||||
|
//ratelimit
|
||||||
|
$query = db::run(
|
||||||
|
"SELECT timestamp FROM feed WHERE userId = :uid AND groupID IS NULL AND timestamp+300 > UNIX_TIMESTAMP()",
|
||||||
|
[":uid" => $userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if($query->rowCount())
|
||||||
|
api::respond(200, false, "Please wait ".GetReadableTime($query->fetchColumn(), ["RelativeTime" => "5 minutes"])." before updating your status");
|
||||||
|
|
||||||
|
db::run("INSERT INTO feed (userId, timestamp, text) VALUES (:uid, UNIX_TIMESTAMP(), :status)", [":uid" => $userId, ":status" => $status]);
|
||||||
|
|
||||||
|
db::run("UPDATE users SET status = :status WHERE id = :uid", [":uid" => $userId, ":status" => $status]);
|
||||||
|
|
||||||
|
if(time() < strtotime("2021-09-07 00:00:00") && stripos($status, "#bezosgang") !== false && !Catalog::OwnsAsset(SESSION["userId"], 2802))
|
||||||
|
{
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO ownedAssets (assetId, userId, timestamp) VALUES (2802, :uid, UNIX_TIMESTAMP())",
|
||||||
|
[":uid" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Discord::SendToWebhook(
|
||||||
|
[
|
||||||
|
"username" => SESSION["userName"],
|
||||||
|
"content" => $status,
|
||||||
|
"avatar_url" => Thumbnails::GetAvatar(SESSION["userId"], 420, 420)
|
||||||
|
],
|
||||||
|
Discord::WEBHOOK_KUSH
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Forum");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "admin" => [Users::STAFF_MODERATOR, Users::STAFF_ADMINISTRATOR], "admin_ratelimit" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST['postType'])){ api::respond(400, false, "Bad Request"); }
|
||||||
|
if(!in_array($_POST['postType'], ["thread", "reply"])){ api::respond(400, false, "Bad Request"); }
|
||||||
|
if(!isset($_POST['postId'])){ api::respond(400, false, "Bad Request"); }
|
||||||
|
if(!is_numeric($_POST['postId'])){ api::respond(400, false, "Bad Request"); }
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$isThread = $_POST['postType'] == "thread";
|
||||||
|
$threadInfo = $isThread ? Forum::GetThreadInfo($_POST['postId']) : Forum::GetReplyInfo($_POST['postId']);
|
||||||
|
|
||||||
|
if(!$threadInfo){ api::respond(400, false, "Post does not exist"); }
|
||||||
|
|
||||||
|
$query = $isThread ? $pdo->prepare("UPDATE forum_threads SET deleted = 1 WHERE id = :id") : $pdo->prepare("UPDATE forum_replies SET deleted = 1 WHERE id = :id");
|
||||||
|
$query->bindParam(":id", $_POST['postId'], PDO::PARAM_INT);
|
||||||
|
|
||||||
|
if($query->execute()){ Users::LogStaffAction("[ Forums ] Deleted forum ".($isThread?"thread":"reply")." ID ".$_POST['postId']); api::respond(200, true, "OK"); }
|
||||||
|
else{ api::respond(500, false, "Internal Server Error"); }
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "admin" => [Users::STAFF_CATALOG, Users::STAFF_ADMINISTRATOR], "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$type = $_POST["type"] ?? false;
|
||||||
|
$page = $_POST["page"] ?? 1;
|
||||||
|
$assets = [];
|
||||||
|
|
||||||
|
if(!Catalog::GetTypeByNum($type)) api::respond(400, false, "Invalid asset type");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM assets WHERE creator = 2 AND type = :type ORDER BY id DESC");
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$pages = ceil($query->fetchColumn()/15);
|
||||||
|
$offset = ($page - 1)*15;
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT * FROM assets WHERE creator = 2 AND type = :type ORDER BY id DESC LIMIT 15 OFFSET $offset");
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
while($asset = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$info = Catalog::GetAssetInfo($asset->id);
|
||||||
|
|
||||||
|
$assets[] =
|
||||||
|
[
|
||||||
|
"name" => htmlspecialchars($asset->name),
|
||||||
|
"id" => $asset->id,
|
||||||
|
"thumbnail" => Thumbnails::GetAsset($asset, 420, 420),
|
||||||
|
"item_url" => "/".encode_asset_name($asset->name)."-item?id=".$asset->id,
|
||||||
|
"config_url" => "/my/item?ID=".$asset->id,
|
||||||
|
"created" => date("n/j/Y", $asset->created),
|
||||||
|
"sales-total" => $info->sales_total,
|
||||||
|
"sales-week" => $info->sales_week
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "assets" => $assets, "pages" => $pages]));
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "admin" => Users::STAFF, "secure" => true]);
|
||||||
|
|
||||||
|
$page = $_POST["page"] ?? 1;
|
||||||
|
$assets = [];
|
||||||
|
|
||||||
|
$query = $pdo->query("SELECT COUNT(*) FROM assets WHERE NOT approved AND (type != 1 || (SELECT COUNT(*) FROM polygon.groups WHERE emblem = assets.id))");
|
||||||
|
$pages = ceil($query->fetchColumn()/18);
|
||||||
|
$offset = ($page - 1)*18;
|
||||||
|
|
||||||
|
if(!$pages) api::respond(200, true, "There are no assets to approve");
|
||||||
|
|
||||||
|
$query = $pdo->prepare(
|
||||||
|
"SELECT assets.*, users.username FROM assets
|
||||||
|
INNER JOIN users ON creator = users.id
|
||||||
|
WHERE NOT approved AND (type != 1 || (SELECT COUNT(*) FROM polygon.groups WHERE emblem = assets.id))
|
||||||
|
LIMIT 18 OFFSET :offset"
|
||||||
|
);
|
||||||
|
$query->bindParam(":offset", $offset, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
while($asset = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$assets[] =
|
||||||
|
[
|
||||||
|
"url" => "item?ID=".$asset->id,
|
||||||
|
"item_id" => $asset->id,
|
||||||
|
"item_name" => htmlspecialchars($asset->name),
|
||||||
|
"item_thumbnail" => Thumbnails::GetAsset($asset, 420, 420, true),
|
||||||
|
"texture_id" => $asset->imageID,
|
||||||
|
"creator_id" => $asset->creator,
|
||||||
|
"creator_name" => $asset->username,
|
||||||
|
"type" => Catalog::GetTypeByNum($asset->type),
|
||||||
|
"created" => date("j/n/y G:i A", $asset->created),
|
||||||
|
"price" => $asset->sale ? $asset->price ? '<i class="fal fa-pizza-slice"></i> '.$asset->price : "Free" : "Off-Sale"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $pages, "assets" => $assets]));
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
header("content-type: text/plain");
|
||||||
|
|
||||||
|
function sendSystemWebhook($message)
|
||||||
|
{
|
||||||
|
// example payload:
|
||||||
|
// $payload = ["username" => "test", "content" => "test", "avatar_url" => "https://polygon.pizzaboxer.xyz/thumbs/avatar?id=1&x=100&y=100"];
|
||||||
|
$payload = ["content" => $message];
|
||||||
|
$ch = curl_init();
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_URL, "https://discord.com/api/webhooks/");
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['payload_json' => json_encode($payload)]));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this should only be used if core.php does not work
|
||||||
|
$emergency = ($_GET["key"] ?? false) == "D5F6E2EAA6C07C991CA2895920A8BBA8BB66CA16";
|
||||||
|
$output = "";
|
||||||
|
$webhook = "";
|
||||||
|
$output_array = [];
|
||||||
|
|
||||||
|
if($emergency)
|
||||||
|
{
|
||||||
|
$webhook .= sprintf("[%s] Git Pull intiated by %s on %s\n", date('d/m/Y h:i:s A'), "[[[OVERRIDE]]]", $_SERVER["HTTP_HOST"]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"]."/api/private/core.php";
|
||||||
|
if(!Users::IsAdmin(Users::STAFF_ADMINISTRATOR)) die(http_response_code(404));
|
||||||
|
$webhook .= sprintf("[%s] Git Pull executed by %s on %s\n", date('d/m/Y h:i:s A'), SESSION["userName"], $_SERVER["HTTP_HOST"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
exec("git pull 2>&1", $output_array, $exitcode);
|
||||||
|
|
||||||
|
foreach($output_array as $line) $output .= "$line\n";
|
||||||
|
if($exitcode != 0) $output .= "\n\nGit exited with code $exitcode";
|
||||||
|
|
||||||
|
echo $output;
|
||||||
|
|
||||||
|
$webhook .= "```yaml\n";
|
||||||
|
$webhook .= $output;
|
||||||
|
$webhook .= "```";
|
||||||
|
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"]."/api/private/components/Discord.php";
|
||||||
|
Discord::SendToWebhook(["content" => $webhook], Discord::WEBHOOK_POLYGON, false);
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "admin" => Users::STAFF_ADMINISTRATOR, "admin_ratelimit" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(SESSION["userId"] != 1){ api::respond(400, false, "Insufficient admin level"); }
|
||||||
|
if(!isset($_POST["username"]) || !isset($_POST["amount"]) || !isset($_POST["reason"])){ api::respond(400, false, "Invalid Request"); }
|
||||||
|
if(!trim($_POST["username"])){ api::respond(400, false, "You haven't set a username"); }
|
||||||
|
|
||||||
|
if(!$_POST["amount"]){ api::respond(400, false, "You haven't set the amount of ".SITE_CONFIG["site"]["currency"]." to give"); }
|
||||||
|
if(!is_numeric($_POST["amount"])){ api::respond(400, false, "The amount of ".SITE_CONFIG["site"]["currency"]." to give must be numerical"); }
|
||||||
|
if($_POST["amount"] > 500 || $_POST["amount"] < -500){ api::respond(400, false, "Maximum amount of ".SITE_CONFIG["site"]["currency"]." you can give/take is 500 at a time"); }
|
||||||
|
|
||||||
|
if(!trim($_POST["reason"])){ api::respond(400, false, "You must set a reason"); }
|
||||||
|
|
||||||
|
$amount = $_POST["amount"];
|
||||||
|
$userInfo = Users::GetInfoFromName($_POST["username"]);
|
||||||
|
if(!$userInfo){ api::respond(400, false, "That user doesn't exist"); }
|
||||||
|
if(($userInfo->currency + $_POST["amount"]) < 0){ api::respond(400, false, "That'll make the user go bankrupt!"); }
|
||||||
|
|
||||||
|
$query = $pdo->prepare("UPDATE users SET currency = currency+:amount WHERE id = :uid");
|
||||||
|
$query->bindParam(":amount", $amount, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $userInfo->id, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
Users::LogStaffAction("[ Currency ] Gave ".$_POST["amount"]." ".SITE_CONFIG["site"]["currency"]." to ".$userInfo->username." ( user ID ".$userInfo->id." ) ( Reason: ".$_POST["reason"]." )");
|
||||||
|
api::respond(200, true, "Gave ".$_POST["amount"]." ".SITE_CONFIG["site"]["currency"]." to ".$userInfo->username);
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "admin" => Users::STAFF, "secure" => true]);
|
||||||
|
|
||||||
|
$assetId = $_POST['assetID'] ?? false;
|
||||||
|
$action = $_POST['action'] ?? false;
|
||||||
|
$action_sql = $action == "approve" ?: 2;
|
||||||
|
$reason = $_POST['reason'] ?? false;
|
||||||
|
$asset = Catalog::GetAssetInfo($assetId);
|
||||||
|
|
||||||
|
if(!in_array($action, ["approve", "decline"])) api::respond(400, false, "Invalid request");
|
||||||
|
if(!$asset) api::respond(400, false, "Asset does not exist");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("UPDATE assets SET approved = :action WHERE id IN (:id, :image)");
|
||||||
|
$query->bindParam(":action", $action_sql, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":id", $asset->id, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":image", $asset->imageID, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
Users::LogStaffAction('[ Asset Moderation ] '.ucfirst($action).'d "'.$asset->name.'" [ID '.$asset->id.']'.($reason ? ' with reason: '.$reason : ''));
|
||||||
|
api::respond(200, true, '"'.htmlspecialchars($asset->name).'" has been '.$action.'d');
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "admin" => [Users::STAFF_MODERATOR, Users::STAFF_ADMINISTRATOR], "admin_ratelimit" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["username"]) || !isset($_POST["banType"]) || !isset($_POST["moderationNote"]) || !isset($_POST["until"])){ api::respond(400, false, "Bad Request"); }
|
||||||
|
if($_POST["banType"] < 1 || $_POST["banType"] > 4){ api::respond(400, false, "Bad Request"); }
|
||||||
|
if($_POST["banType"] != 4 && empty($_POST["moderationNote"])){ api::respond(200, false, "You must supply a reason"); }
|
||||||
|
if(!trim($_POST["username"])){ api::respond(200, false, "You haven't set the username to ban"); }
|
||||||
|
if($_POST["banType"] == 2 && empty($_POST["until"])){ api::respond(200, false, "Ban time not set"); }
|
||||||
|
|
||||||
|
$banType = $_POST["banType"];
|
||||||
|
$staffNote = isset($_POST["staffNote"]) && $_POST["staffNote"] ? $_POST["staffNote"] : "";
|
||||||
|
$userId = SESSION["userId"];
|
||||||
|
$bannerInfo = Users::GetInfoFromName($_POST["username"]);
|
||||||
|
$reason = $_POST["moderationNote"];
|
||||||
|
$bannedUntil = $_POST["banType"] == 2 ? strtotime($_POST["until"]." ".date('G:i:s')) : 0;
|
||||||
|
|
||||||
|
if(!$bannerInfo){ api::respond(200, false, "User does not exist"); }
|
||||||
|
|
||||||
|
if($banType == 4)
|
||||||
|
{
|
||||||
|
if(!Users::GetUserModeration($bannerInfo->id)){ api::respond(200, false, "That user isn't banned!"); }
|
||||||
|
Users::UndoUserModeration($bannerInfo->id, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// if($bannerInfo->id == $userId){ api::respond(200, false, "You cannot moderate yourself!"); }
|
||||||
|
// if($bannerInfo->adminlevel){ api::respond(200, false, "You cannot moderate a staff member"); }
|
||||||
|
if(Users::GetUserModeration($bannerInfo->id)){ api::respond(200, false, "That user is already banned!"); }
|
||||||
|
if($banType == 2 && $bannedUntil < strtotime('tomorrow')){ api::respond(200, false, "Ban time must be at least 1 day long"); }
|
||||||
|
|
||||||
|
$query = $pdo->prepare("INSERT INTO bans (userId, bannerId, timeStarted, timeEnds, reason, banType, note) VALUES (:bid, :uid, UNIX_TIMESTAMP(), :ends, :reason, :type, :note)");
|
||||||
|
$query->bindParam(":bid", $bannerInfo->id, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":ends", $bannedUntil, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":reason", $reason, PDO::PARAM_STR);
|
||||||
|
$query->bindParam(":type", $banType, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":note", $staffNote, PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
$text =
|
||||||
|
[
|
||||||
|
1 => "warned",
|
||||||
|
2 => "banned for ".timeSince("@".($bannedUntil+1), false, false),
|
||||||
|
3 => "permanently banned",
|
||||||
|
4 => "unbanned"
|
||||||
|
];
|
||||||
|
|
||||||
|
$staff =
|
||||||
|
[
|
||||||
|
1 => "Warned ".$bannerInfo->username,
|
||||||
|
2 => "Banned ".$bannerInfo->username." for ".timeSince("@".($bannedUntil+1), false, false),
|
||||||
|
3 => "Permanently banned ".$bannerInfo->username,
|
||||||
|
4 => "Unbanned ".$bannerInfo->username
|
||||||
|
];
|
||||||
|
|
||||||
|
Users::LogStaffAction("[ User Moderation ] ".$staff[$banType]." ( user ID ".$bannerInfo->id." )");
|
||||||
|
api::respond(200, true, $bannerInfo->username." has been ".$text[$banType]);
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "admin" => [Users::STAFF_MODERATOR, Users::STAFF_ADMINISTRATOR], "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["banType"]) || !isset($_POST["moderationNote"]) || !isset($_POST["until"])){ api::respond(400, false, "Invalid Request"); }
|
||||||
|
if($_POST["banType"] < 1 || $_POST["banType"] > 3){ api::respond(400, false, "Invalid Request"); }
|
||||||
|
if(!trim($_POST["moderationNote"])){ api::respond(400, false, "You must supply a reason"); }
|
||||||
|
if($_POST["banType"] == 2 && !trim($_POST["until"])){ api::respond(400, false, "Ban time not set"); }
|
||||||
|
|
||||||
|
$banType = $_POST["banType"];
|
||||||
|
$bannedUntil = strtotime($_POST["until"]." ".date('G:i:s'));
|
||||||
|
|
||||||
|
if($bannedUntil < strtotime('tomorrow')){ api::respond(400, false, "Ban time must be at least 1 day long"); }
|
||||||
|
|
||||||
|
//markdown
|
||||||
|
$markdown = new Parsedown();
|
||||||
|
$markdown->setMarkupEscaped(true);
|
||||||
|
$markdown->setBreaksEnabled(true);
|
||||||
|
$markdown->setSafeMode(true);
|
||||||
|
$markdown->setUrlsLinked(true);
|
||||||
|
|
||||||
|
$text =
|
||||||
|
[
|
||||||
|
"title" =>
|
||||||
|
[
|
||||||
|
1 => "Warning",
|
||||||
|
2 => "Banned for ".timeSince("@".($bannedUntil+1), false, false),
|
||||||
|
3 => "Account Deleted"
|
||||||
|
],
|
||||||
|
|
||||||
|
"header" =>
|
||||||
|
[
|
||||||
|
1 => "This is just a heads-up to remind you to follow the rules",
|
||||||
|
2 => "Your account has been banned for violating our rules",
|
||||||
|
3 => "Your account has been permanently banned for violating our rules"
|
||||||
|
],
|
||||||
|
|
||||||
|
"footer" =>
|
||||||
|
[
|
||||||
|
1 => "Please re-read the <a href='/info/rules'>rules</a> and abide by them to prevent yourself from facing a ban",
|
||||||
|
2 => "Your ban ends at ".date('j/n/Y g:i:s A \G\M\T', $bannedUntil).", or in ".timeSince("@".($bannedUntil+1), true, false)." <br><br> Circumventing your ban on an alternate account while it is active may cause your ban time to be extended",
|
||||||
|
3 => "Circumventing your ban by using an alternate account will lower your chance of appeal (if your ban was appealable) and potentially warrant you an IP ban"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
ob_start(); ?>
|
||||||
|
<h2 class="font-weight-normal"><?=$text["title"][$banType]?></h2>
|
||||||
|
<p class="card-text"><?=$text["header"][$banType]?></p>
|
||||||
|
<p class="card-text">Done at: <?=date('j/n/Y g:i:s A \G\M\T')?></p>
|
||||||
|
<p class="card-text mb-0">Reason:</p>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<?=str_replace('<p>', '<p class="mb-0">', $markdown->text(trim($_POST["moderationNote"])))?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<p class="card-text"><?=$text["footer"][$banType]?></p>
|
||||||
|
<?php if($banType == 1) { ?>
|
||||||
|
<a href="#" class="btn btn-primary disabled">Reactivate</a>
|
||||||
|
<?php } api::respond(200, true, ob_get_clean());
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "admin" => Users::STAFF, "secure" => true]);
|
||||||
|
|
||||||
|
$renderType = $_POST['renderType'] ?? false;
|
||||||
|
$assetID = $_POST['assetID'] ?? false;
|
||||||
|
|
||||||
|
if(!$renderType) api::respond(400, false, "Bad Request");
|
||||||
|
if(!in_array($renderType, ["Avatar", "Asset"])) api::respond(400, false, "Invalid render type");
|
||||||
|
if(!$assetID || !is_numeric($assetID)) api::respond(400, false, "Bad Request");
|
||||||
|
|
||||||
|
if($renderType == "Asset")
|
||||||
|
{
|
||||||
|
$asset = Catalog::GetAssetInfo($assetID);
|
||||||
|
if(!$asset) api::respond(200, false, "The asset you requested does not exist");
|
||||||
|
switch($asset->type)
|
||||||
|
{
|
||||||
|
case 4: Polygon::RequestRender("Mesh", $assetID); break; // mesh
|
||||||
|
case 8: case 19: Polygon::RequestRender("Model", $assetID); break; // hat/gear
|
||||||
|
case 11: case 12: Polygon::RequestRender("Clothing", $assetID); break; // shirt/pants
|
||||||
|
case 17: Polygon::RequestRender("Head", $assetID); break; // head
|
||||||
|
case 10: Polygon::RequestRender("UserModel", $assetID); break; // user generated model
|
||||||
|
default: api::respond(200, false, "This asset cannot be re-rendered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if($renderType == "Avatar")
|
||||||
|
{
|
||||||
|
$user = Users::GetInfoFromID($assetID);
|
||||||
|
if(!$user) api::respond(200, false, "The user you requested does not exist");
|
||||||
|
Polygon::RequestRender("Avatar", $assetID);
|
||||||
|
}
|
||||||
|
|
||||||
|
Users::LogStaffAction("[ Render ] Re-rendered $renderType ID $assetID");
|
||||||
|
api::respond(200, true, "Render request has been successfully submitted! See render status <a href='/admin/render-queue'>here</a>");
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Image");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "admin" => [Users::STAFF_CATALOG, Users::STAFF_ADMINISTRATOR], "secure" => true]);
|
||||||
|
|
||||||
|
$file = $_FILES["file"] ?? false;
|
||||||
|
$name = $_POST["name"] ?? false;
|
||||||
|
$type = $_POST["type"] ?? false;
|
||||||
|
$uploadas = $_POST["creator"] ?? "Polygon";
|
||||||
|
$creator = Users::GetIDFromName($uploadas);
|
||||||
|
|
||||||
|
if(!$file) api::respond(200, false, "You must select a file");
|
||||||
|
if(!$name) api::respond(200, false, "You must specify a name");
|
||||||
|
if(strlen($name) > 50) api::respond(200, false, "Name cannot be longer than 50 characters");
|
||||||
|
if(!$creator) api::respond(400, false, "The user you're trying to create as does not exist");
|
||||||
|
if(Polygon::FilterText($name, false, false, true) != $name) api::respond(400, false, "The name contains inappropriate text");
|
||||||
|
|
||||||
|
//$lastCreation = $pdo->query("SELECT created FROM assets WHERE creator = 2 ORDER BY id DESC")->fetchColumn();
|
||||||
|
//if($lastCreation+60 > time()) api::respond(400, false, "Please wait ".(60-(time()-$lastCreation))." seconds before creating a new asset");
|
||||||
|
|
||||||
|
if($type == 1) //image - this is for textures and stuff
|
||||||
|
{
|
||||||
|
if(!in_array($file["type"], ["image/png", "image/jpg", "image/jpeg"])) api::respond(400, false, "Must be a .png or .jpg file");
|
||||||
|
|
||||||
|
Polygon::ImportLibrary("class.upload");
|
||||||
|
|
||||||
|
$image = new Upload($file);
|
||||||
|
if(!$image->uploaded) api::respond(500, false, "Failed to process image - please contact an admin");
|
||||||
|
$image->allowed = ['image/png', 'image/jpg', 'image/jpeg'];
|
||||||
|
$image->image_convert = 'png';
|
||||||
|
|
||||||
|
$imageId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1]);
|
||||||
|
Image::Process($image, ["name" => "$imageId", "resize" => false, "dir" => "/asset/files/"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "C"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]);
|
||||||
|
}
|
||||||
|
elseif($type == 3) // audio
|
||||||
|
{
|
||||||
|
if(!in_array($file["type"], ["audio/mpeg", "audio/ogg", "audio/mid", "audio/wav"])) api::respond(400, false, "Must be an mpeg, wav, ogg or midi audio. - ".$file["type"]);
|
||||||
|
$assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "audioType" => $file["type"], "approved" => 1]);
|
||||||
|
copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId);
|
||||||
|
Image::RenderFromStaticImage("audio", $assetId);
|
||||||
|
}
|
||||||
|
elseif($type == 4) //mesh
|
||||||
|
{
|
||||||
|
if(!str_ends_with($file["name"], ".mesh")) api::respond(400, false, "Must be a .mesh file");
|
||||||
|
$assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1]);
|
||||||
|
copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId);
|
||||||
|
Polygon::RequestRender("Mesh", $assetId);
|
||||||
|
}
|
||||||
|
elseif($type == 5) //lua
|
||||||
|
{
|
||||||
|
if(!str_ends_with($file["name"], ".lua")) api::respond(400, false, "Must be a .lua file");
|
||||||
|
$assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1]);
|
||||||
|
copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId);
|
||||||
|
Image::RenderFromStaticImage("Script", $assetId);
|
||||||
|
}
|
||||||
|
elseif($type == 8) //hat
|
||||||
|
{
|
||||||
|
if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) api::respond(400, false, "Must be a .rbxm or .xml file");
|
||||||
|
$assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1]);
|
||||||
|
copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId);
|
||||||
|
Polygon::RequestRender("Model", $assetId);
|
||||||
|
}
|
||||||
|
elseif($type == 17) //head
|
||||||
|
{
|
||||||
|
if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) api::respond(400, false, "Must be a .rbxm or .xml file");
|
||||||
|
$assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1]);
|
||||||
|
copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId);
|
||||||
|
Polygon::RequestRender("Head", $assetId);
|
||||||
|
}
|
||||||
|
elseif($type == 18) //faces are literally just decals lmao (with a minor alteration to the xml)
|
||||||
|
{
|
||||||
|
if(!in_array($file["type"], ["image/png", "image/jpg", "image/jpeg"])) api::respond(400, false, "Must be a .png or .jpg file");
|
||||||
|
|
||||||
|
Polygon::ImportLibrary("class.upload");
|
||||||
|
|
||||||
|
$image = new Upload($file);
|
||||||
|
if(!$image->uploaded) api::respond(500, false, "Failed to process image - please contact an admin");
|
||||||
|
$image->allowed = ['image/png', 'image/jpg', 'image/jpeg'];
|
||||||
|
$image->image_convert = 'png';
|
||||||
|
|
||||||
|
$imageId = Catalog::CreateAsset(["type" => 1, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1]);
|
||||||
|
Image::Process($image, ["name" => "$imageId", "resize" => false, "dir" => "/asset/files/"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "C"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]);
|
||||||
|
|
||||||
|
$itemId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "imageID" => $imageId, "approved" => 1]);
|
||||||
|
|
||||||
|
file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML("Face", $imageId));
|
||||||
|
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 420, 230);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 420, 420);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 352, 352);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 250, 250);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 110, 110);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 100, 100);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 75, 75);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 48, 48);
|
||||||
|
}
|
||||||
|
elseif($type == 19) //gear
|
||||||
|
{
|
||||||
|
if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) api::respond(400, false, "Must be a .rbxm or .xml file");
|
||||||
|
|
||||||
|
$assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => "", "approved" => 1, "gear_attributes" => '{"melee":false,"powerup":false,"ranged":false,"navigation":false,"explosive":false,"musical":false,"social":false,"transport":false,"building":false}']);
|
||||||
|
copy($file["tmp_name"], $_SERVER['DOCUMENT_ROOT']."/asset/files/".$assetId);
|
||||||
|
Polygon::RequestRender("Model", $assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Users::LogStaffAction("[ Asset creation ] Created \"$name\" [ID ".($itemId ?? $assetId ?? $imageId)."]");
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "<a href='/item?ID=".($itemId ?? $assetId ?? $imageId)."'>".Catalog::GetTypeByNum($type)."</a> successfully created!"]);
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize();
|
||||||
|
|
||||||
|
if(!isset($_GET['assetID'])) api::respond(400, false, "Bad Request");
|
||||||
|
|
||||||
|
$assetID = $_GET['assetID'];
|
||||||
|
$page = $_GET['page'] ?? 1;
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM asset_comments WHERE assetID = :id");
|
||||||
|
$query->bindParam(":id", $assetID, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$pages = ceil($query->fetchColumn()/15);
|
||||||
|
$offset = ($page - 1)*15;
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT asset_comments.*, users.username FROM asset_comments INNER JOIN users ON users.id = asset_comments.author WHERE assetID = :id ORDER BY id DESC");
|
||||||
|
$query->bindParam(":id", $assetID, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
if(!$query->rowCount()) api::respond(200, true, "This asset has no comments");
|
||||||
|
|
||||||
|
$comments = [];
|
||||||
|
|
||||||
|
while($row = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$comments[] =
|
||||||
|
[
|
||||||
|
"time" => strtolower(timeSince($row->time)),
|
||||||
|
"commenter_name" => $row->username,
|
||||||
|
"commenter_id" => $row->author,
|
||||||
|
"commenter_avatar" => Thumbnails::GetAvatar($row->author, 110, 110),
|
||||||
|
"content" => nl2br(Polygon::FilterText($row->content))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "comments" => $comments, "pages" => $pages]);
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST['assetID']) || !isset($_POST['content']));
|
||||||
|
|
||||||
|
$uid = SESSION["userId"];
|
||||||
|
$id = $_POST['assetID'];
|
||||||
|
$content = $_POST['content'];
|
||||||
|
|
||||||
|
$item = Catalog::GetAssetInfo($id);
|
||||||
|
if(!$item) api::respond(400, false, "Asset does not exist");
|
||||||
|
if(!$item->comments) api::respond(400, false, "Comments are unavailable for this asset");
|
||||||
|
if(!strlen($content)) api::respond(400, false, "Comment cannot be empty");
|
||||||
|
if(strlen($content) > 100) api::respond(400, false, "Comment cannot be longer than 128 characters");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT time FROM asset_comments WHERE time+60 > UNIX_TIMESTAMP() AND author = :uid");
|
||||||
|
$query->bindParam(":uid", $uid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
if($query->rowCount()) api::respond(400, false, "Please wait ".GetReadableTime($query->fetchColumn(), ["RelativeTime" => "1 minute"])." before posting a new comment");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("INSERT INTO asset_comments (author, content, assetID, time) VALUES (:uid, :content, :aid, UNIX_TIMESTAMP())");
|
||||||
|
$query->bindParam(":uid", $uid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":content", $content, PDO::PARAM_STR);
|
||||||
|
$query->bindParam(":aid", $id, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
function getPrice($price)
|
||||||
|
{
|
||||||
|
return $price ? '<span class="text-success"><i class="fal fa-pizza-slice"></i> '.$price.'</span>' : '<span class="text-success">Free</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$uid = SESSION["userId"];
|
||||||
|
$id = $_POST['id'] ?? false;
|
||||||
|
$price = $_POST['price'] ?? 0;
|
||||||
|
|
||||||
|
$item = Catalog::GetAssetInfo($id);
|
||||||
|
if(!$item) api::respond(400, false, "Asset does not exist");
|
||||||
|
if(Catalog::OwnsAsset($uid, $id)) api::respond(400, false, "User already owns asset");
|
||||||
|
if(!$item->sale) api::respond(400, false, "Asset is off-sale");
|
||||||
|
if(SESSION["currency"] - $item->price < 0) api::respond(400, false, "User cannot afford asset");
|
||||||
|
|
||||||
|
if($item->price != $price)
|
||||||
|
{
|
||||||
|
die(json_encode(
|
||||||
|
[
|
||||||
|
"status" => 200,
|
||||||
|
"success" => true,
|
||||||
|
"message" => "Item price changed",
|
||||||
|
"header" => "Item Price Has Changed",
|
||||||
|
"text" => 'While you were shopping, the price of this item changed from '.getPrice($price).' to '.getPrice($item->price).'.',
|
||||||
|
"buttons" => [['class'=>'btn btn-success btn-confirm-purchase', 'text'=>'Buy Now'], ['class'=>'btn btn-secondary', 'dismiss'=>true, 'text'=>'Cancel']],
|
||||||
|
"footer" => 'Your balance after this transaction will be <i class="fal fa-pizza-slice"></i> '.(SESSION["currency"] - $item->price),
|
||||||
|
"newprice" => $item->price
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$IsAlt = false;
|
||||||
|
|
||||||
|
foreach(Users::GetAlternateAccounts($item->creator) as $alt)
|
||||||
|
{
|
||||||
|
if($alt["userid"] == $uid) $IsAlt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$IsAlt)
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("UPDATE users SET currency = currency - :price WHERE id = :uid; UPDATE users SET currency = currency + :price WHERE id = :seller");
|
||||||
|
$query->bindParam(":price", $item->price, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $uid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":seller", $item->creator, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $pdo->prepare("INSERT INTO ownedAssets (assetId, userId, timestamp) VALUES (:aid, :uid, UNIX_TIMESTAMP())");
|
||||||
|
$query->bindParam(":aid", $id, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":uid", $uid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$query = $pdo->prepare("INSERT INTO transactions (purchaser, seller, assetId, amount, timestamp) VALUES (:uid, :sid, :aid, :price, UNIX_TIMESTAMP())");
|
||||||
|
$query->bindParam(":uid", $uid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":sid", $item->creator, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":aid", $id, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":price", $item->price, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
if(time() < strtotime("2021-09-07 00:00:00") && $id == 2692 && !Catalog::OwnsAsset(SESSION["userId"], 2800))
|
||||||
|
{
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO ownedAssets (assetId, userId, timestamp) VALUES (2800, :uid, UNIX_TIMESTAMP())",
|
||||||
|
[":uid" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(
|
||||||
|
[
|
||||||
|
"status" => 200,
|
||||||
|
"success" => true,
|
||||||
|
"message" => "OK",
|
||||||
|
"header" => "Purchase Complete!",
|
||||||
|
"image" => Thumbnails::GetAsset($item, 110, 110),
|
||||||
|
"text" => "You have successfully purchased the ".htmlspecialchars($item->name)." ".Catalog::GetTypeByNum($item->type)." from ".$item->username." for ".getPrice($item->price),
|
||||||
|
"buttons" => [['class' => 'btn btn-primary continue-shopping', 'dismiss' => true, 'text' => 'Continue Shopping']],
|
||||||
|
]));
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$type = $_POST["type"] ?? false;
|
||||||
|
$page = $_POST["page"] ?? 1;
|
||||||
|
$assets = [];
|
||||||
|
|
||||||
|
if(!Catalog::GetTypeByNum($type)) api::respond(400, false, "Invalid asset type");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT * FROM assets WHERE creator = :uid AND type = :type ORDER BY id DESC");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
while($asset = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$info = Catalog::GetAssetInfo($asset->id);
|
||||||
|
|
||||||
|
$assets[] =
|
||||||
|
[
|
||||||
|
"name" => htmlspecialchars($asset->name),
|
||||||
|
"id" => $asset->id,
|
||||||
|
"thumbnail" => Thumbnails::GetAsset($asset, 110, 110),
|
||||||
|
"item_url" => "/".encode_asset_name($asset->name)."-item?id=".$asset->id,
|
||||||
|
"config_url" => "/my/item?ID=".$asset->id,
|
||||||
|
"created" => date("n/j/Y", $asset->created),
|
||||||
|
"sales-total" => $info->sales_total,
|
||||||
|
"sales-week" => $info->sales_week
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "assets" => $assets]));
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Image");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$file = $_FILES["file"] ?? false;
|
||||||
|
$name = $_POST["name"] ?? false;
|
||||||
|
$type = $_POST["type"] ?? false;
|
||||||
|
|
||||||
|
if(!$file) api::respond(200, false, "You must select a file");
|
||||||
|
if(!in_array($file["type"], ["image/png", "image/jpg", "image/jpeg"])) api::respond(200, false, "Must be a .png or .jpg file");
|
||||||
|
if(!$name) api::respond(200, false, "You must specify a name");
|
||||||
|
if(Polygon::IsExplicitlyFiltered($name)) api::respond(200, false, "The name contains inappropriate text");
|
||||||
|
if(!in_array($type, [2, 11, 12, 13])) api::respond(200, false, "You can't upload that type of content!");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT created FROM assets WHERE creator = :uid ORDER BY id DESC");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
$lastCreation = $query->fetchColumn();
|
||||||
|
if($lastCreation+30 > time()) api::respond(200, false, "Please wait ".(30-(time()-$lastCreation))." seconds before creating a new asset");
|
||||||
|
|
||||||
|
// tshirts are a bit messy but straightforward:
|
||||||
|
// the image asset itself must be 128x128 with the texture resized to preserve aspect ratio
|
||||||
|
// the image thumbnail should have the texture positioned top
|
||||||
|
//
|
||||||
|
// shirts and pants should ideally be 585x559 but it doesnt really matter -
|
||||||
|
// just as long as it looks right on the avatar. if it doesnt then disapprove
|
||||||
|
//
|
||||||
|
// decals are a lot more messy:
|
||||||
|
// the image asset itself is scaled to be 256 pixels in width, while preserving the texture ratio
|
||||||
|
// the image thumbnail should have the texture positioned center
|
||||||
|
// the decal asset however must have the texture stretched to 1:1 for all its respective sizes
|
||||||
|
// [example: https://www.roblox.com/Item.aspx?ID=8553820]
|
||||||
|
//
|
||||||
|
// we won't have to worry about image size constraints as they're always gonna be
|
||||||
|
// resized to fit in a smaller resolution
|
||||||
|
//
|
||||||
|
// refer to here for the thumbnail sizes: https://github.com/matthewdean/roblox-web-apis
|
||||||
|
//
|
||||||
|
// THUMBNAIL SIZES FOR EACH ITEM TYPE
|
||||||
|
// legend: [f = fit] [t = top] [c = center] [s = stretch] // [M = Model] [He = Head] [S = Shirt] [P = Pants]
|
||||||
|
//
|
||||||
|
// | 48x48 | 60x62 | 75x75 | 100x100 | 110x110 | 160x100 | 250x250 | 352x352 | 420x230 | 420x420 |
|
||||||
|
// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+
|
||||||
|
// Image | |yes (f)| | | | | | | | yes (t) |
|
||||||
|
// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+
|
||||||
|
// T-Shirt | | | | yes | yes | | | | | yes |
|
||||||
|
// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+
|
||||||
|
// Audio | | | yes | yes | yes | | yes | yes | yes | yes |
|
||||||
|
// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+
|
||||||
|
// Hat/Gear | yes | | yes | yes | yes | | yes | yes | yes (fc)| yes |
|
||||||
|
// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+
|
||||||
|
// Place | yes |yes(fc)| yes | yes | yes | yes | yes | yes | yes | yes |
|
||||||
|
// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+
|
||||||
|
// M/He/S/P | yes | | yes | yes | yes | | yes | yes | | yes |
|
||||||
|
// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+
|
||||||
|
// Decal/Face |yes (s)| |yes (s)| yes (s) | yes (s) | | yes (s) | yes (s) | yes (s) | yes (s) |
|
||||||
|
// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+
|
||||||
|
|
||||||
|
Polygon::ImportLibrary("class.upload");
|
||||||
|
|
||||||
|
$image = new Upload($file);
|
||||||
|
if(!$image->uploaded) api::respond(200, false, "Failed to process image - please contact an admin");
|
||||||
|
$image->allowed = ['image/png', 'image/jpg', 'image/jpeg'];
|
||||||
|
$image->image_convert = 'png';
|
||||||
|
|
||||||
|
$imageId = Catalog::CreateAsset(["type" => 1, "creator" => SESSION["userId"], "name" => $name, "description" => Catalog::GetTypeByNum($type)." Image"]);
|
||||||
|
|
||||||
|
if($type == 2) //tshirt
|
||||||
|
{
|
||||||
|
Image::Process($image, ["name" => "$imageId", "keepRatio" => true, "align" => "T", "x" => 128, "y" => 128, "dir" => "/asset/files/"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "T"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "T"]);
|
||||||
|
|
||||||
|
$itemId = Catalog::CreateAsset(["type" => 2, "creator" => SESSION["userId"], "name" => $name, "description" => "T-Shirt", "imageID" => $imageId]);
|
||||||
|
|
||||||
|
file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML("T-Shirt", $imageId));
|
||||||
|
|
||||||
|
//process initial tshirt thumbnail
|
||||||
|
$template = imagecreatefrompng($_SERVER['DOCUMENT_ROOT']."/img/tshirt-template.png");
|
||||||
|
$shirtdecal = Image::Resize(SITE_CONFIG['paths']['thumbs_assets']."/$imageId-420x420.png", 250, 250);
|
||||||
|
imagesavealpha($template, true);
|
||||||
|
imagesavealpha($shirtdecal, true);
|
||||||
|
Image::MergeLayers($template, $shirtdecal, 85, 85, 0, 0, 250, 250, 100);
|
||||||
|
|
||||||
|
imagepng($template, SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png");
|
||||||
|
Image::Resize(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png", 100, 100, SITE_CONFIG['paths']['thumbs_assets']."/$itemId-100x100.png");
|
||||||
|
Image::Resize(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png", 110, 110, SITE_CONFIG['paths']['thumbs_assets']."/$itemId-110x110.png");
|
||||||
|
|
||||||
|
Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-100x100.png");
|
||||||
|
Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-110x110.png");
|
||||||
|
Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png");
|
||||||
|
}
|
||||||
|
elseif($type == 11 || $type == 12) //shirt / pants
|
||||||
|
{
|
||||||
|
Image::Process($image, ["name" => "$imageId", "x" => 585, "y" => 559, "dir" => "/asset/files/"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "C"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]);
|
||||||
|
|
||||||
|
$itemId = Catalog::CreateAsset(["type" => $type, "creator" => SESSION["userId"], "name" => $name, "description" => Catalog::GetTypeByNum($type), "imageID" => $imageId]);
|
||||||
|
file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML(Catalog::GetTypeByNum($type), $imageId));
|
||||||
|
Polygon::RequestRender("Clothing", $itemId);
|
||||||
|
}
|
||||||
|
elseif($type == 13) //decal
|
||||||
|
{
|
||||||
|
Image::Process($image, ["name" => "$imageId", "x" => 256, "scaleY" => true, "dir" => "/asset/files/"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 60, 62, ["keepRatio" => true, "align" => "C"]);
|
||||||
|
Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]);
|
||||||
|
|
||||||
|
$itemId = Catalog::CreateAsset(["type" => 13, "creator" => SESSION["userId"], "name" => $name, "description" => "Decal", "imageID" => $imageId]);
|
||||||
|
|
||||||
|
file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML("Decal", $imageId));
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 48, 48);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 75, 75);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 100, 100);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 110, 110);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 250, 250);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 352, 352);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 420, 230);
|
||||||
|
Thumbnails::UploadAsset($image, $itemId, 420, 420);
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => Catalog::GetTypeByNum($type)." successfully created!"]);
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"]."/api/private/core.php";
|
||||||
|
api::initialize(["method" => "GET", "api" => "DiscordBot"]);
|
||||||
|
|
||||||
|
if (isset($_GET["Token"]) && isset($_GET["DiscordID"]))
|
||||||
|
{
|
||||||
|
$userInfo = db::run("SELECT * FROM users WHERE discordKey = :key", [":key" => $_GET["Token"]])->fetch(PDO::FETCH_OBJ);
|
||||||
|
if (!$userInfo) api::respond(200, false, "InvalidKey"); // check if verification key is valid
|
||||||
|
if ($userInfo->discordID != NULL) api::respond(200, false, "AlreadyVerified"); // check if mercury account is already verified
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"UPDATE users SET discordID = :id, discordVerifiedTime = UNIX_TIMESTAMP() WHERE discordKey = :key",
|
||||||
|
[":id" => $_GET["DiscordID"], ":key" => $_GET["Token"]]
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, $userInfo->username);
|
||||||
|
}
|
||||||
|
else if (isset($_GET["DiscordID"]))
|
||||||
|
{
|
||||||
|
$username = db::run("SELECT username FROM users WHERE discordID = :id", [":id" => $_GET["DiscordID"]]);
|
||||||
|
if (!$username->rowCount()) api::respond(200, false, "NotVerified"); // check if discord account is already verified
|
||||||
|
api::respond(200, true, $username->fetchColumn());
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond(400, false, "Bad Request");
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"]."/api/private/core.php";
|
||||||
|
api::initialize(["method" => "GET", "api" => "DiscordBot"]);
|
||||||
|
|
||||||
|
if (isset($_GET["UserName"]))
|
||||||
|
{
|
||||||
|
$userInfo = db::run(
|
||||||
|
"SELECT id, username, blurb, adminlevel, jointime, lastonline, discordID FROM users WHERE username = :name",
|
||||||
|
[":name" => $_GET["UserName"]]
|
||||||
|
)->fetch(PDO::FETCH_OBJ);
|
||||||
|
if (!$userInfo) api::respond(200, false, "DoesntExist");
|
||||||
|
}
|
||||||
|
else if (isset($_GET["DiscordID"]))
|
||||||
|
{
|
||||||
|
$userInfo = db::run(
|
||||||
|
"SELECT id, username, blurb, adminlevel, jointime, lastonline, discordID FROM users WHERE discordID = :id",
|
||||||
|
[":id" => $_GET["DiscordID"]]
|
||||||
|
)->fetch(PDO::FETCH_OBJ);
|
||||||
|
if (!$userInfo) api::respond(200, false, "NotVerified");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
api::respond(400, false, "Bad Request");
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInfo->blurb = str_ireplace(["@everyone", "@here"], ["[everyone]", "[here]"], $userInfo->blurb);
|
||||||
|
$userInfo->blurb = preg_replace("/<(@[0-9]+)>/i", "[$1]", $userInfo->blurb);
|
||||||
|
api::respond(200, true, $userInfo);
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$friendid = $_POST['friendID'];
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT * FROM friends WHERE id = :id AND status = 0");
|
||||||
|
$query->bindParam(":id", $friendid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
$friendInfo = $query->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if(!$friendInfo) api::respond(400, false, "Friend request doesn't exist");
|
||||||
|
if($friendInfo->receiverId != SESSION["userId"]) api::respond(400, false, "You are not the receiver of this friend request");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("UPDATE friends SET status = 1 WHERE id = :id");
|
||||||
|
$query->bindParam(":id", $friendid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$page = $_POST['page'] ?? 1;
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM friends WHERE receiverId = :uid AND status = 0");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$pages = ceil($query->fetchColumn()/18);
|
||||||
|
$offset = ($page - 1)*18;
|
||||||
|
|
||||||
|
if(!$pages) api::respond(200, true, "You're all up-to-date with your friend requests!");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT * FROM friends WHERE receiverId = :uid AND status = 0 LIMIT 18 OFFSET :offset");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":offset", $offset, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$friends = [];
|
||||||
|
|
||||||
|
while($row = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$friends[] =
|
||||||
|
[
|
||||||
|
"username" => Users::GetNameFromID($row->requesterId),
|
||||||
|
"userid" => $row->requesterId,
|
||||||
|
"avatar" => Thumbnails::GetAvatar($row->requesterId, 250, 250),
|
||||||
|
"friendid" => $row->id
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "requests" => $friends, "pages" => $pages]);
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
$url = $_SERVER['HTTP_REFERER'] ?? false;
|
||||||
|
$userId = $_POST['userID'] ?? false;
|
||||||
|
$page = $_POST['page'] ?? 1;
|
||||||
|
$order = strpos($url, "/home") ? "lastonline DESC" : "id";
|
||||||
|
$limit = strpos($url, "/friends") ? 18 : 6;
|
||||||
|
$self = str_ends_with($url, "/user") || str_ends_with($url, "/friends") || strpos($url, "/home");
|
||||||
|
|
||||||
|
if(!Users::GetInfoFromID($userId)) api::respond(400, false, "User does not exist");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM friends WHERE :uid IN (requesterId, receiverId) AND status = 1");
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$pages = ceil($query->fetchColumn()/$limit);
|
||||||
|
$offset = ($page - 1)*$limit;
|
||||||
|
|
||||||
|
if(!$pages) api::respond(200, true, ($self ? "You do" : Users::GetNameFromID($userId)." does")."n't have any friends");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("
|
||||||
|
SELECT friends.*, users.username, users.id AS userId, users.status, users.lastonline FROM friends
|
||||||
|
INNER JOIN users ON users.id = (CASE WHEN requesterId = :uid THEN receiverId ELSE requesterId END)
|
||||||
|
WHERE :uid IN (requesterId, receiverId) AND friends.status = 1
|
||||||
|
ORDER BY $order LIMIT :limit OFFSET :offset");
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":limit", $limit, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":offset", $offset, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$friends = [];
|
||||||
|
|
||||||
|
while($row = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$friends[] =
|
||||||
|
[
|
||||||
|
"username" => $row->username,
|
||||||
|
"userid" => $row->userId,
|
||||||
|
"avatar" => Thumbnails::GetAvatar($row->userId, 250, 250),
|
||||||
|
"friendid" => $row->id,
|
||||||
|
"status" => Polygon::FilterText($row->status)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "friends" => $friends, "pages" => $pages]);
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$friendid = $_POST['friendID'];
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT * FROM friends WHERE id = :id AND NOT status = 2");
|
||||||
|
$query->bindParam(":id", $friendid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
$friendInfo = $query->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if(!$friendInfo) api::respond(400, false, "Friend connection doesn't exist");
|
||||||
|
if(!in_array($userid, [$friendInfo->requesterId, $friendInfo->receiverId])) api::respond(400, false, "You are not a part of this friend connection");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("UPDATE friends SET status = 2 WHERE id = :id");
|
||||||
|
$query->bindParam(":id", $friendid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
$friendid = $_POST['userID'] ?? false;
|
||||||
|
|
||||||
|
if(!$friendid) api::respond(400, false, "Bad Request");
|
||||||
|
if($friendid == $userid) api::respond(400, false, "You can't perform friend operations on yourself");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT status FROM friends WHERE :uid IN (requesterId, receiverId) AND :rid IN (requesterId, receiverId) AND NOT status = 2");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":rid", $friendid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
if($query->rowCount()) api::respond(400, false, "Friend connection already exists");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT timeSent FROM friends WHERE requesterId = :uid AND timeSent+30 > UNIX_TIMESTAMP()");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
if($query->rowCount()) api::respond(400, false, "Please wait ".(($query->fetchColumn()+30)-time())." seconds before sending another request");
|
||||||
|
|
||||||
|
$query = $pdo->prepare("INSERT INTO friends (requesterId, receiverId, timeSent) VALUES (:uid, :rid, UNIX_TIMESTAMP())");
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":rid", $friendid, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true]);
|
||||||
|
|
||||||
|
$client = $_POST["client"] ?? "false";
|
||||||
|
$creator = $_POST["creator"] ?? false;
|
||||||
|
$page = $_POST["page"] ?? 1;
|
||||||
|
$pages = 1;
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
$query_params = "1";
|
||||||
|
$value_params = [];
|
||||||
|
|
||||||
|
if($client !== "false")
|
||||||
|
{
|
||||||
|
if(!in_array($client, [2009, 2010, 2011, 2012])) api::respond(400, false, "Bad Request");
|
||||||
|
$query_params .= " AND version = :version";
|
||||||
|
$value_params[":version"] = $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($creator)
|
||||||
|
{
|
||||||
|
$query_params .= " AND hoster = :uid";
|
||||||
|
$value_params[":uid"] = $creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
$servercount = db::run("SELECT COUNT(*) FROM selfhosted_servers WHERE $query_params", $value_params)->fetchColumn();
|
||||||
|
$pages = ceil($servercount/10);
|
||||||
|
$offset = ($page - 1)*10;
|
||||||
|
|
||||||
|
$servers = db::run("
|
||||||
|
SELECT *,
|
||||||
|
(SELECT COUNT(*) FROM client_sessions WHERE ping+35 > UNIX_TIMESTAMP() AND serverID = selfhosted_servers.id AND valid) AS players,
|
||||||
|
(ping+35 > UNIX_TIMESTAMP()) AS online
|
||||||
|
FROM selfhosted_servers WHERE $query_params
|
||||||
|
ORDER BY online DESC, players DESC, ping DESC, created DESC LIMIT 10 OFFSET $offset", $value_params);
|
||||||
|
|
||||||
|
if(!$servers->rowCount()) api::respond(200, true, "No servers matched your query");
|
||||||
|
while($server = $servers->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$gears = [];
|
||||||
|
foreach(json_decode($server->allowed_gears, true) as $gear_attr => $gear_val)
|
||||||
|
if($gear_val) $gears[] = ["name" => Catalog::$GearAttributesDisplay[$gear_attr]["text_sel"], "icon" => Catalog::$GearAttributesDisplay[$gear_attr]["icon"]];
|
||||||
|
$items[] =
|
||||||
|
[
|
||||||
|
"server_name" => Polygon::FilterText($server->name),
|
||||||
|
"server_description" => strlen($server->description) ? Polygon::FilterText($server->description) : "No description available.",
|
||||||
|
"server_id" => $server->id,
|
||||||
|
"server_thumbnail" => Thumbnails::GetAvatar($server->hoster, 420, 420),
|
||||||
|
"hoster_name" => Users::GetNameFromID($server->hoster),
|
||||||
|
"hoster_id" => $server->hoster,
|
||||||
|
"date" => date('n/d/Y g:i:s A', $server->created),
|
||||||
|
"version" => $server->version,
|
||||||
|
"server_online" => $server->ping+35 > time() ? true : false,
|
||||||
|
"players_online" =>$server->ping+35 > time() ? $server->players : 0,
|
||||||
|
"players_max" => $server->maxplayers,
|
||||||
|
"gears" => $gears
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $pages, "items" => $items]));
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
header("Pragma: no-cache");
|
||||||
|
header("Cache-Control: no-cache");
|
||||||
|
api::initialize(["method" => "GET"]);//, "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!SITE_CONFIG["site"]["games"]) api::respond(200, false, "Games are temporarily disabled for maintenance");
|
||||||
|
|
||||||
|
$serverID = $_GET["serverID"] ?? $_GET['placeId'] ?? false;
|
||||||
|
$isTeleport = isset($_GET["isTeleport"]) && $_GET['isTeleport'] == "true";
|
||||||
|
|
||||||
|
if($isTeleport && $_SERVER["HTTP_USER_AGENT"] != "Roblox/WinInet")
|
||||||
|
api::respond_custom([
|
||||||
|
"Error" => "Request is not authorized from specified origin",
|
||||||
|
"userAgent" => $_SERVER["HTTP_USER_AGENT"] ?? null,
|
||||||
|
"referrer" => $_SERVER["HTTP_REFERER"] ?? null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT *, (SELECT COUNT(*) FROM client_sessions WHERE ping+35 > UNIX_TIMESTAMP() AND serverID = selfhosted_servers.id AND valid) AS players FROM selfhosted_servers WHERE id = :sid");
|
||||||
|
$query->bindParam(":sid", $serverID, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
$serverInfo = $query->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if(!$serverInfo) api::respond(400, false, "Server does not exist");
|
||||||
|
if($serverInfo->players >= $serverInfo->maxplayers) api::respond(200, false, "This server is currently full. Please try again later");
|
||||||
|
|
||||||
|
if($isTeleport)
|
||||||
|
{
|
||||||
|
$ticket = $_COOKIE['ticket'] ?? false;
|
||||||
|
$query = $pdo->prepare("SELECT uid FROM client_sessions WHERE ticket = :ticket");
|
||||||
|
$query->bindParam(":ticket", $ticket, PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
if(!$query->rowCount()) api::respond_custom(["Error" => "You are not logged in"]);
|
||||||
|
$userid = $query->fetchColumn();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(!SESSION) api::respond(400, false, "You are not logged in");
|
||||||
|
$userid = SESSION["userId"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket = generateUUID();
|
||||||
|
$securityTicket = generateUUID();
|
||||||
|
$query = $pdo->prepare("INSERT INTO client_sessions (ticket, securityTicket, uid, sessionType, serverID, created, isTeleport) VALUES (:uuid, :security, :uid, 1, :sid, UNIX_TIMESTAMP(), :teleport)");
|
||||||
|
$query->bindParam(":uuid", $ticket, PDO::PARAM_STR);
|
||||||
|
$query->bindParam(":security", $securityTicket, PDO::PARAM_STR);
|
||||||
|
$query->bindParam(":uid", $userid, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":sid", $serverID, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":teleport", $isTeleport, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$Protocol = "https";
|
||||||
|
if($serverInfo->version == 2009) $Protocol = "http";
|
||||||
|
|
||||||
|
api::respond_custom([
|
||||||
|
"status" => 200,
|
||||||
|
"success" => true,
|
||||||
|
"message" => "OK",
|
||||||
|
"version" => $serverInfo->version,
|
||||||
|
"joinScriptUrl" => "{$Protocol}://{$_SERVER['HTTP_HOST']}/game/join?ticket={$ticket}",
|
||||||
|
// these last few params are for teleportservice and lack any function - just ignore
|
||||||
|
"authenticationUrl" => "{$Protocol}://{$_SERVER['HTTP_HOST']}/Login/Negotiate.ashx",
|
||||||
|
"authenticationTicket" => "0",
|
||||||
|
"status" => 2
|
||||||
|
]);
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) api::respond(400, false, "Page is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$Page = $_POST["Page"] ?? 1;
|
||||||
|
$Members = [];
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
$Rank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
|
||||||
|
if($Rank->Level == 0) api::respond(200, false, "You are not a member of this group");
|
||||||
|
if(!$Rank->Permissions->CanManageGroupAdmin) api::respond(200, false, "You are not allowed to perform this action");
|
||||||
|
|
||||||
|
$MemberCount = db::run(
|
||||||
|
"SELECT COUNT(*) FROM groups_members
|
||||||
|
WHERE GroupID = :GroupID AND Rank < :RankLevel AND NOT Pending",
|
||||||
|
[":GroupID" => $GroupID, ":RankLevel" => $Rank->Level]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
$Pages = ceil($MemberCount/12);
|
||||||
|
$Offset = ($Page - 1)*12;
|
||||||
|
|
||||||
|
if(!$Pages) api::respond(200, true, "This group does not have any members.");
|
||||||
|
|
||||||
|
$MembersQuery = db::run(
|
||||||
|
"SELECT users.username, users.id, Rank FROM groups_members
|
||||||
|
INNER JOIN users ON users.id = groups_members.UserID
|
||||||
|
WHERE GroupID = :GroupID AND Rank < :RankLevel AND NOT Pending
|
||||||
|
ORDER BY Joined DESC LIMIT 12 OFFSET $Offset",
|
||||||
|
[":GroupID" => $GroupID, ":RankLevel" => $Rank->Level]
|
||||||
|
);
|
||||||
|
|
||||||
|
while($Member = $MembersQuery->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$Members[] =
|
||||||
|
[
|
||||||
|
"UserName" => $Member->username,
|
||||||
|
"UserID" => $Member->id,
|
||||||
|
"RoleLevel" => $Member->Rank,
|
||||||
|
"Avatar" => Thumbnails::GetAvatar($Member->id, 250, 250)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "count" => $MemberCount, "items" => $Members]));
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$Roles = [];
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
$Rank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
|
||||||
|
if($Rank->Level == 0) api::respond(200, false, "You are not a member of this group");
|
||||||
|
if(!$Rank->Permissions->CanManageGroupAdmin) api::respond(200, false, "You are not allowed to perform this action");
|
||||||
|
|
||||||
|
if($Rank->Level == 255)
|
||||||
|
{
|
||||||
|
$RolesQuery = db::run(
|
||||||
|
"SELECT * FROM groups_ranks WHERE GroupID = :GroupID ORDER BY Rank ASC",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$RolesQuery = db::run(
|
||||||
|
"SELECT * FROM groups_ranks WHERE GroupID = :GroupID AND Rank < :MyRank ORDER BY Rank ASC",
|
||||||
|
[":GroupID" => $GroupID, ":MyRank" => $Rank->Level]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while($Role = $RolesQuery->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$Roles[] =
|
||||||
|
[
|
||||||
|
"Name" => htmlspecialchars($Role->Name),
|
||||||
|
"Description" => htmlspecialchars($Role->Description),
|
||||||
|
"Rank" => $Role->Rank,
|
||||||
|
"Permissions" => json_decode($Role->Permissions)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "items" => $Roles]));
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["Recipient"])) api::respond(400, false, "Recipient is not set");
|
||||||
|
|
||||||
|
if(!isset($_POST["Type"])) api::respond(400, false, "Type is not set");
|
||||||
|
if(!in_array($_POST["Type"], ["ally", "enemy"])) api::respond(400, false, "Type is not valid");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$RecipientName = $_POST["Recipient"] ?? false;
|
||||||
|
$Type = $_POST["Type"] ?? false;
|
||||||
|
$Groups = [];
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
|
||||||
|
$Recipient = db::run("SELECT * FROM groups WHERE name = :GroupName", [":GroupName" => $RecipientName]);
|
||||||
|
$RecipientInfo = $Recipient->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if(!$Recipient->rowCount()) api::respond(200, false, "No group with that name exists");
|
||||||
|
|
||||||
|
$MyRank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
if(!$MyRank->Permissions->CanManageRelationships) api::respond(200, false, "You are not allowed to manage this group's relationships");
|
||||||
|
|
||||||
|
if($RecipientInfo->id == $GroupID)
|
||||||
|
{
|
||||||
|
if($Type == "ally") api::respond(200, false, "You cannot send an ally request to your own group");
|
||||||
|
else if($Type == "enemy") api::respond(200, false, "You cannot declare your own group as an enemy");
|
||||||
|
}
|
||||||
|
|
||||||
|
$Relationship = db::run(
|
||||||
|
"SELECT * FROM groups_relationships WHERE :GroupID IN (Declarer, Recipient) AND :Recipient IN (Declarer, Recipient) AND Status != 2",
|
||||||
|
[":GroupID" => $GroupID, ":Recipient" => $RecipientInfo->id]
|
||||||
|
);
|
||||||
|
$RelationshipInfo = $Relationship->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if($Relationship->rowCount())
|
||||||
|
{
|
||||||
|
if($RelationshipInfo->Type == "Allies")
|
||||||
|
{
|
||||||
|
if($RelationshipInfo->Status == 0)
|
||||||
|
{
|
||||||
|
if($RelationshipInfo->Declarer == $GroupID)
|
||||||
|
{
|
||||||
|
api::respond(200, false, "You already have an outgoing ally request to this group");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
api::respond(200, false, "You already have an incoming ally request from this group");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if($RelationshipInfo->Status == 1)
|
||||||
|
{
|
||||||
|
api::respond(200, false, "You are already allies with this group!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if($RelationshipInfo->Type == "Enemies")
|
||||||
|
{
|
||||||
|
api::respond(200, false, "You are already enemies with this group!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($Type == "ally")
|
||||||
|
{
|
||||||
|
$LastRequest = db::run("SELECT Declared FROM groups_relationships WHERE Declarer = :GroupID AND Declared+3600 > UNIX_TIMESTAMP()", [":GroupID" => $GroupID]);
|
||||||
|
if($LastRequest->rowCount())
|
||||||
|
api::respond(429, false, "Please wait ".GetReadableTime($LastRequest->fetchColumn(), ["RelativeTime" => "1 hour"])." before sending a new ally request");
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO groups_relationships (Type, Declarer, Recipient, Status, Declared)
|
||||||
|
VALUES (\"Allies\", :GroupID, :Recipient, 0, UNIX_TIMESTAMP())",
|
||||||
|
[":GroupID" => $GroupID, ":Recipient" => $RecipientInfo->id]
|
||||||
|
);
|
||||||
|
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Send Ally Request",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> sent an ally request to <a href=\"/groups?gid=%d\">%s</a>",
|
||||||
|
SESSION["userId"], SESSION["userName"], $RecipientInfo->id, htmlspecialchars($RecipientInfo->name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
api::respond(200, true, "Ally request has been sent to ".Polygon::FilterText($RecipientInfo->name));
|
||||||
|
}
|
||||||
|
else if($Type == "enemy")
|
||||||
|
{
|
||||||
|
$LastRequest = db::run("SELECT Declared FROM groups_relationships WHERE Declarer = :GroupID AND Declared+3600 > UNIX_TIMESTAMP()", [":GroupID" => $GroupID]);
|
||||||
|
if($LastRequest->rowCount())
|
||||||
|
api::respond(429, false, "Please wait ".GetReadableTime($LastRequest->fetchColumn(), ["RelativeTime" => "1 hour"])." before sending a new ally request");
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO groups_relationships (Type, Declarer, Recipient, Status, Declared, Established)
|
||||||
|
VALUES (\"Enemies\", :GroupID, :Recipient, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())",
|
||||||
|
[":GroupID" => $GroupID, ":Recipient" => $RecipientInfo->id]
|
||||||
|
);
|
||||||
|
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Create Enemy",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> declared <a href=\"/groups?gid=%d\">%s</a> as an enemy",
|
||||||
|
SESSION["userId"], SESSION["userName"], $RecipientInfo->id, htmlspecialchars($RecipientInfo->name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
api::respond(200, true, Polygon::FilterText($RecipientInfo->name)." is now your enemy!");
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond(200, false, "An unexpected error occurred");
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["UserID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["UserID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(isset($_POST["RoleLevel"]) && !is_numeric($_POST["RoleLevel"])) api::respond(400, false, "RoleLevel is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$UserID = $_POST["UserID"] ?? false;
|
||||||
|
|
||||||
|
$RoleLevel = $_POST["RoleLevel"] ?? false;
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
|
||||||
|
$MyRole = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
$UserRole = Groups::GetUserRank($UserID, $GroupID);
|
||||||
|
|
||||||
|
if($MyRole->Level == 0) api::respond(200, false, "You are not a member of this group");
|
||||||
|
if(!$MyRole->Permissions->CanManageGroupAdmin) api::respond(200, false, "You are not allowed to perform this action");
|
||||||
|
if($UserRole->Level == 0) api::respond(200, false, "That user is not a member of this group");
|
||||||
|
|
||||||
|
if($RoleLevel !== false)
|
||||||
|
{
|
||||||
|
if(!Groups::GetRankInfo($GroupID, $RoleLevel)) api::respond(200, false, "That role does not exist");
|
||||||
|
if($RoleLevel == 0 || $RoleLevel == 255) api::respond(200, false, "That role cannot be manually assigned to a member");
|
||||||
|
|
||||||
|
if($UserRole->Level == $RoleLevel) api::respond(200, false, "The role you tried to assign is the user's current role");
|
||||||
|
if($MyRole->Level <= $RoleLevel) api::respond(200, false, "You can only assign roles lower than yours");
|
||||||
|
if($MyRole->Level <= $UserRole->Level) api::respond(200, false, "You can only modify the role of a user who is a role lower than yours");
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"UPDATE groups_members SET Rank = :RoleLevel WHERE GroupID = :GroupID AND UserID = :UserID",
|
||||||
|
[":GroupID" => $GroupID, ":UserID" => $UserID, ":RoleLevel" => $RoleLevel]
|
||||||
|
);
|
||||||
|
|
||||||
|
$UserName = Users::GetNameFromID($UserID);
|
||||||
|
$RoleName = Groups::GetRankInfo($GroupID, $RoleLevel)->Name;
|
||||||
|
$Action = $RoleLevel > $UserRole->Level ? "promoted" : "demoted";
|
||||||
|
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Change Rank",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> %s <a href=\"/user?ID=%d\">%s</a> from %s to %s",
|
||||||
|
SESSION["userId"], SESSION["userName"], $Action, $UserID, $UserName, htmlspecialchars($UserRole->Name), htmlspecialchars($RoleName)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "$UserName has been $Action to " . htmlspecialchars($RoleName));
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["Recipient"])) api::respond(400, false, "Recipient is not set");
|
||||||
|
if(!is_numeric($_POST["Recipient"])) api::respond(400, false, "Recipient is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["Action"])) api::respond(400, false, "Action is not set");
|
||||||
|
if(!in_array($_POST["Action"], ["accept", "decline"])) api::respond(400, false, "Action is not valid");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$Recipient = $_POST["Recipient"] ?? false;
|
||||||
|
$Action = $_POST["Action"] ?? false;
|
||||||
|
$Groups = [];
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
if(!Groups::GetGroupInfo($Recipient)) api::respond(200, false, "Recipient group does not exist");
|
||||||
|
|
||||||
|
$MyRank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
if(!$MyRank->Permissions->CanManageRelationships) api::respond(200, false, "You are not allowed to manage this group's relationships");
|
||||||
|
|
||||||
|
$Relationship = db::run(
|
||||||
|
"SELECT groups_relationships.*, groups.name FROM groups_relationships
|
||||||
|
INNER JOIN groups ON groups.id = (CASE WHEN Declarer = :GroupID THEN Recipient ELSE Declarer END)
|
||||||
|
WHERE :GroupID IN (Declarer, Recipient) AND :Recipient IN (Declarer, Recipient) AND Status != 2",
|
||||||
|
[":GroupID" => $GroupID, ":Recipient" => $Recipient]
|
||||||
|
);
|
||||||
|
$RelationshipInfo = $Relationship->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if(!$Relationship->rowCount()) api::respond(200, false, "You are not in a relationship with this group");
|
||||||
|
|
||||||
|
if($Action == "accept")
|
||||||
|
{
|
||||||
|
if($RelationshipInfo->Type == "Enemies") api::respond(200, false, "You cannot accept an enemy relationship");
|
||||||
|
if($RelationshipInfo->Status != 0) api::respond(200, false, "You are already in a relationship with this group");
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"UPDATE groups_relationships SET Status = 1, Established = UNIX_TIMESTAMP() WHERE ID = :RelationshipID",
|
||||||
|
[":RelationshipID" => $RelationshipInfo->ID]
|
||||||
|
);
|
||||||
|
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Accept Ally Request",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> accepted an ally request from <a href=\"/groups?gid=%d\">%s</a>",
|
||||||
|
SESSION["userId"], SESSION["userName"], $Recipient, htmlspecialchars($RelationshipInfo->name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "You have accepted {$RelationshipInfo->name}'s ally request");
|
||||||
|
}
|
||||||
|
else if($Action == "decline")
|
||||||
|
{
|
||||||
|
db::run(
|
||||||
|
"UPDATE groups_relationships SET Status = 2, Broken = UNIX_TIMESTAMP() WHERE ID = :RelationshipID",
|
||||||
|
[":RelationshipID" => $RelationshipInfo->ID]
|
||||||
|
);
|
||||||
|
|
||||||
|
if($RelationshipInfo->Type == "Allies")
|
||||||
|
{
|
||||||
|
if($RelationshipInfo->Status == 0)
|
||||||
|
{
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Decline Ally Request",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> declined an ally request from <a href=\"/groups?gid=%d\">%s</a>",
|
||||||
|
SESSION["userId"], SESSION["userName"], $Recipient, htmlspecialchars($RelationshipInfo->name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "You have declined ".Polygon::FilterText($RelationshipInfo->name)."'s ally request");
|
||||||
|
}
|
||||||
|
else if($RelationshipInfo->Status == 1)
|
||||||
|
{
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Delete Ally",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> removed <a href=\"/groups?gid=%d\">%s</a> as an ally",
|
||||||
|
SESSION["userId"], SESSION["userName"], $Recipient, htmlspecialchars($RelationshipInfo->name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "You are no longer allies with ".Polygon::FilterText($RelationshipInfo->name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if($RelationshipInfo->Type == "Enemies")
|
||||||
|
{
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Delete Enemy",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> removed <a href=\"/groups?gid=%d\">%s</a> as an enemy",
|
||||||
|
SESSION["userId"], SESSION["userName"], $Recipient, htmlspecialchars($RelationshipInfo->name)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "You are no longer enemies with ".Polygon::FilterText($RelationshipInfo->name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond(200, false, "An unexpected error occurred");
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["Roles"])) api::respond(400, false, "Roles is not set");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"];
|
||||||
|
$Roles = json_decode($_POST["Roles"]);
|
||||||
|
|
||||||
|
if(!$Roles) api::respond(400, false, "Roles is not valid JSON");
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
|
||||||
|
$MyRole = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
|
||||||
|
if($MyRole->Level == 0) api::respond(200, false, "You are not a member of this group");
|
||||||
|
if($MyRole->Level != 255) api::respond(200, false, "You are not allowed to perform this action");
|
||||||
|
|
||||||
|
function FindRolesWithRank($Rank)
|
||||||
|
{
|
||||||
|
global $Roles;
|
||||||
|
$Count = 0;
|
||||||
|
|
||||||
|
foreach ($Roles as $Role)
|
||||||
|
{
|
||||||
|
if (!isset($Role->Rank)) continue;
|
||||||
|
if ($Role->Rank == $Rank) $Count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FindRoleWithRank($Rank)
|
||||||
|
{
|
||||||
|
global $Roles;
|
||||||
|
|
||||||
|
foreach ($Roles as $Role)
|
||||||
|
{
|
||||||
|
if (!isset($Role->Rank)) continue;
|
||||||
|
if ($Role->Rank == $Rank) return $Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$Permissions =
|
||||||
|
[
|
||||||
|
"CanViewGroupWall",
|
||||||
|
"CanViewGroupStatus",
|
||||||
|
"CanPostOnGroupWall",
|
||||||
|
"CanPostGroupStatus",
|
||||||
|
"CanDeleteGroupWallPosts",
|
||||||
|
"CanAcceptJoinRequests",
|
||||||
|
"CanKickLowerRankedMembers",
|
||||||
|
"CanRoleLowerRankedMembers",
|
||||||
|
"CanManageRelationships",
|
||||||
|
"CanCreateAssets",
|
||||||
|
"CanConfigureAssets",
|
||||||
|
"CanSpendFunds",
|
||||||
|
"CanManageGames",
|
||||||
|
"CanManageGroupAdmin",
|
||||||
|
"CanViewAuditLog"
|
||||||
|
];
|
||||||
|
|
||||||
|
if(FindRolesWithRank(0) == 0) api::respond(200, false, "You can not remove the Guest role");
|
||||||
|
if(FindRolesWithRank(255) == 0) api::respond(200, false, "You can not remove the Owner role");
|
||||||
|
if(count($Roles) < 3) api::respond(200, false, "There must be at least three roles");
|
||||||
|
if(count($Roles) > 10) api::respond(200, false, "There must be no more than ten roles");
|
||||||
|
|
||||||
|
foreach($Roles as $Role)
|
||||||
|
{
|
||||||
|
if(!isset($Role->Name) || !isset($Role->Description) || !isset($Role->Rank) || !isset($Role->Permissions))
|
||||||
|
api::respond(200, false, "Roles are missing parameters");
|
||||||
|
|
||||||
|
if($Role->Rank < 0 || $Role->Rank > 255) api::respond(200, false, "Each role must have a rank number between 0 and 255");
|
||||||
|
if(FindRolesWithRank($Role->Rank) > 1) api::respond(200, false, "Each role must have a unique rank number");
|
||||||
|
|
||||||
|
$CurrentRole = Groups::GetRankInfo($GroupID, $Role->Rank);
|
||||||
|
|
||||||
|
if($CurrentRole === false) $Role->Action = "Create";
|
||||||
|
else $Role->Action = "Update";
|
||||||
|
|
||||||
|
if($Role->Rank == 0)
|
||||||
|
{
|
||||||
|
if($Role->Name != $CurrentRole->Name || $Role->Description != $CurrentRole->Description)
|
||||||
|
api::respond(200, false, "You can not modify the Guest role");
|
||||||
|
}
|
||||||
|
|
||||||
|
if($Role->Rank == 255 && $Role->Permissions != $CurrentRole->Permissions)
|
||||||
|
api::respond(200, false, "You can not modify the permissions of the Owner role");
|
||||||
|
|
||||||
|
if(strlen($Role->Name) < 3) api::respond(200, false, "Role names must be at least 3 characters long");
|
||||||
|
if(strlen($Role->Name) > 15) api::respond(200, false, "Role names must be no longer than 15 characters");
|
||||||
|
|
||||||
|
if(strlen($Role->Description) < 3) api::respond(200, false, "Role description must at least 3 characters long");
|
||||||
|
if(strlen($Role->Description) > 64) api::respond(200, false, "Role description must be no longer than 64 characters");
|
||||||
|
|
||||||
|
foreach ($Permissions as $Permission)
|
||||||
|
{
|
||||||
|
if(!isset($Role->Permissions->$Permission)) api::respond(200, false, "Role is missing a permission");
|
||||||
|
if(!is_bool($Role->Permissions->$Permission)) api::respond(200, false, "Role permission property must have a boolean value");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(count((array)$Role->Permissions) != count($Permissions)) api::respond(200, false, "Role permissions contains an incorrect number of permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($Roles as $Role)
|
||||||
|
{
|
||||||
|
if($Role->Action == "Create")
|
||||||
|
{
|
||||||
|
// if(SESSION["userId"] == 1) echo "Creating Role {$Role->Rank}\r\n";
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO groups_ranks (GroupID, Name, Description, Rank, Permissions, Created)
|
||||||
|
VALUES (:GroupID, :Name, :Description, :Rank, :Permissions, UNIX_TIMESTAMP())",
|
||||||
|
[":GroupID" => $GroupID, ":Name" => $Role->Name, ":Description" => $Role->Description, ":Rank" => $Role->Rank, ":Permissions" => json_encode($Role->Permissions)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$GroupRoles = Groups::GetGroupRanks($GroupID, true);
|
||||||
|
while($ExistingRole = $GroupRoles->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$Role = FindRoleWithRank($ExistingRole->Rank);
|
||||||
|
|
||||||
|
if($Role == false)
|
||||||
|
{
|
||||||
|
// if(SESSION["userId"] == 1) echo "Deleting Role {$ExistingRole->Rank}\r\n";
|
||||||
|
|
||||||
|
// for this one we gotta move the members with a role thats being deleted to the lowest rank
|
||||||
|
// slight issue with this is for a brief period, members assigned the role thats being deleted
|
||||||
|
// will have no role - if the timing is just right this could mess up the view of the group page
|
||||||
|
|
||||||
|
// delete the rank by the oldest id - so that we dont accidentally delete the new one
|
||||||
|
db::run(
|
||||||
|
"DELETE FROM groups_ranks WHERE GroupID = :GroupID AND Rank = :Rank ORDER BY id ASC LIMIT 1",
|
||||||
|
[":GroupID" => $GroupID, ":Rank" => $ExistingRole->Rank]
|
||||||
|
);
|
||||||
|
|
||||||
|
$NewRank = db::run(
|
||||||
|
"SELECT Rank FROM polygon.groups_ranks WHERE GroupID = :GroupID AND Rank != 0 ORDER BY Rank ASC LIMIT 1",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
// if(SESSION["userId"] == 1) echo "Updating existing members to {$NewRank}\r\n";
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"UPDATE groups_members SET Rank = :NewRank WHERE GroupID = :GroupID AND Rank = :Rank",
|
||||||
|
[":GroupID" => $GroupID, ":Rank" => $ExistingRole->Rank, ":NewRank" => $NewRank]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if(isset($Role->Action) && $Role->Action == "Update")
|
||||||
|
{
|
||||||
|
// if(SESSION["userId"] == 1) echo "Updating Role {$Role->Rank}\r\n";
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"UPDATE groups_ranks SET Name = :Name, Description = :Description, Permissions = :Permissions
|
||||||
|
WHERE GroupID = :GroupID AND Rank = :Rank",
|
||||||
|
[":GroupID" => $GroupID, ":Name" => $Role->Name, ":Description" => $Role->Description, ":Rank" => $Role->Rank, ":Permissions" => json_encode($Role->Permissions)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "Group roles have successfully been updated"]));
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["PostID"])) api::respond(400, false, "PostID is not set");
|
||||||
|
if(!is_numeric($_POST["PostID"])) api::respond(400, false, "PostID is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$PostID = $_POST["PostID"] ?? false;
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
|
||||||
|
$Rank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
if(!$Rank->Permissions->CanDeleteGroupWallPosts) api::respond(200, false, "You are not allowed to delete wall posts on this group");
|
||||||
|
|
||||||
|
$PostInfo = db::run(
|
||||||
|
"SELECT * FROM groups_wall WHERE id = :PostID AND :GroupID = :GroupID",
|
||||||
|
[":PostID" => $PostID, ":GroupID" => $GroupID]
|
||||||
|
)->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if(!$PostInfo) api::respond(200, false, "Wall post does not exist");
|
||||||
|
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Delete Post",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> deleted post \"%s\" by <a href=\"/user?ID=%d\">%s</a>",
|
||||||
|
SESSION["userId"], SESSION["userName"], htmlspecialchars($PostInfo->Content), $PostInfo->PosterID, Users::GetNameFromID($PostInfo->PosterID)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"DELETE FROM groups_wall WHERE id = :PostID AND :GroupID = :GroupID",
|
||||||
|
[":PostID" => $PostID, ":GroupID" => $GroupID]
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) api::respond(400, false, "Page is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$Filter = $_POST["Filter"] ?? "All Actions";
|
||||||
|
$Page = $_POST["Page"] ?? 1;
|
||||||
|
$Logs = [];
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
$MyRank = Groups::GetUserRank(SESSION["userId"] ?? 0, $GroupID);
|
||||||
|
if(!$MyRank->Permissions->CanViewAuditLog) api::respond(200, false, "You cannot audit this group");
|
||||||
|
|
||||||
|
if($Filter == "All Actions")
|
||||||
|
{
|
||||||
|
$LogCount = db::run(
|
||||||
|
"SELECT COUNT(*) FROM groups_audit WHERE GroupID = :GroupID",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
)->fetchColumn();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$LogCount = db::run(
|
||||||
|
"SELECT COUNT(*) FROM groups_audit WHERE GroupID = :GroupID AND Category = :Action",
|
||||||
|
[":GroupID" => $GroupID, ":Action" => $Filter]
|
||||||
|
)->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
$Pages = ceil($LogCount/20);
|
||||||
|
$Offset = ($Page - 1)*20;
|
||||||
|
|
||||||
|
if(!$Pages) api::respond(200, true, "This group does not have any logs for this action.");
|
||||||
|
|
||||||
|
if($Filter == "All Actions")
|
||||||
|
{
|
||||||
|
$LogsQuery = db::run(
|
||||||
|
"SELECT groups_audit.*, users.username FROM groups_audit
|
||||||
|
INNER JOIN users ON users.id = UserId
|
||||||
|
WHERE GroupID = :GroupID
|
||||||
|
ORDER BY Time DESC LIMIT 20 OFFSET $Offset",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$LogsQuery = db::run(
|
||||||
|
"SELECT groups_audit.*, users.username FROM groups_audit
|
||||||
|
INNER JOIN users ON users.id = UserId
|
||||||
|
WHERE GroupID = :GroupID AND Category = :Action
|
||||||
|
ORDER BY Time DESC LIMIT 20 OFFSET $Offset",
|
||||||
|
[":GroupID" => $GroupID, ":Action" => $Filter]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while($Log = $LogsQuery->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$Logs[] =
|
||||||
|
[
|
||||||
|
"Date" => date('j/n/y G:i', $Log->Time),
|
||||||
|
"UserName" => $Log->username,
|
||||||
|
"UserID" => $Log->UserID,
|
||||||
|
"UserAvatar" => Thumbnails::GetAvatar($Log->UserID, 250, 250),
|
||||||
|
"Rank" => Polygon::FilterText($Log->Rank),
|
||||||
|
"Description" => Polygon::FilterText($Log->Description, false)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "items" => $Logs]));
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["RankLevel"])) api::respond(400, false, "RankLevel is not set");
|
||||||
|
if(!is_numeric($_POST["RankLevel"])) api::respond(400, false, "RankLevel is not a number");
|
||||||
|
|
||||||
|
if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) api::respond(400, false, "Page is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$RankID = $_POST["RankLevel"] ?? false;
|
||||||
|
$Page = $_POST["Page"] ?? 1;
|
||||||
|
$Members = [];
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
if(!Groups::GetRankInfo($GroupID, $RankID)) api::respond(200, false, "Group rank does not exist");
|
||||||
|
|
||||||
|
$MemberCount = db::run(
|
||||||
|
"SELECT COUNT(*) FROM groups_members WHERE GroupID = :GroupID AND Rank = :RankID AND NOT Pending",
|
||||||
|
[":GroupID" => $GroupID, ":RankID" => $RankID]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
$Pages = ceil($MemberCount/12);
|
||||||
|
$Offset = ($Page - 1)*12;
|
||||||
|
|
||||||
|
if(!$Pages) api::respond(200, true, "This group does not have any members of this rank.");
|
||||||
|
|
||||||
|
$MembersQuery = db::run(
|
||||||
|
"SELECT users.username, users.id, Rank FROM groups_members
|
||||||
|
INNER JOIN users ON users.id = groups_members.UserID
|
||||||
|
WHERE GroupID = :GroupID AND Rank = :RankID AND NOT Pending
|
||||||
|
ORDER BY Joined DESC LIMIT 12 OFFSET $Offset",
|
||||||
|
[":GroupID" => $GroupID, ":RankID" => $RankID]
|
||||||
|
);
|
||||||
|
|
||||||
|
while($Member = $MembersQuery->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$Members[] =
|
||||||
|
[
|
||||||
|
"UserName" => $Member->username,
|
||||||
|
"UserID" => $Member->id,
|
||||||
|
"RoleLevel" => $Member->Rank,
|
||||||
|
"Avatar" => Thumbnails::GetAvatar($Member->id, 250, 250)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "count" => $MemberCount, "items" => $Members]));
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["Type"])) api::respond(400, false, "Type is not set");
|
||||||
|
if(!in_array($_POST["Type"], ["Pending Allies", "Allies", "Enemies"])) api::respond(400, false, "Type is not valid");
|
||||||
|
|
||||||
|
if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) api::respond(400, false, "Page is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$Type = $_POST["Type"] ?? false;
|
||||||
|
$Page = $_POST["Page"] ?? 1;
|
||||||
|
$Groups = [];
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
|
||||||
|
if($Type == "Pending Allies")
|
||||||
|
{
|
||||||
|
if(!SESSION) api::respond(200, false, "You are not allowed to get this group's pending allies");
|
||||||
|
$MyRank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
if(!$MyRank->Permissions->CanManageRelationships) api::respond(200, false, "You are not allowed to get this group's pending allies");
|
||||||
|
|
||||||
|
$GroupsCount = db::run(
|
||||||
|
"SELECT COUNT(*) FROM groups_relationships WHERE Recipient = :GroupID AND Type = \"Allies\" AND Status = 0",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
)->fetchColumn();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$GroupsCount = db::run(
|
||||||
|
"SELECT COUNT(*) FROM groups_relationships WHERE :GroupID IN (Declarer, Recipient) AND Type = :Type AND Status = 1",
|
||||||
|
[":GroupID" => $GroupID, ":Type" => $Type]
|
||||||
|
)->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
$Pages = ceil($GroupsCount/12);
|
||||||
|
$Offset = ($Page - 1)*12;
|
||||||
|
|
||||||
|
if(!$Pages) api::respond(200, true, "This group does not have any $Type");
|
||||||
|
|
||||||
|
if($Type == "Pending Allies")
|
||||||
|
{
|
||||||
|
$GroupsQuery = db::run(
|
||||||
|
"SELECT groups.name, groups.id, groups.emblem,
|
||||||
|
(SELECT COUNT(*) FROM groups_members WHERE GroupID = groups.id AND NOT Pending) AS MemberCount
|
||||||
|
FROM groups_relationships
|
||||||
|
INNER JOIN groups ON groups.id = (CASE WHEN Declarer = :GroupID THEN Recipient ELSE Declarer END)
|
||||||
|
WHERE Recipient = :GroupID AND Type = \"Allies\" AND Status = 0
|
||||||
|
ORDER BY Declared DESC LIMIT 12 OFFSET $Offset",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$GroupsQuery = db::run(
|
||||||
|
"SELECT groups.name, groups.id, groups.emblem,
|
||||||
|
(SELECT COUNT(*) FROM groups_members WHERE GroupID = groups.id AND NOT Pending) AS MemberCount
|
||||||
|
FROM groups_relationships
|
||||||
|
INNER JOIN groups ON groups.id = (CASE WHEN Declarer = :GroupID THEN Recipient ELSE Declarer END)
|
||||||
|
WHERE :GroupID IN (Declarer, Recipient) AND Type = :Type AND Status = 1
|
||||||
|
ORDER BY Established DESC LIMIT 12 OFFSET $Offset",
|
||||||
|
[":GroupID" => $GroupID, ":Type" => $Type]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while($Group = $GroupsQuery->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$Groups[] =
|
||||||
|
[
|
||||||
|
"Name" => Polygon::FilterText($Group->name),
|
||||||
|
"ID" => $Group->id,
|
||||||
|
"MemberCount" => $Group->MemberCount,
|
||||||
|
"Emblem" => Thumbnails::GetAssetFromID($Group->emblem, 420, 420)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "items" => $Groups]));
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(isset($_POST["Page"]) && !is_numeric($_POST["Page"])) api::respond(400, false, "Page is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$Page = $_POST["Page"] ?? 1;
|
||||||
|
$Wall = [];
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
|
||||||
|
if(SESSION) $Rank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
else $Rank = Groups::GetRankInfo($GroupID, 0);
|
||||||
|
|
||||||
|
if(!$Rank->Permissions->CanViewGroupWall) api::respond(200, false, "You are not allowed to view this group wall");
|
||||||
|
|
||||||
|
$PostCount = db::run(
|
||||||
|
"SELECT COUNT(*) FROM groups_wall WHERE GroupID = :GroupID AND NOT Deleted",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
$Pages = ceil($PostCount/15);
|
||||||
|
$Offset = ($Page - 1)*15;
|
||||||
|
|
||||||
|
if(!$Pages) api::respond(200, true, "This group does not have any wall posts.");
|
||||||
|
|
||||||
|
$PostQuery = db::run(
|
||||||
|
"SELECT groups_wall.id, users.username AS PosterName, PosterID, Content, TimePosted FROM groups_wall
|
||||||
|
INNER JOIN users ON users.id = PosterID WHERE GroupID = :GroupID AND NOT Deleted
|
||||||
|
ORDER BY TimePosted DESC LIMIT 15 OFFSET $Offset",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
);
|
||||||
|
|
||||||
|
while($Post = $PostQuery->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$Wall[] =
|
||||||
|
[
|
||||||
|
"id" => $Post->id,
|
||||||
|
"username" => $Post->PosterName,
|
||||||
|
"userid" => $Post->PosterID,
|
||||||
|
"content" => nl2br(Polygon::FilterText($Post->Content)),
|
||||||
|
"time" => date('j/n/Y g:i:s A', $Post->TimePosted),
|
||||||
|
"avatar" => Thumbnails::GetAvatar($Post->PosterID, 420, 420)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "count" => $PostCount, "items" => $Wall]));
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
if(Groups::CheckIfUserInGroup(SESSION["userId"], $GroupID)) api::respond(200, false, "You are already in this group");
|
||||||
|
|
||||||
|
if(Groups::GetUserGroups(SESSION["userId"])->rowCount() >= 20) api::respond(200, false, "You have reached the maximum number of groups");
|
||||||
|
|
||||||
|
$RateLimit = db::run("SELECT Joined FROM groups_members WHERE UserID = :UserID AND Joined+300 > UNIX_TIMESTAMP()", [":UserID" => SESSION["userId"]]);
|
||||||
|
if($RateLimit->rowCount())
|
||||||
|
api::respond(200, false, "Please wait ".GetReadableTime($RateLimit->fetchColumn(), ["RelativeTime" => "5 minutes"])." before joining a new group");
|
||||||
|
|
||||||
|
$RankLevel = db::run(
|
||||||
|
"SELECT Rank FROM groups_ranks WHERE GroupID = :GroupID AND Rank != 0 ORDER BY Rank ASC LIMIT 1",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO groups_members (GroupID, UserID, Rank, Joined) VALUES (:GroupID, :UserID, :RankLevel, UNIX_TIMESTAMP())",
|
||||||
|
[":GroupID" => $GroupID, ":UserID" => SESSION["userId"], ":RankLevel" => $RankLevel]
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$GroupInfo = Groups::GetGroupInfo($GroupID);
|
||||||
|
|
||||||
|
if(!$GroupInfo) api::respond(200, false, "Group does not exist");
|
||||||
|
if($GroupInfo->creator == SESSION["userId"]) api::respond(200, false, "You are the creator of this group");
|
||||||
|
if(!Groups::CheckIfUserInGroup(SESSION["userId"], $GroupID)) api::respond(200, false, "You are not in this group");
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"DELETE FROM groups_members WHERE GroupID = :GroupID AND UserID = :UserID",
|
||||||
|
[":GroupID" => $GroupID, ":UserID" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
Polygon::ImportClass("Discord");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["Content"])) api::respond(400, false, "Content is not set");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$Content = $_POST["Content"] ?? false;
|
||||||
|
$GroupInfo = Groups::GetGroupInfo($GroupID);
|
||||||
|
|
||||||
|
if(!$GroupInfo) api::respond(200, false, "Group does not exist");
|
||||||
|
|
||||||
|
$Rank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
|
||||||
|
if(!$Rank->Permissions->CanPostGroupStatus) api::respond(200, false, "You are not allowed to post on this group wall");
|
||||||
|
|
||||||
|
if(strlen($Content) < 3) api::respond(200, false, "Group shout must be at least 3 characters long");
|
||||||
|
if(strlen($Content) > 255) api::respond(200, false, "Group shout can not be longer than 64 characters");
|
||||||
|
|
||||||
|
$LastPost = db::run(
|
||||||
|
"SELECT timestamp FROM feed WHERE groupId = :GroupID AND userId = :UserID AND timestamp+300 > UNIX_TIMESTAMP()",
|
||||||
|
[":GroupID" => $GroupID, ":UserID" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
|
||||||
|
if($LastPost->rowCount())
|
||||||
|
api::respond(200, false, "Please wait ".GetReadableTime($LastPost->fetchColumn(), ["RelativeTime" => "5 minutes"])." before posting a group shout");
|
||||||
|
|
||||||
|
Groups::LogAction(
|
||||||
|
$GroupID, "Post Shout",
|
||||||
|
sprintf(
|
||||||
|
"<a href=\"/user?ID=%d\">%s</a> changed the group status to: %s",
|
||||||
|
SESSION["userId"], SESSION["userName"], htmlspecialchars($Content)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO feed (groupId, userId, text, timestamp) VALUES (:GroupID, :UserID, :Content, UNIX_TIMESTAMP())",
|
||||||
|
[":GroupID" => $GroupID, ":UserID" => SESSION["userId"], ":Content" => $Content]
|
||||||
|
);
|
||||||
|
|
||||||
|
Discord::SendToWebhook(
|
||||||
|
[
|
||||||
|
"username" => $GroupInfo->name,
|
||||||
|
"content" => $Content."\n(Posted by ".SESSION["userName"].")",
|
||||||
|
"avatar_url" => Thumbnails::GetAssetFromID($GroupInfo->emblem, 420, 420)
|
||||||
|
],
|
||||||
|
Discord::WEBHOOK_KUSH
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Groups");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST", "logged_in" => true, "secure" => true]);
|
||||||
|
|
||||||
|
if(!isset($_POST["GroupID"])) api::respond(400, false, "GroupID is not set");
|
||||||
|
if(!is_numeric($_POST["GroupID"])) api::respond(400, false, "GroupID is not a number");
|
||||||
|
|
||||||
|
if(!isset($_POST["Content"])) api::respond(400, false, "Content is not set");
|
||||||
|
|
||||||
|
$GroupID = $_POST["GroupID"] ?? false;
|
||||||
|
$Content = $_POST["Content"] ?? false;
|
||||||
|
|
||||||
|
if(!Groups::GetGroupInfo($GroupID)) api::respond(200, false, "Group does not exist");
|
||||||
|
|
||||||
|
$Rank = Groups::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
|
||||||
|
if(!$Rank->Permissions->CanPostOnGroupWall) api::respond(200, false, "You are not allowed to post on this group wall");
|
||||||
|
|
||||||
|
if(strlen($Content) < 3) api::respond(200, false, "Wall post must be at least 3 characters long");
|
||||||
|
if(strlen($Content) > 255) api::respond(200, false, "Wall post can not be longer than 255 characters");
|
||||||
|
|
||||||
|
$LastPost = db::run(
|
||||||
|
"SELECT TimePosted FROM groups_wall
|
||||||
|
WHERE GroupID = :GroupID AND PosterID = :UserID AND TimePosted+60 > UNIX_TIMESTAMP()
|
||||||
|
ORDER BY TimePosted DESC LIMIT 1",
|
||||||
|
[":GroupID" => $GroupID, ":UserID" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
|
||||||
|
if(SESSION["userId"] != 1 && $LastPost->rowCount())
|
||||||
|
api::respond(200, false, "Please wait ".(60-(time()-$LastPost->fetchColumn()))." seconds before posting a new wall post");
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO groups_wall (GroupID, PosterID, Content, TimePosted) VALUES (:GroupID, :UserID, :Content, UNIX_TIMESTAMP())",
|
||||||
|
[":GroupID" => $GroupID, ":UserID" => SESSION["userId"], ":Content" => $Content]
|
||||||
|
);
|
||||||
|
|
||||||
|
api::respond(200, true, "OK");
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
$categories =
|
||||||
|
[
|
||||||
|
0=>"Bricks",
|
||||||
|
1=>"Robots",
|
||||||
|
2=>"Chassis",
|
||||||
|
3=>"Furniture",
|
||||||
|
4=>"Roads",
|
||||||
|
5=>"Billboards",
|
||||||
|
6=>"Game Objects",
|
||||||
|
"MyDecals"=>"My Decals",
|
||||||
|
"FreeDecals"=>"Free Decals",
|
||||||
|
"MyModels"=>"My Models",
|
||||||
|
"FreeModels"=>"Free Models"
|
||||||
|
];
|
||||||
|
|
||||||
|
$category = isset($_POST['category']) && isset($categories[$_POST['category']]) ? $_POST['category'] : "FreeModels";
|
||||||
|
$categoryText = $categories[$category];
|
||||||
|
$type = strpos($category, "Decals") ? 13 : 10;
|
||||||
|
$page = $_POST['page'] ?? 1;
|
||||||
|
$keywd = $_POST['keyword'] ?? false;
|
||||||
|
$keywd_sql = $keywd ? "%".$keywd."%" : "%";
|
||||||
|
|
||||||
|
if(is_numeric($category)) //static category
|
||||||
|
{
|
||||||
|
//$query = $pdo->prepare("SELECT COUNT(*) FROM catalog_items WHERE toolboxCategory = :category");
|
||||||
|
//$query->bindParam(":category", $categoryText, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
else //dynamic category - user assets, catalog assets
|
||||||
|
{
|
||||||
|
if(SESSION && strpos($categoryText, "My") !== false) //get assets from inventory
|
||||||
|
{
|
||||||
|
$userId = SESSION["userId"];
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM assets WHERE type = :type AND approved = 1 AND id IN (SELECT assetId FROM ownedAssets WHERE userId = :uid)");
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
}
|
||||||
|
else //get assets from catalog
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM assets WHERE type = :type AND approved = 1 AND (name LIKE :q OR description LIKE :q)");
|
||||||
|
$query->bindParam(":q", $keywd_sql, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->execute();
|
||||||
|
$items = $query->fetchColumn();
|
||||||
|
$pages = ceil($items/20);
|
||||||
|
$offset = ($page - 1)*20;
|
||||||
|
|
||||||
|
if(is_numeric($category)) //static category
|
||||||
|
{
|
||||||
|
//$query = $pdo->prepare("SELECT * FROM catalog_items WHERE toolboxCategory = :category ORDER BY id ASC LIMIT 20 OFFSET :offset");
|
||||||
|
//$query->bindParam(":category", $categoryText, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
else //dynamic category - user assets, catalog assets
|
||||||
|
{
|
||||||
|
if(strpos($categoryText, "My") !== false) //get assets from inventory
|
||||||
|
{
|
||||||
|
$userId = SESSION["userId"];
|
||||||
|
$query = $pdo->prepare("SELECT assets.* FROM ownedAssets INNER JOIN assets ON assets.id = assetId WHERE userId = :uid AND assets.type = :type ORDER BY timestamp DESC LIMIT 20 OFFSET :offset"); //all of this just to order by time bought...
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
}
|
||||||
|
else //get assets from catalog
|
||||||
|
{
|
||||||
|
$query = $pdo->prepare("SELECT * FROM assets WHERE type = :type AND approved = 1 AND (name LIKE :q OR description LIKE :q) ORDER BY updated DESC LIMIT 20 OFFSET :offset");
|
||||||
|
$query->bindParam(":q", $keywd_sql, PDO::PARAM_STR);
|
||||||
|
$query->bindParam(":q2", $keywd_sql, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->bindParam(":offset", $offset, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
?>
|
||||||
|
<div id="ToolBoxPage">
|
||||||
|
<div>
|
||||||
|
<?php if($pages>1) { ?>
|
||||||
|
<div id="pNavigation" style="display:table">
|
||||||
|
<div class="Navigation">
|
||||||
|
<div id="Previous">
|
||||||
|
<a href="#" onclick="getToolbox('<?=$category?>', '<?=$keywd?>', <?=$page-1?>)" id="PreviousPage" <?=$page <= 1 ? 'style="visibility:hidden"':''?>><span class="NavigationIndicators"><<</span>
|
||||||
|
Prev</a>
|
||||||
|
</div>
|
||||||
|
<div id="Next">
|
||||||
|
<a href="#" onclick="getToolbox('<?=$category?>', '<?=$keywd?>', <?=$page+1?>)" id="NextPage" <?=$page >= $pages ? 'style="visibility:hidden"':''?>>Next <span class="NavigationIndicators">>></span></a>
|
||||||
|
</div>
|
||||||
|
<div id="Location">
|
||||||
|
<span id="PagerLocation"><?=number_format((($page-1)*20)+1)?>-<?=number_format($page*20)?> of <?=number_format($items)?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
<div id="ToolboxItems">
|
||||||
|
<?php while($row = $query->fetch(PDO::FETCH_OBJ)) { $name = Polygon::FilterText($row->name); ?>
|
||||||
|
<a class="ToolboxItem" title="<?=$name?>" href="javascript:insertContent(<?=$row->id?>)" ondragstart="dragRBX(<?=$row->id?>)" onmouseover="this.style.borderStyle='outset'" onmouseout="this.style.borderStyle='solid'" style="border-style: solid;display:inline-block;height:60px;width:60px;cursor:pointer;">
|
||||||
|
<img width="60" src="<?=Thumbnails::GetAsset($row, 75, 75)?>" border="0" id="img" alt="<?=$name?>">
|
||||||
|
</a>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<?php if($pages>1) { ?>
|
||||||
|
<div id="pNavigation" style="display:table">
|
||||||
|
<div class="Navigation">
|
||||||
|
<div id="Previous">
|
||||||
|
<a href="#" onclick="getToolbox('<?=$category?>', '<?=$keywd?>', <?=$page-1?>)" id="PreviousPage" <?=$page <= 1 ? 'style="visibility:hidden"':''?>><span class="NavigationIndicators"><<</span>
|
||||||
|
Prev</a>
|
||||||
|
</div>
|
||||||
|
<div id="Next">
|
||||||
|
<a href="#" onclick="getToolbox('<?=$category?>', '<?=$keywd?>', <?=$page+1?>)" id="NextPage" <?=$page >= $pages ? 'style="visibility:hidden"':''?>>Next <span class="NavigationIndicators">>></span></a>
|
||||||
|
</div>
|
||||||
|
<div id="Location">
|
||||||
|
<span id="PagerLocation"><?=number_format((($page-1)*20)+1)?>-<?=number_format($page*20)?> of <?=number_format($items)?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
{
|
||||||
|
// i wonder if its worth putting the plain password only in the constructor
|
||||||
|
// for the sake of efficiency - usually it works out well, however in the
|
||||||
|
// change password api you end up having to instantiate two auth objects
|
||||||
|
// oh well
|
||||||
|
// by the way, this is like the only OOP thing in the entirety of polygon
|
||||||
|
// (apart from third party libaries). maybe i should change that. todo?
|
||||||
|
|
||||||
|
private $plaintext = "";
|
||||||
|
private $key = "";
|
||||||
|
|
||||||
|
function CreatePassword()
|
||||||
|
{
|
||||||
|
return \ParagonIE\PasswordLock\PasswordLock::hashAndEncrypt($this->plaintext, $this->key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VerifyPassword($storedtext)
|
||||||
|
{
|
||||||
|
if(strpos($storedtext, "$2y$10") !== false) //standard bcrypt - used since 04/09/2020
|
||||||
|
return password_verify($this->plaintext, $storedtext);
|
||||||
|
elseif(strpos($storedtext, "def50200") !== false) //argon2id w/ encryption - used since 26/02/2021
|
||||||
|
return \ParagonIE\PasswordLock\PasswordLock::decryptAndVerify($this->plaintext, $storedtext, $this->key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdatePassword($userId)
|
||||||
|
{
|
||||||
|
$pwhash = $this->createPassword();
|
||||||
|
db::run("UPDATE users SET password = :hash, lastpwdchange = UNIX_TIMESTAMP() WHERE id = :id", [":hash" => $pwhash, ":id" => $userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __construct($plaintext)
|
||||||
|
{
|
||||||
|
if(!class_exists('Defuse\Crypto\Key')) Polygon::ImportLibrary("PasswordLock");
|
||||||
|
$this->plaintext = $plaintext;
|
||||||
|
$this->key = \Defuse\Crypto\Key::loadFromAsciiSafeString(SITE_CONFIG["keys"]["passwordEncryption"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Catalog
|
||||||
|
{
|
||||||
|
public static array $types =
|
||||||
|
[
|
||||||
|
1 => "Image", // (internal use only - this is used for asset images)
|
||||||
|
2 => "T-Shirt",
|
||||||
|
3 => "Audio",
|
||||||
|
4 => "Mesh", // (internal use only)
|
||||||
|
5 => "Lua", // (internal use only - use this for corescripts and linkedtool scripts)
|
||||||
|
6 => "HTML", // (deprecated - dont use)
|
||||||
|
7 => "Text", // (deprecated - dont use)
|
||||||
|
8 => "Hat",
|
||||||
|
9 => "Place", // (unused as of now)
|
||||||
|
10 => "Model",
|
||||||
|
11 => "Shirt",
|
||||||
|
12 => "Pants",
|
||||||
|
13 => "Decal",
|
||||||
|
16 => "Avatar", // (deprecated - dont use)
|
||||||
|
17 => "Head",
|
||||||
|
18 => "Face",
|
||||||
|
19 => "Gear",
|
||||||
|
21 => "Badge" // (unused as of now)
|
||||||
|
];
|
||||||
|
|
||||||
|
static function GetTypeByNum($type)
|
||||||
|
{
|
||||||
|
return self::$types[$type] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static array $GearAttributesDisplay =
|
||||||
|
[
|
||||||
|
"melee" => ["text_sel" => "Melee", "text_item" => "Melee Weapon", "icon" => "far fa-sword"],
|
||||||
|
"powerup" => ["text_sel" => "Power ups", "text_item" => "Power Up", "icon" => "far fa-arrow-alt-up"],
|
||||||
|
"ranged" => ["text_sel" => "Ranged", "text_item" => "Ranged Weapon", "icon" => "far fa-bow-arrow"],
|
||||||
|
"navigation" => ["text_sel" => "Navigation", "text_item" => "Melee", "icon" => "far fa-compass"],
|
||||||
|
"explosive" => ["text_sel" => "Explosives", "text_item" => "Explosive", "icon" => "far fa-bomb"],
|
||||||
|
"musical" => ["text_sel" => "Musical", "text_item" => "Musical", "icon" => "far fa-music"],
|
||||||
|
"social" => ["text_sel" => "Social", "text_item" => "Social Item", "icon" => "far fa-laugh"],
|
||||||
|
"transport" => ["text_sel" => "Transport", "text_item" => "Personal Transport", "icon" => "far fa-motorcycle"],
|
||||||
|
"building" => ["text_sel" => "Building", "text_item" => "Melee", "icon" => "far fa-hammer"]
|
||||||
|
];
|
||||||
|
|
||||||
|
public static array $GearAttributes =
|
||||||
|
[
|
||||||
|
"melee" => false,
|
||||||
|
"powerup" => false,
|
||||||
|
"ranged" => false,
|
||||||
|
"navigation" => false,
|
||||||
|
"explosive" => false,
|
||||||
|
"musical" => false,
|
||||||
|
"social" => false,
|
||||||
|
"transport" => false,
|
||||||
|
"building" => false
|
||||||
|
];
|
||||||
|
|
||||||
|
static function ParseGearAttributes()
|
||||||
|
{
|
||||||
|
$gears = self::$GearAttributes;
|
||||||
|
foreach($gears as $gear => $enabled) $gears[$gear] = isset($_POST["gear_$gear"]) && $_POST["gear_$gear"] == "on";
|
||||||
|
self::$GearAttributes = $gears;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetAssetInfo($id)
|
||||||
|
{
|
||||||
|
return db::run(
|
||||||
|
"SELECT assets.*, users.username,
|
||||||
|
(SELECT COUNT(*) FROM ownedAssets WHERE assetId = assets.id AND userId != assets.creator) AS sales_total,
|
||||||
|
(SELECT COUNT(*) FROM ownedAssets WHERE assetId = assets.id AND userId != assets.creator AND timestamp > :sda) AS sales_week
|
||||||
|
FROM assets INNER JOIN users ON creator = users.id WHERE assets.id = :id",
|
||||||
|
[":sda" => strtotime('7 days ago', time()), ":id" => $id])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function CreateAsset($options)
|
||||||
|
{
|
||||||
|
global $pdo;
|
||||||
|
$columns = array_keys($options);
|
||||||
|
|
||||||
|
$querystring = "INSERT INTO assets (".implode(", ", $columns).", created, updated) ";
|
||||||
|
array_walk($columns, function(&$value, $_){ $value = ":$value"; });
|
||||||
|
$querystring .= "VALUES (".implode(", ", $columns).", UNIX_TIMESTAMP(), UNIX_TIMESTAMP())";
|
||||||
|
|
||||||
|
$query = $pdo->prepare($querystring);
|
||||||
|
foreach($options as $option => $val) $query->bindParam(":$option", $options[$option], is_numeric($val) ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$aid = $pdo->lastInsertId();
|
||||||
|
$uid = $options["creator"] ?? SESSION["userId"];
|
||||||
|
|
||||||
|
db::run("INSERT INTO ownedAssets (assetId, userId, timestamp) VALUES (:aid, :uid, UNIX_TIMESTAMP())", [":aid" => $aid, ":uid" => $uid]);
|
||||||
|
|
||||||
|
return $aid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function OwnsAsset($uid, $aid)
|
||||||
|
{
|
||||||
|
return db::run("SELECT COUNT(*) FROM ownedAssets WHERE assetId = :aid AND userId = :uid", [":aid" => $aid, ":uid" => $uid])->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GenerateGraphicXML($type, $assetID)
|
||||||
|
{
|
||||||
|
$strings =
|
||||||
|
[
|
||||||
|
"T-Shirt" => ["class" => "ShirtGraphic", "contentName" => "Graphic", "stringName" => "Shirt Graphic"],
|
||||||
|
"Decal" => ["class" => "Decal", "contentName" => "Texture", "stringName" => "Decal"],
|
||||||
|
"Face" => ["class" => "Decal", "contentName" => "Texture", "stringName" => "face"],
|
||||||
|
"Shirt" => ["class" => "Shirt", "contentName" => "ShirtTemplate", "stringName" => "Shirt"],
|
||||||
|
"Pants" => ["class" => "Pants", "contentName" => "PantsTemplate", "stringName" => "Pants"]
|
||||||
|
];
|
||||||
|
ob_start(); ?>
|
||||||
|
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
|
||||||
|
<External>null</External>
|
||||||
|
<External>nil</External>
|
||||||
|
<Item class="<?=$strings[$type]["class"]?>" referent="RBX0">
|
||||||
|
<Properties>
|
||||||
|
<?php if($type == "Decal" || $type == "Face") { ?>
|
||||||
|
<token name="Face">5</token>
|
||||||
|
<string name="Name"><?=$strings[$type]["stringName"]?></string>
|
||||||
|
<float name="Shiny">20</float>
|
||||||
|
<float name="Specular">0</float>
|
||||||
|
<Content name="Texture">
|
||||||
|
<url>%ASSETURL%<?=$assetID?></url>
|
||||||
|
</Content>
|
||||||
|
<?php } else { ?>
|
||||||
|
<Content name="<?=$strings[$type]["contentName"]?>">
|
||||||
|
<url>%ASSETURL%<?=$assetID?></url>
|
||||||
|
</Content>
|
||||||
|
<string name="Name"><?=$strings[$type]["stringName"]?></string>
|
||||||
|
<?php } ?>
|
||||||
|
<bool name="archivable">true</bool>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
|
<?php return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Discord
|
||||||
|
{
|
||||||
|
const WEBHOOK_POLYGON = "https://discord.com/api/webhooks/";
|
||||||
|
const WEBHOOK_KUSH = "https://discord.com/api/webhooks/";
|
||||||
|
|
||||||
|
static function GetUserInfo($UserID)
|
||||||
|
{
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt_array($ch,
|
||||||
|
[
|
||||||
|
CURLOPT_URL => "https://discord.com/api/v8/users/$UserID",
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => ["Authorization: Bot"]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
if ($httpcode != 200) return false;
|
||||||
|
|
||||||
|
$response = json_decode($response);
|
||||||
|
if ($response == NULL) return false;
|
||||||
|
|
||||||
|
return (object)
|
||||||
|
[
|
||||||
|
"username" => $response->username,
|
||||||
|
"tag" => $response->discriminator,
|
||||||
|
"id" => $response->id,
|
||||||
|
"avatar" => "https://cdn.discordapp.com/avatars/{$response->id}/{$response->avatar}.png",
|
||||||
|
"color" => $response->accent_color,
|
||||||
|
"banner" => $response->banner,
|
||||||
|
"banner_color" => $response->banner_color
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static function SendToWebhook($Payload, $Webhook, $EscapeContent = true)
|
||||||
|
{
|
||||||
|
// example payload:
|
||||||
|
// $payload = ["username" => "test", "content" => "test", "avatar_url" => "https://polygon.pizzaboxer.xyz/thumbs/avatar?id=1&x=100&y=100"];
|
||||||
|
|
||||||
|
if($EscapeContent)
|
||||||
|
{
|
||||||
|
$Payload["content"] = str_ireplace(["\\", "`"], ["\\\\", "\\`"], $Payload["content"]);
|
||||||
|
$Payload["content"] = str_ireplace(["@everyone", "@here"], ["`@everyone`", "`@here`"], $Payload["content"]);
|
||||||
|
$Payload["content"] = preg_replace("/(<@[0-9]+>)/i", "`$1`", $Payload["content"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $Webhook);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['payload_json' => json_encode($Payload)]));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class ErrorHandler
|
||||||
|
{
|
||||||
|
private $Exception;
|
||||||
|
private $Type;
|
||||||
|
private $String;
|
||||||
|
private $File;
|
||||||
|
private $Line;
|
||||||
|
|
||||||
|
private function GetType($Type)
|
||||||
|
{
|
||||||
|
switch ($Type)
|
||||||
|
{
|
||||||
|
case E_ERROR: case E_USER_ERROR: return "Fatal error";
|
||||||
|
case E_WARNING: case E_USER_WARNING: return "Warning";
|
||||||
|
case E_NOTICE: case E_USER_NOTICE: return "Warning";
|
||||||
|
case E_DEPRECATED: case E_USER_DEPRECATED: return "Deprecated";
|
||||||
|
default: return "Unknown error type $Type";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function GetVerboseMessage()
|
||||||
|
{
|
||||||
|
$VerboseMessage = "";
|
||||||
|
|
||||||
|
if($this->Type == "Exception")
|
||||||
|
{
|
||||||
|
$VerboseMessage .= sprintf("Fatal Error: Uncaught Exception: %s in %s:%d\n", $this->Exception->getMessage(), $this->Exception->getFile(), $this->Exception->getLine());
|
||||||
|
$VerboseMessage .= "Stack trace:\n";
|
||||||
|
$VerboseMessage .= sprintf("%s\n", $this->Exception->getTraceAsString());
|
||||||
|
$VerboseMessage .= sprintf(" thrown in %s on line %d", $this->Exception->getFile(), $this->Exception->getLine());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$VerboseMessage .= sprintf("%s: %s in %s on line %s", $this->Type, $this->String, $this->File, $this->Line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $VerboseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function WriteLog()
|
||||||
|
{
|
||||||
|
$LogFile = $_SERVER['DOCUMENT_ROOT']."/api/private/ErrorLog.json";
|
||||||
|
$LogID = generateUUID();
|
||||||
|
|
||||||
|
if(!file_exists($LogFile)) file_put_contents($LogFile, "[]");
|
||||||
|
|
||||||
|
$Log = json_decode(file_get_contents($LogFile), true);
|
||||||
|
|
||||||
|
$Log[$LogID] =
|
||||||
|
[
|
||||||
|
"Timestamp" => time(),
|
||||||
|
"GETParameters" => $_GET,
|
||||||
|
"Message" => $this->GetVerboseMessage()
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents($LogFile, json_encode($Log));
|
||||||
|
|
||||||
|
return $LogID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function HandleError($Type, $String, $File, $Line)
|
||||||
|
{
|
||||||
|
$this->Type = $this->GetType($Type);
|
||||||
|
$this->String = $String;
|
||||||
|
$this->File = $File;
|
||||||
|
$this->Line = $Line;
|
||||||
|
|
||||||
|
$LogID = $this->WriteLog();
|
||||||
|
|
||||||
|
if(headers_sent())
|
||||||
|
{
|
||||||
|
die("An unexpected error occurred! More info: $LogID");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
redirect("/error?id=$LogID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function HandleException($Exception)
|
||||||
|
{
|
||||||
|
$this->Type = "Exception";
|
||||||
|
$this->Exception = $Exception;
|
||||||
|
|
||||||
|
$LogID = $this->WriteLog();
|
||||||
|
|
||||||
|
if(headers_sent())
|
||||||
|
{
|
||||||
|
die("An unexpected error occurred! More info: $LogID");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
redirect("/error?id=$LogID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
set_error_handler([$this, "HandleError"]);
|
||||||
|
set_exception_handler([$this, "HandleException"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function GetLog($LogID = false)
|
||||||
|
{
|
||||||
|
$LogFile = $_SERVER['DOCUMENT_ROOT']."/api/private/ErrorLog.json";
|
||||||
|
if(!file_exists($LogFile)) file_put_contents($LogFile, "[]");
|
||||||
|
|
||||||
|
$Log = json_decode(file_get_contents($LogFile), true);
|
||||||
|
|
||||||
|
if($LogID !== false) return $Log[$LogID] ?? false;
|
||||||
|
return $Log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Forum
|
||||||
|
{
|
||||||
|
static function GetThreadInfo($id)
|
||||||
|
{
|
||||||
|
return db::run("SELECT * FROM forum_threads WHERE id = :id", [":id" => $id])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetReplyInfo($id)
|
||||||
|
{
|
||||||
|
return db::run("SELECT * FROM forum_replies WHERE id = :id", [":id" => $id])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetThreadReplies($id)
|
||||||
|
{
|
||||||
|
return db::run("SELECT COUNT(*) FROM forum_replies WHERE threadId = :id AND NOT deleted", [":id" => $id])->fetchColumn() ?: "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetSubforumInfo($id)
|
||||||
|
{
|
||||||
|
return db::run("SELECT * FROM forum_subforums WHERE id = :id", [":id" => $id])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetSubforumThreadCount($id, $includeReplies = false)
|
||||||
|
{
|
||||||
|
$threads = db::run("SELECT COUNT(*) FROM forum_threads WHERE subforumid = :id", [":id" => $id])->fetchColumn();
|
||||||
|
if(!$includeReplies) return $threads ?: '-';
|
||||||
|
|
||||||
|
$replies = db::run("SELECT COUNT(*) from forum_replies WHERE threadId IN (SELECT id FROM forum_threads WHERE subforumid = :id)", [":id" => $id])->fetchColumn();
|
||||||
|
$total = $threads + $replies;
|
||||||
|
|
||||||
|
return $total ?: '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class pagination
|
||||||
|
{
|
||||||
|
// this is ugly and sucks
|
||||||
|
// really this is only for the forums
|
||||||
|
// everything else uses standard next and back pagination
|
||||||
|
|
||||||
|
public static int $page = 1;
|
||||||
|
public static int $pages = 1;
|
||||||
|
public static string $url = '/';
|
||||||
|
public static array $pager = [1 => 1, 2 => 1, 3 => 1];
|
||||||
|
|
||||||
|
public static function initialize()
|
||||||
|
{
|
||||||
|
self::$pager[1] = self::$page-1; self::$pager[2] = self::$page; self::$pager[3] = self::$page+1;
|
||||||
|
|
||||||
|
if(self::$page <= 2){ self::$pager[1] = self::$page; self::$pager[2] = self::$page+1; self::$pager[3] = self::$page+2; }
|
||||||
|
if(self::$page == 1){ self::$pager[1] = self::$page+1; }
|
||||||
|
|
||||||
|
if(self::$page >= self::$pages-1){ self::$pager[1] = self::$pages-3; self::$pager[2] = self::$pages-2; self::$pager[3] = self::$pages-1; }
|
||||||
|
if(self::$page == self::$pages){ self::$pager[1] = self::$pages-1; self::$pager[2] = self::$pages-2; }
|
||||||
|
if(self::$page == self::$pages-1){ self::$pager[1] = self::$pages-2; self::$pager[2] = self::$pages-1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function insert()
|
||||||
|
{
|
||||||
|
if(self::$pages <= 1) return;
|
||||||
|
?>
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination justify-content-end mb-0">
|
||||||
|
<li class="page-item<?=self::$page<=1?' disabled':''?>">
|
||||||
|
<a class="page-link" <?=self::$page>1?'href="'.self::$url.(self::$page-1).'"':''?>aria-label="Previous">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
<span class="sr-only">Previous</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item<?=self::$page==1?' active':''?>"><a class="page-link"<?=self::$page!=1?' href="'.self::$url.'1" ':''?>>1</a></li>
|
||||||
|
<?php if(self::$pages > 2){ if(self::$page > 3){ ?><li class="page-item disabled"><a class="page-link" href="#" tabindex="-1">…</a></li><?php } ?>
|
||||||
|
<?php for($i=1; $i<4; $i++){ if(self::$page == $i-1 || self::$pages-self::$page == $i-2) break; ?>
|
||||||
|
<li class="page-item<?=self::$page==self::$pager[$i]?' active':''?>"><a class="page-link"<?=self::$page!=self::$pager[$i]?' href="'.self::$url.self::$pager[$i].'" ':''?>><?=number_format(self::$pager[$i])?></a></li>
|
||||||
|
<?php } ?>
|
||||||
|
<?php if(self::$page < self::$pages-2){ ?><li class="page-item disabled"><a class="page-link" href="#" tabindex="-1">…</a></li><?php } } ?>
|
||||||
|
<li class="page-item<?=self::$page==self::$pages?' active':''?>"><a class="page-link"<?=self::$page!=self::$pages?' href="'.self::$url.self::$pages.'" ':''?>><?=number_format(self::$pages)?></a></li>
|
||||||
|
<li class="page-item<?=self::$page>self::$pages?' disabled':''?>">
|
||||||
|
<a class="page-link" <?=self::$page<self::$pages?'href="'.self::$url.(self::$page+1).'"':''?>aria-label="Next">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
<span class="sr-only">Previous</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Games
|
||||||
|
{
|
||||||
|
static function GetServerInfo($id)
|
||||||
|
{
|
||||||
|
return db::run("
|
||||||
|
SELECT selfhosted_servers.*,
|
||||||
|
users.username,
|
||||||
|
users.jointime,
|
||||||
|
(SELECT COUNT(*) FROM client_sessions WHERE ping+35 > UNIX_TIMESTAMP() AND serverID = selfhosted_servers.id AND valid) AS players,
|
||||||
|
(ping+35 > UNIX_TIMESTAMP()) AS online
|
||||||
|
FROM selfhosted_servers INNER JOIN users ON users.id = hoster WHERE selfhosted_servers.id = :id", [":id" => $id])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetPlayersInServer($serverID)
|
||||||
|
{
|
||||||
|
return db::run("
|
||||||
|
SELECT users.* FROM selfhosted_servers
|
||||||
|
INNER JOIN client_sessions ON client_sessions.ping+35 > UNIX_TIMESTAMP() AND serverID = selfhosted_servers.id AND valid
|
||||||
|
INNER JOIN users ON users.id = uid
|
||||||
|
WHERE selfhosted_servers.id = :id GROUP BY client_sessions.uid", [":id" => $serverID]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Groups
|
||||||
|
{
|
||||||
|
static function GetGroupInfo($GroupID, $Barebones = false, $Force = false)
|
||||||
|
{
|
||||||
|
if($Barebones)
|
||||||
|
{
|
||||||
|
$GroupInfo = db::run("SELECT * FROM groups WHERE groups.id = :id", [":id" => $GroupID])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$GroupInfo = db::run(
|
||||||
|
"SELECT groups.*,
|
||||||
|
(SELECT COUNT(*) FROM groups_members WHERE GroupID = :id AND NOT Pending) AS MemberCount,
|
||||||
|
users.username AS ownername FROM groups
|
||||||
|
INNER JOIN users ON users.id = groups.owner
|
||||||
|
WHERE groups.id = :id",
|
||||||
|
[":id" => $GroupID]
|
||||||
|
)->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$Force && $GroupInfo && $GroupInfo->deleted) return false;
|
||||||
|
return $GroupInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetGroupStatus($GroupID)
|
||||||
|
{
|
||||||
|
return db::run(
|
||||||
|
"SELECT feed.*, users.username FROM feed
|
||||||
|
INNER JOIN users ON users.id = feed.userId
|
||||||
|
WHERE groupId = :GroupID ORDER BY id DESC LIMIT 1",
|
||||||
|
[":GroupID" => $GroupID]
|
||||||
|
)->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetLastGroupUserJoined($UserID)
|
||||||
|
{
|
||||||
|
$GroupID = db::run(
|
||||||
|
"SELECT GroupID FROM groups_members WHERE UserID = :UserID ORDER BY Joined DESC LIMIT 1",
|
||||||
|
[":UserID" => $UserID]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
return self::GetGroupInfo($GroupID);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetRankInfo($GroupID, $RankLevel)
|
||||||
|
{
|
||||||
|
$RankInfo = db::run(
|
||||||
|
"SELECT * FROM groups_ranks WHERE GroupID = :GroupID AND Rank = :RankLevel",
|
||||||
|
[":GroupID" => $GroupID, ":RankLevel" => $RankLevel]
|
||||||
|
)->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if(!$RankInfo) return false;
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
"Name" => $RankInfo->Name,
|
||||||
|
"Description" => $RankInfo->Description,
|
||||||
|
"Level" => $RankInfo->Rank,
|
||||||
|
"Permissions" => json_decode($RankInfo->Permissions)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetGroupRanks($GroupID, $includeGuest = false)
|
||||||
|
{
|
||||||
|
if($includeGuest)
|
||||||
|
return db::run("SELECT * FROM groups_ranks WHERE GroupID = :id ORDER BY Rank ASC", [":id" => $GroupID]);
|
||||||
|
else
|
||||||
|
return db::run("SELECT * FROM groups_ranks WHERE GroupID = :id AND Rank != 0 ORDER BY Rank ASC", [":id" => $GroupID]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function CheckIfUserInGroup($UserID, $GroupID)
|
||||||
|
{
|
||||||
|
return db::run(
|
||||||
|
"SELECT * FROM groups_members WHERE UserID = :UserID AND GroupID = :GroupID",
|
||||||
|
[":UserID" => $UserID, ":GroupID" => $GroupID]
|
||||||
|
)->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetUserRank($UserID, $GroupID)
|
||||||
|
{
|
||||||
|
$RankLevel = db::run(
|
||||||
|
"SELECT Rank FROM groups_members WHERE UserID = :UserID And GroupID = :GroupID",
|
||||||
|
[":UserID" => $UserID, ":GroupID" => $GroupID]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
if(!$RankLevel) return self::GetRankInfo($GroupID, 0);
|
||||||
|
|
||||||
|
return self::GetRankInfo($GroupID, $RankLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetUserGroups($UserID)
|
||||||
|
{
|
||||||
|
return db::run(
|
||||||
|
"SELECT groups.* FROM groups_members
|
||||||
|
INNER JOIN groups ON groups.id = groups_members.GroupID
|
||||||
|
WHERE groups_members.UserID = :UserID
|
||||||
|
ORDER BY groups_members.Joined DESC",
|
||||||
|
[":UserID" => $UserID]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function LogAction($GroupID, $Category, $Description)
|
||||||
|
{
|
||||||
|
// small note: when using this, you gotta be very careful about what you pass into the description
|
||||||
|
// the description must be sanitized when inserted into the db, not when fetched from an api
|
||||||
|
// this is because the description may contain hyperlinks or other html elements
|
||||||
|
// also here's a list of categories:
|
||||||
|
|
||||||
|
// Delete Post
|
||||||
|
// Remove Member
|
||||||
|
// Accept Join Request
|
||||||
|
// Decline Join Request
|
||||||
|
// Post Shout
|
||||||
|
// Change Rank
|
||||||
|
// Buy Ad
|
||||||
|
// Send Ally Request
|
||||||
|
// Create Enemy
|
||||||
|
// Accept Ally Request
|
||||||
|
// Decline Ally Request
|
||||||
|
// Delete Ally
|
||||||
|
// Delete Enemy
|
||||||
|
// Add Group Place
|
||||||
|
// Delete Group Place
|
||||||
|
// Create Items
|
||||||
|
// Configure Items
|
||||||
|
// Spend Group Funds
|
||||||
|
// Change Owner
|
||||||
|
// Delete
|
||||||
|
// Adjust Currency Amounts
|
||||||
|
// Abandon
|
||||||
|
// Claim
|
||||||
|
// Rename
|
||||||
|
// Change Description
|
||||||
|
// Create Group Asset
|
||||||
|
// Update Group Asset
|
||||||
|
// Configure Group Asset
|
||||||
|
// Revert Group Asset
|
||||||
|
// Create Group Developer Product
|
||||||
|
// Configure Group Game
|
||||||
|
// Lock
|
||||||
|
// Unlock
|
||||||
|
// Create Pass
|
||||||
|
// Create Badge
|
||||||
|
// Configure Badge
|
||||||
|
// Save Place
|
||||||
|
// Publish Place
|
||||||
|
// Invite to Clan
|
||||||
|
// Kick from Clan
|
||||||
|
// Cancel Clan Invite
|
||||||
|
// Buy Clan
|
||||||
|
|
||||||
|
if(!SESSION) return false;
|
||||||
|
$MyRank = self::GetUserRank(SESSION["userId"], $GroupID);
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO groups_audit (GroupID, Category, Time, UserID, Rank, Description)
|
||||||
|
VALUES (:GroupID, :Category, UNIX_TIMESTAMP(), :UserID, :Rank, :Description)",
|
||||||
|
[":GroupID" => $GroupID, ":Category" => $Category, ":UserID" => SESSION["userId"], ":Rank" => $MyRank->Name, ":Description" => $Description]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Gzip
|
||||||
|
{
|
||||||
|
// this is to compress models and places to help conserve space
|
||||||
|
// this should be used only for models and places, nothing else
|
||||||
|
|
||||||
|
//http://stackoverflow.com/questions/6073397/how-do-you-create-a-gz-file-using-php
|
||||||
|
static function Compress(string $inFilename, int $level = 9): string
|
||||||
|
{
|
||||||
|
// Is the file gzipped already?
|
||||||
|
$extension = pathinfo($inFilename, PATHINFO_EXTENSION);
|
||||||
|
if ($extension == "gz") { return $inFilename; }
|
||||||
|
|
||||||
|
// Open input file
|
||||||
|
$inFile = fopen($inFilename, "rb");
|
||||||
|
if ($inFile === false) { throw new \Exception("Unable to open input file: $inFilename"); }
|
||||||
|
|
||||||
|
// Open output file
|
||||||
|
$gzFilename = $inFilename.".gz";
|
||||||
|
$gzFile = gzopen($gzFilename, "wb".$level);
|
||||||
|
if ($gzFile === false)
|
||||||
|
{
|
||||||
|
fclose($inFile);
|
||||||
|
throw new \Exception("Unable to open output file: $gzFilename");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream copy
|
||||||
|
$length = 65536 * 1024; // 512 kB
|
||||||
|
while (!feof($inFile)) { gzwrite($gzFile, fread($inFile, $length)); }
|
||||||
|
|
||||||
|
// Close files
|
||||||
|
fclose($inFile);
|
||||||
|
gzclose($gzFile);
|
||||||
|
|
||||||
|
// Return the new filename
|
||||||
|
//delete original
|
||||||
|
unlink($inFilename);
|
||||||
|
rename($gzFilename, $inFilename);
|
||||||
|
return $gzFilename;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function Decompress($filename, $buffer_size = 8192)
|
||||||
|
{
|
||||||
|
$buffer = "";
|
||||||
|
$file = gzopen($filename, 'rb');
|
||||||
|
while(!gzeof($file)) { $buffer .= gzread($file, $buffer_size); }
|
||||||
|
gzclose($file);
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Image
|
||||||
|
{
|
||||||
|
static function Process($handle, $options)
|
||||||
|
{
|
||||||
|
$image = $options["image"] ?? true;
|
||||||
|
$resize = $options["resize"] ?? true;
|
||||||
|
$keepRatio = $options["keepRatio"] ?? false;
|
||||||
|
$scaleX = $options["scaleX"] ?? false;
|
||||||
|
$scaleY = $options["scaleY"] ?? false;
|
||||||
|
|
||||||
|
$handle->file_new_name_ext = "";
|
||||||
|
$handle->file_new_name_body = $options["name"];
|
||||||
|
|
||||||
|
if($image)
|
||||||
|
{
|
||||||
|
$handle->image_convert = "png";
|
||||||
|
$handle->image_resize = $resize;
|
||||||
|
if($resize)
|
||||||
|
{
|
||||||
|
if($keepRatio) $handle->image_ratio_fill = $options["align"];
|
||||||
|
if($scaleX) $handle->image_ratio_x = true; else $handle->image_x = $options["x"];
|
||||||
|
if($scaleY) $handle->image_ratio_y = true; else $handle->image_y = $options["y"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(strlen($options["name"]) && file_exists(ROOT.$options["dir"].$options["name"]))
|
||||||
|
unlink(ROOT.$options["dir"].$options["name"]);
|
||||||
|
|
||||||
|
$handle->process(ROOT.$options["dir"]);
|
||||||
|
if(!$handle->processed) return $handle->error;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function Resize($file, $w, $h, $path = false)
|
||||||
|
{
|
||||||
|
list($width, $height) = getimagesize($file);
|
||||||
|
$src = imagecreatefrompng($file);
|
||||||
|
$dst = imagecreatetruecolor($w, $h);
|
||||||
|
imagealphablending($dst, false);
|
||||||
|
imagesavealpha($dst, true);
|
||||||
|
imagecopyresampled($dst, $src, 0, 0, 0, 0, $w, $h, $width, $height);
|
||||||
|
|
||||||
|
// this resize function is used in conjunction with an imagepng function
|
||||||
|
// to resize an existing image and upload - having to do this eve
|
||||||
|
if($path) imagepng($dst, $path);
|
||||||
|
|
||||||
|
return $dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function MergeLayers($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct)
|
||||||
|
{
|
||||||
|
$cut = imagecreatetruecolor($src_w, $src_h);
|
||||||
|
imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h);
|
||||||
|
imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h);
|
||||||
|
imagecopymerge($dst_im, $cut, $dst_x, $dst_y, 0, 0, $src_w, $src_h, $pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre rendered thumbnails (scripts and audios) are all rendered with the same size
|
||||||
|
// so this just sorta cleans up the whole thing
|
||||||
|
static function RenderFromStaticImage($img, $assetID)
|
||||||
|
{
|
||||||
|
Image::Resize(ROOT."/thumbs/$img.png", 75, 75, SITE_CONFIG['paths']['thumbs_assets']."/$assetID-75x75.png");
|
||||||
|
Image::Resize(ROOT."/thumbs/$img.png", 100, 100, SITE_CONFIG['paths']['thumbs_assets']."/$assetID-100x100.png");
|
||||||
|
Image::Resize(ROOT."/thumbs/$img.png", 110, 110, SITE_CONFIG['paths']['thumbs_assets']."/$assetID-110x110.png");
|
||||||
|
Image::Resize(ROOT."/thumbs/$img.png", 250, 250, SITE_CONFIG['paths']['thumbs_assets']."/$assetID-250x250.png");
|
||||||
|
Image::Resize(ROOT."/thumbs/$img.png", 352, 352, SITE_CONFIG['paths']['thumbs_assets']."/$assetID-352x352.png");
|
||||||
|
Image::Resize(ROOT."/thumbs/$img.png", 420, 230, SITE_CONFIG['paths']['thumbs_assets']."/$assetID-420x230.png");
|
||||||
|
Image::Resize(ROOT."/thumbs/$img.png", 420, 420, SITE_CONFIG['paths']['thumbs_assets']."/$assetID-420x420.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class RBXClient
|
||||||
|
{
|
||||||
|
static function CryptGetSignature($data)
|
||||||
|
{
|
||||||
|
openssl_sign($data, $signature, openssl_pkey_get_private("file://".ROOT."/../polygon_private.pem"));
|
||||||
|
return base64_encode($signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function CryptSignScript($data, $assetID = false)
|
||||||
|
{
|
||||||
|
if($assetID) $data = "%{$assetID}%\n{$data}";
|
||||||
|
else $data = "\n{$data}";
|
||||||
|
$signedScript = "%" . self::CryptGetSignature($data) . "%{$data}";
|
||||||
|
return $signedScript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class System
|
||||||
|
{
|
||||||
|
static function GetFileSize($bytes, $binaryPrefix = true)
|
||||||
|
{
|
||||||
|
$unit=array('B','KB','MB','GB','TB','PB');
|
||||||
|
if (!$bytes) return '0 ' . $unit[0];
|
||||||
|
if ($binaryPrefix) return round($bytes/pow(1024,($i=floor(log($bytes,1024)))),2) .' '. ($unit[$i] ?? 'B');
|
||||||
|
return round($bytes/pow(1000,($i=floor(log($bytes,1000)))),2) .' '. ($unit[$i] ?? 'B');
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetFolderSize($path, $raw = false)
|
||||||
|
{
|
||||||
|
$io = popen("du -sb $path", "r");
|
||||||
|
$size = (int)filter_var(explode($path, fgets($io, 4096), 2)[0], FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
pclose($io);
|
||||||
|
|
||||||
|
if($raw) return $size;
|
||||||
|
return self::getFileSize($size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetMemoryUsage()
|
||||||
|
{
|
||||||
|
$lines = explode("\n", file_get_contents('/proc/meminfo'));
|
||||||
|
$total = (int) filter_var($lines[0], FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
$free = (int) filter_var($lines[1], FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
return (object)["total" => $total*1024, "free" => $free*1024];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class Thumbnails
|
||||||
|
{
|
||||||
|
// this is for use with the new polygon cdn
|
||||||
|
|
||||||
|
// currently this just calculates the sha1 hash
|
||||||
|
// of the user's current thumbnail on the fly
|
||||||
|
|
||||||
|
// from what ive seen it doesnt affect performance
|
||||||
|
// too much but ideally i would have the hash cached
|
||||||
|
// in the database, but for now this should do fine
|
||||||
|
|
||||||
|
private static string $BaseURL = "https://polygoncdn.pizzaboxer.xyz/";
|
||||||
|
|
||||||
|
private static array $StatusThumbnails =
|
||||||
|
[
|
||||||
|
"pending-100x100.png" => "0180a01964362301c67cc47344ff34c2041573c0",
|
||||||
|
"pending-110x110.png" => "e3dd8134956391d4b29070f3d4fc8db1a604f160",
|
||||||
|
"pending-250x250.png" => "d2c46fc832fb48e1d24935893124d21f16cb5824",
|
||||||
|
"pending-352x352.png" => "a4ce4cc7e648fba21da9093bcacf1c33c3903ab9",
|
||||||
|
"pending-420x420.png" => "2f4e0764e8ba3946f52e2b727ce5986776a8a0de",
|
||||||
|
"pending-48x48.png" => "4e3da1b2be713426b48ddddbd4ead386aadec461",
|
||||||
|
"pending-75x75.png" => "6ab927863f95d37af1546d31e3bf8b096cc9ed4a",
|
||||||
|
|
||||||
|
"rendering-100x100.png" => "b67cc4a3d126f29a0c11e7cba3843e6aceadb769",
|
||||||
|
"rendering-110x110.png" => "d059575ffed532648d3dcf6b1429defcc98fc8b1",
|
||||||
|
"rendering-250x250.png" => "9794c31aa3c4779f9cb2c541cedf2c25fa3397fe",
|
||||||
|
"rendering-352x352.png" => "f523775cc3da917e15c3b15e4165fee2562c0ff1",
|
||||||
|
"rendering-420x420.png" => "a9e786b5c339f29f9016d21858bf22c54146855c",
|
||||||
|
"rendering-48x48.png" => "d7a9b5d7044636d3011541634aee43ca4a86ade6",
|
||||||
|
"rendering-75x75.png" => "fa2ec2e53a4d50d9103a6e4370a3299ba5391544",
|
||||||
|
|
||||||
|
"unapproved-100x100.png" => "d4b4b1f0518597bafcd9cf342b6466275db34bbc",
|
||||||
|
"unapproved-110x110.png" => "7ad17e54cf834efd298d76c8799f58daf9c6829f",
|
||||||
|
"unapproved-250x250.png" => "cddec9d17ee3afc5da51d2fbf8011e562362e39a",
|
||||||
|
"unapproved-352x352.png" => "509b6c7bdb121e4185662987096860dd7f54ae11",
|
||||||
|
"unapproved-420x420.png" => "f31bc4f3d5008732f91ac90608d4e77fcd8d8d2b",
|
||||||
|
"unapproved-48x48.png" => "82da22ba47414d25ee544a253f6129d106cf17ef",
|
||||||
|
"unapproved-75x75.png" => "13ad6ad9ab4f84f03c58165bc8468a181d07339c"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static function GetCDNLocation($hash)
|
||||||
|
{
|
||||||
|
return self::$BaseURL.$hash.".png";
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetStatus($status, $x, $y)
|
||||||
|
{
|
||||||
|
return self::GetCDNLocation(self::$StatusThumbnails["{$status}-{$x}x{$y}.png"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function UploadToCDN($filepath)
|
||||||
|
{
|
||||||
|
$hash = sha1_file($filepath);
|
||||||
|
file_put_contents($_SERVER["DOCUMENT_ROOT"]."/../polygoncdn/".$hash.".png", file_get_contents($filepath));
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetAsset($sqlResult, $x, $y, $force = false)
|
||||||
|
{
|
||||||
|
// for this we need to pass in an sql pdo result
|
||||||
|
// this is so we can check if the asset is under review or disapproved
|
||||||
|
// passing in the sql result here saves us from having to do another query
|
||||||
|
// if we implement hash caching then we'd also use this for that
|
||||||
|
|
||||||
|
$assetID = $sqlResult->id;
|
||||||
|
$filepath = SITE_CONFIG['paths']['thumbs_assets']."/{$assetID}-{$x}x{$y}.png";
|
||||||
|
if(!file_exists($filepath)) return self::GetStatus("rendering", $x, $y);
|
||||||
|
|
||||||
|
if($force || $sqlResult->approved == 1) return self::GetCDNLocation(sha1_file($filepath));
|
||||||
|
if($sqlResult->approved == 0) return self::GetStatus("pending", $x, $y);
|
||||||
|
if($sqlResult->approved == 2) return self::GetStatus("unapproved", $x, $y);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetAssetFromID($AssetID, $x, $y, $force = false)
|
||||||
|
{
|
||||||
|
// primarily used for fetching group emblems
|
||||||
|
// we dont need to block this as group emblems are fine to show publicly
|
||||||
|
|
||||||
|
$AssetInfo = db::run("SELECT * FROM assets WHERE id = :id", [":id" => $AssetID]);
|
||||||
|
if(!$AssetInfo->rowCount()) return false;
|
||||||
|
return self::GetAsset($AssetInfo->fetch(PDO::FETCH_OBJ), $x, $y, $force);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetAvatar($avatarID, $x, $y)
|
||||||
|
{
|
||||||
|
if(!SESSION) return self::GetStatus("rendering", $x, $y);
|
||||||
|
|
||||||
|
$filepath = SITE_CONFIG['paths']['thumbs_avatars']."/{$avatarID}-{$x}x{$y}.png";
|
||||||
|
if(!file_exists($filepath)) return self::GetStatus("rendering", $x, $y);
|
||||||
|
return self::GetCDNLocation(sha1_file($filepath));
|
||||||
|
}
|
||||||
|
|
||||||
|
static function UploadAsset($handle, $assetID, $x, $y, $additionalOptions = [])
|
||||||
|
{
|
||||||
|
Polygon::ImportClass("Image");
|
||||||
|
|
||||||
|
$options = ["name" => "{$assetID}-{$x}x{$y}.png", "x" => $x, "y" => $y, "dir" => "/thumbs/assets/"];
|
||||||
|
$options = array_merge($options, $additionalOptions);
|
||||||
|
|
||||||
|
Image::Process($handle, $options);
|
||||||
|
self::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/{$assetID}-{$x}x{$y}.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
static function UploadAvatar($handle, $avatarID, $x, $y)
|
||||||
|
{
|
||||||
|
Polygon::ImportClass("Image");
|
||||||
|
|
||||||
|
Image::Process($handle, ["name" => "{$avatarID}-{$x}x{$y}.png", "x" => $x, "y" => $y, "dir" => "/thumbs/avatars/"]);
|
||||||
|
self::UploadToCDN(SITE_CONFIG['paths']['thumbs_avatars']."/{$avatarID}-{$x}x{$y}.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class TwoFactorAuth
|
||||||
|
{
|
||||||
|
static function Initialize()
|
||||||
|
{
|
||||||
|
require ROOT.'/api/private/vendors/2fa/FixedBitNotation.php';
|
||||||
|
require ROOT.'/api/private/vendors/2fa/GoogleQrUrl.php';
|
||||||
|
require ROOT.'/api/private/vendors/2fa/GoogleAuthenticatorInterface.php';
|
||||||
|
require ROOT.'/api/private/vendors/2fa/GoogleAuthenticator.php';
|
||||||
|
return new \Google\Authenticator\GoogleAuthenticator();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function Toggle()
|
||||||
|
{
|
||||||
|
if(!SESSION) return false;
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"UPDATE users SET twofa = :2fa WHERE id = :uid",
|
||||||
|
[":2fa" => (int)!SESSION["2fa"], ":uid" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GenerateRecoveryCodes()
|
||||||
|
{
|
||||||
|
if(!SESSION) return false;
|
||||||
|
|
||||||
|
$codes = str_split(bin2hex(random_bytes(60)), 12);
|
||||||
|
db::run(
|
||||||
|
"UPDATE users SET twofaRecoveryCodes = :json WHERE id = :uid",
|
||||||
|
[":json" => json_encode(array_fill_keys($codes, true)), ":uid" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
return $codes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GenerateNewSecret($GoogleAuthenticator)
|
||||||
|
{
|
||||||
|
if(!SESSION) return false;
|
||||||
|
|
||||||
|
$secret = $GoogleAuthenticator->generateSecret();
|
||||||
|
db::run(
|
||||||
|
"UPDATE users SET twofaSecret = :secret WHERE id = :uid",
|
||||||
|
[":secret" => $secret, ":uid" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
return $secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$pdo = new PDO(
|
||||||
|
'mysql:host='.SITE_CONFIG["database"]["host"].';
|
||||||
|
dbname='.SITE_CONFIG["database"]["schema"].';
|
||||||
|
charset=utf8mb4',
|
||||||
|
SITE_CONFIG["database"]["username"],
|
||||||
|
SITE_CONFIG["database"]["password"]
|
||||||
|
);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
}
|
||||||
|
catch(PDOException $e)
|
||||||
|
{
|
||||||
|
echo "Project Polygon is currently undergoing maintenance. We will be back soon!";
|
||||||
|
if(isset($_GET['showError'])) echo $e->getMessage();
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
class db
|
||||||
|
{
|
||||||
|
static function run($sql, $params = false)
|
||||||
|
{
|
||||||
|
global $pdo;
|
||||||
|
if(!$params) return $pdo->query($sql);
|
||||||
|
|
||||||
|
$query = $pdo->prepare($sql);
|
||||||
|
$query->execute($params);
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,585 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class pageBuilder
|
||||||
|
{
|
||||||
|
public static string $additionalFooterStuff = "";
|
||||||
|
|
||||||
|
public static array $JSdependencies =
|
||||||
|
[
|
||||||
|
"https://code.jquery.com/jquery-3.0.0.min.js",
|
||||||
|
"/js/toastr.js"
|
||||||
|
];
|
||||||
|
|
||||||
|
// this is separate from js dependencies as these MUST be loaded at the bottom
|
||||||
|
public static array $polygonScripts = ["/js/polygon/core.js?t=35"];
|
||||||
|
|
||||||
|
public static array $CSSdependencies =
|
||||||
|
[
|
||||||
|
"/css/fontawesome-pro-v5.15.2/css/all.css",
|
||||||
|
"/css/toastr.css",
|
||||||
|
"/css/polygon.css?t=15"
|
||||||
|
];
|
||||||
|
|
||||||
|
public static array $pageConfig =
|
||||||
|
[
|
||||||
|
"title" => false,
|
||||||
|
"og:site_name" => SITE_CONFIG["site"]["name"],
|
||||||
|
"og:url" => "https://polygon.pizzaboxer.xyz",
|
||||||
|
"og:description" => "yeah its a website about shapes and squares and triangles and stuff and ummmmm",
|
||||||
|
"og:image" => "https://polygon.pizzaboxer.xyz/img/ProjectPolygon.png",
|
||||||
|
"includeNav" => true,
|
||||||
|
"includeFooter" => true,
|
||||||
|
"app-attributes" => ""
|
||||||
|
];
|
||||||
|
|
||||||
|
// todo - in retrospect this shouldnt even be a thing and so
|
||||||
|
// whatever uses this probably smells so uh should work on that
|
||||||
|
static function showStaticNotification($type, $text)
|
||||||
|
{
|
||||||
|
self::$additionalFooterStuff .= '<script type="text/javascript">$(function(){ toastr["'.$type.'"]("'.$text.'"); });</script>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo - change this to use the newer polygon.buildmodal function
|
||||||
|
static function showStaticModal($options)
|
||||||
|
{
|
||||||
|
//$body = trim(preg_replace('/\s\s+/', ' ', $body));
|
||||||
|
//$body = str_replace('"', '\"', $body);
|
||||||
|
self::$additionalFooterStuff .= '<script type="text/javascript">$(function(){ polygon.buildModal('.json_encode($options).'); });</script>';
|
||||||
|
}
|
||||||
|
|
||||||
|
static function buildHeader()
|
||||||
|
{
|
||||||
|
$theme = "light";
|
||||||
|
|
||||||
|
// ideally i should probably have this loaded in from
|
||||||
|
// core.php instead of doing the php query on the fly here
|
||||||
|
global $pdo, $announcements, $markdown;
|
||||||
|
if(SESSION)
|
||||||
|
{
|
||||||
|
if(SESSION["adminLevel"])
|
||||||
|
{
|
||||||
|
$pendingAssets = db::run("SELECT COUNT(*) FROM assets WHERE NOT approved AND (type != 1 || (SELECT COUNT(*) FROM polygon.groups WHERE emblem = assets.id))")->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
$theme = SESSION["userInfo"]["theme"];
|
||||||
|
|
||||||
|
if($theme == "dark") self::$CSSdependencies[] = "/css/polygon-dark.css?t=4";
|
||||||
|
else if($theme == "2013") self::$CSSdependencies[] = "/css/polygon-2013.css";
|
||||||
|
else if($theme == "hitius") self::$CSSdependencies[] = "/css/polygon-hitius.css";
|
||||||
|
else if($theme == "2014")
|
||||||
|
{
|
||||||
|
self::$CSSdependencies[] = "/css/polygon-2014.css?t=".time();
|
||||||
|
self::$JSdependencies[] = "/js/polygon/Navigation2014.js?t=".time();
|
||||||
|
self::$pageConfig["app-attributes"] .= " id=\"navContent\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="polygon-csrf" content="<?=SESSION?SESSION["csrfToken"]:'false'?>">
|
||||||
|
<link rel='shortcut icon' type='image/x-icon' href='/img/ProjectPolygon.ico' />
|
||||||
|
<title><?=(self::$pageConfig["title"] ? self::$pageConfig["title"] . " - " : "") . SITE_CONFIG["site"]["name"]?></title>
|
||||||
|
<meta name="theme-color" content="#eb4034">
|
||||||
|
<meta property="og:title" content="<?=self::$pageConfig["title"]?>">
|
||||||
|
<meta property="og:site_name" content="<?=self::$pageConfig["og:site_name"]?>">
|
||||||
|
<meta property="og:url" content="<?=self::$pageConfig["og:url"]?>">
|
||||||
|
<meta property="og:description" content="<?=self::$pageConfig["og:description"]?>">
|
||||||
|
<meta property="og:type" content="Website">
|
||||||
|
<meta property="og:image" content="<?=self::$pageConfig["og:image"]?>">
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
|
||||||
|
<?php foreach(self::$CSSdependencies as $url){ ?>
|
||||||
|
<link rel="stylesheet" href="<?=$url?>">
|
||||||
|
<?php } foreach(self::$JSdependencies as $url){ ?>
|
||||||
|
<script type="text/javascript" src="<?=$url?>"></script>
|
||||||
|
<?php } ?>
|
||||||
|
<script>
|
||||||
|
var polygon = {};
|
||||||
|
|
||||||
|
polygon.user =
|
||||||
|
{
|
||||||
|
<?php if(SESSION) { ?>
|
||||||
|
logged_in: true,
|
||||||
|
name: "<?=SESSION["userName"]?>",
|
||||||
|
id: <?=SESSION["userId"]?>,
|
||||||
|
money: <?=SESSION["currency"]?>,
|
||||||
|
<?php } else { ?>
|
||||||
|
logged_in: false,
|
||||||
|
<?php } ?>
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<?php if($theme == "2014") { ?>
|
||||||
|
<body class="layout-2014 mt-5">
|
||||||
|
<div class="nav-container no-gutter-ads">
|
||||||
|
<div class="navigation" id="navigation" onselectstart="return false;">
|
||||||
|
<div class="navigation-container">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<div class="user">
|
||||||
|
<div class="menu-item">
|
||||||
|
<div class="username"><a href="/user?ID=<?=SESSION["userId"]?>"><?=SESSION["userName"]?></a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-my-roblox">
|
||||||
|
<a class="menu-item" href="/home">
|
||||||
|
<span class="icon"></span>Home
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-profile">
|
||||||
|
<a class="menu-item" href="/user">
|
||||||
|
<span class="icon"></span>Profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!--li class="nav2014-messages">
|
||||||
|
<a class="menu-item" href="/user">
|
||||||
|
<span class="icon"></span>Messages
|
||||||
|
</a>
|
||||||
|
</li-->
|
||||||
|
<li class="nav2014-friends">
|
||||||
|
<a class="menu-item" href="/friends">
|
||||||
|
<span class="icon"></span>Friends
|
||||||
|
<span class="notification-icon text-light friend-requests-indicator<?=!SESSION["friendRequests"]?' d-none':''?>"><?=SESSION["friendRequests"]?></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-character">
|
||||||
|
<a class="menu-item" href="/my/character">
|
||||||
|
<span class="icon"></span>Character
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-inventory">
|
||||||
|
<a class="menu-item" href="/my/stuff">
|
||||||
|
<span class="icon"></span>Inventory
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-develop">
|
||||||
|
<a class="menu-item" href="/develop">
|
||||||
|
<span class="icon"></span>Develop
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-groups">
|
||||||
|
<a class="menu-item" href="/groups">
|
||||||
|
<span class="icon"></span>Groups
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-forum">
|
||||||
|
<a class="menu-item" href="/forum">
|
||||||
|
<span class="icon"></span>Forum
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php if(SESSION["adminLevel"]) { ?>
|
||||||
|
<li class="nav2014-profile">
|
||||||
|
<a class="menu-item" href="/admin">
|
||||||
|
<span class="icon"></span>Admin
|
||||||
|
<span class="notification-icon text-light <?=!$pendingAssets?' d-none':''?>"><?=$pendingAssets?></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php } ?>
|
||||||
|
<!--li class="nav2014-blog">
|
||||||
|
<a class="menu-item" href="https://web.archive.org/web/20140821165031/http://blog.roblox.com/">
|
||||||
|
<span class="icon"></span>Blog
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="upgrade-now">
|
||||||
|
<a href="/web/20140821165031/http://www.roblox.com/Upgrades/BuildersClubMemberships.aspx" class="nav-button" id="builders-club-button">Upgrade Now</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-events">
|
||||||
|
<span class="events-text">Events</span>
|
||||||
|
</li>
|
||||||
|
<li class="nav2014-sponsor">
|
||||||
|
<a class="menu-item" href="/web/20140821165031/http://www.roblox.com/event/clanbattle" title="Clan Battle">
|
||||||
|
<img src="https://web.archive.org/web/20140821165031im_/http://images.rbxcdn.com/6d4be1b1b1e67971c837da0dae72e82d">
|
||||||
|
</a>
|
||||||
|
</li-->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark navbar-orange header-2014 fixed-top py-0">
|
||||||
|
<div class="nav-icon" onselectstart="return false;"></div>
|
||||||
|
<a class="navbar-brand pt-0" href="/"><img src="/img/2013/roblox_logo.png"></a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#primaryNavbar" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="primaryNavbar">
|
||||||
|
<ul class="navbar-nav header-links mr-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/games">Games</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/catalog">Catalog</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/develop">Develop</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/forum">Forum</a>
|
||||||
|
</li>
|
||||||
|
<div class="search">
|
||||||
|
<div class="search-input-container">
|
||||||
|
<input type="text" placeholder="Search">
|
||||||
|
</div>
|
||||||
|
<div class="search-icon"></div>
|
||||||
|
<div class="universal-search-dropdown">
|
||||||
|
<div class="universal-search-option" data-searchurl="/games?Keyword=">
|
||||||
|
<div class="universal-search-text">Search <span class="universal-search-string"></span> in Games</div>
|
||||||
|
</div>
|
||||||
|
<div class="universal-search-option" data-searchurl="/browse?Category=People&SearchTextBox=">
|
||||||
|
<div class="universal-search-text">Search <span class="universal-search-string"></span> in People</div>
|
||||||
|
</div>
|
||||||
|
<div class="universal-search-option selected" data-searchurl="/browse?Category=Groups&SearchTextBox=">
|
||||||
|
<div class="universal-search-text">Search <span class="universal-search-string"></span> in Groups</div>
|
||||||
|
</div>
|
||||||
|
<div class="universal-search-option" data-searchurl="/catalog?Keyword=">
|
||||||
|
<div class="universal-search-text">Search <span class="universal-search-string"></span> in Catalog</div>
|
||||||
|
</div>
|
||||||
|
<div class="universal-search-option" data-searchurl="/catalog?CurrencyType=0&SortType=1&Category=6&Keyword=">
|
||||||
|
<div class="universal-search-text">Search <span class="universal-search-string"></span> in Library</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<div class="navbar-button-container">
|
||||||
|
<a class="btn btn-sm mr-4" data-toggle="tooltip" data-html="true" data-placement="bottom" title="<?=SESSION["currency"]?> <?=SITE_CONFIG["site"]["currency"]?> <br> Next stipend in <?=timeSince("@".SESSION["nextCurrencyStipend"], true, false, false, true)?>" href="/my/money"><i class="fas fa-pizza-slice mr-1"></i> <?=SESSION["currency"]?></a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-button-container dropdown-hover">
|
||||||
|
<a class="btn btn-sm" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-cog"></i></a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right mx-2" aria-labelledby="settings-dropdown">
|
||||||
|
<a class="dropdown-item" href="/my/account">Settings</a>
|
||||||
|
<a class="dropdown-item" href="/logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<noscript>
|
||||||
|
<div id="ctl00_Announcement">
|
||||||
|
<div id="ctl00_SystemAlertDiv" class="SystemAlert" style="background-color:red">
|
||||||
|
<div id="ctl00_SystemAlertTextColor" class="SystemAlertText">
|
||||||
|
<div id="ctl00_LabelAnnouncement">Please enable Javascript to use all the features on this site.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<?php foreach($announcements as $announcement) { ?>
|
||||||
|
<div id="ctl00_Announcement">
|
||||||
|
<div id="ctl00_SystemAlertDiv" class="SystemAlert" style="background-color:<?=$announcement["bgcolor"]?>">
|
||||||
|
<div id="ctl00_SystemAlertTextColor" class="SystemAlertText">
|
||||||
|
<div id="ctl00_LabelAnnouncement"><?=$markdown->line($announcement["text"])?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php } /* foreach($announcements as $announcement) */ ?>
|
||||||
|
<div class="app container py-4 nav-content"<?=self::$pageConfig['app-attributes']?>>
|
||||||
|
<?php } else { ?>
|
||||||
|
<body>
|
||||||
|
<?php if(self::$pageConfig["includeNav"]) { ?>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark navbar-orange navbar-top py-0">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="/"><?=SITE_CONFIG["site"]["name"]?></a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#primaryNavbar" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="primaryNavbar">
|
||||||
|
<ul class="navbar-nav header-links mr-auto">
|
||||||
|
<?php if(SESSION) { ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/games">Games</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/catalog">Catalog</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/develop">Develop</a>
|
||||||
|
</li>
|
||||||
|
<?php } ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/forum">Forum</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/browse">People</a>
|
||||||
|
</li>
|
||||||
|
<?php if(SESSION) { ?>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="moreDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">More</a>
|
||||||
|
<div class="dropdown-menu mt-0" aria-labelledby="moreDropdown">
|
||||||
|
<!--a class="dropdown-item" href="/browse">People</a>
|
||||||
|
<div class="dropdown-divider"></div-->
|
||||||
|
<a class="dropdown-item" href="/discord">Discord</a>
|
||||||
|
<!--a class="dropdown-item" href="https://twitter.com/boxerpizza">Twitter</a-->
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<?php } ?>
|
||||||
|
</ul>
|
||||||
|
<div class="navbar-nav">
|
||||||
|
<?php if(SESSION) { ?>
|
||||||
|
<a class="nav-link mr-2" href="/user?ID=<?=SESSION["userId"]?>"><?=SESSION["userName"]?></a>
|
||||||
|
<div class="navbar-button-container">
|
||||||
|
<a class="btn btn-sm btn-light py-0 px-1 friend-requests-indicator<?=!SESSION["friendRequests"]?' d-none':''?>"><?=SESSION["friendRequests"]?></a>
|
||||||
|
<a class="btn btn-sm btn-outline-light my-1 mr-2" title="My Friends" href="/friends" data-toggle="tooltip" data-html="true" data-placement="bottom">
|
||||||
|
<i class="fas fa-user-friends"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-button-container">
|
||||||
|
<a class="btn btn-sm btn-outline-light my-1 mr-2" data-toggle="tooltip" data-html="true" data-placement="bottom" title="<?=SESSION["currency"]?> <?=SITE_CONFIG["site"]["currency"]?> <br> Next stipend in <?=timeSince("@".SESSION["nextCurrencyStipend"], true, false, false, true)?>" href="/my/money"><i class="fal fa-pizza-slice"></i> <?=SESSION["currency"]?></a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-button-container">
|
||||||
|
<a class="btn btn-sm btn-light my-1 mr-2 px-3" href="/logout">Logout</a>
|
||||||
|
</div>
|
||||||
|
<?php } else { ?>
|
||||||
|
<a class="nav-link" href="/">Sign Up</a>
|
||||||
|
<span class="nav-link darken px-0">or</span>
|
||||||
|
<a class="btn btn-sm btn-light my-1 mx-2 px-4" href="/login">Login</a>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<?php if(SESSION) { ?>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark navbar-top bg-dark py-0">
|
||||||
|
<div class="container">
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#secondaryNavbar" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="secondaryNavbar">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-1" href="/user">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-1" href="/my/character">Character</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-1" href="/friends">Friends</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-1" href="/groups">Groups</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-1" href="/my/stuff">Inventory</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-1" href="/my/money">Money</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-1" href="/my/account">Account</a>
|
||||||
|
</li>
|
||||||
|
<?php if(SESSION && SESSION["adminLevel"]) { ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-1" href="/admin">Admin <span class="btn btn-sm btn-outline-light py-0<?=!$pendingAssets?' d-none':''?>" style="margin-top:-3px"><?=$pendingAssets?></span></a>
|
||||||
|
</li>
|
||||||
|
<?php } ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<?php } /* if(SESSION) */ ?>
|
||||||
|
</div>
|
||||||
|
<noscript>
|
||||||
|
<div class="alert py-2 mb-0 rounded-0 text-center text-light bg-danger" role="alert">
|
||||||
|
disabling javascript breaks the ux in half so dont do it pls
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
<?php foreach($announcements as $announcement) { ?>
|
||||||
|
<div class="alert py-2 mb-0 rounded-0 text-center text-<?=$announcement["textcolor"]?>" role="alert" style="background-color: <?=$announcement["bgcolor"]?>">
|
||||||
|
<?=$markdown->text($announcement["text"])?>
|
||||||
|
</div>
|
||||||
|
<?php } /* foreach($announcements as $announcement) */ ?>
|
||||||
|
<?php } /* if(self::$pageConfig["includeNav"]) */ ?>
|
||||||
|
<div class="app container py-4"<?=self::$pageConfig['app-attributes']?>>
|
||||||
|
<?php } /* if($theme == "2014") */ ?>
|
||||||
|
<?php } static function buildFooter() { ?>
|
||||||
|
</div>
|
||||||
|
<?php if(self::$pageConfig["includeFooter"]) { ?>
|
||||||
|
<nav class="footer navbar navbar-light navbar-orange">
|
||||||
|
<div class="container py-2 text-light text-center">
|
||||||
|
<!--div class="row" style="width:100%">
|
||||||
|
<div class="col-sm-3 text-center pt-1" style="border-right: 1px solid #ccc;">
|
||||||
|
<h3 class="font-weight-normal"><?=SITE_CONFIG["site"]["name"]?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-9 pl-4">
|
||||||
|
<p class="font-weight-normal mb-0 pl-2">made by pizzaboxer</p>
|
||||||
|
<p class="font-weight-normal mb-0 pl-2">copyright © <?=SITE_CONFIG["site"]["name"]?> 2020</p>
|
||||||
|
</div>
|
||||||
|
</div-->
|
||||||
|
<div class="mx-auto">
|
||||||
|
<span><small class="px-2">Copyright © <?=SITE_CONFIG["site"]["name"]?> 2020-<?=date('Y')?></small> | <small class="px-2"><?=db::run("SELECT COUNT(*) FROM users")->fetchColumn()?> users registered</small> | <a href="/info/privacy" class="text-light px-2">Privacy Policy</a> | <a href="/info/terms-of-service" class="text-light px-2">Terms of Service</a></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<?php } ?>
|
||||||
|
<div class="global modal fade" tabindex="-1" role="dialog" aria-labelledby="primaryModalCenter" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header card-header bg-cardpanel py-2">
|
||||||
|
<h3 class="col-12 modal-title text-center font-weight-normal"></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center text-break">
|
||||||
|
your smell
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer text-center">
|
||||||
|
<div class="mx-auto">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if(SESSION) { ?>
|
||||||
|
<div class="placelauncher modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content"></div>
|
||||||
|
</div>
|
||||||
|
<div class="launch template d-none">
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<span class="jumbo spinner-border text-danger mb-3" role="status"></span>
|
||||||
|
<h5 class="font-weight-normal mb-3">Starting <?=SITE_CONFIG["site"]["name"]?>...</h5>
|
||||||
|
<a class="btn btn-sm btn-outline-danger btn-block px-4" data-dismiss="modal">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="install template d-none">
|
||||||
|
<div class="modal-body text-center pb-0">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
<img src="/img/ProjectPolygon.png" class="img-fluid pl-3 py-3 pr-1" style="max-width: 150px">
|
||||||
|
<h2 class="font-weight-normal">Welcome to <?=SITE_CONFIG["site"]["name"]?>!</h2>
|
||||||
|
<h5 class="font-weight-normal">Seems like you don't have <span class="year">2010</span> installed</h5>
|
||||||
|
<a class="btn btn-success btn-block mx-auto mt-3 install" style="max-width:18rem">Download</a>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer text-center py-2">
|
||||||
|
<small class="mx-auto">If you do have the client installed, just ignore this</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<?php if(SESSION && SESSION["adminLevel"]){ ?>
|
||||||
|
<script>
|
||||||
|
//admin.js
|
||||||
|
if (polygon.admin == undefined) polygon.admin = {};
|
||||||
|
|
||||||
|
polygon.admin.forum =
|
||||||
|
{
|
||||||
|
moderate_post_prompt: function(type, id)
|
||||||
|
{
|
||||||
|
polygon.buildModal({
|
||||||
|
header: "Delete Post",
|
||||||
|
body: 'Are you sure you want to delete this post?',
|
||||||
|
buttons: [{class:'btn btn-danger px-4 post-delete-confirm', attributes:[{attr:'data-type', val:type}, {attr: 'data-id', val:id}], dismiss:true, text:'Yes'}, {class:'btn btn-secondary px-4', dismiss:true, text:'No'}]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
moderate_post: function(type, id)
|
||||||
|
{
|
||||||
|
$.post('/api/admin/delete-post', {"postType": type, "postId": id}, function(data)
|
||||||
|
{
|
||||||
|
if(data.success)
|
||||||
|
{
|
||||||
|
toastr["success"]("Post has been deleted");
|
||||||
|
setTimeout(function(){ window.location.reload(); }, 3);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toastr["error"](data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
polygon.admin.gitpull = function()
|
||||||
|
{
|
||||||
|
polygon.buildModal({
|
||||||
|
header: "<i class=\"fab fa-git-alt text-danger\"></i> Git Pull",
|
||||||
|
body: "<span class=\"spinner-border spinner-border-sm text-danger\" role=\"status\" aria-hidden=\"true\"></span> Executing Git Pull...",
|
||||||
|
buttons:
|
||||||
|
[
|
||||||
|
{class: 'btn btn-outline-primary', dismiss: true, text: 'Close'},
|
||||||
|
{class: 'btn btn-outline-danger disabled', attributes: [{attr: "disabled", val: "disabled"}], text: 'Run Again'}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
$.get("/api/admin/git-pull", function(data)
|
||||||
|
{
|
||||||
|
polygon.buildModal({
|
||||||
|
header: "<i class=\"fab fa-git-alt text-danger\"></i> Git Pull",
|
||||||
|
body: "<pre class=\"mb-0\">"+data+"</pre>",
|
||||||
|
buttons:
|
||||||
|
[
|
||||||
|
{class: 'btn btn-outline-primary', dismiss: true, text: 'Close'},
|
||||||
|
{class: 'btn btn-outline-success gitpull', text: 'Run Again'}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("body").on("click", ".gitpull", polygon.admin.gitpull);
|
||||||
|
|
||||||
|
$("body").keydown(function(event)
|
||||||
|
{
|
||||||
|
if (event.originalEvent.ctrlKey && event.originalEvent.key == "/") polygon.admin.gitpull();
|
||||||
|
});
|
||||||
|
|
||||||
|
polygon.admin.request_render = function(type, id)
|
||||||
|
{
|
||||||
|
$.post('/api/admin/request-render', {"renderType": type, "assetID": id}, function(data)
|
||||||
|
{
|
||||||
|
if(data.success) toastr["success"](data.message);
|
||||||
|
else toastr["error"](data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("body").on("click", ".post-delete", function(){ polygon.admin.forum.moderate_post_prompt($(this).attr("data-type"), $(this).attr("data-id")); });
|
||||||
|
$("body").on("click", ".post-delete-confirm", function(){ polygon.admin.forum.moderate_post($(this).attr("data-type"), $(this).attr("data-id")); });
|
||||||
|
$("body").on("click", ".request-render", function(){ polygon.admin.request_render($(this).attr("data-type"), $(this).attr("data-id")); });
|
||||||
|
</script>
|
||||||
|
<?php } ?>
|
||||||
|
<?php foreach(self::$polygonScripts as $url){ ?>
|
||||||
|
<script type="text/javascript" src="<?=$url?>"></script>
|
||||||
|
<?php } ?>
|
||||||
|
<?=self::$additionalFooterStuff?>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
ob_end_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function errorCode($code, $customText = false)
|
||||||
|
{
|
||||||
|
http_response_code($code);
|
||||||
|
|
||||||
|
$text =
|
||||||
|
[
|
||||||
|
400 => ["title" => "Bad request", "text" => "There was a problem with your request"],
|
||||||
|
404 => ["title" => "Requested page not found", "text" => "You may have clicked an expired link or mistyped the address"],
|
||||||
|
420 => ["title" => "Website is currently under maintenance", "text" => "check back later"],
|
||||||
|
500 => ["title" => "Unexpected error with your request", "text" => "Please try again after a few moments"]
|
||||||
|
];
|
||||||
|
|
||||||
|
if(!isset($text[$code])) $code = 500;
|
||||||
|
|
||||||
|
if (is_array($customText) && count($customText)) $text[$code] = $customText;
|
||||||
|
|
||||||
|
self::buildHeader();
|
||||||
|
?>
|
||||||
|
<div class="card mx-auto" style="max-width:640px;">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<img src="/img/error.png">
|
||||||
|
<h2 class="font-weight-normal"><?=$text[$code]["title"]?></h2>
|
||||||
|
<?=$text[$code]["text"]?>
|
||||||
|
<?php if (!is_array($customText) && !empty($customText) && $code == 500) { ?>
|
||||||
|
<pre class="mt-4"><?=$customText?></pre>
|
||||||
|
<?php } ?>
|
||||||
|
<hr>
|
||||||
|
<a class="btn btn-outline-primary mx-1 mt-1 py-1" onclick="window.history.back()">Go to Previous Page</a>
|
||||||
|
<a class="btn btn-outline-primary mx-1 mt-1 py-1" href="/">Return Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
self::buildFooter();
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
define("SITE_CONFIG",
|
||||||
|
[
|
||||||
|
"database" =>
|
||||||
|
[
|
||||||
|
"host" => "127.0.0.1",
|
||||||
|
"schema" => "polygon",
|
||||||
|
"username" => "polygon",
|
||||||
|
"password" => ""
|
||||||
|
],
|
||||||
|
|
||||||
|
"site" =>
|
||||||
|
[
|
||||||
|
"name" => "Project Polygon",
|
||||||
|
"name_secondary" => "Polygon",
|
||||||
|
"currency" => "Pizzas",
|
||||||
|
"private" => true,
|
||||||
|
"games" => true,
|
||||||
|
"thumbserver" => true
|
||||||
|
],
|
||||||
|
|
||||||
|
"keys" => // DO NOT ALTER ANY OF THESE UNLESS NECESSARY
|
||||||
|
[
|
||||||
|
// use \Defuse\Crypto\Key::createNewRandomKey()->saveToAsciiSafeString(); for this
|
||||||
|
"passwordEncryption" => "",
|
||||||
|
"renderserverApi" => "",
|
||||||
|
],
|
||||||
|
|
||||||
|
"api" => // deprecated - use above
|
||||||
|
[
|
||||||
|
"renderserverKey" => ""
|
||||||
|
],
|
||||||
|
|
||||||
|
"paths" =>
|
||||||
|
[
|
||||||
|
"assets" => $_SERVER['DOCUMENT_ROOT']."/asset/files/",
|
||||||
|
"thumbs_assets" => $_SERVER['DOCUMENT_ROOT']."/thumbs/assets/",
|
||||||
|
"thumbs_avatars" => $_SERVER['DOCUMENT_ROOT']."/thumbs/avatars/"
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
@ -0,0 +1,902 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// project polygon core backend
|
||||||
|
// made by pizzaboxer
|
||||||
|
// development started on 4th september 2020
|
||||||
|
|
||||||
|
// lets just hope this doesnt devolve into yandev spaghetti
|
||||||
|
// like goodblox and roblonium
|
||||||
|
// tho those were already spaghetti bfeore i ever became staff on them
|
||||||
|
// this should be a good clean and fresh start from scratch
|
||||||
|
// hell, not even any php frameworks
|
||||||
|
|
||||||
|
// btw if i eventually open source this then hi
|
||||||
|
|
||||||
|
// as of 24th february 2021, project polygon's core functionality is complete
|
||||||
|
// asset uploading, catalog, games, etc
|
||||||
|
// funnily enough this is the first ever web project ive actually completed since
|
||||||
|
// pizzaboxer.ml 1 and a half years ago, though thats beside the point. anyway:
|
||||||
|
// theres still a lot left to do here. code improvement, minor features, etc.
|
||||||
|
// literally just ctrl+f for the word "todo" and you'll see how much. speaking of:
|
||||||
|
|
||||||
|
// heres some general todos that i dont have anywhere to put so they're here
|
||||||
|
// - allow user to configure their date format (dd/mm/yyyy, etc)
|
||||||
|
// - allow user to configure their timezone
|
||||||
|
// - add official themes (dark, 2013, etc)
|
||||||
|
// - fix ordering on recently played games
|
||||||
|
// - implement user badges properly
|
||||||
|
|
||||||
|
// small shorthand as having to type out $_SERVER['DOCUMENT_ROOT'] everytime sucks
|
||||||
|
define("ROOT", $_SERVER['DOCUMENT_ROOT']);
|
||||||
|
|
||||||
|
$bypassRules =
|
||||||
|
[
|
||||||
|
"2FA" =>
|
||||||
|
[
|
||||||
|
"/directory_login/2fa.php",
|
||||||
|
"/logout.php"
|
||||||
|
],
|
||||||
|
|
||||||
|
"Moderation" =>
|
||||||
|
[
|
||||||
|
"/moderation.php",
|
||||||
|
"/info/terms-of-service.php",
|
||||||
|
"/info/privacy.php",
|
||||||
|
"/info/selfhosting.php",
|
||||||
|
"/directory_login/2fa.php",
|
||||||
|
"/logout.php"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
if($_SERVER["HTTP_HOST"] == "chef.pizzaboxer.xyz")
|
||||||
|
{
|
||||||
|
header('HTTP/1.1 301 Moved Permanently');
|
||||||
|
header('Location: http://polygon.pizzaboxer.xyz'.$_SERVER['REQUEST_URI']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(
|
||||||
|
!isset($disableHTTPS) &&
|
||||||
|
isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
|
||||||
|
$_SERVER['HTTP_X_FORWARDED_PROTO'] == "http" &&
|
||||||
|
$_SERVER["DOCUMENT_URI"] != "/error.php"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
header('HTTP/1.1 301 Moved Permanently');
|
||||||
|
header('Location: https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($_SERVER['REQUEST_METHOD'] == 'POST') foreach($_POST as $key => $val){ $_POST[$key] = trim($val); }
|
||||||
|
foreach($_GET as $key => $val){ $_GET[$key] = trim($val); }
|
||||||
|
|
||||||
|
// functions that arent strictly specifically for polygon and moreof to just
|
||||||
|
// extend basic php functionality like string manipulation or some small
|
||||||
|
// utilities are typically just put here classless
|
||||||
|
|
||||||
|
// we're still using php7 soooo if we move to php8 dont forget to nuke these
|
||||||
|
// however i also added array support for str_ends_with as it's pretty handy
|
||||||
|
// in retrospect i shoulda named str_ends_with something else but that was
|
||||||
|
// before i added array support
|
||||||
|
|
||||||
|
function str_starts_with($haystack, $needle)
|
||||||
|
{
|
||||||
|
return substr($haystack, 0, strlen($needle)) === $needle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function str_ends_with($haystack, $needle)
|
||||||
|
{
|
||||||
|
if(gettype($needle) == "array")
|
||||||
|
{
|
||||||
|
foreach ($needle as $ending) if(substr($haystack, -strlen($ending)) === $ending) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return substr($haystack, -strlen($needle)) === $needle;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vowel($string)
|
||||||
|
{
|
||||||
|
if(in_array(strtolower(substr($string, 0, 1)), ["a", "e", "i", "o", "u"])) return "an $string";
|
||||||
|
return "a $string";
|
||||||
|
}
|
||||||
|
|
||||||
|
function plural($string)
|
||||||
|
{
|
||||||
|
if(str_ends_with($string, "s")) return $string;
|
||||||
|
return $string."s";
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode_asset_name($string)
|
||||||
|
{
|
||||||
|
$string = str_replace(["[", "]", '"', "'", "(", ")"], "", $string);
|
||||||
|
return preg_replace("![^a-z0-9]+!i", "-", $string);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUUID()
|
||||||
|
{
|
||||||
|
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||||
|
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
||||||
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbtohex($rgb)
|
||||||
|
{
|
||||||
|
$rgb_parsed = sscanf($rgb, "rgb(%i, %i, %i)");
|
||||||
|
return sprintf('%02X%02X%02X', $rgb_parsed[0], $rgb_parsed[1], $rgb_parsed[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirect($url)
|
||||||
|
{
|
||||||
|
die(header("Location: $url"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetIPAddress()
|
||||||
|
{
|
||||||
|
return $_SERVER["HTTP_CF_CONNECTING_IP"] ?? $_SERVER["HTTP_X_REAL_IP"] ?? $_SERVER["REMOTE_ADDR"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function VerifyReCAPTCHA()
|
||||||
|
{
|
||||||
|
$context = stream_context_create(
|
||||||
|
[
|
||||||
|
'http' =>
|
||||||
|
[
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => 'Content-type: application/x-www-form-urlencoded',
|
||||||
|
'content' => http_build_query(
|
||||||
|
[
|
||||||
|
'secret' => SITE_CONFIG["keys"]["captcha"]["secret"],
|
||||||
|
'response' => $_POST['g-recaptcha-response'] ?? "",
|
||||||
|
'remoteip' => GetIPAddress()
|
||||||
|
])
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context);
|
||||||
|
$result = json_decode($response);
|
||||||
|
|
||||||
|
if ($result === false || !$result->success) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEPRECATED: use GetReadableTime() instead
|
||||||
|
function timeSince($datetime, $full = false, $ending = true, $truncate = false, $abbreviate = false)
|
||||||
|
{
|
||||||
|
if(strpos($datetime, '@') === false) $datetime = "@$datetime";
|
||||||
|
if($datetime == "@") return "-";
|
||||||
|
|
||||||
|
if($truncate && ltrim($datetime, "@") < strtotime("1 year ago", time()))
|
||||||
|
return date("n/j/Y", ltrim($datetime, "@"));
|
||||||
|
|
||||||
|
$now = new DateTime;
|
||||||
|
$ago = new DateTime($datetime);
|
||||||
|
$diff = $now->diff($ago);
|
||||||
|
|
||||||
|
$diff->w = floor($diff->d / 7);
|
||||||
|
$diff->d -= $diff->w * 7;
|
||||||
|
|
||||||
|
$string = array(
|
||||||
|
'y' => 'year',
|
||||||
|
'm' => 'month',
|
||||||
|
'w' => 'week',
|
||||||
|
'd' => 'day',
|
||||||
|
'h' => 'hour',
|
||||||
|
'i' => 'minute',
|
||||||
|
's' => 'second',
|
||||||
|
);
|
||||||
|
|
||||||
|
if($abbreviate)
|
||||||
|
{
|
||||||
|
$string = ['y' => 'y', 'm' => 'm', 'w' => 'w', 'd' => 'd', 'h' => 'h', 'i' => 'm', 's' => 's'];
|
||||||
|
|
||||||
|
foreach ($string as $k => &$v)
|
||||||
|
{
|
||||||
|
if ($diff->$k) $v = $diff->$k.$v;
|
||||||
|
else unset($string[$k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' ', $string);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($string as $k => &$v)
|
||||||
|
{
|
||||||
|
if ($diff->$k) $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
|
||||||
|
else unset($string[$k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$full) $string = array_slice($string, 0, 1);
|
||||||
|
if($ending){ return $string ? implode(', ', $string) . ' ago' : 'Just now'; }
|
||||||
|
return implode(', ', $string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/1416697/converting-timestamp-to-time-ago-in-php-e-g-1-day-ago-2-days-ago
|
||||||
|
// btw when you use this be sure to put the regular date format as a title or tooltip attribute
|
||||||
|
function GetReadableTime($Timestamp, $Options = [])
|
||||||
|
{
|
||||||
|
$Timestamp += 1;
|
||||||
|
|
||||||
|
$RelativeTime = $Options["RelativeTime"] ?? false;
|
||||||
|
$Full = $Options["Full"] ?? false;
|
||||||
|
$Ending = $Options["Ending"] ?? true;
|
||||||
|
$Abbreviate = $Options["Abbreviate"] ?? false;
|
||||||
|
$Threshold = $Options["Threshold"] ?? false;
|
||||||
|
|
||||||
|
if($RelativeTime !== false)
|
||||||
|
{
|
||||||
|
$Full = true;
|
||||||
|
$Ending = false;
|
||||||
|
$Timestamp = ($Timestamp+strtotime($RelativeTime, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($Threshold !== false && $Timestamp < strtotime($Threshold, time()))
|
||||||
|
{
|
||||||
|
return date("j/n/Y g:i:s A", $Timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$TimeNow = new DateTime;
|
||||||
|
$TimeAgo = new DateTime("@$Timestamp");
|
||||||
|
$TimeDifference = $TimeNow->diff($TimeAgo);
|
||||||
|
|
||||||
|
$TimeDifference->w = floor($TimeDifference->d / 7);
|
||||||
|
$TimeDifference->d -= $TimeDifference->w * 7;
|
||||||
|
|
||||||
|
if($Abbreviate)
|
||||||
|
{
|
||||||
|
$Components =
|
||||||
|
[
|
||||||
|
'y' => 'y',
|
||||||
|
'm' => 'm',
|
||||||
|
'w' => 'w',
|
||||||
|
'd' => 'd',
|
||||||
|
'h' => 'h',
|
||||||
|
'i' => 'm',
|
||||||
|
's' => 's'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($Components as $Character => &$String)
|
||||||
|
{
|
||||||
|
if ($TimeDifference->$Character) $String = $TimeDifference->$Character . $String;
|
||||||
|
else unset($Components[$Character]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$Components =
|
||||||
|
[
|
||||||
|
'y' => 'year',
|
||||||
|
'm' => 'month',
|
||||||
|
'w' => 'week',
|
||||||
|
'd' => 'day',
|
||||||
|
'h' => 'hour',
|
||||||
|
'i' => 'minute',
|
||||||
|
's' => 'second',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($Components as $Character => &$String)
|
||||||
|
{
|
||||||
|
if ($TimeDifference->$Character) $String = $TimeDifference->$Character . ' ' . $String . ($TimeDifference->$Character > 1 ? 's' : '');
|
||||||
|
else unset($Components[$Character]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$Full) $Components = array_slice($Components, 0, 1);
|
||||||
|
|
||||||
|
$FirstComponent = [join(', ', array_slice($Components, 0, -1))];
|
||||||
|
$LastComponent = array_slice($Components, -1);
|
||||||
|
$ReadableTime = join(' and ', array_filter(array_merge($FirstComponent, $LastComponent), "strlen"));
|
||||||
|
|
||||||
|
if ($Ending) return $Components ? "$ReadableTime ago" : "Just now";
|
||||||
|
return $ReadableTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
class api
|
||||||
|
{
|
||||||
|
static function respond_custom($data)
|
||||||
|
{
|
||||||
|
die(json_encode($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
static function respond($status, $success, $message)
|
||||||
|
{
|
||||||
|
self::respond_custom(["status" => $status, "success" => $success, "message" => $message]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function initialize($options = [])
|
||||||
|
{
|
||||||
|
$secure = $options["secure"] ?? false;
|
||||||
|
$method = $options["method"] ?? "GET";
|
||||||
|
$logged_in = $options["logged_in"] ?? $options["admin"] ?? false;
|
||||||
|
$admin = $options["admin"] ?? false;
|
||||||
|
$api = $options["api"] ?? false;
|
||||||
|
$admin_ratelimit = $options["admin_ratelimit"] ?? false;
|
||||||
|
|
||||||
|
if($admin && (!SESSION || !SESSION["adminLevel"])) pageBuilder::errorCode(404);
|
||||||
|
|
||||||
|
header("content-type: application/json");
|
||||||
|
if($secure) header("referrer-policy: same-origin");
|
||||||
|
if($method && $_SERVER['REQUEST_METHOD'] !== $method) self::respond(405, false, "Method Not Allowed");
|
||||||
|
|
||||||
|
if(isset(SITE_CONFIG["keys"][$api]))
|
||||||
|
{
|
||||||
|
if($method == "POST") $key = $_POST["ApiKey"] ?? false;
|
||||||
|
else $key = $_GET["ApiKey"] ?? false;
|
||||||
|
if(SITE_CONFIG["keys"][$api] !== $key) self::respond(401, false, "Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if($logged_in)
|
||||||
|
{
|
||||||
|
if(!SESSION || SESSION["2fa"] && !SESSION["2faVerified"]) self::respond(401, false, "You are not logged in");
|
||||||
|
if(!isset($_SERVER['HTTP_X_POLYGON_CSRF'])) self::respond(401, false, "Unauthorized");
|
||||||
|
if($_SERVER['HTTP_X_POLYGON_CSRF'] != SESSION["csrfToken"]) self::respond(401, false, "Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
if($admin !== false)
|
||||||
|
{
|
||||||
|
if(!Users::IsAdmin($admin)) self::respond(403, false, "Forbidden");
|
||||||
|
if(!SESSION["2fa"]) self::respond(403, false, "Your account must have two-factor authentication enabled before you can do any administrative actions");
|
||||||
|
if(!$admin_ratelimit) return;
|
||||||
|
|
||||||
|
$lastAction = db::run("SELECT time FROM stafflogs WHERE adminId = :uid AND time + 2 > UNIX_TIMESTAMP()", [":uid" => SESSION["userId"]]);
|
||||||
|
if($lastAction->rowCount()) self::respond(429, false, "Please wait ".(($lastAction->fetchColumn()+2)-time())." seconds before doing another administrative action");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Polygon
|
||||||
|
{
|
||||||
|
public static array $ImportedClasses = ["Polygon"];
|
||||||
|
|
||||||
|
static function ImportClass($Class)
|
||||||
|
{
|
||||||
|
if(!file_exists(ROOT."/api/private/components/{$Class}.php")) return false;
|
||||||
|
if(in_array($Class, self::$ImportedClasses)) return false;
|
||||||
|
|
||||||
|
require ROOT."/api/private/components/{$Class}.php";
|
||||||
|
self::$ImportedClasses[] = $Class;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function IsClientBrowser()
|
||||||
|
{
|
||||||
|
return strpos($_SERVER["HTTP_USER_AGENT"], "MSIE 7.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
static function CanBypass($rule)
|
||||||
|
{
|
||||||
|
global $bypassRules;
|
||||||
|
return !in_array($_SERVER['DOCUMENT_URI'], $bypassRules[$rule]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function FilterText($text, $sanitize = true, $highlight = true, $force = false)
|
||||||
|
{
|
||||||
|
if($sanitize) $text = htmlspecialchars($text);
|
||||||
|
if(!$force && SESSION && !SESSION["filter"]) return $text;
|
||||||
|
|
||||||
|
$filters = rand(0, 1) ? "baba booey" : "Kyle";
|
||||||
|
$filtertext = $highlight ? "<strong><em>$filters</em></strong>" : $filters;
|
||||||
|
|
||||||
|
// todo - make this json-based?
|
||||||
|
return str_ireplace([], $filtertext, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function IsFiltered($text)
|
||||||
|
{
|
||||||
|
return self::FilterText($text, false, false, true) !== $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function IsExplicitlyFiltered($text)
|
||||||
|
{
|
||||||
|
// how likely would this lead to false positives?
|
||||||
|
$text = preg_replace("#[[:punct:]]#", "", $text);
|
||||||
|
$text = str_replace(" ", "", $text);
|
||||||
|
return str_ireplace([], "", $text) != $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function ReplaceVars($string)
|
||||||
|
{
|
||||||
|
$string = str_replace("%site_name%", SITE_CONFIG["site"]["name"], $string);
|
||||||
|
$string = str_replace("%site_name_secondary%", SITE_CONFIG["site"]["name_secondary"], $string);
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function ImportLibrary($filename)
|
||||||
|
{
|
||||||
|
require ROOT."/api/private/vendors/$filename.php";
|
||||||
|
}
|
||||||
|
|
||||||
|
static function RequestRender($type, $assetID)
|
||||||
|
{
|
||||||
|
$pending = db::run(
|
||||||
|
"SELECT COUNT(*) FROM renderqueue WHERE renderType = :type AND assetID = :assetID AND renderStatus IN (0, 1)",
|
||||||
|
[":type" => $type, ":assetID" => $assetID]
|
||||||
|
)->fetchColumn();
|
||||||
|
if($pending) return;
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO renderqueue (jobID, renderType, assetID, timestampRequested) VALUES (:jobID, :type, :assetID, UNIX_TIMESTAMP())",
|
||||||
|
[":jobID" => generateUUID(), ":type" => $type, ":assetID" => $assetID]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetPendingRenders()
|
||||||
|
{
|
||||||
|
return db::run("SELECT COUNT(*) FROM renderqueue WHERE renderStatus IN (0, 1)")->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetServerPing($id)
|
||||||
|
{
|
||||||
|
return db::run("SELECT ping FROM servers WHERE id = :id", [":id" => $id])->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetAnnouncements()
|
||||||
|
{
|
||||||
|
global $announcements;
|
||||||
|
// TODO - make this json-based instead of relying on sql?
|
||||||
|
// should somewhat help with speed n stuff since it doesnt
|
||||||
|
// have to query the database on every single page load
|
||||||
|
$announcements = db::run("SELECT * FROM announcements WHERE activated ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if(!SITE_CONFIG["site"]["thumbserver"])
|
||||||
|
{
|
||||||
|
$announcements[] =
|
||||||
|
[
|
||||||
|
"text" => "Avatar and asset rendering has been temporarily disabled for maintenance",
|
||||||
|
"textcolor" => "light",
|
||||||
|
"bgcolor" => "#F76E19"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this itself has become such a huge mess
|
||||||
|
// some of the functions here need to be put in rbxclient or session or polygon or even a new class
|
||||||
|
class Users
|
||||||
|
{
|
||||||
|
const STAFF = 0; // this is not a normal user - just means every admin
|
||||||
|
const STAFF_CATALOG = 3; // catalog manager
|
||||||
|
const STAFF_MODERATOR = 1; // moderator
|
||||||
|
const STAFF_ADMINISTRATOR = 2; // administrator
|
||||||
|
|
||||||
|
// todo - maybe put in a separate json file?
|
||||||
|
static array $brickcolors =
|
||||||
|
[
|
||||||
|
"F2F3F3" => 1, "A1A5A2" => 2, "F9E999" => 3, "D7C59A" => 5, "C2DAB8" => 6, "E8BAC8" => 9, "80BBDB" => 11, "CB8442" => 12, "CC8E69" => 18, "C4281C" => 21, "C470A0" => 22, "0D69AC" => 23, "F5CD30" => 24, "624732" => 25, "1B2A35" => 26, "6D6E6C" => 27, "287F47" => 28, "A1C48C" => 29, "F3CF9B" => 36, "4B974B" => 37, "A05F35" => 38, "C1CADE" => 39, "ECECEC" => 40, "CD544B" => 41, "C1DFF0" => 42, "7BB6E8" => 43, "F7F18D" => 44, "B4D2E4" => 45, "D9856C" => 47, "84B68D" => 48, "F8F184" => 49, "ECE8DE" => 50, "EEC4B6" => 100, "DA867A" => 101, "6E99CA" => 102, "C7C1B7" => 103, "6B327C" => 104, "E29B40" => 105, "DA8541" => 106, "008F9C" => 107, "685C43" => 108, "435493" => 110, "BFB7B1" => 111, "6874AC" => 112, "E5ADC8" => 113, "C7D23C" => 115, "55A5AF" => 116, "B7D7D5" => 118, "A4BD47" => 119, "D9E4A7" => 120, "E7AC58" => 121, "D36F4C" => 123, "923978" => 124, "EAB892" => 125, "A5A5CB" => 126, "DCBC81" => 127, "AE7A59" => 128, "9CA3A8" => 131, "D5733D" => 133, "D8DD56" => 134, "74869D" => 135, "877C90" => 136, "E09864" => 137, "958A73" => 138, "203A56" => 140, "27462D" => 141, "CFE2F7" => 143, "7988A1" => 145, "958EA3" => 146, "938767" => 147, "575857" => 148, "161D32" => 149, "ABADAC" => 150, "789082" => 151, "957977" => 153, "7B2E2F" => 154, "FFF67B" => 157, "E1A4C2" => 158, "756C62" => 168, "97695B" => 176, "B48455" => 178, "898788" => 179, "D7A94B" => 180, "F9D62E" => 190, "E8AB2D" => 191, "694028" => 192, "CF6024" => 193, "A3A2A5" => 194, "4667A4" => 195, "23478B" => 196, "8E4285" => 198, "635F62" => 199, "828A5D" => 200, "E5E4DF" => 208, "B08E44" => 209, "709578" => 210, "79B5B5" => 211, "9FC3E9" => 212, "6C81B7" => 213, "904C2A" => 216, "7C5C46" => 217, "96709F" => 218, "6B629B" => 219, "A7A9CE" => 220, "CD6298" => 221, "E4ADC8" => 222, "DC9095" => 223, "F0D5A0" => 224, "EBB87F" => 225, "FDEA8D" => 226, "7DBBDD" => 232, "342B75" => 268, "506D54" => 301, "5B5D69" => 302, "0010B0" => 303, "2C651D" => 304, "527CAE" => 305, "335882" => 306, "102ADC" => 307, "3D1585" => 308, "348E40" => 309, "5B9A4C" => 310, "9FA1AC" => 311, "592259" => 312, "1F801D" => 313, "9FADC0" => 314, "0989CF" => 315, "7B007B" => 316, "7C9C6B" => 317, "8AAB85" => 318, "B9C4B1" => 319, "CACBD1" => 320, "A75E9B" => 321, "7B2F7B" => 322, "94BE81" => 323, "A8BD99" => 324, "DFDFDE" => 325, "970000" => 327, "B1E5A6" => 328, "98C2DB" => 329, "FF98DC" => 330, "FF5959" => 331, "750000" => 332, "EFB838" => 333, "F8D96D" => 334, "E7E7EC" => 335, "C7D4E4" => 336, "FF9494" => 337, "BE6862" => 338, "562424" => 339, "F1E7C7" => 340, "FEF3BB" => 341, "E0B2D0" => 342, "D490BD" => 343, "965555" => 344, "8F4C2A" => 345, "D3BE96" => 346, "E2DCBC" => 347, "EDEAEA" => 348, "E9DADA" => 349, "883E3E" => 350, "BC9B5D" => 351, "C7AC78" => 352, "CABFA3" => 353, "BBB3B2" => 354, "6C584B" => 355, "A0844F" => 356, "958988" => 357, "ABA89E" => 358, "AF9483" => 359, "966766" => 360, "564236" => 361, "7E683F" => 362, "69665C" => 363, "5A4C42" => 364, "6A3909" => 365, "F8F8F8" => 1001, "CDCDCD" => 1002, "111111" => 1003, "FF0000" => 1004, "FFB000" => 1005, "B480FF" => 1006, "A34B4B" => 1007, "C1BE42" => 1008, "FFFF00" => 1009, "0000FF" => 1010, "002060" => 1011, "2154B9" => 1012, "04AFEC" => 1013, "AA5500" => 1014, "AA00AA" => 1015, "FF66CC" => 1016, "FFAF00" => 1017, "12EED4" => 1018, "00FFFF" => 1019, "00FF00" => 1020, "3A7D15" => 1021, "7F8E64" => 1022, "8C5B9F" => 1023, "AFDDFF" => 1024, "FFC9C9" => 1025, "B1A7FF" => 1026, "9FF3E9" => 1027, "CCFFCC" => 1028, "FFFFCC" => 1029, "FFCC99" => 1030, "6225D1" => 1031, "FF00BF" => 1032
|
||||||
|
];
|
||||||
|
|
||||||
|
static function hex2bc($hex)
|
||||||
|
{
|
||||||
|
return self::$brickcolors[$hex] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function bc2hex($brickcolor)
|
||||||
|
{
|
||||||
|
return array_flip(self::$brickcolors)[$brickcolor] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetIDFromName($username)
|
||||||
|
{
|
||||||
|
return db::run("SELECT id FROM users WHERE username = :username", [":username" => $username])->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetNameFromID($userId)
|
||||||
|
{
|
||||||
|
return db::run("SELECT username FROM users WHERE id = :uid", [":uid" => $userId])->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetInfoFromName($username)
|
||||||
|
{
|
||||||
|
return db::run("SELECT * FROM users WHERE username = :username", [":username" => $username])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetInfoFromID($userId)
|
||||||
|
{
|
||||||
|
return db::run("SELECT * FROM users WHERE id = :uid", [":uid" => $userId])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetCharacterAppearance($userId, $serverId = false, $assetHost = false)
|
||||||
|
{
|
||||||
|
// this is a mess
|
||||||
|
|
||||||
|
if(!$assetHost) $assetHost = $_SERVER['HTTP_HOST'];
|
||||||
|
$charapp = "http://$assetHost/Asset/BodyColors.ashx?userId=".$userId;
|
||||||
|
|
||||||
|
$querystring =
|
||||||
|
"SELECT *, assets.type, assets.gear_attributes FROM ownedAssets
|
||||||
|
INNER JOIN assets ON assets.id = assetId
|
||||||
|
WHERE userId = :uid AND wearing";
|
||||||
|
|
||||||
|
if($serverId == -1) //thumbnail server - only get the last gear the user equipped
|
||||||
|
{
|
||||||
|
$querystring .= " AND type != 19";
|
||||||
|
|
||||||
|
$query = db::run(
|
||||||
|
"SELECT *, assets.type FROM ownedAssets
|
||||||
|
INNER JOIN assets ON assets.id = assetId
|
||||||
|
WHERE userId = :uid AND wearing AND type = 19
|
||||||
|
ORDER BY last_toggle DESC LIMIT 1",
|
||||||
|
[":uid" => $userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
while($asset = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$charapp .= ";http://$assetHost/Asset/?id=".$asset->assetId;
|
||||||
|
if($assetHost != $_SERVER['HTTP_HOST'])
|
||||||
|
$charapp .= "&host=$assetHost";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif($serverId)
|
||||||
|
{
|
||||||
|
$query = db::run("SELECT allowed_gears FROM selfhosted_servers WHERE id = :id", [":id" => $serverId]);
|
||||||
|
$gears = $query->fetchColumn();
|
||||||
|
if($gears)
|
||||||
|
{
|
||||||
|
$gears = json_decode($gears, true);
|
||||||
|
$querystring .= " AND (gear_attributes IS NULL";
|
||||||
|
|
||||||
|
foreach($gears as $gear_attr => $gear_val)
|
||||||
|
if($gear_val)
|
||||||
|
$querystring .= " OR gear_attributes LIKE '%\"".$gear_attr."\":true%'";
|
||||||
|
|
||||||
|
$querystring .= ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = db::run($querystring, [":uid" => $userId]);
|
||||||
|
while($asset = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$charapp .= ";http://$assetHost/Asset/?id=".$asset->assetId;
|
||||||
|
if($assetHost != $_SERVER['HTTP_HOST'])
|
||||||
|
$charapp .= "&host=$assetHost";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $charapp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function CheckIfFriends($userId1, $userId2, $status = false)
|
||||||
|
{
|
||||||
|
if($status === false)
|
||||||
|
{
|
||||||
|
$query = db::run(
|
||||||
|
"SELECT * FROM friends WHERE :uid1 IN (requesterId, receiverId) AND :uid2 IN (requesterId, receiverId) AND NOT status = 2",
|
||||||
|
[":uid1" => $userId1, ":uid2" => $userId2]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$query = db::run(
|
||||||
|
"SELECT * FROM friends WHERE :uid1 IN (requesterId, receiverId) AND :uid2 IN (requesterId, receiverId) AND status = :status",
|
||||||
|
[":uid1" => $userId1, ":uid2" => $userId2, ":status" => $status]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetFriendCount($userId)
|
||||||
|
{
|
||||||
|
return db::run("SELECT COUNT(*) FROM friends WHERE :uid IN (requesterId, receiverId) AND status = 1", [":uid" => $userId])->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetFriendRequestCount($userId)
|
||||||
|
{
|
||||||
|
return db::run("SELECT COUNT(*) FROM friends WHERE receiverId = :uid AND status = 0", [":uid" => $userId])->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetForumPostCount($userId)
|
||||||
|
{
|
||||||
|
return db::run("
|
||||||
|
SELECT (SELECT COUNT(*) FROM polygon.forum_threads WHERE author = :id AND NOT deleted) +
|
||||||
|
(SELECT COUNT(*) FROM polygon.forum_replies WHERE author = :id AND NOT deleted) AS totalPosts",
|
||||||
|
[":id" => $userId]
|
||||||
|
)->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function UpdatePing()
|
||||||
|
{
|
||||||
|
// i have never managed to make this work properly
|
||||||
|
// TODO - make this work properly for once
|
||||||
|
if(!SESSION) return false;
|
||||||
|
|
||||||
|
// update currency stipend
|
||||||
|
if(SESSION["nextCurrencyStipend"] <= time())
|
||||||
|
{
|
||||||
|
$days = floor((time() - SESSION["userInfo"]["lastonline"]) / 86400);
|
||||||
|
if(!$days) $days = 1;
|
||||||
|
$stipend = $days * 10;
|
||||||
|
$nextstipend = SESSION["userInfo"]["nextCurrencyStipend"] + ($days+1 * 86400);
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"UPDATE users SET currency = currency + :stipend, nextCurrencyStipend = :nextstipend WHERE id = :uid",
|
||||||
|
[":stipend" => $stipend, ":nextstipend" => $nextstipend, ":uid" => SESSION["userId"]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update presence
|
||||||
|
db::run(
|
||||||
|
"UPDATE users SET lastonline = UNIX_TIMESTAMP() WHERE id = :id; UPDATE sessions SET lastonline = UNIX_TIMESTAMP() WHERE sessionKey = :key",
|
||||||
|
[":id" => SESSION["userId"], ":key" => SESSION["sessionKey"]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetOnlineStatus($userId)
|
||||||
|
{
|
||||||
|
// this is also a mess
|
||||||
|
global $pdo;
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
"online" => false,
|
||||||
|
"text" => false,
|
||||||
|
"attributes" => false
|
||||||
|
];
|
||||||
|
|
||||||
|
$info = db::run(
|
||||||
|
"SELECT client_sessions.ping, name, serverID FROM client_sessions
|
||||||
|
INNER JOIN selfhosted_servers ON selfhosted_servers.id = serverID
|
||||||
|
WHERE uid = :id AND valid ORDER BY client_sessions.ping DESC LIMIT 1",
|
||||||
|
[":id" => $userId]
|
||||||
|
)->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if($info && ($info->ping+35) > time())
|
||||||
|
return
|
||||||
|
[
|
||||||
|
"online" => true,
|
||||||
|
"text" => 'Playing <a href="/games/server?ID='.$info->serverID.'">'.Polygon::FilterText($info->name).'</a>',
|
||||||
|
"attributes" => ' class="text-danger"'
|
||||||
|
];
|
||||||
|
|
||||||
|
$query = db::run("SELECT lastonline FROM users WHERE id = :id", [":id" => $userId]);
|
||||||
|
$time = $query->fetchColumn();
|
||||||
|
|
||||||
|
if(!$query->rowCount()) return $response;
|
||||||
|
|
||||||
|
if($time+30 > time())
|
||||||
|
{
|
||||||
|
$response =
|
||||||
|
[
|
||||||
|
"online" => true,
|
||||||
|
"text" => "Website",
|
||||||
|
"attributes" => ' class="text-danger"'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//if(($time + 604800) > time())
|
||||||
|
$response["text"] = timeSince($time);
|
||||||
|
//else
|
||||||
|
// $response["text"] = date('j/n/Y g:i A', $time);
|
||||||
|
|
||||||
|
$response["attributes"] = ' data-toggle="tooltip" data-placement="right" title="'.date('j/n/Y g:i A', $time).'"';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetUsersOnline()
|
||||||
|
{
|
||||||
|
return db::run("SELECT COUNT(*) FROM users WHERE lastonline+35 > UNIX_TIMESTAMP()")->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function RequireLogin($studio = false)
|
||||||
|
{
|
||||||
|
if(!SESSION) die(header("Location: /login?ReturnUrl=".urlencode($_SERVER['REQUEST_URI']).($studio?"&embedded=true":"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
static function RequireLoggedOut()
|
||||||
|
{
|
||||||
|
if(SESSION) die(header("Location: /home"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static function IsAdmin($level = self::STAFF)
|
||||||
|
{
|
||||||
|
if(!SESSION || SESSION["adminLevel"] == 0) return false;
|
||||||
|
if($level === self::STAFF) return true;
|
||||||
|
|
||||||
|
if(gettype($level) == "array")
|
||||||
|
{
|
||||||
|
if(in_array(SESSION["adminLevel"], $level)) return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(SESSION["adminLevel"] == $level) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function RequireAdmin($level = self::STAFF)
|
||||||
|
{
|
||||||
|
if(!self::IsAdmin($level))
|
||||||
|
pageBuilder::errorCode(404);
|
||||||
|
|
||||||
|
if(!SESSION["2fa"])
|
||||||
|
pageBuilder::errorCode(403, [
|
||||||
|
"title" => "2FA is not enabled",
|
||||||
|
"text" => "Your account must have two-factor authentication enabled before you can do any administrative actions"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetUserModeration($userId)
|
||||||
|
{
|
||||||
|
return db::run("SELECT * FROM bans WHERE userId = :id AND NOT isDismissed ORDER BY id DESC LIMIT 1", [":id" => $userId])->fetch(PDO::FETCH_OBJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function UndoUserModeration($userId, $admin = false)
|
||||||
|
{
|
||||||
|
if($admin) db::run("UPDATE bans SET isDismissed = 1 WHERE userId = :id AND NOT isDismissed", [":id" => $userId]);
|
||||||
|
else db::run("UPDATE bans SET isDismissed = 1 WHERE userId = :id AND NOT isDismissed AND NOT banType = 3 AND timeEnds < UNIX_TIMESTAMP()", [":id" => $userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function LogStaffAction($action)
|
||||||
|
{
|
||||||
|
if(!SESSION || !SESSION["adminLevel"]) return false;
|
||||||
|
db::run("INSERT INTO stafflogs (time, adminId, action) VALUES (UNIX_TIMESTAMP(), :uid, :action)", [":uid" => SESSION["userId"], ":action" => $action]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function GetAlternateAccounts($data)
|
||||||
|
{
|
||||||
|
$alts = [];
|
||||||
|
$usedIPs = [];
|
||||||
|
$usedIDs = [];
|
||||||
|
|
||||||
|
if(is_numeric($data)) // user id
|
||||||
|
{
|
||||||
|
$ips = db::run("SELECT loginIp FROM sessions WHERE userId = :uid GROUP BY loginIp", [":uid" => $data]);
|
||||||
|
}
|
||||||
|
else // ip address
|
||||||
|
{
|
||||||
|
$ips = db::run(
|
||||||
|
"SELECT loginIp FROM sessions
|
||||||
|
WHERE userId IN (SELECT userId FROM sessions WHERE loginIp = :ip GROUP BY userId) GROUP BY loginIp",
|
||||||
|
[":ip" => $data]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
while($ip = $ips->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
if(in_array($ip->loginIp, $usedIPs)) continue;
|
||||||
|
$usedIPs[] = $ip->loginIp;
|
||||||
|
|
||||||
|
$altsquery = db::run(
|
||||||
|
"SELECT users.username, userId, users.jointime, loginIp FROM sessions
|
||||||
|
INNER JOIN users ON users.id = userId WHERE loginIp = :ip GROUP BY userId",
|
||||||
|
[":ip" => $ip->loginIp]
|
||||||
|
);
|
||||||
|
|
||||||
|
while($row = $altsquery->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
if(in_array($row->userId, $usedIDs)) continue;
|
||||||
|
$usedIDs[] = $row->userId;
|
||||||
|
|
||||||
|
$alts[] = ["username" => $row->username, "userid" => $row->userId, "created" => $row->jointime, "ip" => $row->loginIp];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $alts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class session
|
||||||
|
{
|
||||||
|
static function createSession($userId)
|
||||||
|
{
|
||||||
|
keygen:
|
||||||
|
$sessionkey = bin2hex(random_bytes(128)); // me concatenating md5() like 20 times be like
|
||||||
|
if(db::run("SELECT COUNT(*) FROM sessions WHERE sessionKey = :key", [":key" => $sessionkey])->fetchColumn()) goto keygen;
|
||||||
|
|
||||||
|
db::run(
|
||||||
|
"INSERT INTO sessions (`sessionKey`, `userAgent`, `userId`, `loginIp`, `created`, `lastonline`, `csrf`)
|
||||||
|
VALUES (:sesskey, :useragent, :userid, :ip, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :csrf)",
|
||||||
|
[":sesskey" => $sessionkey, ":useragent" => $_SERVER['HTTP_USER_AGENT'], ":userid" => $userId, ":ip" => GetIPAddress(), ":csrf" => bin2hex(random_bytes(32))]
|
||||||
|
);
|
||||||
|
|
||||||
|
setcookie("polygon_session", $sessionkey, time()+(157700000*3), "/"); //expires in 5 years
|
||||||
|
}
|
||||||
|
|
||||||
|
// these two functions are sorta ambiguous
|
||||||
|
// especially cause they're named so similarly
|
||||||
|
static function destroySession($sesskey)
|
||||||
|
{
|
||||||
|
db::run("UPDATE sessions SET valid = 0 WHERE sessionKey = :key", [":key" => $sesskey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function clearSession($sesskey = false)
|
||||||
|
{
|
||||||
|
setcookie("polygon_session", "", 1, "/");
|
||||||
|
if(strlen($sesskey)) self::destroySession($sesskey);
|
||||||
|
die(header("Refresh: 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static function getSessionData($sessionkey, $strict = true)
|
||||||
|
{
|
||||||
|
$query = db::run("SELECT * FROM sessions WHERE sessionKey = :sesskey AND valid AND lastonline+432000 > UNIX_TIMESTAMP()", [":sesskey" => $sessionkey]);
|
||||||
|
if(!$query->rowCount()) return false;
|
||||||
|
$row = $query->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
// todo - figure out "remember me" cookies instead of just making the session 5 years long
|
||||||
|
if($row->created+(157700000*3) < time()) return false;
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require ROOT.'/api/private/config.php';
|
||||||
|
|
||||||
|
// errorhandler include
|
||||||
|
|
||||||
|
Polygon::ImportClass("ErrorHandler");
|
||||||
|
new ErrorHandler();
|
||||||
|
|
||||||
|
// parsedown include
|
||||||
|
|
||||||
|
Polygon::ImportLibrary("Parsedown");
|
||||||
|
$markdown = new Parsedown();
|
||||||
|
$markdown->setMarkupEscaped(true);
|
||||||
|
$markdown->setBreaksEnabled(true);
|
||||||
|
$markdown->setSafeMode(true);
|
||||||
|
$markdown->setUrlsLinked(true);
|
||||||
|
|
||||||
|
// db include
|
||||||
|
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"].'/api/private/components/db.php';
|
||||||
|
Polygon::GetAnnouncements();
|
||||||
|
|
||||||
|
// pagebuilder include
|
||||||
|
|
||||||
|
Polygon::ImportClass("pagebuilder");
|
||||||
|
|
||||||
|
if(GetIPAddress() == "76.190.219.176")
|
||||||
|
{
|
||||||
|
define("SESSION", false);
|
||||||
|
pageBuilder::buildHeader();
|
||||||
|
echo "<img src=\"https://cdn.discordapp.com/attachments/754743899200684194/880942764672569354/3dgifmaker59323.gif\" width=\"100%\">";
|
||||||
|
pageBuilder::buildFooter();
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($_COOKIE['polygon_session']))
|
||||||
|
{
|
||||||
|
$session = session::getSessionData($_COOKIE['polygon_session']);
|
||||||
|
if($session)
|
||||||
|
{
|
||||||
|
$userInfo = Users::GetInfoFromID($session->userId);
|
||||||
|
define('SESSION',
|
||||||
|
[
|
||||||
|
"userName" => $userInfo->username,
|
||||||
|
"userId" => $userInfo->id,
|
||||||
|
"2fa" => $userInfo->twofa,
|
||||||
|
"2faVerified" => $session->twofaVerified,
|
||||||
|
"friendRequests" => Users::GetFriendRequestCount($userInfo->id),
|
||||||
|
"status" => $userInfo->status,
|
||||||
|
"currency" => $userInfo->currency,
|
||||||
|
"nextCurrencyStipend" => $userInfo->nextCurrencyStipend,
|
||||||
|
"adminLevel" => $userInfo->adminlevel,
|
||||||
|
"filter" => $userInfo->filter,
|
||||||
|
"sessionKey" => $session->sessionKey,
|
||||||
|
"csrfToken" => $session->csrf,
|
||||||
|
"userInfo" => (array)$userInfo
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(SESSION["2fa"] && !SESSION["2faVerified"] && Polygon::CanBypass("2FA"))
|
||||||
|
{
|
||||||
|
die(header("Location: /login/2fa"));
|
||||||
|
}
|
||||||
|
else if(Users::GetUserModeration(SESSION["userId"]) && Polygon::CanBypass("Moderation"))
|
||||||
|
{
|
||||||
|
die(header("Location: /moderation"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Users::UpdatePing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
session::clearSession($_COOKIE['polygon_session']);
|
||||||
|
define('SESSION', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
define('SESSION', false);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sonata Project package.
|
||||||
|
*
|
||||||
|
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Sonata\GoogleAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FixedBitNotation.
|
||||||
|
*
|
||||||
|
* The FixedBitNotation class is for binary to text conversion. It
|
||||||
|
* can handle many encoding schemes, formally defined or not, that
|
||||||
|
* use a fixed number of bits to encode each character.
|
||||||
|
*
|
||||||
|
* @author Andre DeMarre
|
||||||
|
*/
|
||||||
|
final class FixedBitNotation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $chars;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $bitsPerCharacter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $radix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $rightPadFinalBits;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $padFinalGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $padCharacter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private $charmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $bitsPerCharacter Bits to use for each encoded character
|
||||||
|
* @param string $chars Base character alphabet
|
||||||
|
* @param bool $rightPadFinalBits How to encode last character
|
||||||
|
* @param bool $padFinalGroup Add padding to end of encoded output
|
||||||
|
* @param string $padCharacter Character to use for padding
|
||||||
|
*/
|
||||||
|
public function __construct(int $bitsPerCharacter, ?string $chars = null, bool $rightPadFinalBits = false, bool $padFinalGroup = false, string $padCharacter = '=')
|
||||||
|
{
|
||||||
|
// Ensure validity of $chars
|
||||||
|
if (!\is_string($chars) || ($charLength = \strlen($chars)) < 2) {
|
||||||
|
$chars =
|
||||||
|
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,';
|
||||||
|
$charLength = 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure validity of $bitsPerCharacter
|
||||||
|
if ($bitsPerCharacter < 1) {
|
||||||
|
// $bitsPerCharacter must be at least 1
|
||||||
|
$bitsPerCharacter = 1;
|
||||||
|
$radix = 2;
|
||||||
|
} elseif ($charLength < 1 << $bitsPerCharacter) {
|
||||||
|
// Character length of $chars is too small for $bitsPerCharacter
|
||||||
|
// Set $bitsPerCharacter to greatest acceptable value
|
||||||
|
$bitsPerCharacter = 1;
|
||||||
|
$radix = 2;
|
||||||
|
|
||||||
|
while ($charLength >= ($radix <<= 1) && $bitsPerCharacter < 8) {
|
||||||
|
++$bitsPerCharacter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$radix >>= 1;
|
||||||
|
} elseif ($bitsPerCharacter > 8) {
|
||||||
|
// $bitsPerCharacter must not be greater than 8
|
||||||
|
$bitsPerCharacter = 8;
|
||||||
|
$radix = 256;
|
||||||
|
} else {
|
||||||
|
$radix = 1 << $bitsPerCharacter;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->chars = $chars;
|
||||||
|
$this->bitsPerCharacter = $bitsPerCharacter;
|
||||||
|
$this->radix = $radix;
|
||||||
|
$this->rightPadFinalBits = $rightPadFinalBits;
|
||||||
|
$this->padFinalGroup = $padFinalGroup;
|
||||||
|
$this->padCharacter = $padCharacter[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a string.
|
||||||
|
*
|
||||||
|
* @param string $rawString Binary data to encode
|
||||||
|
*/
|
||||||
|
public function encode($rawString): string
|
||||||
|
{
|
||||||
|
// Unpack string into an array of bytes
|
||||||
|
$bytes = unpack('C*', $rawString);
|
||||||
|
$byteCount = \count($bytes);
|
||||||
|
|
||||||
|
$encodedString = '';
|
||||||
|
$byte = array_shift($bytes);
|
||||||
|
$bitsRead = 0;
|
||||||
|
|
||||||
|
$chars = $this->chars;
|
||||||
|
$bitsPerCharacter = $this->bitsPerCharacter;
|
||||||
|
$rightPadFinalBits = $this->rightPadFinalBits;
|
||||||
|
$padFinalGroup = $this->padFinalGroup;
|
||||||
|
$padCharacter = $this->padCharacter;
|
||||||
|
|
||||||
|
// Generate encoded output;
|
||||||
|
// each loop produces one encoded character
|
||||||
|
for ($c = 0; $c < $byteCount * 8 / $bitsPerCharacter; ++$c) {
|
||||||
|
// Get the bits needed for this encoded character
|
||||||
|
if ($bitsRead + $bitsPerCharacter > 8) {
|
||||||
|
// Not enough bits remain in this byte for the current
|
||||||
|
// character
|
||||||
|
// Save the remaining bits before getting the next byte
|
||||||
|
$oldBitCount = 8 - $bitsRead;
|
||||||
|
$oldBits = $byte ^ ($byte >> $oldBitCount << $oldBitCount);
|
||||||
|
$newBitCount = $bitsPerCharacter - $oldBitCount;
|
||||||
|
|
||||||
|
if (!$bytes) {
|
||||||
|
// Last bits; match final character and exit loop
|
||||||
|
if ($rightPadFinalBits) {
|
||||||
|
$oldBits <<= $newBitCount;
|
||||||
|
}
|
||||||
|
$encodedString .= $chars[$oldBits];
|
||||||
|
|
||||||
|
if ($padFinalGroup) {
|
||||||
|
// Array of the lowest common multiples of
|
||||||
|
// $bitsPerCharacter and 8, divided by 8
|
||||||
|
$lcmMap = [1 => 1, 2 => 1, 3 => 3, 4 => 1, 5 => 5, 6 => 3, 7 => 7, 8 => 1];
|
||||||
|
$bytesPerGroup = $lcmMap[$bitsPerCharacter];
|
||||||
|
$pads = (int) ($bytesPerGroup * 8 / $bitsPerCharacter
|
||||||
|
- ceil((\strlen($rawString) % $bytesPerGroup)
|
||||||
|
* 8 / $bitsPerCharacter));
|
||||||
|
$encodedString .= str_repeat($padCharacter[0], $pads);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next byte
|
||||||
|
$byte = array_shift($bytes);
|
||||||
|
$bitsRead = 0;
|
||||||
|
} else {
|
||||||
|
$oldBitCount = 0;
|
||||||
|
$newBitCount = $bitsPerCharacter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read only the needed bits from this byte
|
||||||
|
$bits = $byte >> 8 - ($bitsRead + $newBitCount);
|
||||||
|
$bits ^= $bits >> $newBitCount << $newBitCount;
|
||||||
|
$bitsRead += $newBitCount;
|
||||||
|
|
||||||
|
if ($oldBitCount) {
|
||||||
|
// Bits come from seperate bytes, add $oldBits to $bits
|
||||||
|
$bits = ($oldBits << $newBitCount) | $bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encodedString .= $chars[$bits];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $encodedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a string.
|
||||||
|
*
|
||||||
|
* @param string $encodedString Data to decode
|
||||||
|
* @param bool $caseSensitive
|
||||||
|
* @param bool $strict Returns null if $encodedString contains
|
||||||
|
* an undecodable character
|
||||||
|
*/
|
||||||
|
public function decode($encodedString, $caseSensitive = true, $strict = false): string
|
||||||
|
{
|
||||||
|
if (!$encodedString || !\is_string($encodedString)) {
|
||||||
|
// Empty string, nothing to decode
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$chars = $this->chars;
|
||||||
|
$bitsPerCharacter = $this->bitsPerCharacter;
|
||||||
|
$radix = $this->radix;
|
||||||
|
$rightPadFinalBits = $this->rightPadFinalBits;
|
||||||
|
$padCharacter = $this->padCharacter;
|
||||||
|
|
||||||
|
// Get index of encoded characters
|
||||||
|
if ($this->charmap) {
|
||||||
|
$charmap = $this->charmap;
|
||||||
|
} else {
|
||||||
|
$charmap = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $radix; ++$i) {
|
||||||
|
$charmap[$chars[$i]] = $i;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->charmap = $charmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last encoded character is $encodedString[$lastNotatedIndex]
|
||||||
|
$lastNotatedIndex = \strlen($encodedString) - 1;
|
||||||
|
|
||||||
|
// Remove trailing padding characters
|
||||||
|
while ($encodedString[$lastNotatedIndex] === $padCharacter[0]) {
|
||||||
|
$encodedString = substr($encodedString, 0, $lastNotatedIndex);
|
||||||
|
--$lastNotatedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawString = '';
|
||||||
|
$byte = 0;
|
||||||
|
$bitsWritten = 0;
|
||||||
|
|
||||||
|
// Convert each encoded character to a series of unencoded bits
|
||||||
|
for ($c = 0; $c <= $lastNotatedIndex; ++$c) {
|
||||||
|
if (!isset($charmap[$encodedString[$c]]) && !$caseSensitive) {
|
||||||
|
// Encoded character was not found; try other case
|
||||||
|
if (isset($charmap[$cUpper = strtoupper($encodedString[$c])])) {
|
||||||
|
$charmap[$encodedString[$c]] = $charmap[$cUpper];
|
||||||
|
} elseif (isset($charmap[$cLower = strtolower($encodedString[$c])])) {
|
||||||
|
$charmap[$encodedString[$c]] = $charmap[$cLower];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($charmap[$encodedString[$c]])) {
|
||||||
|
$bitsNeeded = 8 - $bitsWritten;
|
||||||
|
$unusedBitCount = $bitsPerCharacter - $bitsNeeded;
|
||||||
|
|
||||||
|
// Get the new bits ready
|
||||||
|
if ($bitsNeeded > $bitsPerCharacter) {
|
||||||
|
// New bits aren't enough to complete a byte; shift them
|
||||||
|
// left into position
|
||||||
|
$newBits = $charmap[$encodedString[$c]] << $bitsNeeded
|
||||||
|
- $bitsPerCharacter;
|
||||||
|
$bitsWritten += $bitsPerCharacter;
|
||||||
|
} elseif ($c !== $lastNotatedIndex || $rightPadFinalBits) {
|
||||||
|
// Zero or more too many bits to complete a byte;
|
||||||
|
// shift right
|
||||||
|
$newBits = $charmap[$encodedString[$c]] >> $unusedBitCount;
|
||||||
|
$bitsWritten = 8; //$bitsWritten += $bitsNeeded;
|
||||||
|
} else {
|
||||||
|
// Final bits don't need to be shifted
|
||||||
|
$newBits = $charmap[$encodedString[$c]];
|
||||||
|
$bitsWritten = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
$byte |= $newBits;
|
||||||
|
|
||||||
|
if (8 === $bitsWritten || $c === $lastNotatedIndex) {
|
||||||
|
// Byte is ready to be written
|
||||||
|
$rawString .= pack('C', $byte);
|
||||||
|
|
||||||
|
if ($c !== $lastNotatedIndex) {
|
||||||
|
// Start the next byte
|
||||||
|
$bitsWritten = $unusedBitCount;
|
||||||
|
$byte = ($charmap[$encodedString[$c]]
|
||||||
|
^ ($newBits << $unusedBitCount)) << 8 - $bitsWritten;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($strict) {
|
||||||
|
// Unable to decode character; abort
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rawString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEXT_MAJOR: Remove class alias
|
||||||
|
class_alias('Sonata\GoogleAuthenticator\FixedBitNotation', 'Google\Authenticator\FixedBitNotation', false);
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sonata Project package.
|
||||||
|
*
|
||||||
|
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Sonata\GoogleAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||||
|
*/
|
||||||
|
final class GoogleAuthenticator implements GoogleAuthenticatorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $passCodeLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $secretLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $pinModulo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \DateTimeInterface
|
||||||
|
*/
|
||||||
|
private $instanceTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $codePeriod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $periodSize = 30;
|
||||||
|
|
||||||
|
public function __construct(int $passCodeLength = 6, int $secretLength = 10, ?\DateTimeInterface $instanceTime = null, int $codePeriod = 30)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* codePeriod is the duration in seconds that the code is valid.
|
||||||
|
* periodSize is the length of a period to calculate periods since Unix epoch.
|
||||||
|
* periodSize cannot be larger than the codePeriod.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->passCodeLength = $passCodeLength;
|
||||||
|
$this->secretLength = $secretLength;
|
||||||
|
$this->codePeriod = $codePeriod;
|
||||||
|
$this->periodSize = $codePeriod < $this->periodSize ? $codePeriod : $this->periodSize;
|
||||||
|
$this->pinModulo = 10 ** $passCodeLength;
|
||||||
|
$this->instanceTime = $instanceTime ?? new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $secret
|
||||||
|
* @param string $code
|
||||||
|
* @param int $discrepancy
|
||||||
|
*/
|
||||||
|
public function checkCode($secret, $code, $discrepancy = 1): bool
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Discrepancy is the factor of periodSize ($discrepancy * $periodSize) allowed on either side of the
|
||||||
|
* given codePeriod. For example, if a code with codePeriod = 60 is generated at 10:00:00, a discrepancy
|
||||||
|
* of 1 will allow a periodSize of 30 seconds on either side of the codePeriod resulting in a valid code
|
||||||
|
* from 09:59:30 to 10:00:29.
|
||||||
|
*
|
||||||
|
* The result of each comparison is stored as a timestamp here instead of using a guard clause
|
||||||
|
* (https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html). This is to implement
|
||||||
|
* constant time comparison to make side-channel attacks harder. See
|
||||||
|
* https://cryptocoding.net/index.php/Coding_rules#Compare_secret_strings_in_constant_time for details.
|
||||||
|
* Each comparison uses hash_equals() instead of an operator to implement constant time equality comparison
|
||||||
|
* for each code.
|
||||||
|
*/
|
||||||
|
$periods = floor($this->codePeriod / $this->periodSize);
|
||||||
|
|
||||||
|
$result = 0;
|
||||||
|
for ($i = -$discrepancy; $i < $periods + $discrepancy; ++$i) {
|
||||||
|
$dateTime = new \DateTimeImmutable('@'.($this->instanceTime->getTimestamp() - ($i * $this->periodSize)));
|
||||||
|
$result = hash_equals($this->getCode($secret, $dateTime), $code) ? $dateTime->getTimestamp() : $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEXT_MAJOR: add the interface typehint to $time and remove deprecation.
|
||||||
|
*
|
||||||
|
* @param string $secret
|
||||||
|
* @param float|string|int|\DateTimeInterface|null $time
|
||||||
|
*/
|
||||||
|
public function getCode($secret, /* \DateTimeInterface */$time = null): string
|
||||||
|
{
|
||||||
|
if (null === $time) {
|
||||||
|
$time = $this->instanceTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($time instanceof \DateTimeInterface) {
|
||||||
|
$timeForCode = floor($time->getTimestamp() / $this->periodSize);
|
||||||
|
} else {
|
||||||
|
@trigger_error(
|
||||||
|
'Passing anything other than null or a DateTimeInterface to $time is deprecated as of 2.0 '.
|
||||||
|
'and will not be possible as of 3.0.',
|
||||||
|
E_USER_DEPRECATED
|
||||||
|
);
|
||||||
|
$timeForCode = $time;
|
||||||
|
}
|
||||||
|
|
||||||
|
$base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true);
|
||||||
|
$secret = $base32->decode($secret);
|
||||||
|
|
||||||
|
$timeForCode = str_pad(pack('N', $timeForCode), 8, \chr(0), STR_PAD_LEFT);
|
||||||
|
|
||||||
|
$hash = hash_hmac('sha1', $timeForCode, $secret, true);
|
||||||
|
$offset = \ord(substr($hash, -1));
|
||||||
|
$offset &= 0xF;
|
||||||
|
|
||||||
|
$truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF;
|
||||||
|
|
||||||
|
return str_pad((string) ($truncatedHash % $this->pinModulo), $this->passCodeLength, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEXT_MAJOR: Remove this method.
|
||||||
|
*
|
||||||
|
* @param string $user
|
||||||
|
* @param string $hostname
|
||||||
|
* @param string $secret
|
||||||
|
*
|
||||||
|
* @deprecated deprecated as of 2.1 and will be removed in 3.0. Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.
|
||||||
|
*/
|
||||||
|
public function getUrl($user, $hostname, $secret): string
|
||||||
|
{
|
||||||
|
@trigger_error(sprintf(
|
||||||
|
'Using %s() is deprecated as of 2.1 and will be removed in 3.0. '.
|
||||||
|
'Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.',
|
||||||
|
__METHOD__
|
||||||
|
), E_USER_DEPRECATED);
|
||||||
|
|
||||||
|
$issuer = \func_get_args()[3] ?? null;
|
||||||
|
$accountName = sprintf('%s@%s', $user, $hostname);
|
||||||
|
|
||||||
|
// manually concat the issuer to avoid a change in URL
|
||||||
|
$url = GoogleQrUrl::generate($accountName, $secret);
|
||||||
|
|
||||||
|
if ($issuer) {
|
||||||
|
$url .= '%26issuer%3D'.$issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateSecret(): string
|
||||||
|
{
|
||||||
|
return (new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true))
|
||||||
|
->encode(random_bytes($this->secretLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hashToInt(string $bytes, int $start): int
|
||||||
|
{
|
||||||
|
return unpack('N', substr(substr($bytes, $start), 0, 4))[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEXT_MAJOR: Remove class alias
|
||||||
|
class_alias('Sonata\GoogleAuthenticator\GoogleAuthenticator', 'Google\Authenticator\GoogleAuthenticator', false);
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sonata Project package.
|
||||||
|
*
|
||||||
|
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Sonata\GoogleAuthenticator;
|
||||||
|
|
||||||
|
interface GoogleAuthenticatorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $secret
|
||||||
|
* @param string $code
|
||||||
|
*/
|
||||||
|
public function checkCode($secret, $code, $discrepancy = 1): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEXT_MAJOR: add the interface typehint to $time and remove deprecation.
|
||||||
|
*
|
||||||
|
* @param string $secret
|
||||||
|
* @param float|string|int|\DateTimeInterface|null $time
|
||||||
|
*/
|
||||||
|
public function getCode($secret, /* \DateTimeInterface */$time = null): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEXT_MAJOR: Remove this method.
|
||||||
|
*
|
||||||
|
* @param string $user
|
||||||
|
* @param string $hostname
|
||||||
|
* @param string $secret
|
||||||
|
*
|
||||||
|
* @deprecated deprecated as of 2.1 and will be removed in 3.0. Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.
|
||||||
|
*/
|
||||||
|
public function getUrl($user, $hostname, $secret): string;
|
||||||
|
|
||||||
|
public function generateSecret(): string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sonata Project package.
|
||||||
|
*
|
||||||
|
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Sonata\GoogleAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for QR image url generation.
|
||||||
|
*
|
||||||
|
* @see http://goqr.me/api/
|
||||||
|
* @see http://goqr.me/api/doc/
|
||||||
|
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||||
|
*
|
||||||
|
* @author Iltar van der Berg <kjarli@gmail.com>
|
||||||
|
*/
|
||||||
|
final class GoogleQrUrl
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Private by design.
|
||||||
|
*/
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a URL that is used to show a QR code.
|
||||||
|
*
|
||||||
|
* Account names may not contain a double colon (:). Valid account name
|
||||||
|
* examples:
|
||||||
|
* - "John.Doe@gmail.com"
|
||||||
|
* - "John Doe"
|
||||||
|
* - "John_Doe_976"
|
||||||
|
*
|
||||||
|
* The Issuer may not contain a double colon (:). The issuer is recommended
|
||||||
|
* to pass along. If used, it will also be appended before the accountName.
|
||||||
|
*
|
||||||
|
* The previous examples with the issuer "Acme inc" would result in label:
|
||||||
|
* - "Acme inc:John.Doe@gmail.com"
|
||||||
|
* - "Acme inc:John Doe"
|
||||||
|
* - "Acme inc:John_Doe_976"
|
||||||
|
*
|
||||||
|
* The contents of the label, issuer and secret will be encoded to generate
|
||||||
|
* a valid URL.
|
||||||
|
*
|
||||||
|
* @param string $accountName The account name to show and identify
|
||||||
|
* @param string $secret The secret is the generated secret unique to that user
|
||||||
|
* @param string|null $issuer Where you log in to
|
||||||
|
* @param int $size Image size in pixels, 200 will make it 200x200
|
||||||
|
*/
|
||||||
|
public static function generate(string $accountName, string $secret, ?string $issuer = null, int $size = 200): string
|
||||||
|
{
|
||||||
|
if ('' === $accountName || false !== strpos($accountName, ':')) {
|
||||||
|
throw RuntimeException::InvalidAccountName($accountName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $secret) {
|
||||||
|
throw RuntimeException::InvalidSecret();
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $accountName;
|
||||||
|
$otpauthString = 'otpauth://totp/%s?secret=%s';
|
||||||
|
|
||||||
|
if (null !== $issuer) {
|
||||||
|
if ('' === $issuer || false !== strpos($issuer, ':')) {
|
||||||
|
throw RuntimeException::InvalidIssuer($issuer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use both the issuer parameter and label prefix as recommended by Google for BC reasons
|
||||||
|
$label = $issuer.':'.$label;
|
||||||
|
$otpauthString .= '&issuer=%s';
|
||||||
|
}
|
||||||
|
|
||||||
|
$otpauthString = rawurlencode(sprintf($otpauthString, $label, $secret, $issuer));
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'https://api.qrserver.com/v1/create-qr-code/?size=%1$dx%1$d&data=%2$s&ecc=M',
|
||||||
|
$size,
|
||||||
|
$otpauthString
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEXT_MAJOR: Remove class alias
|
||||||
|
class_alias('Sonata\GoogleAuthenticator\GoogleQrUrl', 'Google\Authenticator\GoogleQrUrl', false);
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sonata Project package.
|
||||||
|
*
|
||||||
|
* (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Sonata\GoogleAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains runtime exception templates.
|
||||||
|
*
|
||||||
|
* @author Iltar van der Berg <kjarli@gmail.com>
|
||||||
|
*/
|
||||||
|
final class RuntimeException extends \RuntimeException
|
||||||
|
{
|
||||||
|
public static function InvalidAccountName(string $accountName): self
|
||||||
|
{
|
||||||
|
return new self(sprintf(
|
||||||
|
'The account name may not contain a double colon (:) and may not be an empty string. Given "%s".',
|
||||||
|
$accountName
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function InvalidIssuer(string $issuer): self
|
||||||
|
{
|
||||||
|
return new self(sprintf(
|
||||||
|
'The issuer name may not contain a double colon (:) and may not be an empty string. Given "%s".',
|
||||||
|
$issuer
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function InvalidSecret(): self
|
||||||
|
{
|
||||||
|
return new self('The secret name may not be an empty string.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEXT_MAJOR: Remove class alias
|
||||||
|
class_alias('Sonata\GoogleAuthenticator\RuntimeException', 'Google\Authenticator\RuntimeException', false);
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
header("content-type: application/json");
|
||||||
|
|
||||||
|
$response = ["success" => true, "message" => ""];
|
||||||
|
|
||||||
|
if(!isset($_GET['username'])){ die(http_response_code(400)); }
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM blacklistednames WHERE (exact AND lower(username) = lower(:name)) OR (NOT exact AND lower(CONCAT('%', :name, '%')) LIKE lower(CONCAT('%', username, '%')))");
|
||||||
|
$query->bindParam(":name", $_GET['username'], PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
if($query->fetchColumn()){ $response["success"] = false; $response["message"] = "That username is unavailable. Sorry!"; }
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM users WHERE lower(username) = lower(:name)");
|
||||||
|
$query->bindParam(":name", $_GET['username'], PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
if($query->fetchColumn()){ $response["success"] = false; $response["message"] = "Someone already has that username! Try choosing a different one."; }
|
||||||
|
|
||||||
|
die(json_encode($response));
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php $asset = $_GET['asset'] ?? false; ?>
|
||||||
|
http://<?=$_SERVER['HTTP_HOST']?>/api/thumbs/whitecharacter.xml;http://<?=$_SERVER['HTTP_HOST']?>/asset/?id=<?=$asset?>;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
include $_SERVER['DOCUMENT_ROOT']."/api/private/config.php";
|
||||||
|
if(SITE_CONFIG["api"]["renderserverKey"] != ($_GET['accessKey'] ?? false)) die(http_response_code(403));
|
||||||
|
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
openssl_sign($_GET['script'], $signature, openssl_pkey_get_private("file://private_key.pem"));
|
||||||
|
echo "%" . base64_encode($signature) . "%" . $_GET['script'];
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
<?php $asset = $_GET['asset'] ?? false; ?>
|
||||||
|
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
|
||||||
|
<External>null</External>
|
||||||
|
<External>nil</External>
|
||||||
|
<Item class="Workspace" referent="RBX2">
|
||||||
|
<Properties>
|
||||||
|
<double name="DistributedGameTime">0</double>
|
||||||
|
<CoordinateFrame name="ModelInPrimary">
|
||||||
|
<X>0</X>
|
||||||
|
<Y>0</Y>
|
||||||
|
<Z>0</Z>
|
||||||
|
<R00>1</R00>
|
||||||
|
<R01>0</R01>
|
||||||
|
<R02>0</R02>
|
||||||
|
<R10>0</R10>
|
||||||
|
<R11>1</R11>
|
||||||
|
<R12>0</R12>
|
||||||
|
<R20>0</R20>
|
||||||
|
<R21>0</R21>
|
||||||
|
<R22>1</R22>
|
||||||
|
</CoordinateFrame>
|
||||||
|
<string name="Name">Workspace</string>
|
||||||
|
<Ref name="PrimaryPart">null</Ref>
|
||||||
|
<bool name="archivable">true</bool>
|
||||||
|
</Properties>
|
||||||
|
<Item class="Part" referent="RBX0">
|
||||||
|
<Properties>
|
||||||
|
<bool name="Anchored">false</bool>
|
||||||
|
<float name="BackParamA">-0.5</float>
|
||||||
|
<float name="BackParamB">0.5</float>
|
||||||
|
<token name="BackSurface">0</token>
|
||||||
|
<token name="BackSurfaceInput">0</token>
|
||||||
|
<float name="BottomParamA">-0.5</float>
|
||||||
|
<float name="BottomParamB">0.5</float>
|
||||||
|
<token name="BottomSurface">4</token>
|
||||||
|
<token name="BottomSurfaceInput">0</token>
|
||||||
|
<int name="BrickColor">194</int>
|
||||||
|
<CoordinateFrame name="CFrame">
|
||||||
|
<X>0</X>
|
||||||
|
<Y>0.600000024</Y>
|
||||||
|
<Z>0</Z>
|
||||||
|
<R00>1</R00>
|
||||||
|
<R01>0</R01>
|
||||||
|
<R02>0</R02>
|
||||||
|
<R10>0</R10>
|
||||||
|
<R11>1</R11>
|
||||||
|
<R12>0</R12>
|
||||||
|
<R20>0</R20>
|
||||||
|
<R21>0</R21>
|
||||||
|
<R22>1</R22>
|
||||||
|
</CoordinateFrame>
|
||||||
|
<bool name="CanCollide">true</bool>
|
||||||
|
<bool name="DraggingV1">false</bool>
|
||||||
|
<float name="Elasticity">0.5</float>
|
||||||
|
<token name="FormFactor">1</token>
|
||||||
|
<float name="Friction">0.300000012</float>
|
||||||
|
<float name="FrontParamA">-0.5</float>
|
||||||
|
<float name="FrontParamB">0.5</float>
|
||||||
|
<token name="FrontSurface">0</token>
|
||||||
|
<token name="FrontSurfaceInput">0</token>
|
||||||
|
<float name="LeftParamA">-0.5</float>
|
||||||
|
<float name="LeftParamB">0.5</float>
|
||||||
|
<token name="LeftSurface">0</token>
|
||||||
|
<token name="LeftSurfaceInput">0</token>
|
||||||
|
<bool name="Locked">false</bool>
|
||||||
|
<token name="Material">256</token>
|
||||||
|
<string name="Name">Part</string>
|
||||||
|
<float name="Reflectance">0</float>
|
||||||
|
<float name="RightParamA">-0.5</float>
|
||||||
|
<float name="RightParamB">0.5</float>
|
||||||
|
<token name="RightSurface">0</token>
|
||||||
|
<token name="RightSurfaceInput">0</token>
|
||||||
|
<Vector3 name="RotVelocity">
|
||||||
|
<X>0</X>
|
||||||
|
<Y>0</Y>
|
||||||
|
<Z>0</Z>
|
||||||
|
</Vector3>
|
||||||
|
<float name="TopParamA">-0.5</float>
|
||||||
|
<float name="TopParamB">0.5</float>
|
||||||
|
<token name="TopSurface">3</token>
|
||||||
|
<token name="TopSurfaceInput">0</token>
|
||||||
|
<float name="Transparency">0</float>
|
||||||
|
<Vector3 name="Velocity">
|
||||||
|
<X>0</X>
|
||||||
|
<Y>0</Y>
|
||||||
|
<Z>0</Z>
|
||||||
|
</Vector3>
|
||||||
|
<bool name="archivable">true</bool>
|
||||||
|
<token name="shape">1</token>
|
||||||
|
<Vector3 name="size">
|
||||||
|
<X>1</X>
|
||||||
|
<Y>1.20000005</Y>
|
||||||
|
<Z>1</Z>
|
||||||
|
</Vector3>
|
||||||
|
</Properties>
|
||||||
|
<Item class="SpecialMesh" referent="RBX1">
|
||||||
|
<Properties>
|
||||||
|
<token name="LODX">2</token>
|
||||||
|
<token name="LODY">2</token>
|
||||||
|
<Content name="MeshId"><url>http://<?=$_SERVER['HTTP_HOST']?>/asset/?id=<?=$asset?></url></Content>
|
||||||
|
<token name="MeshType">5</token>
|
||||||
|
<string name="Name">Mesh</string>
|
||||||
|
<Vector3 name="Offset">
|
||||||
|
<X>0</X>
|
||||||
|
<Y>0</Y>
|
||||||
|
<Z>0</Z>
|
||||||
|
</Vector3>
|
||||||
|
<Vector3 name="Scale">
|
||||||
|
<X>1</X>
|
||||||
|
<Y>1</Y>
|
||||||
|
<Z>1</Z>
|
||||||
|
</Vector3>
|
||||||
|
<Content name="TextureId"><null></null></Content>
|
||||||
|
<Vector3 name="VertexColor">
|
||||||
|
<X>1</X>
|
||||||
|
<Y>1</Y>
|
||||||
|
<Z>1</Z>
|
||||||
|
</Vector3>
|
||||||
|
<bool name="archivable">true</bool>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"].'/api/private/config.php';
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"].'/api/private/components/db.php';
|
||||||
|
|
||||||
|
if(SITE_CONFIG["api"]["renderserverKey"] != ($_GET['accessKey'] ?? false)) die(http_response_code(403));
|
||||||
|
|
||||||
|
$query = $pdo->query("UPDATE servers SET ping = UNIX_TIMESTAMP() WHERE id = 1");
|
||||||
|
echo "pinged";
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIICXQIBAAKBgQDM2nAU3iPVFbvirzU0YXw9W/jAVZefaRDeN4RiW3ksdW7Wgx6l14nlK6eKpxOdTM5yiS0SVMvjjImZuIrqLwwexGJ92or6aA3n3qf9etxZdKqROjngo3DVMTrVZIfQpkf9r+LEOAx4K3hH71s6Ni54rPLXTIHgJsQO0cnxWnEdEQIDAQABAoGBAL+JmnSYg45wJN2+DpQsdjr07LABF6TQWxo7dId2meTs5DakIJrV3jQtzhiBQYC5WOqUwlS6fm0DcYEOoKx4Uu4ftuL4CGzNtbY0E8CN2xMk0S70KrvsgoO4rqFzPGd+dJoVLlw2bVXrF+vVtFgnxClRx+VSWMD09TGe2R2YXirlAkEA0dnPhhjPuIqwLSLv3xuTfbTlfqoeoaUIahAo7RL9o34xMw2vxLQMw/uLBByxZl9JhFcC+lqxpmwwAzo2nMD+UwJBAPnnRmEh1Ewp3Cn7LjrcKaI7x1NC2nqwo69Qf51nSQl/Udz7PfL053K+J0pJj2IFWS/hmxFo/yTA+MV7+rigIosCQFSh8ncThJrZnCnoADPLzFUTYscN1yK8C0OzVr4ePZr1ZuQ/Ldc4Ajn8NdmntMgjv+OWsAXGFAWZdlem36WilC8CQEyYVVr6Gm7JucBoS3AhAOXHur1LVVmbgGAApUyiVqGBk57OptsrszDZFYPQbhEWIJLrbDL24pTqTJWC/YLPGicCQQCTCCNew5hZ9p0DRx7I6oa1qLMctatrwfFR14MaQzqP961+pbxLK4ShKe1VnqsK+aLrR+TkKYjB64RD2YNOXkNK
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"].'/api/private/config.php';
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"].'/api/private/components/db.php';
|
||||||
|
|
||||||
|
if(SITE_CONFIG["api"]["renderserverKey"] != ($_GET['accessKey'] ?? false)) die(http_response_code(403));
|
||||||
|
header('Content-type: text/javascript');
|
||||||
|
if(!SITE_CONFIG["site"]["thumbserver"]) die("fart");
|
||||||
|
|
||||||
|
$data = $pdo->query("SELECT renderStatus, jobID, renderType, assetID FROM renderqueue WHERE renderStatus IN (0, 1) ORDER BY timestampRequested LIMIT 1")->fetch(PDO::FETCH_OBJ);
|
||||||
|
if (!$data) die("fart");
|
||||||
|
echo json_encode($data, JSON_PRETTY_PRINT);
|
||||||
|
|
||||||
|
$query = $pdo->prepare('UPDATE renderqueue SET renderStatus = 1, timestampAcknowledged = UNIX_TIMESTAMP() WHERE jobID = :jobid');
|
||||||
|
$query->bindValue(':jobid', $data->jobID, PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
include $_SERVER['DOCUMENT_ROOT']."/api/private/core.php";
|
||||||
|
if(SITE_CONFIG["api"]["renderserverKey"] != ($_GET['accessKey'] ?? false)) die(http_response_code(403));
|
||||||
|
|
||||||
|
$asset = $_GET['asset'] ?? false;
|
||||||
|
$renderType = $_GET['type'] ?? false;
|
||||||
|
|
||||||
|
header("Pragma: no-cache");
|
||||||
|
header("Cache-Control: no-cache");
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
game:GetService('Visit'):SetUploadUrl('')
|
||||||
|
for _,v in pairs(game.GuiRoot:GetChildren()) do v:Remove() end
|
||||||
|
<?php if($renderType == "Avatar") { ?>
|
||||||
|
if not game.Players.LocalPlayer then game.Players:CreateLocalPlayer(0) end
|
||||||
|
plr = game.Players.LocalPlayer
|
||||||
|
|
||||||
|
--plr.CharacterAppearance = "http://<?=$_SERVER['HTTP_HOST']?>/Asset/CharacterFetch.ashx?userId=<?=$asset?>"
|
||||||
|
plr.CharacterAppearance = "<?=Users::GetCharacterAppearance($asset, -1)?>"
|
||||||
|
plr:LoadCharacter()
|
||||||
|
|
||||||
|
wait(2)
|
||||||
|
|
||||||
|
for _,v in ipairs(plr.Character:GetChildren()) do
|
||||||
|
if v.className == "Tool" then
|
||||||
|
plr.Character.Torso["Right Shoulder"].CurrentAngle = math.pi/2
|
||||||
|
plr.Character["Right Arm"].Transparency = 1
|
||||||
|
plr.Character["Right Arm"].Transparency = 0
|
||||||
|
v.Handle.Transparency = 1
|
||||||
|
v.Handle.Transparency = 0
|
||||||
|
--v.Parent = plr.Character
|
||||||
|
--break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(1)
|
||||||
|
|
||||||
|
for _,v in pairs(game.RelativePanel:GetChildren()) do v:Remove() end
|
||||||
|
|
||||||
|
<?php } elseif($renderType == "Head") { ?>
|
||||||
|
if not game.Players:GetChildren()[1] then game.Players:CreateLocalPlayer(0) end
|
||||||
|
plr = game.Players.LocalPlayer
|
||||||
|
|
||||||
|
plr.CharacterAppearance = "http://<?=$_SERVER['HTTP_HOST']?>/api/thumbs/whitecharacter.xml;http://<?=$_SERVER['HTTP_HOST']?>/asset/?id=<?=$asset?>&force=true;"
|
||||||
|
plr:LoadCharacter()
|
||||||
|
|
||||||
|
for _,v in ipairs(plr.Character:GetChildren()) do
|
||||||
|
if v.className == "Part" and v.Name ~= "Head" then v:Remove() end
|
||||||
|
end
|
||||||
|
|
||||||
|
wait(2)
|
||||||
|
<?php } elseif($renderType == "Clothing") { ?>
|
||||||
|
if not game.Players:GetChildren()[1] then game.Players:CreateLocalPlayer(0) end
|
||||||
|
plr = game.Players.LocalPlayer
|
||||||
|
|
||||||
|
plr.CharacterAppearance = "http://<?=$_SERVER['HTTP_HOST']?>/api/thumbs/whitecharacter.xml;http://<?=$_SERVER['HTTP_HOST']?>/asset/?id=<?=$asset?>&force=true;"
|
||||||
|
plr:LoadCharacter()
|
||||||
|
|
||||||
|
wait(2)
|
||||||
|
<?php } elseif($renderType == "Mesh") { ?>
|
||||||
|
game:Load("http://<?=$_SERVER['HTTP_HOST']?>/api/thumbs/mesh?asset=<?=$asset?>&force=true")
|
||||||
|
error("hi")
|
||||||
|
<?php } elseif($renderType == "Model" || $renderType == "UserModel") { ?>
|
||||||
|
--game.Lighting.GeographicLatitude = 40
|
||||||
|
--game.Lighting.TimeOfDay = '12:00:00'
|
||||||
|
<?php if($renderType == "UserModel") { ?>game:Load("rbxasset://whitesky.rbxm")<?php } ?>
|
||||||
|
game:Load("http://<?=$_SERVER['HTTP_HOST']?>/asset/?id=<?=$asset?>&force=true")
|
||||||
|
game:GetChildren()[20].Parent = workspace
|
||||||
|
--[[while wait() do
|
||||||
|
local model = game:GetChildren()[20]
|
||||||
|
if model ~= nil then
|
||||||
|
--if model.Name ~= "FilteredSelection" and model.Name ~= "SpawnerService" then
|
||||||
|
model.Parent = workspace
|
||||||
|
--end
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end--]]
|
||||||
|
error("hi")
|
||||||
|
<?php } elseif($renderType == "Place") { ?>
|
||||||
|
game:Load("http://<?=$_SERVER['HTTP_HOST']?>/asset/?id=<?=$asset?>")
|
||||||
|
print("loaded <?=$asset?>")
|
||||||
|
|
||||||
|
for _,v in ipairs(game.Lighting:GetChildren()) do
|
||||||
|
if v.className == "Sky" then skybox = true end
|
||||||
|
end
|
||||||
|
if not skybox then game:Load('rbxasset://sky.rbxm') end
|
||||||
|
|
||||||
|
for _,v in pairs(game.StarterPack:GetChildren()) do v:Remove() end
|
||||||
|
|
||||||
|
error("hi")
|
||||||
|
<?php }
|
||||||
|
|
||||||
|
$script = ob_get_clean();
|
||||||
|
openssl_sign($script, $signature, openssl_pkey_get_private("file://private_key.pem"));
|
||||||
|
echo "%" . base64_encode($signature) . "%" . $script;
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"].'/api/private/config.php';
|
||||||
|
require $_SERVER["DOCUMENT_ROOT"].'/api/private/components/db.php';
|
||||||
|
|
||||||
|
if(SITE_CONFIG["api"]["renderserverKey"] != ($_GET['accessKey'] ?? false)) die(http_response_code(403));
|
||||||
|
|
||||||
|
$completetype = $_GET['type'] ?? false; //1 = success; 2 = error;
|
||||||
|
$response = $_GET['response'] ?? false;
|
||||||
|
|
||||||
|
switch ($completetype)
|
||||||
|
{
|
||||||
|
case 1: //success
|
||||||
|
$query = $pdo->query("UPDATE renderqueue SET renderStatus = 4 WHERE renderStatus = 1 LIMIT 1");
|
||||||
|
// moved this to upload
|
||||||
|
echo "success";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: //error
|
||||||
|
$query = $pdo->prepare("UPDATE renderqueue SET renderStatus = 3, additionalInfo = :response, timestampCompleted = UNIX_TIMESTAMP() WHERE renderStatus = 1 LIMIT 1");
|
||||||
|
$query->bindParam(':response', $response, PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
echo "success";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
die("invalid type");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php include $_SERVER['DOCUMENT_ROOT']."/api/private/core.php";
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Image");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
if(SITE_CONFIG["api"]["renderserverKey"] != ($_GET['accessKey'] ?? false)) die(http_response_code(403));
|
||||||
|
|
||||||
|
$jobid = $_GET['jobID'] ?? false;
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT * FROM renderqueue WHERE jobID = :jobID");
|
||||||
|
$query->bindParam(":jobID", $jobid, PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
$data = $query->fetch(PDO::FETCH_OBJ);
|
||||||
|
if(!$data) die("doesnt exist");
|
||||||
|
|
||||||
|
//$query = $pdo->prepare("UPDATE renderqueue SET renderStatus = 4 WHERE jobID = :jobID");
|
||||||
|
//$query->bindParam(":jobID", $jobid, PDO::PARAM_STR);
|
||||||
|
//$query->execute();
|
||||||
|
|
||||||
|
$assetID = $data->assetID;
|
||||||
|
|
||||||
|
Polygon::ImportLibrary("class.upload");
|
||||||
|
|
||||||
|
$image = new Upload($_FILES["file"]);
|
||||||
|
$image->allowed = ['image/png', 'image/jpg', 'image/jpeg'];
|
||||||
|
$image->image_convert = 'png';
|
||||||
|
|
||||||
|
if($data->renderType == "Avatar")
|
||||||
|
{
|
||||||
|
/* Image::Process($image, ["name" => "$assetID-420x420.png", "x" => 420, "y" => 420, "dir" => "/thumbs/avatars/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-352x352.png", "x" => 352, "y" => 352, "dir" => "/thumbs/avatars/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-250x250.png", "x" => 250, "y" => 250, "dir" => "/thumbs/avatars/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-110x110.png", "x" => 110, "y" => 110, "dir" => "/thumbs/avatars/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-100x100.png", "x" => 100, "y" => 100, "dir" => "/thumbs/avatars/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-75x75.png", "x" => 75, "y" => 75, "dir" => "/thumbs/avatars/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-48x48.png", "x" => 48, "y" => 48, "dir" => "/thumbs/avatars/"]); */
|
||||||
|
Thumbnails::UploadAvatar($image, $assetID, 420, 420);
|
||||||
|
Thumbnails::UploadAvatar($image, $assetID, 352, 352);
|
||||||
|
Thumbnails::UploadAvatar($image, $assetID, 250, 250);
|
||||||
|
Thumbnails::UploadAvatar($image, $assetID, 110, 110);
|
||||||
|
Thumbnails::UploadAvatar($image, $assetID, 100, 100);
|
||||||
|
Thumbnails::UploadAvatar($image, $assetID, 75, 75);
|
||||||
|
Thumbnails::UploadAvatar($image, $assetID, 48, 48);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$type = Catalog::GetAssetInfo($assetID)->type;
|
||||||
|
if(in_array($type, [4, 8, 10, 11, 12, 17, 19]))
|
||||||
|
{
|
||||||
|
/* Image::Process($image, ["name" => "$assetID-420x420.png", "x" => 420, "y" => 420, "dir" => "/thumbs/assets/"]);
|
||||||
|
if(in_array($type, [8, 19])) Image::Process($image, ["name" => "$assetID-420x230.png", "keepRatio" => true, "align" => "C", "x" => 420, "y" => 230, "dir" => "/thumbs/assets/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-352x352.png", "x" => 352, "y" => 352, "dir" => "/thumbs/assets/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-250x250.png", "x" => 250, "y" => 250, "dir" => "/thumbs/assets/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-110x110.png", "x" => 110, "y" => 110, "dir" => "/thumbs/assets/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-100x100.png", "x" => 100, "y" => 100, "dir" => "/thumbs/assets/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-75x75.png", "x" => 75, "y" => 75, "dir" => "/thumbs/assets/"]);
|
||||||
|
Image::Process($image, ["name" => "$assetID-48x48.png", "x" => 48, "y" => 48, "dir" => "/thumbs/assets/"]); */
|
||||||
|
|
||||||
|
if(in_array($type, [8, 19])) Thumbnails::UploadAsset($image, $assetID, 420, 230, ["align" => "C"]);
|
||||||
|
Thumbnails::UploadAsset($image, $assetID, 420, 420);
|
||||||
|
Thumbnails::UploadAsset($image, $assetID, 352, 352);
|
||||||
|
Thumbnails::UploadAsset($image, $assetID, 250, 250);
|
||||||
|
Thumbnails::UploadAsset($image, $assetID, 110, 110);
|
||||||
|
Thumbnails::UploadAsset($image, $assetID, 100, 100);
|
||||||
|
Thumbnails::UploadAsset($image, $assetID, 75, 75);
|
||||||
|
Thumbnails::UploadAsset($image, $assetID, 48, 48);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $pdo->prepare("UPDATE renderqueue SET renderStatus = 2, timestampCompleted = UNIX_TIMESTAMP() WHERE jobID = :jobID");
|
||||||
|
$query->bindParam(":jobID", $jobid, PDO::PARAM_STR);
|
||||||
|
$query->execute();
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
|
||||||
|
<External>null</External>
|
||||||
|
<External>nil</External>
|
||||||
|
<Item class="BodyColors">
|
||||||
|
<Properties>
|
||||||
|
<int name="HeadColor">1</int>
|
||||||
|
<int name="LeftArmColor">1</int>
|
||||||
|
<int name="LeftLegColor">1</int>
|
||||||
|
<string name="Name">Body Colors</string>
|
||||||
|
<int name="RightArmColor">1</int>
|
||||||
|
<int name="RightLegColor">1</int>
|
||||||
|
<int name="TorsoColor">1</int>
|
||||||
|
<bool name="archivable">true</bool>
|
||||||
|
</Properties>
|
||||||
|
</Item>
|
||||||
|
</roblox>
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
if(!isset($_POST['userID'])) api::respond(400, false, "Invalid Request - userID not set");
|
||||||
|
if(!is_numeric($_POST['userID'])) api::respond(400, false, "Invalid Request - userID is not numeric");
|
||||||
|
if(!isset($_POST['type'])) api::respond(400, false, "Invalid Request - badge type not set");
|
||||||
|
if(!in_array($_POST['type'], ["player", "polygon"])) api::respond(400, false, "Invalid Request - invalid badge type");
|
||||||
|
|
||||||
|
$selfProfile = isset($_SERVER['HTTP_REFERER']) && str_ends_with($_SERVER['HTTP_REFERER'], "/user");
|
||||||
|
$page = $_POST['page'] ?? 1;
|
||||||
|
$pages = 1;
|
||||||
|
$type = $_POST['type'] ?? false;
|
||||||
|
|
||||||
|
$userinfo = Users::GetInfoFromID($_POST['userID']);
|
||||||
|
if(!$userinfo) api::respond(400, false, "User does not exist");
|
||||||
|
|
||||||
|
$badges = [];
|
||||||
|
|
||||||
|
if($type == "polygon")
|
||||||
|
{
|
||||||
|
if($userinfo->adminlevel == Users::STAFF_ADMINISTRATOR)
|
||||||
|
$badges[] =
|
||||||
|
[
|
||||||
|
"name" => "Administrator",
|
||||||
|
"image" => "/img/ProjectPolygon.png",
|
||||||
|
"info" => "This badge identifies an account as belonging to a ".SITE_CONFIG["site"]["name"]." administrator. Only official ".SITE_CONFIG["site"]["name"]." administrators will possess this badge. If someone claims to be an admin, but does not have this badge, they are potentially trying to mislead you."
|
||||||
|
];
|
||||||
|
|
||||||
|
if($userinfo->adminlevel == Users::STAFF_MODERATOR)
|
||||||
|
$badges[] =
|
||||||
|
[
|
||||||
|
"name" => "Moderator",
|
||||||
|
"image" => "/img/badges/Moderator.png",
|
||||||
|
"info" => "Users with this badge are moderators. Moderators have special powers on ".SITE_CONFIG["site"]["name"]." that allow them to moderate users and catalog items that other users upload. Users who are exemplary citizens on ".SITE_CONFIG["site"]["name"]." over a long period of time may be invited to be moderators. This badge is granted by invitation only."
|
||||||
|
];
|
||||||
|
|
||||||
|
if($userinfo->adminlevel == Users::STAFF_CATALOG)
|
||||||
|
$badges[] =
|
||||||
|
[
|
||||||
|
"name" => "Catalog Manager",
|
||||||
|
"image" => "/img/badges/CatalogManager.png",
|
||||||
|
"info" => "Users with this badge are catalog managers. Catalog managers have special powers on ".SITE_CONFIG["site"]["name"]." that allow them to create and moderate catalog items that other users upload. Users who are exemplary citizens on ".SITE_CONFIG["site"]["name"]." over a long period of time may be invited to be catalog managers. This badge is granted by invitation only."
|
||||||
|
];
|
||||||
|
|
||||||
|
if(Users::GetFriendCount($userinfo->id) >= 20)
|
||||||
|
$badges[] =
|
||||||
|
[
|
||||||
|
"name" => "Friendship",
|
||||||
|
"image" => "/img/badges/Friends.png",
|
||||||
|
"info" => "This badge is given to players who have embraced the ".SITE_CONFIG["site"]["name"]." community and have made at least 20 friends. People who have this badge are good people to know and can probably help you out if you are having trouble."
|
||||||
|
];
|
||||||
|
|
||||||
|
if(time() >= strtotime("1 year", $userinfo->jointime))
|
||||||
|
$badges[] =
|
||||||
|
[
|
||||||
|
"name" => "Veteran",
|
||||||
|
"image" => "/img/badges/Veteran.png",
|
||||||
|
"info" => "This decoration is awarded to all citizens who have played ".SITE_CONFIG["site"]["name"]." for at least a year. It recognizes stalwart community members who have stuck with us over countless releases and have helped shape ".SITE_CONFIG["site"]["name"]." into the game that it is today. These medalists are the true steel, the core of the Polygonian history ... and its future."
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: add when we get dedicated servers
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if($badges == [])
|
||||||
|
{
|
||||||
|
$responsemsg = ($selfProfile?"You have":$userinfo->username." has")."n't earned any ";
|
||||||
|
$responsemsg .= $type == "polygon" ? SITE_CONFIG["site"]["name"]." badges" : "player badges";
|
||||||
|
api::respond(200, true, $responsemsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $pages, "items" => $badges]));
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
if(!isset($_POST['userID'])) api::respond(400, false, "Invalid Request - userID not set");
|
||||||
|
if(!is_numeric($_POST['userID'])) api::respond(400, false, "Invalid Request - userID is not numeric");
|
||||||
|
|
||||||
|
$SelfProfile = isset($_SERVER['HTTP_REFERER']) && str_ends_with($_SERVER['HTTP_REFERER'], "/user");
|
||||||
|
|
||||||
|
$UserID = $_POST['userID'] ?? false;
|
||||||
|
$Page = $_POST['page'] ?? 1;
|
||||||
|
$Groups = [];
|
||||||
|
$UserInfo = Users::GetInfoFromID($UserID);
|
||||||
|
|
||||||
|
if(!$UserInfo) api::respond(400, false, "User does not exist");
|
||||||
|
|
||||||
|
$GroupsCount = db::run(
|
||||||
|
"SELECT COUNT(*) FROM groups_members
|
||||||
|
INNER JOIN groups ON groups.id = groups_members.GroupID
|
||||||
|
WHERE groups_members.UserID = :UserID AND NOT groups.deleted",
|
||||||
|
[":UserID" => $UserID]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
if(!$GroupsCount)
|
||||||
|
{
|
||||||
|
api::respond(200, true, ($SelfProfile ? "You are" : "{$UserInfo->username} is")."n't in any groups");
|
||||||
|
}
|
||||||
|
|
||||||
|
$Pages = ceil($GroupsCount/8);
|
||||||
|
$Offset = ($Page - 1)*8;
|
||||||
|
|
||||||
|
$GroupsQuery = db::run(
|
||||||
|
"SELECT groups.name, groups.id, groups.emblem, groups_ranks.Name AS Role,
|
||||||
|
(SELECT COUNT(*) FROM groups_members WHERE GroupID = groups.id AND NOT Pending) AS MemberCount
|
||||||
|
FROM groups_members
|
||||||
|
INNER JOIN groups_ranks ON groups_ranks.GroupID = groups_members.GroupID AND groups_ranks.Rank = groups_members.Rank
|
||||||
|
INNER JOIN groups ON groups.id = groups_members.GroupID
|
||||||
|
WHERE groups_members.UserID = :UserID AND NOT groups.deleted
|
||||||
|
GROUP BY id ORDER BY Joined DESC LIMIT 8 OFFSET $Offset",
|
||||||
|
[":UserID" => $UserID]
|
||||||
|
);
|
||||||
|
|
||||||
|
while($Group = $GroupsQuery->Fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$Groups[] =
|
||||||
|
[
|
||||||
|
"Name" => Polygon::FilterText($Group->name),
|
||||||
|
"ID" => $Group->id,
|
||||||
|
"Role" => Polygon::FilterText($Group->Role),
|
||||||
|
"MemberCount" => $Group->MemberCount,
|
||||||
|
"Emblem" => Thumbnails::GetAssetFromID($Group->emblem, 420, 420)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
api::respond_custom(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "items" => $Groups]);
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
api::initialize(["method" => "POST"]);
|
||||||
|
|
||||||
|
$self = isset($_SERVER['HTTP_REFERER']) && (str_ends_with($_SERVER['HTTP_REFERER'], "/my/stuff") || str_ends_with($_SERVER['HTTP_REFERER'], "/user"));
|
||||||
|
$userId = $_POST["userId"] ?? false;
|
||||||
|
$type = $_POST["type"] ?? false;
|
||||||
|
$page = $_POST["page"] ?? 1;
|
||||||
|
$assets = [];
|
||||||
|
|
||||||
|
if(!Catalog::GetTypeByNum($type)) api::respond(400, false, "Invalid asset type");
|
||||||
|
if(!in_array($type, [17, 18, 19, 8, 2, 11, 12, 13, 10, 3])) api::respond(400, false, "Invalid asset type");
|
||||||
|
|
||||||
|
$type_str = Catalog::GetTypeByNum($type);
|
||||||
|
|
||||||
|
$query = $pdo->prepare("SELECT COUNT(*) FROM ownedAssets INNER JOIN assets ON assets.id = assetId WHERE userId = :uid AND assets.type = :type");
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
$pages = ceil($query->fetchColumn()/18);
|
||||||
|
$offset = ($page - 1)*18;
|
||||||
|
|
||||||
|
if(!$pages) api::respond(200, true, ($self?'You have':Users::GetNameFromID($userId).' has').' not purchased any '.plural($type_str));
|
||||||
|
|
||||||
|
$query = $pdo->prepare("
|
||||||
|
SELECT assets.*, users.username,
|
||||||
|
(SELECT COUNT(*) FROM ownedAssets WHERE assetId = assets.id AND userId != assets.creator) AS sales_total
|
||||||
|
FROM ownedAssets
|
||||||
|
INNER JOIN assets ON assets.id = assetId
|
||||||
|
INNER JOIN users ON creator = users.id
|
||||||
|
WHERE userId = :uid AND assets.type = :type ORDER BY ownedAssets.id DESC LIMIT 18 OFFSET :offset");
|
||||||
|
$query->bindParam(":uid", $userId, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":type", $type, PDO::PARAM_INT);
|
||||||
|
$query->bindParam(":offset", $offset, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
while($asset = $query->fetch(PDO::FETCH_OBJ))
|
||||||
|
{
|
||||||
|
$price = '<span class="text-success">';
|
||||||
|
if($asset->sale) $price .= $asset->price ? '<i class="fal fa-pizza-slice"></i> '.$asset->price : 'Free';
|
||||||
|
$price .= '</span>';
|
||||||
|
|
||||||
|
$assets[] =
|
||||||
|
[
|
||||||
|
"url" => "/".encode_asset_name($asset->name)."-item?id=".$asset->id,
|
||||||
|
"item_id" => $asset->id,
|
||||||
|
"item_name" => htmlspecialchars($asset->name),
|
||||||
|
"item_thumbnail" => Thumbnails::GetAsset($asset, 420, 420),
|
||||||
|
"creator_id" => $asset->creator,
|
||||||
|
"creator_name" => $asset->username,
|
||||||
|
"price" => $price
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $pages, "items" => $assets]));
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Gzip");
|
||||||
|
Polygon::ImportClass("RBXClient");
|
||||||
|
|
||||||
|
header("Cache-Control: max-age=120");
|
||||||
|
|
||||||
|
$ExemptIDs =
|
||||||
|
[
|
||||||
|
69281057, // stamper speaker
|
||||||
|
69281292, // stamper boombox
|
||||||
|
];
|
||||||
|
|
||||||
|
$SwapIDs =
|
||||||
|
[
|
||||||
|
60059129 => 2599, // stamper rock
|
||||||
|
60051616 => 2600, // stamper funk
|
||||||
|
60049010 => 2601, // stamper electronic
|
||||||
|
];
|
||||||
|
|
||||||
|
$AssetID = $_GET['ID'] ?? $_GET['id'] ?? false;
|
||||||
|
$AssetHost = $_GET['host'] ?? $_SERVER['HTTP_HOST'];
|
||||||
|
|
||||||
|
$ForceRequest = isset($_GET['force']);
|
||||||
|
$RobloxAsset = false;
|
||||||
|
|
||||||
|
$AssetID = $SwapIDs[$AssetID] ?? $AssetID;
|
||||||
|
|
||||||
|
$Asset = db::run("SELECT * FROM assets WHERE id = :id", [":id" => $AssetID])->fetch(PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
if(!$Asset || isset($_GET['forcerblxasset'])) $RobloxAsset = true;
|
||||||
|
|
||||||
|
// so i dont have a url redirect in the client just yet
|
||||||
|
// meaning that we're gonna have to replace the roblox.com urls on the fly
|
||||||
|
// and in order to do that the server has to actually fetch the asset data
|
||||||
|
// this is absolutely gonna tank performance but for the meantime theres
|
||||||
|
// not much else i can do
|
||||||
|
|
||||||
|
if($RobloxAsset)
|
||||||
|
{
|
||||||
|
// we're only interested in replacing the asset urls of models
|
||||||
|
// $apidata = json_decode(file_get_contents("https://api.roblox.com/marketplace/productinfo?assetId=".$_GET['id']));
|
||||||
|
// if(!in_array($apidata->AssetTypeId, [9, 10])) die(header("Location: https://assetdelivery.roblox.com/v1/asset/?".$_SERVER['QUERY_STRING']));
|
||||||
|
|
||||||
|
// /asset/?id= just redirects to a url on roblox's cdn so we need to get that redirect url
|
||||||
|
$curl = curl_init();
|
||||||
|
curl_setopt_array($curl,
|
||||||
|
[
|
||||||
|
CURLOPT_URL => "https://assetdelivery.roblox.com/v1/asset/?".$_SERVER['QUERY_STRING'],
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_HTTPHEADER => ["User-Agent: Roblox/WinInet"]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$AssetData = curl_exec($curl);
|
||||||
|
$HttpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
|
||||||
|
if($HttpCode != 200) die(http_response_code($HttpCode));
|
||||||
|
if(!stripos($AssetData, 'roblox')) die(header("Location: https://assetdelivery.roblox.com/v1/asset/?".$_SERVER['QUERY_STRING']));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(!file_exists("./files/".$Asset->id)) die(http_response_code(404));
|
||||||
|
|
||||||
|
if(!$ForceRequest)
|
||||||
|
{
|
||||||
|
if($Asset->approved != 1) die(http_response_code(403));
|
||||||
|
if(!$Asset->publicDomain && (!SESSION || !Catalog::OwnsAsset(SESSION["userId"], $Asset->id))) die(http_response_code(403));
|
||||||
|
}
|
||||||
|
|
||||||
|
$AssetData = file_get_contents("./files/".$Asset->id);
|
||||||
|
if($Asset->type == 10 && !stripos($AssetData, 'roblox')) $AssetData = Gzip::Decompress("./files/".$Asset->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace asset urls
|
||||||
|
if($ForceRequest) $AssetData = preg_replace("/%ASSETURL%([0-9]+)/i", "http://$AssetHost/asset/?id=$1&force=true", $AssetData);
|
||||||
|
else $AssetData = str_replace("%ASSETURL%", "http://$AssetHost/asset/?id=", $AssetData);
|
||||||
|
|
||||||
|
// we need to make an exception for the stamper tool speaker as it needs to be able to load polygon assets
|
||||||
|
if($RobloxAsset && !in_array($AssetID, $ExemptIDs))
|
||||||
|
{
|
||||||
|
$AssetData = str_ireplace(
|
||||||
|
["http://www.roblox.com/asset", "http://roblox.com/asset", "http://www.roblox.com/thumbs", "http://roblox.com/thumbs"],
|
||||||
|
["https://assetdelivery.roblox.com/v1/asset", "https://assetdelivery.roblox.com/v1/asset", "http://$AssetHost/thumbs", "http://$AssetHost/thumbs"],
|
||||||
|
$AssetData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$AssetData = str_ireplace(
|
||||||
|
["www.roblox.com/asset", "roblox.com/asset", "www.roblox.com/thumbs", "roblox.com/thumbs"],
|
||||||
|
["$AssetHost/asset", "$AssetHost/asset", "$AssetHost/thumbs", "$AssetHost/thumbs"],
|
||||||
|
$AssetData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$RobloxAsset && $Asset->type == 3 && isset($_GET['audiostream']))
|
||||||
|
{
|
||||||
|
$FileExtensions = ["audio/mpeg" => ".mp3", "audio/ogg" => ".ogg", "audio/mid" => ".mid", "audio/wav" => ".wav"];
|
||||||
|
|
||||||
|
header('Content-Type: '.$Asset->audioType);
|
||||||
|
header('Content-Disposition: attachment; filename="'.htmlentities($Asset->name).$FileExtensions[$Asset->audioType].'"');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
header('Content-Type: binary/octet-stream');
|
||||||
|
if($RobloxAsset) header('Content-Disposition: attachment; filename="'.sha1($AssetData).'"');
|
||||||
|
else header('Content-Disposition: attachment; filename="'.sha1_file("./files/".$Asset->id).'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$RobloxAsset && $Asset->type == 5) $AssetData = RBXClient::CryptSignScript($AssetData, $Asset->id);
|
||||||
|
|
||||||
|
header('Content-Length: '.strlen($AssetData));
|
||||||
|
die($AssetData);
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
$category = $_GET['Category'] ?? "Users";
|
||||||
|
$page = $_GET['PageNumber'] ?? 1;
|
||||||
|
$keyword = $_GET['SearchTextBox'] ?? "";
|
||||||
|
$keyword_sql = "%";
|
||||||
|
|
||||||
|
if(strlen($keyword)) $keyword_sql = "%{$keyword}%";
|
||||||
|
|
||||||
|
// if($keyword) $keyword_sql = "*{$keyword}*";
|
||||||
|
// else $keyword_sql = "*";
|
||||||
|
|
||||||
|
if($category == "Groups")
|
||||||
|
{
|
||||||
|
// WHERE MATCH (name) AGAINST (:keywd IN NATURAL LANGUAGE MODE)
|
||||||
|
|
||||||
|
$querycount = "SELECT COUNT(*) FROM groups WHERE name LIKE :keywd AND NOT deleted";
|
||||||
|
|
||||||
|
$querystring =
|
||||||
|
"SELECT *,
|
||||||
|
(SELECT COUNT(*) FROM groups_members WHERE GroupID = groups.id AND NOT Pending) AS MemberCount
|
||||||
|
FROM groups WHERE name LIKE :keywd AND NOT deleted
|
||||||
|
ORDER BY MemberCount DESC";
|
||||||
|
|
||||||
|
pageBuilder::$pageConfig["title"] = "Browse Groups";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// WHERE MATCH (username) AGAINST (:keywd IN NATURAL LANGUAGE MODE)
|
||||||
|
|
||||||
|
$querycount =
|
||||||
|
"SELECT COUNT(*) FROM users WHERE username LIKE :keywd
|
||||||
|
AND NOT (SELECT COUNT(*) FROM bans WHERE userId = users.id AND NOT isDismissed)";
|
||||||
|
|
||||||
|
$querystring =
|
||||||
|
"SELECT * FROM users WHERE username LIKE :keywd
|
||||||
|
AND NOT (SELECT COUNT(*) FROM bans WHERE userId = users.id AND NOT isDismissed)
|
||||||
|
ORDER BY lastonline DESC";
|
||||||
|
|
||||||
|
pageBuilder::$pageConfig["title"] = "Browse People";
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = db::run($querycount, [":keywd" => $keyword_sql])->fetchColumn();
|
||||||
|
|
||||||
|
$pages = ceil($count/15);
|
||||||
|
if($page > $pages) $page = $pages;
|
||||||
|
if(!is_numeric($page) || $page < 1) $page = 1;
|
||||||
|
$offset = ($page - 1)*15;
|
||||||
|
|
||||||
|
$results = db::run(
|
||||||
|
"$querystring LIMIT 15 OFFSET $offset",
|
||||||
|
[":keywd" => $keyword_sql]
|
||||||
|
);
|
||||||
|
|
||||||
|
function buildURL($page)
|
||||||
|
{
|
||||||
|
global $keyword;
|
||||||
|
global $category;
|
||||||
|
|
||||||
|
$url = "?";
|
||||||
|
if($keyword) $url .= "SearchTextBox=$keyword&";
|
||||||
|
$url .= "Category=$category&";
|
||||||
|
$url .= "PageNumber=$page";
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageBuilder::buildHeader();
|
||||||
|
?>
|
||||||
|
<form>
|
||||||
|
<div class="form-group row m-0">
|
||||||
|
<div class="col-sm-9 px-1 mb-2">
|
||||||
|
<input type="text" class="form-control form-control-sm" name="SearchTextBox" id="SearchTextBox" value="<?=htmlspecialchars($keyword)?>" placeholder="Search...">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3 px-0 d-inline-flex">
|
||||||
|
<div class="w-50 px-1 d-inline-block">
|
||||||
|
<button class="btn btn-sm btn-block btn-light px-1" name="Category" value="Users">Search Users</button>
|
||||||
|
</div>
|
||||||
|
<div class="w-50 px-1 d-inline-block">
|
||||||
|
<button class="btn btn-sm btn-block btn-light px-1" name="Category" value="Groups">Search Groups</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php if($results->rowCount()) { ?>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<?php if($category != "Groups") { ?>
|
||||||
|
<thead class="bg-light">
|
||||||
|
<tr>
|
||||||
|
<th class="font-weight-normal py-2" scope="col" style="width:5%">Avatar</th>
|
||||||
|
<th class="font-weight-normal py-2" scope="col" style="width:20%">Name</th>
|
||||||
|
<th class="font-weight-normal py-2" scope="col" style="width:50%">Blurb</th>
|
||||||
|
<th class="font-weight-normal py-2" scope="col" style="width:20%">Location / Last Seen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php while($row = $results->fetch(PDO::FETCH_OBJ)) { $status = Users::GetOnlineStatus($row->id); ?>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/user?ID=<?=$row->id?>"><img src="<?=Thumbnails::GetAvatar($row->id, 100, 100)?>" title="<?=$row->username?>" alt="<?=$row->username?>" width="63" height="63"></a></td>
|
||||||
|
<td class="text-break"><a href="/user?ID=<?=$row->id?>"><?=$row->username?></a></td>
|
||||||
|
<td class="text-break"><?=Polygon::FilterText($row->blurb)?></td>
|
||||||
|
<td><span<?=$status["attributes"]?:''?>><?=$status["text"]?></span></td>
|
||||||
|
</tr>
|
||||||
|
<?php } ?>
|
||||||
|
</tbody>
|
||||||
|
<?php } elseif($category == "Groups") { ?>
|
||||||
|
<thead class="bg-light">
|
||||||
|
<tr>
|
||||||
|
<th class="font-weight-normal py-2" scope="col" style="width:5%"></th>
|
||||||
|
<th class="font-weight-normal py-2" scope="col" style="width:20%">Group</th>
|
||||||
|
<th class="font-weight-normal py-2" scope="col" style="width:70%">Description</th>
|
||||||
|
<th class="font-weight-normal py-2" scope="col" style="width:5%">Members</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php while($row = $results->fetch(PDO::FETCH_OBJ)) { $status = Users::GetOnlineStatus($row->id); ?>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/groups?gid=<?=$row->id?>"><img src="<?=Thumbnails::GetAssetFromID($row->emblem, 420, 420)?>" title="<?=$row->name?>" alt="<?=$row->name?>" width="63" height="63"></a></td>
|
||||||
|
<td class="text-break"><a href="/groups?gid=<?=$row->id?>"><?=Polygon::FilterText($row->name)?></a></td>
|
||||||
|
<td class="text-break"><?=Polygon::FilterText($row->description)?></td>
|
||||||
|
<td><?=$row->MemberCount?></td>
|
||||||
|
</tr>
|
||||||
|
<?php } ?>
|
||||||
|
</tbody>
|
||||||
|
<?php } ?>
|
||||||
|
</table>
|
||||||
|
<?php } else { ?>
|
||||||
|
<p class="text-center">No results matched your search query</p>
|
||||||
|
<?php } if($pages > 1) { ?>
|
||||||
|
<div class="pagination form-inline justify-content-center">
|
||||||
|
<a class="btn btn-light back<?=$page<=1?' disabled':'" href="'.buildURL($page-1)?>"><h5 class="mb-0"><i class="fal fa-caret-left"></i></h5></a>
|
||||||
|
<span class="px-3">Page <?=$page?> of <?=$pages?></span>
|
||||||
|
<a class="btn btn-light next<?=$page>=$pages?' disabled':'" href="'.buildURL($page+1)?>"><h5 class="mb-0"><i class="fal fa-caret-right"></i></h5></a>
|
||||||
|
</div>
|
||||||
|
<?php } pageBuilder::buildFooter(); ?>
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
<?php require $_SERVER['DOCUMENT_ROOT'].'/api/private/core.php';
|
||||||
|
Polygon::ImportClass("Catalog");
|
||||||
|
Polygon::ImportClass("Thumbnails");
|
||||||
|
|
||||||
|
Users::RequireLogin();
|
||||||
|
|
||||||
|
// this is a catastrophe
|
||||||
|
|
||||||
|
$cats =
|
||||||
|
[
|
||||||
|
1 => ["name" => "All Categories", "price" => true],
|
||||||
|
3 => ["name" => "Clothing", "subcategories" => [8, 11, 2, 12], "price" => true],
|
||||||
|
4 => ["name" => "Body Parts", "subcategories" => [17, 18], "price" => true],
|
||||||
|
5 => ["name" => "Gear", "type" => 19, "price" => true],
|
||||||
|
6 => ["name" => "Models", "type" => 10, "price" => false],
|
||||||
|
8 => ["name" => "Decals", "type" => 13, "price" => false],
|
||||||
|
9 => ["name" => "Audio", "type" => 3, "price" => false],
|
||||||
|
];
|
||||||
|
|
||||||
|
$sorts =
|
||||||
|
[
|
||||||
|
0 => "sales DESC",
|
||||||
|
1 => "updated DESC",
|
||||||
|
2 => "price DESC",
|
||||||
|
3 => "price"
|
||||||
|
];
|
||||||
|
|
||||||
|
function getUrl($strings)
|
||||||
|
{
|
||||||
|
global $cat, $subcat;
|
||||||
|
$url = "?";
|
||||||
|
if($subcat) $url .= "Subcategory=".$subcat."&";
|
||||||
|
$url .= "CurrencyType=%currency%&SortType=%sort%&Category=".$cat;
|
||||||
|
|
||||||
|
return str_replace(["%currency%", "%sort%"], $strings, $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cat = $_GET['Category'] ?? 3;
|
||||||
|
$subcat = $_GET['Subcategory'] ?? false;
|
||||||
|
$keyword = $_GET['Keyword'] ?? "";
|
||||||
|
$keyword_sql = "%$keyword%";
|
||||||
|
$currency = $_GET['CurrencyType'] ?? 0;
|
||||||
|
$sort = $_GET['SortType'] ?? 1;
|
||||||
|
$page = $_GET['PageNumber'] ?? 1;
|
||||||
|
|
||||||
|
if($cat == 3 && !isset($_GET['Category']))
|
||||||
|
$subcat = 8;
|
||||||
|
|
||||||
|
if(!isset($cats[$cat]))
|
||||||
|
die(header("Location: /catalog"));
|
||||||
|
|
||||||
|
if($subcat && ($cat == 1 || isset($cats[$cat]["type"]) || !in_array($subcat, $cats[$cat]["subcategories"])))
|
||||||
|
die(header("Location: /catalog?Category=".$cat));
|
||||||
|
|
||||||
|
if(!in_array($currency, [0, 1, 2]))
|
||||||
|
die(header("Location: ".getUrl([0, $sort])));
|
||||||
|
|
||||||
|
if(!isset($sorts[$sort]))
|
||||||
|
die(header("Location: ".getUrl([$currency, 0])));
|
||||||
|
|
||||||
|
$queryparam = "";
|
||||||
|
$type = $subcat ?: $cats[$cat]["type"] ?? 2;
|
||||||
|
|
||||||
|
// adding "is not null" fetches the item even if the price is 0
|
||||||
|
$unavailable = isset($_GET['IncludeNotForSale']) && $_GET['IncludeNotForSale'] == "true";
|
||||||
|
if($unavailable) $queryparam .= "IS NOT NULL ";
|
||||||
|
|
||||||
|
// process query parameters for the item type
|
||||||
|
$queryparam .= "AND type";
|
||||||
|
if($cat == 1) $queryparam .= " IN (2, 3, 8, 10, 11, 12, 13, 17, 18, 19)";
|
||||||
|
elseif(isset($cats[$cat]["type"]) || $subcat) $queryparam .= " = ".($cats[$cat]["type"] ?? $subcat);
|
||||||
|
else $queryparam .= " IN (".implode(", ", $cats[$cat]["subcategories"]).")";
|
||||||
|
|
||||||
|
// process query parameters for the item price
|
||||||
|
$queryparam .= " AND price";
|
||||||
|
if(is_numeric($currency) && $currency == 0) $queryparam .= " IS NOT NULL";
|
||||||
|
elseif($currency == 2) $queryparam .= " = 0";
|
||||||
|
|
||||||
|
// get the number of assets matching the query
|
||||||
|
$results = db::run(
|
||||||
|
"SELECT COUNT(*) FROM assets WHERE type != 1 AND name LIKE :keywd AND approved != 2 AND sale $queryparam",
|
||||||
|
[":keywd" => $keyword_sql]
|
||||||
|
)->fetchColumn();
|
||||||
|
|
||||||
|
$pages = ceil($results/18);
|
||||||
|
if($page > $pages) $page = $pages;
|
||||||
|
if(!is_numeric($page) || $page < 1) $page = 1;
|
||||||
|
$offset = ($page - 1)*18;
|
||||||
|
|
||||||
|
$query = $pdo->prepare("
|
||||||
|
SELECT assets.*, users.username,
|
||||||
|
(SELECT COUNT(*) FROM ownedAssets WHERE assetId = assets.id AND userId != assets.creator) AS sales
|
||||||
|
FROM assets INNER JOIN users ON creator = users.id
|
||||||
|
WHERE type != 1 AND name LIKE :keywd AND approved != 2 AND sale $queryparam
|
||||||
|
ORDER BY ".$sorts[$sort]." LIMIT 18 OFFSET :offset");
|
||||||
|
$query->bindParam(":keywd", $keyword_sql, PDO::PARAM_STR);
|
||||||
|
$query->bindParam(":offset", $offset, PDO::PARAM_INT);
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
pageBuilder::$polygonScripts[] = "/js/polygon/catalog.js?t=1";
|
||||||
|
pageBuilder::$pageConfig["title"] = "Avatar Items, Virtual Avatars, Virtual Goods";
|
||||||
|
pageBuilder::buildHeader();
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
polygon.catalog =
|
||||||
|
{
|
||||||
|
PageNumber: <?=isset($_GET['PageNumber']) ? $page : "null"?>,
|
||||||
|
Subcategory: <?=$subcat ?: "null"?>,
|
||||||
|
Category: <?=$cat?>,
|
||||||
|
Keyword: <?='"'.urlencode($keyword).'"' ?: "null"?>,
|
||||||
|
CurrencyType: <?=$currency?>,
|
||||||
|
SortType: <?=$sort?>,
|
||||||
|
IncludeNotForSale: <?=$unavailable ? "true" : "null"?>,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-2 col-lg-3 col-md-3">
|
||||||
|
<h2 class="font-weight-normal">Catalog</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-10 col-lg-9 col-md-9">
|
||||||
|
<div class="input-group CatalogSearchBar">
|
||||||
|
<input type="text" class="form-control mb-2 keywordTextbox" value="<?=htmlspecialchars($keyword)?>">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<select class="form-control mb-2 categoriesForKeyword rounded-0" style="width:auto">
|
||||||
|
<?php if(isset($cats[$cat]["subcategories"]) && $subcat) { ?>
|
||||||
|
<option value="Custom" selected="selected"><?=plural(Catalog::GetTypeByNum($type))?></option>
|
||||||
|
<?php } foreach($cats as $cat_id => $cat_data) { ?>
|
||||||
|
<option value="<?=$cat_id?>"<?=!isset($_GET['Subcategory']) && $cat==$cat_id?' selected':''?>><?=$cat_data["name"]?></option>
|
||||||
|
<?php } ?>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-outline-primary mb-2 submitSearchButton">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-xl-2 col-lg-3 col-md-3 pb-4 pl-3 pr-0 divider-right">
|
||||||
|
<div class="dropdown show mr-3 mb-4">
|
||||||
|
<a class="btn btn-secondary btn-block text-left"<?=isset($_GET['Category'])?' href="#" role="button" id="browseByCategory" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"':''?>>
|
||||||
|
<h5 class="font-weight-normal m-0">Category <i class="mt-1 ml-2 mr-1 fas fa-caret-down float-right"></i></h5>
|
||||||
|
</a>
|
||||||
|
<div class="bg-cardpanel dropdown-menu w-100<?=!isset($_GET['Category'])?' d-block':''?>" style="min-width:5rem;" aria-labelledby="browseByCategory">
|
||||||
|
<?php foreach($cats as $dropdown_id => $dropdown_cat) { ?>
|
||||||
|
<a href="#category=<?=strtolower(str_replace(' ', '', $dropdown_cat["name"]))?>" class="dropdown-item assetTypeFilter" data-category="<?=$dropdown_id?>"><?=$dropdown_cat["name"]?></a>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php if(isset($_GET['Category'])) { ?>
|
||||||
|
<h3 class="font-weight-normal pb-0">Filters</h3>
|
||||||
|
<div class="filters mt-1">
|
||||||
|
<?php if(strlen($keyword)) { ?>
|
||||||
|
<h6 class="ml-2 font-weight-normal mb-1">Category</h6>
|
||||||
|
<div class="ml-3 filter-category">
|
||||||
|
<?php if(isset($cats[$cat]["subcategories"]) && $subcat) { ?>
|
||||||
|
<p class="m-0"><?=plural(Catalog::GetTypeByNum($type))?></p>
|
||||||
|
<?php } foreach($cats as $cat_id => $cat_data) { ?>
|
||||||
|
<p class="m-0"><a href="#category=<?=$cat_data["name"]?>" class="assetTypeFilter<?=$cat==$cat_id?' text-dark text-decoration-none':''?>" data-keepfilters="true" data-category="<?=$cat_id?>"><?=$cat_data["name"]?></a></p>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<div class="divider-bottom my-3"></div>
|
||||||
|
<?php } elseif(isset($cats[$cat]["subcategories"])) { ?>
|
||||||
|
<h6 class="ml-2 font-weight-normal mb-1"><?=$cats[$cat]["name"]?> Type</h6>
|
||||||
|
<div class="ml-3 filter-subcategory">
|
||||||
|
<p class="m-0"><a href="#category=All <?=$cats[$cat]["name"]?>" class="assetTypeFilter<?=!$subcat?' text-dark text-decoration-none':''?>" data-types="3">All <?=$cats[$cat]["name"]?></a></p>
|
||||||
|
<?php foreach($cats[$cat]["subcategories"] as $cat_id) { ?>
|
||||||
|
<p class="m-0"><a href="#category=<?=plural(Catalog::GetTypeByNum($cat_id))?>" class="assetTypeFilter<?=$subcat==$cat_id?' text-dark text-decoration-none':''?>" data-types="<?=$cat_id?>"><?=plural(Catalog::GetTypeByNum($cat_id))?></a></p>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<div class="divider-bottom my-3"></div>
|
||||||
|
<?php } ?>
|
||||||
|
<h6 class="ml-2 font-weight-normal mb-1">Currency / Price</h6>
|
||||||
|
<div class="ml-3 filter-clothing-type">
|
||||||
|
<p class="m-0"><a href="#price=0" class="priceFilter<?=$currency==0?' text-dark text-decoration-none':''?>" data-currencytype="0">All Currency</a></p>
|
||||||
|
<?php if($cats[$cat]["price"]){ ?>
|
||||||
|
<p class="m-0"><a href="#price=1"class="priceFilter<?=$currency==1?' text-dark text-decoration-none':''?>" data-currencytype="1">Pizzas</a></p>
|
||||||
|
<?php } ?>
|
||||||
|
<p class="m-0"><a href="#price=2" class="priceFilter<?=$currency==2?' text-dark text-decoration-none':''?>" data-currencytype="2">Free</a></p>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class=" form-check-input" id="includeNotForSaleCheckbox"<?=$unavailable?' checked="checked"':''?>>
|
||||||
|
<label class="form-check-label col-form-label-sm pt-0" for="includeNotForSaleCheckbox">Show off-sale items</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-10 col-lg-9 col-md-9 p-0 pl-3 pr-4">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb p-0 m-0" style="background-color:transparent">
|
||||||
|
<?php if(!$subcat || isset($cats[$cat]["type"])) { ?>
|
||||||
|
<li class="breadcrumb-item text-dark active"><?=$cats[$cat]["name"]?></li>
|
||||||
|
<?php } else { ?>
|
||||||
|
<li class="breadcrumb-item text-dark"><a href="?SortType=1&SortCurrency=0&Category=<?=$cat?>"><?=$cats[$cat]["name"]?></a></li>
|
||||||
|
<li class="breadcrumb-item text-dark active"><?=plural(Catalog::GetTypeByNum($type))?></li>
|
||||||
|
<?php } ?>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="catalog-container">
|
||||||
|
<?php if($query->rowCount()) { ?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-9 col-lg-8 col-md-6">
|
||||||
|
<p>Showing <?=number_format($offset+1)?> - <?=number_format($offset+$query->rowCount())?> of <?=number_format($results)?> results</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-3 col-lg-4 col-md-6 pr-2 mb-2 d-flex">
|
||||||
|
<label class="form-label form-label-sm" for="sortBy" style="width:6rem;">Sort by: </label>
|
||||||
|
<select class="Sort form-control form-control-sm" id="sortBy">
|
||||||
|
<option value="0"<?=$sort==0?' selected="selected"':''?>>Bestselling</option>
|
||||||
|
<option value="1"<?=$sort==1?' selected="selected"':''?>>Recently updated</option>
|
||||||
|
<option value="2"<?=$sort==2?' selected="selected"':''?>>Price (High to Low)</option>
|
||||||
|
<option value="3"<?=$sort==3?' selected="selected"':''?>>Price (Low to High)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php } else { ?>
|
||||||
|
<p class="text-center">No results matched your criteria</p>
|
||||||
|
<?php } ?>
|
||||||
|
<div class="items row pl-2">
|
||||||
|
<?php while($item = $query->fetch(PDO::FETCH_OBJ)) { ?>
|
||||||
|
<div class="item col-xl-2 col-lg-3 col-md-3 col-sm-4 col-6 pb-3 px-2" style="line-height:normal">
|
||||||
|
<div class="card info hover">
|
||||||
|
<a href="/<?=encode_asset_name($item->name)?>-item?id=<?=$item->id?>"><img src="<?=Thumbnails::GetStatus("rendering", 420, 420)?>" preload-src="<?=Thumbnails::GetAsset($item, 420, 420)?>" class="card-img-top img-fluid p-2" title="<?=Polygon::FilterText($item->name)?>" alt="<?=Polygon::FilterText($item->name)?>"></a>
|
||||||
|
<div class="card-body pt-0 px-2 pb-2">
|
||||||
|
<p class="text-truncate text-primary m-0" title="<?=Polygon::FilterText($item->name)?>"><a href="/<?=encode_asset_name($item->name)?>-item?id=<?=$item->id?>"><?=Polygon::FilterText($item->name)?></a></p>
|
||||||
|
<?php if($item->sale) { ?><p class="m-0<?=$item->price?' text-success':''?>"><?=$item->price ? '<i class="fal fa-pizza-slice"></i> '.number_format($item->price):'Free'?></p><?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="details-wrapper">
|
||||||
|
<div class="card details d-none">
|
||||||
|
<div class="card-body pt-0 px-2 pb-2">
|
||||||
|
<p class="text-truncate m-0"><small class="text-muted">Creator: <a href="/user?ID=<?=$item->creator?>"><?=$item->username?></a></small></p>
|
||||||
|
<p class="text-truncate m-0"><small class="text-muted">Updated: <span class="text-dark"><?=timeSince($item->updated)?></span></small></p>
|
||||||
|
<p class="text-truncate m-0"><small class="text-muted">Sales: <span class="text-dark"><?=number_format($item->sales)?></span></small></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<?php if($pages > 1) { ?>
|
||||||
|
<div class="pagination form-inline justify-content-center">
|
||||||
|
<button type="button" class="btn btn-light mx-3 back"<?=$page<=1?' disabled':''?>><h5 class="mb-0"><i class="fal fa-caret-left"></i></h5></button>
|
||||||
|
<span>Page</span>
|
||||||
|
<input class="form-control form-control-sm text-center mx-1 page" type="text" style="width:30px" value="<?=$page?>">
|
||||||
|
<span>of <?=$pages?></span>
|
||||||
|
<button type="button" class="btn btn-light mx-3 next"<?=$page>=$pages?' disabled':''?>><h5 class="mb-0"><i class="fal fa-caret-right"></i></h5></button>
|
||||||
|
</div>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php pageBuilder::buildFooter(); ?>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue