diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..843c6ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +thumbs/assets/* +thumbs/avatars/* +asset/files/* +api/private/config.php \ No newline at end of file diff --git a/RobloxOld.css b/RobloxOld.css new file mode 100644 index 0000000..50c2bf8 --- /dev/null +++ b/RobloxOld.css @@ -0,0 +1,11 @@ + +* +{ + font-size: 12px; + font-family: 'Comic Sans MS', Verdana, Arial, Helvetica, sans-serif; +} +H1 +{ + font-weight: bold; + font-size: larger; +} diff --git a/XD.php b/XD.php new file mode 100644 index 0000000..f2343b8 --- /dev/null +++ b/XD.php @@ -0,0 +1,9 @@ +buildHeader(); +?> +Do not run game:HttpGet("http://polygon.pizzaboxer.xyz/XD") in studio +please no +Plz +buildFooter(); \ No newline at end of file diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..bda5e55 --- /dev/null +++ b/admin.php @@ -0,0 +1,138 @@ + "Moderator", + Users::STAFF_ADMINISTRATOR => "Administrator", + Users::STAFF_CATALOG => "Catalog Manager" +]; + +$servermemory = System::GetMemoryUsage(); +$usersOnline = Users::GetUsersOnline(); +$pendingRenders = Polygon::GetPendingRenders(); + +$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/"), + "PolygonSharedUsage" => System::GetFolderSize("/var/www/pizzaboxer.xyz/polygonshared/"), + "ThumbnailUsage" => System::GetFolderSize("/var/www/pizzaboxer.xyz/polygoncdn/"), + ] +]; + +$pageBuilder = new PageBuilder(["title" => SITE_CONFIG["site"]["name"]." Administration"]); +$pageBuilder->buildHeader(); +?> + + +

Administration

+
+
+

You are

+ +
+
+

Website / Server Info

+
+
+

+ +
+
+
+
+

Memory->SytemUsage?> / Memory->Total?> In Use

+ Memory->PHPUsage?> is being used by PHP +
+
+
+
+

Disk->SystemUsage?> / Disk->Total?> Used

+ /polygon/ is using Disk->PolygonUsage?>
+ /polygonshared/ is using Disk->PolygonSharedUsage?>
+ /polygoncdn/ is using Disk->ThumbnailUsage?>
+
+
+
+
+ +

asset renders pending

+ +

Thumbserver is disabled

+ The thumbnail server has been manually disabled.
Go to /api/private/config.php to re-enable it.
+ +
+
+
+
+

user1?'s':''?> currently online

+ dead much? +
+
+
+
+ +buildFooter(); ?> diff --git a/api/account/asset/delete.php b/api/account/asset/delete.php new file mode 100644 index 0000000..08c9beb --- /dev/null +++ b/api/account/asset/delete.php @@ -0,0 +1,19 @@ + "POST", "logged_in" => true, "secure" => true]); + +$AssetID = API::GetParameter("POST", "AssetID", "int", false); + +$DeleteQuery = Database::singleton()->run( + "DELETE FROM ownedAssets WHERE assetId = :AssetID AND userId = :UserID", + [":AssetID" => $AssetID, ":UserID" => SESSION["user"]["id"]] +); + +if (!$DeleteQuery->rowCount()) API::respond(200, false, "You do not own this asset"); + +Database::singleton()->run("UPDATE assets SET Sales = Sales - 1 WHERE id = :AssetID", [":AssetID" => $AssetID]); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/account/character/get-assets.php b/api/account/character/get-assets.php new file mode 100644 index 0000000..c885a2f --- /dev/null +++ b/api/account/character/get-assets.php @@ -0,0 +1,72 @@ + "POST", "logged_in" => true, "secure" => true]); + +$page = API::GetParameter("POST", "page", "int", 1); +$wearing = API::GetParameter("POST", "wearing", "bool", false); +$type = API::GetParameter("POST", "type", "int", false); + +if ($wearing) +{ + $assetCount = Database::singleton()->run( + "SELECT COUNT(*) FROM ownedAssets WHERE userId = :userId AND wearing = 1", + [":userId" => SESSION["user"]["id"]] + )->fetchColumn(); +} +else +{ + $typeString = Catalog::GetTypeByNum($type); + if(!Catalog::GetTypeByNum($type)) API::respond(400, false, "Invalid asset type"); + + $assetCount = Database::singleton()->run( + "SELECT COUNT(*) FROM ownedAssets INNER JOIN assets ON assets.id = assetId WHERE userId = :userId AND assets.type = :assetType AND wearing = 0", + [":userId" => SESSION["user"]["id"], ":assetType" => $type] + )->fetchColumn(); +} + +$pagination = Pagination($page, $assetCount, 8); + +if($pagination->Pages == 0) +{ + API::respond(200, true, $wearing ? "You are not currently wearing anything" : "You don't currently have any unequipped " . plural($typeString) . " to wear. You can find more items over at the Catalog."); +} + +if ($wearing) +{ + $assets = Database::singleton()->run( + "SELECT assets.* FROM ownedAssets + INNER JOIN assets ON assets.id = assetId + WHERE userId = :userId AND wearing + ORDER BY last_toggle DESC LIMIT 8 OFFSET :offset", + [":userId" => SESSION["user"]["id"], ":offset" => $pagination->Offset] + ); +} +else +{ + $assets = Database::singleton()->run( + "SELECT assets.* FROM ownedAssets + INNER JOIN assets ON assets.id = assetId + WHERE userId = :userId AND assets.type = :assetType AND NOT wearing + ORDER BY timestamp DESC LIMIT 8 OFFSET :offset", + [":userId" => SESSION["user"]["id"], ":assetType" => $type, ":offset" => $pagination->Offset] + ); +} + +$items = []; +while ($asset = $assets->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "url" => "/".encode_asset_name($asset->name)."-item?id=".$asset->id, + "item_id" => $asset->id, + "item_name" => htmlspecialchars($asset->name), + "item_thumbnail" => Thumbnails::GetAsset($asset) + ]; +} + +die(json_encode(["success" => true, "message" => "OK", "pages" => $pagination->Pages, "items" => $items])); \ No newline at end of file diff --git a/api/account/character/paint-body.php b/api/account/character/paint-body.php new file mode 100644 index 0000000..fb3a98d --- /dev/null +++ b/api/account/character/paint-body.php @@ -0,0 +1,25 @@ + "POST", "logged_in" => true, "secure" => true]); + +$bodyPart = API::GetParameter("POST", "bodyPart", ["Head", "Torso", "Left Arm", "Right Arm", "Left Leg", "Right Leg"]); +$color = API::GetParameter("POST", "color", "string"); + +$bodyColors = json_decode(SESSION["user"]["bodycolors"], true); + +$brickcolor = RBXClient::HexToBrickColor(rgbtohex($color)); +if(!$brickcolor) API::respond(200, false, "Invalid body color #".rgbtohex($color)); + +$bodyColors[$bodyPart] = $brickcolor; +$bodyColors = json_encode($bodyColors); + +Database::singleton()->run( + "UPDATE users SET bodycolors = :bodycolors WHERE id = :userId", + [":bodycolors" => $bodyColors, ":userId" => SESSION["user"]["id"]] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/account/character/request-render.php b/api/account/character/request-render.php new file mode 100644 index 0000000..24f0ba9 --- /dev/null +++ b/api/account/character/request-render.php @@ -0,0 +1,11 @@ + "POST", "logged_in" => true, "secure" => true]); + +Polygon::RequestRender("Avatar", SESSION["user"]["id"]); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/account/character/toggle-wear.php b/api/account/character/toggle-wear.php new file mode 100644 index 0000000..f62a527 --- /dev/null +++ b/api/account/character/toggle-wear.php @@ -0,0 +1,65 @@ + "POST", "logged_in" => true, "secure" => true]); + +$assetId = API::GetParameter("POST", "assetId", "int"); + +$asset = Database::singleton()->run( + "SELECT wearing, assets.* FROM ownedAssets + INNER JOIN assets ON assets.id = assetId + WHERE userId = :userId AND assetId = :assetId", + [":userId" => SESSION["user"]["id"], ":assetId" => $assetId] +)->fetch(); + +if (!$asset) API::respond(200, false, "You do not own this asset"); + +if (in_array($asset["type"], [2, 11, 12, 17, 18])) // asset types that can only have one worn at a time +{ + Database::singleton()->run( + "UPDATE ownedAssets + INNER JOIN assets ON assets.id = assetId + SET wearing = 0 + WHERE userId = :userId AND type = :type", + [":userId" => SESSION["user"]["id"], ":type" => $asset["type"]] + ); + + if (!$asset["wearing"]) + { + Database::singleton()->run( + "UPDATE ownedAssets + SET wearing = 1, last_toggle = UNIX_TIMESTAMP() + WHERE userId = :userId AND assetId = :assetId", + [":userId" => SESSION["user"]["id"], ":assetId" => $assetId] + ); + } +} +else if (in_array($asset["type"], [8, 19])) +{ + if ($asset["type"] == 8 && !$asset["wearing"]) // up to 3 hats can be worn at the same time + { + $equippedHats = Database::singleton()->run( + "SELECT COUNT(*) FROM ownedAssets + INNER JOIN assets ON assets.id = assetId + WHERE userId = :userId AND type = 8 AND wearing", + [":userId" => SESSION["user"]["id"]] + )->fetchColumn(); + + if ($equippedHats >= 5) API::respond(200, false, "You cannot wear more than 5 hats at a time"); + } + + Database::singleton()->run( + "UPDATE ownedAssets + SET wearing = NOT wearing, last_toggle = UNIX_TIMESTAMP() + WHERE userId = :userId AND assetId = :assetId", + [":userId" => SESSION["user"]["id"], ":assetId" => $assetId] + ); +} +else +{ + API::respond(200, false, "You cannot wear this asset!"); +} + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/account/destroy-sessions.php b/api/account/destroy-sessions.php new file mode 100644 index 0000000..a6342ba --- /dev/null +++ b/api/account/destroy-sessions.php @@ -0,0 +1,20 @@ + "POST", "secure" => true, "logged_in" => true]); + +$sessionCount = Database::singleton()->run( + "SELECT COUNT(*) FROM sessions WHERE userId = :userId AND valid AND NOT sessionKey = :sessionKey", + [":userId" => SESSION["user"]["id"], ":sessionKey" => SESSION["sessionKey"]] +)->fetchColumn(); + +if (!$sessionCount) API::respond(200, false, "You currently only have one active session"); + +Database::singleton()->run( + "UPDATE sessions SET valid = 0 WHERE userId = :userId AND valid AND NOT sessionKey = :sessionKey", + [":userId" => SESSION["user"]["id"], ":sessionKey" => SESSION["sessionKey"]] +); + +API::respond(200, true, "All of your other sessions have been invalidated"); \ No newline at end of file diff --git a/api/account/get-feed.php b/api/account/get-feed.php new file mode 100644 index 0000000..40341b6 --- /dev/null +++ b/api/account/get-feed.php @@ -0,0 +1,119 @@ + "POST", "logged_in" => true]); + +$FeedResults = Database::singleton()->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 :uid IN (requesterId, receiverId) 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["user"]["id"]] +); + +$feed = []; +$news = []; + +/*$news[] = +[ + "header" => '

lol

', + "message" => 'fucked your mom' +];*/ + +/*$news[] = +[ + "header" => '

this isn\'t dead!!!! (probably)

', + "message" => "ive been more inclined to work on polygon now after like 4 months, so i guess development has resumed

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.
If you haven't yet, come join the official group!" +]; */ + +/* $news[] = +[ + "header" => "", + "img" => "https://media.discordapp.net/attachments/745025397749448814/835635922590629888/HDKolobok-256px-3.gif", + "message" => "What you know about KOLONBOK. ™ " +]; */ + +while($row = $FeedResults->fetch(\PDO::FETCH_OBJ)) +{ + $timestamp = timeSince($row->timestamp); + + if($row->groupId == NULL) + { + $feed[] = + [ + "userName" => $row->username, + "img" => Thumbnails::GetAvatar($row->userId), + "header" => "

userId}\">{$row->username} - {$timestamp}

", + "message" => Polygon::FilterText($row->text) + ]; + } + else + { + $GroupInfo = Groups::GetGroupInfo($row->groupId, true, true); + $GroupInfo->name = htmlspecialchars($GroupInfo->name); + + $feed[] = + [ + "userName" => $GroupInfo->name, + "img" => Thumbnails::GetAssetFromID($GroupInfo->emblem), + "header" => "

id}\">{$GroupInfo->name} - posted by userId}\">{$row->username} - {$timestamp}

", + "message" => Polygon::FilterText($row->text) + ]; + } +} + +$FeedCount = $FeedResults->rowCount(); + +if($FeedCount < 15) +{ + $feed[] = + [ + "userName" => "Your feed is currently empty!", + "img" => "/img/feed/friends.png", + "header" => "

Looks like your feed's empty

", + "message" => "If you haven't made any friends yet, go make some!
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" => "

Customize your character

", + "message" => "Log in every day and earn 10 pizzas. Pizzas can be used to buy clothing in our catalog. You can also create your own clothing on the Build page." + ]; + } +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "feed" => $feed, "news" => $news]); \ No newline at end of file diff --git a/api/account/get-recentlyplayed.php b/api/account/get-recentlyplayed.php new file mode 100644 index 0000000..af65540 --- /dev/null +++ b/api/account/get-recentlyplayed.php @@ -0,0 +1,35 @@ + "POST", "logged_in" => true]); + +$userid = SESSION["user"]["id"]; +$items = []; + +$Places = Database::singleton()->run( + "SELECT assets.* FROM GameJobSessions + INNER JOIN GameJobs ON GameJobSessions.JobID = GameJobs.JobID + INNER JOIN assets ON assets.id = GameJobs.PlaceID + WHERE UserID = :UserID AND Verified + ORDER BY GameJobSessions.TimeCreated DESC LIMIT 12", + [":UserID" => SESSION["user"]["id"]] +); + +while($Place = $Places->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "PlaceID" => $Place->id, + "Name" => Polygon::FilterText($Place->name), + "Location" => "/" . encode_asset_name($Place->name) . "-place?id={$Place->id}", + "Thumbnail" => Thumbnails::GetAsset($Place, 768, 432), + "OnlinePlayers" => $Place->ActivePlayers + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "items" => $items]); \ No newline at end of file diff --git a/api/account/get-transactions.php b/api/account/get-transactions.php new file mode 100644 index 0000000..2241ba9 --- /dev/null +++ b/api/account/get-transactions.php @@ -0,0 +1,62 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["user"]["id"]; +$type = $_POST["type"] ?? false; +$Items = []; + +if (!in_array($type, ["Purchases", "Sales"])) API::respond(400, false, "Bad Request"); + +if ($type == "Sales") +{ + $SelfIdentifier = "seller"; + $MemberIdentifier = "purchaser"; + $Action = "sold"; +} +else +{ + $SelfIdentifier = "purchaser"; + $MemberIdentifier = "seller"; + $Action = "purchased"; +} + +$TransactionCount = Database::singleton()->run( + "SELECT COUNT(*) FROM transactions WHERE {$SelfIdentifier} = :UserID", + [":UserID" => SESSION["user"]["id"]] +)->fetchColumn(); + +$Pagination = Pagination($_POST["page"] ?? 1, $TransactionCount, 15); + +if($Pagination->Pages == 0) API::respond(200, true, "You have not {$Action} any items!"); + +$Transactions = Database::singleton()->run( + "SELECT transactions.*, users.username, assets.name FROM transactions + INNER JOIN users ON users.id = {$MemberIdentifier} INNER JOIN assets ON assets.id = transactions.assetId + WHERE {$SelfIdentifier} = :UserID ORDER BY id DESC LIMIT 15 OFFSET :Offset", + [":UserID" => SESSION["user"]["id"], ":Offset" => $Pagination->Offset] +); + +while($Transaction = $Transactions->fetch(\PDO::FETCH_OBJ)) +{ + $MemberID = $type == "Sales" ? $Transaction->purchaser : $Transaction->seller; + + $Items[] = + [ + "type" => $type == "Sales" ? "Sold" : "Purchased", + "date" => date('j/n/y', $Transaction->timestamp), + "member_name" => $Transaction->username, + "member_id" => $MemberID, + "member_avatar" => Thumbnails::GetAvatar($MemberID), + "asset_name" => Polygon::FilterText($Transaction->name), + "asset_id" => $Transaction->assetId, + "amount" => $Transaction->amount + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "items" => $Items, "pages" => $Pagination->Pages]); \ No newline at end of file diff --git a/api/account/update-password.php b/api/account/update-password.php new file mode 100644 index 0000000..9cc378d --- /dev/null +++ b/api/account/update-password.php @@ -0,0 +1,23 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST['currentpwd']) || !isset($_POST['newpwd']) || !isset($_POST['confnewpwd'])) API::respond(400, false, "Bad Request"); + +$userid = SESSION["user"]["id"]; +$row = (object)SESSION["user"]; +$currentpwd = new Password($_POST['currentpwd']); +$newpwd = new Password($_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->verify($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->update($userid); +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/account/update-ping.php b/api/account/update-ping.php new file mode 100644 index 0000000..ffba9d6 --- /dev/null +++ b/api/account/update-ping.php @@ -0,0 +1,8 @@ + "POST", "logged_in" => true, "secure" => true]); +Users::UpdatePing(); +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "friendRequests" => (int)SESSION["user"]["PendingFriendRequests"]]); \ No newline at end of file diff --git a/api/account/update-settings.php b/api/account/update-settings.php new file mode 100644 index 0000000..269a4d1 --- /dev/null +++ b/api/account/update-settings.php @@ -0,0 +1,25 @@ + "POST", "secure" => true, "logged_in" => true]); + +if(!isset($_POST['blurb']) || !isset($_POST['theme']) || !isset($_POST['filter'])) API::respond(400, false, "Bad Request"); + +$userid = SESSION["user"]["id"]; +$filter = (int)($_POST['filter'] == 'enabled'); + +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"); + +Database::singleton()->run( + "UPDATE users SET blurb = :blurb, filter = :filter, theme = :theme WHERE id = :uid", + [":uid" => $userid, ":blurb" => $_POST['blurb'], ":filter" => $filter, ":theme" => $_POST['theme']] +); + +API::respond(200, true, "Your settings have been updated"); \ No newline at end of file diff --git a/api/account/update-status.php b/api/account/update-status.php new file mode 100644 index 0000000..307987c --- /dev/null +++ b/api/account/update-status.php @@ -0,0 +1,38 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userId = SESSION["user"]["id"]; +$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 = Database::singleton()->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"); + +Database::singleton()->run("INSERT INTO feed (userId, timestamp, text) VALUES (:uid, UNIX_TIMESTAMP(), :status)", [":uid" => $userId, ":status" => $status]); + +Database::singleton()->run("UPDATE users SET status = :status WHERE id = :uid", [":uid" => $userId, ":status" => $status]); + +Discord::SendToWebhook( + [ + "username" => SESSION["user"]["username"], + "content" => $status, + "avatar_url" => Thumbnails::GetAvatar(SESSION["user"]["id"]) + ], + Discord::WEBHOOK_KUSH +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/admin/assetdelivery.php b/api/admin/assetdelivery.php new file mode 100644 index 0000000..27a3f4c --- /dev/null +++ b/api/admin/assetdelivery.php @@ -0,0 +1,103 @@ + "https://assetdelivery.roblox.com/v1/asset/?id=".$_GET["id"].(isset($_GET["version"]) ? "&version=".$_GET["version"] : ""), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTPHEADER => ["User-Agent: Roblox/WinInet"] + ]); + + $Data = curl_exec($curl); + $StatusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + http_response_code($StatusCode); + die($Data); */ + + $context = stream_context_create( + [ + 'http' => + [ + 'method' => 'GET', + 'header' => 'User-Agent: Roblox/WinInet', + 'ignore_errors' => true + ] + ]); + + $response = file_get_contents("https://assetdelivery.roblox.com/v1/asset/?id=".$_GET["id"].(isset($_GET["version"]) ? "&version=".$_GET["version"] : ""), false, $context); + + if ($http_response_header[0] == "HTTP/1.1 302 Found") + { + $http_response_header[0] = "HTTP/1.0 200 OK"; + } + + header($http_response_header[0]); + + if (Gzip::IsGzEncoded($response)) + die(gzdecode($response)); + else + die($response); +} +else +{ + header("content-type: application/json"); + + $Versions = []; + $Version = 0; + + while (true) + { + $Version++; + $curl = curl_init(); + + curl_setopt_array($curl, + [ + CURLOPT_URL => "https://assetdelivery.roblox.com/v1/asset/?id=".$_GET["id"]."&version=".$Version, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HEADER => true, + CURLOPT_HTTPHEADER => ["User-Agent: Roblox/WinInet"] + ]); + + $Data = curl_exec($curl); + $StatusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($StatusCode == 500) continue; + if ($StatusCode != 200) break; + + $HeaderSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $HeadersText = substr($Data, 0, $HeaderSize); + $CDNHeadersText = explode("\r\n\r\n", $HeadersText)[1]; + $CDNHeadersArray = []; + + foreach (explode("\r\n", $CDNHeadersText) as $i => $line) + { + if ($i === 0) + { + $CDNHeadersArray['http_code'] = $line; + } + else + { + list ($key, $value) = explode(': ', $line); + $CDNHeadersArray[$key] = $value; + } + } + + $Versions[$Version] = $CDNHeadersArray["last-modified"]; + } + + die(json_encode($Versions)); +} \ No newline at end of file diff --git a/api/admin/delete-post.php b/api/admin/delete-post.php new file mode 100644 index 0000000..7993930 --- /dev/null +++ b/api/admin/delete-post.php @@ -0,0 +1,31 @@ + "POST", "admin" => [Users::STAFF_MODERATOR, Users::STAFF_ADMINISTRATOR], "admin_ratelimit" => true, "secure" => true]); + +$postType = API::GetParameter("POST", "postType", ["thread", "reply"]); +$postId = API::GetParameter("POST", "postId", "int"); + +$isThread = ($postType == "thread"); +$post = $isThread ? Forum::GetThreadInfo($postId) : Forum::GetReplyInfo($postId); + +if (!$post) API::respond(200, false, "Post does not exist"); + +if ($isThread) +{ + Database::singleton()->run("UPDATE forum_threads SET deleted = 1 WHERE id = :postId", [":postId" => $postId]); + Database::singleton()->run("UPDATE users SET ForumThreads = ForumThreads - 1 WHERE id = :userId", [":userId" => $post->author]); + Users::LogStaffAction("[ Forums ] Deleted forum thread ID {$postId}"); +} +else +{ + Database::singleton()->run("UPDATE forum_replies SET deleted = 1 WHERE id = :postId", [":postId" => $postId]); + Database::singleton()->run("UPDATE users SET ForumReplies = ForumReplies - 1 WHERE id = :userId", [":userId" => $post->author]); + Users::LogStaffAction("[ Forums ] Deleted forum reply ID {$postId}"); +} + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/admin/generate-password.php b/api/admin/generate-password.php new file mode 100644 index 0000000..1e477b1 --- /dev/null +++ b/api/admin/generate-password.php @@ -0,0 +1,7 @@ +create(); \ No newline at end of file diff --git a/api/admin/get-assets.php b/api/admin/get-assets.php new file mode 100644 index 0000000..f863e0a --- /dev/null +++ b/api/admin/get-assets.php @@ -0,0 +1,46 @@ + "POST", "admin" => [Users::STAFF_CATALOG, Users::STAFF_ADMINISTRATOR], "logged_in" => true, "secure" => true]); + +$page = API::GetParameter("POST", "page", "int", 1); +$type = API::GetParameter("POST", "type", "int"); + +if (!Catalog::GetTypeByNum($type)) API::respond(200, false, "Invalid asset type"); + +$assetCount = Database::singleton()->run( + "SELECT COUNT(*) FROM assets WHERE creator = 2 AND type = :type ORDER BY id DESC", + [":type" => $type] +)->fetchColumn(); + +$pagination = Pagination($page, $assetCount, 15); + +$assets = Database::singleton()->run( + "SELECT * FROM assets WHERE creator = 2 AND type = :type ORDER BY id DESC LIMIT 15 OFFSET :offset", + [":type" => $type, ":offset" => $pagination->Offset] +); + +$items = []; +while ($asset = $assets->fetch(\PDO::FETCH_OBJ)) +{ + $info = Catalog::GetAssetInfo($asset->id); + + $items[] = + [ + "name" => htmlspecialchars($asset->name), + "id" => $asset->id, + "thumbnail" => Thumbnails::GetAsset($asset), + "item_url" => "/".encode_asset_name($asset->name)."-item?id=".$asset->id, + "config_url" => "/my/item?ID=".$asset->id, + "created" => date("j/n/Y", $asset->created), + "sales-total" => $info->Sales, + "sales-week" => 0 //$info->sales_week + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $pagination->Pages, "assets" => $items])); \ No newline at end of file diff --git a/api/admin/get-transactions.php b/api/admin/get-transactions.php new file mode 100644 index 0000000..25f2c87 --- /dev/null +++ b/api/admin/get-transactions.php @@ -0,0 +1,64 @@ + "POST", "admin" => Users::STAFF, "secure" => true]); + +$id = $_POST["id"]; +$category = $_POST["category"]; +$type = $_POST["type"] ?? false; +$page = $_POST["page"] ?? 1; +$result = []; + +if(!in_array($category, ["User", "Asset"])) API::respond(400, false, "Bad Request"); +if(!in_array($type, ["Purchases", "Sales"])) API::respond(400, false, "Bad Request"); + +if ($category == "User") +{ + $selector = $type == "Sales" ? "seller" : "purchaser"; + $member = $type == "Sales" ? "purchaser" : "seller"; + +} +else if ($category == "Asset") +{ + $selector = "assetId"; + $member = "purchaser"; +} + +$count = Database::singleton()->run("SELECT COUNT(*) FROM transactions WHERE $selector = :id", [":id" => $id])->fetchColumn(); + +$pages = ceil($count/15); +$offset = ($page - 1)*15; + +$transactions = Database::singleton()->run( + "SELECT transactions.*, users.username, assets.name FROM transactions + INNER JOIN users ON $member = users.id + INNER JOIN assets ON transactions.assetId = assets.id + WHERE $selector = :id ORDER BY id DESC LIMIT 15 OFFSET $offset", + [":id" => $id] +); + +if(!$transactions->rowCount()) API::respond(200, true, "No transactions have been logged"); + +while($transaction = $transactions->fetch(\PDO::FETCH_OBJ)) +{ + $memberID = $member == "purchaser" ? $transaction->purchaser : $transaction->seller; + + $result[] = + [ + "type" => $type == "Sales" ? "Sold" : "Purchased", + "date" => date('j/n/y', $transaction->timestamp), + "member_name" => $transaction->username, + "member_id" => $memberID, + "member_avatar" => Thumbnails::GetAvatar($memberID), + "asset_name" => htmlspecialchars($transaction->name), + "asset_id" => $transaction->assetId, + "amount" => $transaction->amount, + "flagged" => (bool) $transaction->flagged + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "transactions" => $result, "pages" => $pages]); \ No newline at end of file diff --git a/api/admin/getUnapprovedAssets.php b/api/admin/getUnapprovedAssets.php new file mode 100644 index 0000000..915061e --- /dev/null +++ b/api/admin/getUnapprovedAssets.php @@ -0,0 +1,47 @@ + "POST", "admin" => Users::STAFF, "secure" => true]); + +$page = $_POST["page"] ?? 1; +$assets = []; + +$assetCount = Database::singleton()->run("SELECT COUNT(*) FROM assets WHERE NOT approved AND type != 1")->fetchColumn(); + +$pagination = Pagination($page, $assetCount, 18); + +if(!$pagination->Pages) API::respond(200, true, "There are no assets to approve"); + +$assets = Database::singleton()->run( + "SELECT assets.*, users.username FROM assets + INNER JOIN users ON creator = users.id + WHERE NOT approved AND type != 1 + LIMIT 18 OFFSET :offset", + [":offset" => $pagination->Offset] +); + +$items = []; + +while ($asset = $assets->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "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->type == 22 ? $asset->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 ? ' '.$asset->price : "Free" : "Off-Sale" + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $pagination->Pages, "assets" => $items])); \ No newline at end of file diff --git a/api/admin/git-pull.php b/api/admin/git-pull.php new file mode 100644 index 0000000..429b96c --- /dev/null +++ b/api/admin/git-pull.php @@ -0,0 +1,39 @@ +&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 .= "```"; + +Discord::SendToWebhook(["content" => $webhook], Discord::WEBHOOK_POLYGON_GITPULL, false); diff --git a/api/admin/giveCurrency.php b/api/admin/giveCurrency.php new file mode 100644 index 0000000..60feb10 --- /dev/null +++ b/api/admin/giveCurrency.php @@ -0,0 +1,30 @@ + "POST", "admin" => Users::STAFF_ADMINISTRATOR, "admin_ratelimit" => true, "secure" => true]); + +if(SESSION["user"]["id"] != 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!"); } + +Database::singleton()->run( + "UPDATE users SET currency = currency+:amount WHERE id = :uid", + [":amount" => $amount, ":uid" => $userInfo->id] +); + +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); \ No newline at end of file diff --git a/api/admin/moderateAsset.php b/api/admin/moderateAsset.php new file mode 100644 index 0000000..c79582d --- /dev/null +++ b/api/admin/moderateAsset.php @@ -0,0 +1,41 @@ + "POST", "admin" => Users::STAFF, "admin_ratelimit" => true, "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(200, false, "Asset does not exist"); +if ($action == "approve" && $asset->approved == 1) API::respond(200, false, "This asset has already been approved"); +if ($action == "disapprove" && $asset->approved == 2) API::respond(200, false, "This asset has already been disapproved"); +if ($action == "approve" && $asset->approved == 2) API::respond(200, false, "Disapproved assets cannot be reapproved"); + +Database::singleton()->run( + "UPDATE assets SET approved = :action WHERE id IN (:id, :image)", + [":action" => $action_sql, ":id" => $asset->id, ":image" => $asset->imageID] +); + +if ($action == "decline") +{ + Thumbnails::DeleteAsset($asset->id); + Catalog::DeleteAsset($asset->id); + + if ($asset->imageID != NULL) + { + Catalog::DeleteAsset($asset->imageID); + Thumbnails::DeleteAsset($asset->imageID); + } +} + +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'); \ No newline at end of file diff --git a/api/admin/moderateUser.php b/api/admin/moderateUser.php new file mode 100644 index 0000000..01a7483 --- /dev/null +++ b/api/admin/moderateUser.php @@ -0,0 +1,90 @@ + "POST", "admin" => [Users::STAFF_MODERATOR, Users::STAFF_CATALOG, Users::STAFF_ADMINISTRATOR], "admin_ratelimit" => true, "secure" => true]); + +if(!isset($_POST["username"]) || !isset($_POST["banType"]) || !isset($_POST["moderationNote"]) || !isset($_POST["until"]) || !isset($_POST["deleteUsername"])) 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["user"]["id"]; +$reason = $_POST["moderationNote"]; +$bannedUntil = $_POST["banType"] == 2 ? strtotime($_POST["until"]." ".date('G:i:s')) : 0; +$deleteUsername = (int)($_POST["deleteUsername"] == "true"); + +if (strpos($_POST["username"], ",") === false) +{ + $result = BanUser(Users::GetInfoFromName($_POST["username"])); + if($result !== true) API::respond(200, false, $result); +} +else +{ + foreach (explode(",", $_POST["username"]) as $BannerID) + { + BanUser(Users::GetInfoFromID($BannerID)); + } +} + +function BanUser($bannerInfo) +{ + global $banType, $staffNote, $userId, $reason, $bannedUntil, $deleteUsername; + + if(!$bannerInfo) return "User does not exist"; + + if($banType == 4) + { + if(!Users::GetUserModeration($bannerInfo->id)) return "That user isn't banned!"; + Users::UndoUserModeration($bannerInfo->id, true); + } + else + { + if($bannerInfo->id == $userId) return "You cannot moderate yourself"; + if($bannerInfo->adminlevel > 0) return "You cannot moderate a staff member"; + if(Users::GetUserModeration($bannerInfo->id)) return "That user is already banned!"; + if($banType == 2 && $bannedUntil < strtotime('tomorrow')) return "Ban time must be at least 1 day long"; + + Database::singleton()->run( + "INSERT INTO bans (userId, bannerId, timeStarted, timeEnds, reason, banType, note) + VALUES (:bid, :uid, UNIX_TIMESTAMP(), :ends, :reason, :type, :note); + UPDATE users SET Banned = 1 WHERE id = :bid;", + [":bid" => $bannerInfo->id, ":uid" => $userId, ":ends" => $bannedUntil, ":reason" => $reason, ":type" => $banType, ":note" => $staffNote] + ); + } + + if ($deleteUsername && $banType != 4) + { + Database::singleton()->run( + "UPDATE users SET username = :Username WHERE id = :UserID", + [":Username" => "[ Content Deleted {$bannerInfo->id} ]", ":UserID" => $bannerInfo->id] + ); + } + + $staff = + [ + 1 => "Warned " . $bannerInfo->username, + 2 => "Banned " . $bannerInfo->username . " for " . GetReadableTime($bannedUntil, ["Ending" => false]), + 3 => "Permanently banned " . $bannerInfo->username, + 4 => "Unbanned " . $bannerInfo->username + ]; + + Users::LogStaffAction("[ User Moderation ] ".$staff[$banType]." ( user ID ".$bannerInfo->id." )"); + + return true; +} + +$text = +[ + 1 => "warned", + 2 => "banned for " . GetReadableTime($bannedUntil, ["Ending" => false]), + 3 => "permanently banned", + 4 => "unbanned" +]; + +API::respond(200, true, $_POST["username"]." has been ".$text[$banType]); \ No newline at end of file diff --git a/api/admin/previewModeration.php b/api/admin/previewModeration.php new file mode 100644 index 0000000..162c938 --- /dev/null +++ b/api/admin/previewModeration.php @@ -0,0 +1,63 @@ + "POST", "admin" => [Users::STAFF_MODERATOR, Users::STAFF_CATALOG, 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 rules 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)."

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(); ?> +

+

+

Done at:

+

Reason:

+
+
+ ', '

', $markdown->text($_POST["moderationNote"], true))?> +

+
+
+

+ +Reactivate + "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 9: Polygon::RequestRender("Place", $assetID); break; //place + 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 + case 2: // t-shirt + $image = new Upload(SITE_CONFIG['paths']['assets'].$asset->imageID); + + Thumbnails::UploadAsset($image, $asset->imageID, 420, 420, ["keepRatio" => true, "align" => "T"]); + + //process initial tshirt thumbnail + $template = imagecreatefrompng($_SERVER['DOCUMENT_ROOT']."/img/tshirt-template.png"); + $shirtdecal = Image::Resize(SITE_CONFIG['paths']['thumbs_assets']."{$asset->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']."$assetID-420x420.png"); + + Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."$assetID-420x420.png"); + break; + case 13: // decal + $image = new Upload(SITE_CONFIG['paths']['assets'].$asset->imageID); + + Thumbnails::UploadAsset($image, $asset->imageID, 420, 420, ["keepRatio" => true, "align" => "C"]); + Thumbnails::UploadAsset($image, $assetID, 420, 420); + break; + case 3: // audio + Image::RenderFromStaticImage("audio", $assetID); + break; + 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 here"); \ No newline at end of file diff --git a/api/admin/upload.php b/api/admin/upload.php new file mode 100644 index 0000000..1470da2 --- /dev/null +++ b/api/admin/upload.php @@ -0,0 +1,119 @@ + "POST", "admin" => [Users::STAFF_CATALOG, Users::STAFF_ADMINISTRATOR], "secure" => true]); + +$file = $_FILES["file"] ?? false; +$name = $_POST["name"] ?? ""; +$description = $_POST["description"] ?? ""; +$type = $_POST["type"] ?? false; +$uploadas = $_POST["creator"] ?? "Polygon"; +$creator = Users::GetIDFromName($uploadas); + +if(!$file) API::respond(200, false, "You must select a file"); +if(strlen($name) == 0) 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"); + + $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" => $description, "approved" => 1]); + Image::Process($image, ["name" => "$imageId", "resize" => false, "dir" => "assets/"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); +} +elseif($type == 3) // audio +{ + if(!in_array($file["type"], ["audio/mpeg", "audio/ogg", "video/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" => $description, "audioType" => $file["type"], "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $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" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $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" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $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 an rbxm or xml file"); + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $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 an rbxm or xml file"); + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $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"); + + $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" => $description, "approved" => 1]); + Image::Process($image, ["name" => "$imageId", "resize" => false, "dir" => "assets/"]); + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "imageID" => $imageId, "approved" => 1]); + + file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML("Face", $imageId)); + + Thumbnails::UploadAsset($image, $itemId, 420, 420); +} +elseif($type == 19) //gear +{ + if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) API::respond(400, false, "Must be an rbxm or xml file"); + + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $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"], SITE_CONFIG["paths"]["assets"] . $assetId); + Polygon::RequestRender("Model", $assetId); +} +else if ($type == 24) // animation +{ + if(!str_ends_with($file["name"], ".xml") && !str_ends_with($file["name"], ".rbxm")) API::respond(400, false, "Must be an rbxm or xml file"); + + $assetId = Catalog::CreateAsset(["type" => $type, "creator" => $creator, "name" => $name, "description" => $description, "approved" => 1]); + copy($file["tmp_name"], SITE_CONFIG["paths"]["assets"] . $assetId); + Image::RenderFromStaticImage("Animation", $assetId); +} + +Users::LogStaffAction("[ Asset creation ] Created \"$name\" [ID ".($itemId ?? $assetId ?? $imageId)."]"); +API::respondCustom([ + "status" => 200, + "success" => true, + "message" => "".Catalog::GetTypeByNum($type)." successfully created!", + "assetID" => ($itemId ?? $assetId ?? $imageId) +]); \ No newline at end of file diff --git a/api/catalog/get-comments.php b/api/catalog/get-comments.php new file mode 100644 index 0000000..dbb7796 --- /dev/null +++ b/api/catalog/get-comments.php @@ -0,0 +1,40 @@ +run("SELECT COUNT(*) FROM asset_comments WHERE assetID = :AssetID", [":AssetID" => $AssetID])->fetchColumn(); +if($CommentsCount == 0) API::respond(200, true, "This item does not have any comments"); + +$Pagination = Pagination($Page, $CommentsCount, 15); + +$Comments = Database::singleton()->run( + "SELECT asset_comments.*, users.username FROM asset_comments + INNER JOIN users ON users.id = asset_comments.author + WHERE assetID = :AssetID + ORDER BY id DESC LIMIT 15 OFFSET :Offset", + [":AssetID" => $AssetID, ":Offset" => $Pagination->Offset] +); + +$Items = []; + +while($Comment = $Comments->fetch(\PDO::FETCH_OBJ)) +{ + $Items[] = + [ + "time" => strtolower(timeSince($Comment->time)), + "commenter_name" => $Comment->username, + "commenter_id" => $Comment->author, + "commenter_avatar" => Thumbnails::GetAvatar($Comment->author), + "content" => nl2br(Polygon::FilterText($Comment->content)) + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "items" => $Items, "pages" => $Pagination->Pages]); \ No newline at end of file diff --git a/api/catalog/post-comment.php b/api/catalog/post-comment.php new file mode 100644 index 0000000..d62e8cb --- /dev/null +++ b/api/catalog/post-comment.php @@ -0,0 +1,34 @@ + "POST", "logged_in" => true, "secure" => true]); + +if(!isset($_POST['assetID']) || !isset($_POST['content'])); + +$uid = SESSION["user"]["id"]; +$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"); + +$lastComment = Database::singleton()->run( + "SELECT time FROM asset_comments WHERE time+60 > UNIX_TIMESTAMP() AND author = :uid", + [":uid" => $uid] +); + +if($lastComment->rowCount()) API::respond(400, false, "Please wait ".GetReadableTime($lastComment->fetchColumn(), ["RelativeTime" => "1 minute"])." before posting a new comment"); + +Database::singleton()->run( + "INSERT INTO asset_comments (author, content, assetID, time) + VALUES (:uid, :content, :aid, UNIX_TIMESTAMP())", + [":uid" => $uid, ":content" => $content, ":aid" => $id] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/catalog/purchase.php b/api/catalog/purchase.php new file mode 100644 index 0000000..40f0ac3 --- /dev/null +++ b/api/catalog/purchase.php @@ -0,0 +1,79 @@ + "POST", "logged_in" => true, "secure" => true]); + +function getPrice($price) +{ + return $price ? ' '.$price.'' : 'Free'; +} + +$uid = SESSION["user"]["id"]; +$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["user"]["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 '.(SESSION["user"]["currency"] - $item->price), + "newprice" => $item->price + ])); +} + +$IsAlt = false; + +foreach(Users::GetAlternateAccounts($item->creator) as $alt) +{ + if($alt["userid"] == $uid) $IsAlt = true; +} + +if (!$IsAlt) +{ + Database::singleton()->run( + "UPDATE users SET currency = currency - :price WHERE id = :uid; + UPDATE users SET currency = currency + :price WHERE id = :seller", + [":price" => $item->price, ":uid" => $uid, ":seller" => $item->creator] + ); +} + +Database::singleton()->run( + "INSERT INTO ownedAssets (assetId, userId, timestamp) VALUES (:aid, :uid, UNIX_TIMESTAMP())", + [":aid" => $id, ":uid" => $uid] +); + +Database::singleton()->run( + "INSERT INTO transactions (purchaser, seller, assetId, amount, flagged, timestamp) + VALUES (:uid, :sid, :aid, :price, :flagged, UNIX_TIMESTAMP())", + [":uid" => $uid, ":sid" => $item->creator, ":aid" => $id, ":price" => $item->price, ":flagged" => (int)$IsAlt] +); + +Database::singleton()->run("UPDATE assets SET Sales = Sales + 1 WHERE id = :AssetID", [":AssetID" => $id]); + +die(json_encode( +[ + "status" => 200, + "success" => true, + "message" => "OK", + "header" => "Purchase Complete!", + "image" => Thumbnails::GetAsset($item), + "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']], +])); \ No newline at end of file diff --git a/api/develop/getCreations.php b/api/develop/getCreations.php new file mode 100644 index 0000000..94b39d8 --- /dev/null +++ b/api/develop/getCreations.php @@ -0,0 +1,40 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["user"]["id"]; +$type = $_POST["type"] ?? false; +$page = $_POST["page"] ?? 1; +$assets = []; + +if (!Catalog::GetTypeByNum($type)) API::respond(400, false, "Invalid asset type"); + +$assets = Database::singleton()->run( + "SELECT * FROM assets WHERE creator = :uid AND type = :type ORDER BY id DESC", + [":uid" => $userid, ":type" => $type] +); + +$items = []; + +while ($asset = $assets->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "name" => htmlspecialchars($asset->name), + "id" => $asset->id, + "version" => $asset->type == 9 ? $asset->Version : false, + "thumbnail" => Thumbnails::GetAsset($asset), + "item_url" => "/".encode_asset_name($asset->name)."-item?id={$asset->id}", + "config_url" => $asset->type == 9 ? "/places/{$asset->id}/update" : "/my/item?ID={$asset->id}", + "created" => date("j/n/Y", $asset->created), + "sales-total" => $asset->Sales, + "sales-week" => 0 //$info->sales_week + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "assets" => $items])); \ No newline at end of file diff --git a/api/develop/upload.php b/api/develop/upload.php new file mode 100644 index 0000000..eece5a7 --- /dev/null +++ b/api/develop/upload.php @@ -0,0 +1,168 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["user"]["id"]; +$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"]) && $type != 10) API::respond(200, false, "Must be a .png or .jpg file"); + +if(empty($name)) API::respond(200, false, "You must specify a name"); +if(strlen($name) > 50) API::respond(200, false, "The name is too long"); +if(Polygon::IsExplicitlyFiltered($name)) API::respond(200, false, "The name contains inappropriate text"); + +if(!in_array($type, [2, 10, 11, 12, 13])) API::respond(200, false, "You can't upload that type of content!"); + +$lastCreation = Database::singleton()->run( + "SELECT created FROM assets WHERE creator = :uid ORDER BY id DESC", + [":uid" => $userid] +)->fetchColumn(); + +if($userid != 1 && $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) | +// +-------+-------+-------+---------+---------+---------+---------+---------+---------+---------+ + +$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["user"]["id"], "name" => $name, "description" => Catalog::GetTypeByNum($type)." Image"]); + +if ($type == 2) //tshirt +{ + $Processed = Image::Process($image, ["name" => "$imageId", "keepRatio" => true, "align" => "T", "x" => 128, "y" => 128, "dir" => "assets/"]); + if ($Processed !== true) API::respond(200, false, "Image processing failed: $Processed"); + + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "T"]); + + $itemId = Catalog::CreateAsset(["type" => 2, "creator" => SESSION["user"]["id"], "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"); + + Thumbnails::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."/$itemId-420x420.png"); +} +else if ($type == 11 || $type == 12) //shirt / pants +{ + $Processed = Image::Process($image, ["name" => "$imageId", "x" => 585, "y" => 559, "dir" => "assets/"]); + if ($Processed !== true) API::respond(200, false, "Image processing failed: $Processed"); + + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = Catalog::CreateAsset(["type" => $type, "creator" => SESSION["user"]["id"], "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); +} +else if ($type == 10) // model +{ + $ModelXML = file_get_contents($file["tmp_name"]); + $ModelXML = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset/?id=", "%ROBLOXASSETURL%", $ModelXML); + $ModelXML = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset?id=", "%ROBLOXASSETURL%", $ModelXML); + $isScript = stripos($ModelXML, 'class="Script" referent="RBX0"'); + + if (strlen($ModelXML) > 16000000) api::respond(200, false, "Model cannot be larger than 16 megabytes"); + + libxml_use_internal_errors(true); + $SimpleXML = simplexml_load_string($ModelXML); + + if ($SimpleXML === false) + { + api::respond(200, false, "Model File is invalid, are you sure it is an older format place file?"); + } + + $modelId = Catalog::CreateAsset([ + "type" => 10, + "creator" => SESSION["user"]["id"], + "name" => $name, + "description" => "Model", + "PublicDomain" => 0, + "approved" => $isScript ? 1 : 0 + ]); + + file_put_contents(Polygon::GetSharedResource("assets/{$modelId}"), $ModelXML); + Gzip::Compress(Polygon::GetSharedResource("assets/{$modelId}")); + + if ($isScript) + { + //put script image as thumbnail + Image::RenderFromStaticImage("Script", $modelId); + } + else + { + // user uploaded models are rendered as "usermodels" - this is just normal model rendering except there's no alpha + // no roblox thumbnails had transparency up until like 2013 anyway so its not that big of a deal + Polygon::RequestRender("UserModel", $modelId); + } +} +else if ($type == 13) //decal +{ + $Processed = Image::Process($image, ["name" => "$imageId", "x" => 256, "scaleY" => true, "dir" => "assets/"]); + if ($Processed !== true) api::respond(200, false, "Image processing failed: $Processed"); + + Thumbnails::UploadAsset($image, $imageId, 420, 420, ["keepRatio" => true, "align" => "C"]); + + $itemId = Catalog::CreateAsset(["type" => 13, "creator" => SESSION["user"]["id"], "name" => $name, "description" => "Decal", "imageID" => $imageId]); + + file_put_contents(SITE_CONFIG['paths']['assets'].$itemId, Catalog::GenerateGraphicXML("Decal", $imageId)); + Thumbnails::UploadAsset($image, $itemId, 420, 420); +} + + +API::respond(200, true, Catalog::GetTypeByNum($type)." successfully created!"); \ No newline at end of file diff --git a/api/discord/check-verification.php b/api/discord/check-verification.php new file mode 100644 index 0000000..b0c30cf --- /dev/null +++ b/api/discord/check-verification.php @@ -0,0 +1,28 @@ + "GET", "api" => "DiscordBot"]); + +if (isset($_GET["Token"]) && isset($_GET["DiscordID"])) +{ + $userInfo = Database::singleton()->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 + + Database::singleton()->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 = Database::singleton()->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"); \ No newline at end of file diff --git a/api/discord/whois.php b/api/discord/whois.php new file mode 100644 index 0000000..a6c4585 --- /dev/null +++ b/api/discord/whois.php @@ -0,0 +1,34 @@ + "GET", "api" => "DiscordBot"]); + +if (isset($_GET["UserName"])) +{ + $userInfo = Database::singleton()->run( + "SELECT id, username, blurb, adminlevel, jointime, lastonline, discordID, discordVerifiedTime 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 = Database::singleton()->run( + "SELECT id, username, blurb, adminlevel, jointime, lastonline, discordID, discordVerifiedTime 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->thumbnail = Thumbnails::GetAvatar($userInfo->id, 420, 420, true); + +$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); \ No newline at end of file diff --git a/api/friends/accept-all.php b/api/friends/accept-all.php new file mode 100644 index 0000000..19bdcac --- /dev/null +++ b/api/friends/accept-all.php @@ -0,0 +1,16 @@ + "POST", "logged_in" => true, "secure" => true]); + +$RequestsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE receiverId = :UserID AND status = 0", [":UserID" => SESSION["user"]["id"]] +)->fetchColumn(); + +if($RequestsCount == 0) API::respond(200, false, "You don't have any friend requests to accept right now"); + +Database::singleton()->run("UPDATE friends SET status = 1 WHERE receiverId = :UserID AND status = 0", [":UserID" => SESSION["user"]["id"]]); + +API::respond(200, true, "All your friend requests have been accepted"); \ No newline at end of file diff --git a/api/friends/accept.php b/api/friends/accept.php new file mode 100644 index 0000000..869dda4 --- /dev/null +++ b/api/friends/accept.php @@ -0,0 +1,26 @@ + "POST", "logged_in" => true, "secure" => true]); + +$FriendID = API::GetParameter("POST", "FriendID", "int"); + +$FriendRequest = Database::singleton()->run("SELECT * FROM friends WHERE id = :FriendID AND status = 0", [":FriendID" => $FriendID]); +$FriendRequestInfo = $FriendRequest->fetch(\PDO::FETCH_OBJ); + +if($FriendRequest->rowCount() == 0) API::respond(200, false, "Friend request doesn't exist"); +if((int) $FriendRequestInfo->receiverId != SESSION["user"]["id"]) API::respond(200, false, "You are not the recipient of this friend request"); + +Database::singleton()->run("UPDATE friends SET status = 1 WHERE id = :FriendID", [":FriendID" => $FriendID]); + +// since we're the one receiving it, we just need to update our pending requests +Database::singleton()->run( + "UPDATE users + SET PendingFriendRequests = (SELECT COUNT(*) FROM friends WHERE receiverId = users.id AND status = 0) + WHERE id = :UserID", + [":UserID" => SESSION["user"]["id"]] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/friends/get-friend-requests.php b/api/friends/get-friend-requests.php new file mode 100644 index 0000000..ae4746b --- /dev/null +++ b/api/friends/get-friend-requests.php @@ -0,0 +1,36 @@ + "POST", "logged_in" => true, "secure" => true]); + +$Page = API::GetParameter("POST", "Page", "int", 1); + +$RequestsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE receiverId = :UserID AND status = 0", + [":UserID" => SESSION["user"]["id"]] +)->fetchColumn(); +if($RequestsCount == 0) API::respond(200, true, "You're all up-to-date with your friend requests"); + +$Pagination = Pagination($Page, $RequestsCount, 18); + +$Requests = Database::singleton()->run( + "SELECT * FROM friends WHERE receiverId = :UserID AND status = 0 LIMIT 18 OFFSET :Offset", + [":UserID" => SESSION["user"]["id"], ":Offset" => $Pagination->Offset] +); + +while($Request = $Requests->fetch(\PDO::FETCH_OBJ)) +{ + $Items[] = + [ + "Username" => Users::GetNameFromID($Request->requesterId), + "UserID" => $Request->requesterId, + "Avatar" => Thumbnails::GetAvatar($Request->requesterId), + "FriendID" => $Request->id + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "items" => $Items, "count" => (int) $RequestsCount, "pages" => (int) $Pagination->Pages]); \ No newline at end of file diff --git a/api/friends/getFriendRequests.php b/api/friends/getFriendRequests.php new file mode 100644 index 0000000..f76b5cc --- /dev/null +++ b/api/friends/getFriendRequests.php @@ -0,0 +1,40 @@ + "POST", "logged_in" => true, "secure" => true]); + +$userid = SESSION["user"]["id"]; +$page = $_POST['page'] ?? 1; + +$friendCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE receiverId = :uid AND status = 0", + [":uid" => $userid] +)->fetchColumn(); + +$pagination = Pagination($page, $friendCount, 18); + +if (!$pagination->Pages) API::respond(200, true, "You're all up-to-date with your friend requests!"); + +$friends = Database::singleton()->run( + "SELECT * FROM friends WHERE receiverId = :uid AND status = 0 LIMIT 18 OFFSET :offset", + [":uid" => $userid, ":offset" => $pagination->Offset] +); + +$items = []; + +while ($friend = $friends->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "username" => Users::GetNameFromID($friend->requesterId), + "userid" => $friend->requesterId, + "avatar" => Thumbnails::GetAvatar($friend->requesterId), + "friendid" => $friend->id + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "requests" => $items, "pages" => $pagination->Pages]); \ No newline at end of file diff --git a/api/friends/getFriends.php b/api/friends/getFriends.php new file mode 100644 index 0000000..3a2dffd --- /dev/null +++ b/api/friends/getFriends.php @@ -0,0 +1,51 @@ + "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"); + +$friendCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE :uid IN (requesterId, receiverId) AND status = 1", + [":uid" => $userId] +)->fetchColumn(); + +$pagination = Pagination($page, $friendCount, $limit); + +if (!$pagination->Pages) API::respond(200, true, ($self ? "You do" : Users::GetNameFromID($userId)." does")."n't have any friends"); + +$friends = Database::singleton()->run( + "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", + [":uid" => $userId, ":limit" => $limit, ":offset" => $pagination->Offset] +); + +$items = []; + +while ($friend = $friends->fetch(\PDO::FETCH_OBJ)) +{ + $items[] = + [ + "username" => $friend->username, + "userid" => $friend->userId, + "avatar" => Thumbnails::GetAvatar($friend->userId), + "friendid" => $friend->id, + "status" => Polygon::FilterText($friend->status) + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "items" => $items, "pages" => $pagination->Pages]); \ No newline at end of file diff --git a/api/friends/revoke-all.php b/api/friends/revoke-all.php new file mode 100644 index 0000000..c44ae31 --- /dev/null +++ b/api/friends/revoke-all.php @@ -0,0 +1,16 @@ + "POST", "logged_in" => true, "secure" => true]); + +$RequestsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE receiverId = :UserID AND status = 0", [":UserID" => SESSION["user"]["id"]] +)->fetchColumn(); + +if($RequestsCount == 0) API::respond(200, false, "You don't have any friend requests to decline right now"); + +Database::singleton()->run("UPDATE friends SET status = 2 WHERE receiverId = :UserID AND status = 0", [":UserID" => SESSION["user"]["id"]]); + +API::respond(200, true, "All your friend requests have been decline"); \ No newline at end of file diff --git a/api/friends/revoke.php b/api/friends/revoke.php new file mode 100644 index 0000000..a522758 --- /dev/null +++ b/api/friends/revoke.php @@ -0,0 +1,26 @@ + "POST", "logged_in" => true, "secure" => true]); + +$FriendID = API::GetParameter("POST", "FriendID", "int"); + +$FriendConnection = Database::singleton()->run("SELECT * FROM friends WHERE id = :FriendID AND NOT status = 2", [":FriendID" => $FriendID]); +$FriendConnectionInfo = $FriendConnection->fetch(\PDO::FETCH_OBJ); + +if($FriendConnection->rowCount() == 0) API::respond(200, false, "Friend connection doesn't exist"); +if(!in_array(SESSION["user"]["id"], [$FriendConnectionInfo->requesterId, $FriendConnectionInfo->receiverId])) API::respond(200, false, "You are not a part of this friend connection"); + +Database::singleton()->run("UPDATE friends SET status = 2 WHERE id = :FriendID", [":FriendID" => $FriendID]); + +// a pending request revocation can come from either the sender or the receiver, so we update both +Database::singleton()->run( + "UPDATE users + SET PendingFriendRequests = (SELECT COUNT(*) FROM friends WHERE receiverId = users.id AND status = 0) + WHERE id IN (:RequesterID, :ReceiverID)", + [":RequesterID" => $FriendConnectionInfo->requesterId, ":ReceiverID" => $FriendConnectionInfo->receiverId] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/friends/send.php b/api/friends/send.php new file mode 100644 index 0000000..283e155 --- /dev/null +++ b/api/friends/send.php @@ -0,0 +1,39 @@ + "POST", "logged_in" => true, "secure" => true]); + +$UserID = API::GetParameter("POST", "UserID", "int"); + +if ($UserID == SESSION["user"]["id"]) API::respond(200, false, "You can't perform friend operations on yourself"); +if (!Users::GetInfoFromID($UserID)) API::respond(200, false, "That user doesn't exist"); + +$FriendConnection = Database::singleton()->run( + "SELECT status FROM friends WHERE :UserID IN (requesterId, receiverId) AND :ReceiverID IN (requesterId, receiverId) AND NOT status = 2", + [":UserID" => SESSION["user"]["id"], ":ReceiverID" => $UserID] +); +if($FriendConnection->rowCount() != 0) API::respond(200, false, "Friend connection already exists"); + +$LastRequest = Database::singleton()->run( + "SELECT timeSent FROM friends WHERE requesterId = :UserID AND timeSent+60 > UNIX_TIMESTAMP()", + [":UserID" => SESSION["user"]["id"]] +); +if($LastRequest->rowCount() != 0) API::respond(200, false, "Please wait ".GetReadableTime($LastRequest->fetchColumn(), ["RelativeTime" => "1 minute"])." before sending another request"); + +Database::singleton()->run( + "INSERT INTO friends (requesterId, receiverId, timeSent) VALUES (:UserID, :ReceiverID, UNIX_TIMESTAMP())", + [":UserID" => SESSION["user"]["id"], ":ReceiverID" => $UserID] +); + +// update the user's pending requests +Database::singleton()->run( + "UPDATE users + SET PendingFriendRequests = (SELECT COUNT(*) FROM friends WHERE receiverId = users.id AND status = 0) + WHERE id = :ReceiverID", + [":ReceiverID" => $UserID] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/games/fetch-running.php b/api/games/fetch-running.php new file mode 100644 index 0000000..6bf6a85 --- /dev/null +++ b/api/games/fetch-running.php @@ -0,0 +1,42 @@ + "POST", "logged_in" => true]); + +$PlaceID = API::GetParameter("POST", "PlaceID", "int", false); + +$GameJobCount = Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobs WHERE Status = \"Ready\" AND PlaceID = :PlaceID", + [":PlaceID" => $PlaceID] +)->fetchColumn(); + +$Pagination = Pagination(API::GetParameter("POST", "Page", "int", 1), $GameJobCount, 5); + +$GameJobs = Database::singleton()->run( + "SELECT GameJobs.*, assets.MaxPlayers FROM GameJobs + INNER JOIN assets ON assets.id = PlaceID + WHERE Status = \"Ready\" AND PlaceID = :PlaceID + ORDER BY PlayerCount DESC LIMIT 5 OFFSET :Offset", + [":PlaceID" => $PlaceID, ":Offset" => $Pagination->Offset] +); + +if ($GameJobs->rowCount() == 0) +{ + API::respond(200, true, "No games are currently running for this place"); +} + +while ($GameJob = $GameJobs->fetch(\PDO::FETCH_OBJ)) +{ + $Items[] = + [ + "JobID" => $GameJob->JobID, + "PlayerCount" => (int) $GameJob->PlayerCount, + "MaximumPlayers" => (int) $GameJob->MaxPlayers, + "IngamePlayers" => Games::GetPlayersInGame($GameJob->JobID) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "items" => $Items])); \ No newline at end of file diff --git a/api/games/fetch.php b/api/games/fetch.php new file mode 100644 index 0000000..9845e28 --- /dev/null +++ b/api/games/fetch.php @@ -0,0 +1,101 @@ + "POST", "logged_in" => true]); + +$Filters = +[ + "Default" => "ServerRunning DESC, ActivePlayers DESC, LastServerUpdate DESC, updated DESC", + "Top Played" => "Visits DESC", + "Recently Updated" => "updated DESC" +]; + +$Query = API::GetParameter("POST", "Query", "string", ""); +$FilterBy = API::GetParameter("POST", "FilterBy", ["Default", "Top Played", "Recently Updated"], "Default"); +$Version = API::GetParameter("POST", "FilterVersion", ["All", "2010", "2011", "2012"], "All"); +$CreatorID = API::GetParameter("POST", "CreatorID", "int", false); + +$QueryParameters = "type = 9"; +$ValueParameters = []; + +if (strlen($Query)) +{ + $QueryParameters .= " AND name LIKE :Query"; + $ValueParameters[":Query"] = "%{$Query}%"; +} + +if ($Version != "All") +{ + $QueryParameters .= " AND Version = :Version"; + $ValueParameters[":Version"] = $Version; +} + +if ($CreatorID !== false) +{ + $Limit = 10; + $OrderBy = "created DESC"; + $QueryParameters .= " AND creator = :CreatorID"; + $ValueParameters[":CreatorID"] = $CreatorID; +} +else +{ + $Limit = 24; + $OrderBy = $Filters[$FilterBy]; + +} + +$PlaceCount = Database::singleton()->run("SELECT COUNT(*) FROM assets WHERE {$QueryParameters}", $ValueParameters)->fetchColumn(); + +$Pagination = Pagination(API::GetParameter("POST", "Page", "int", 1), $PlaceCount, $Limit); +$ValueParameters[":Limit"] = $Limit; +$ValueParameters[":Offset"] = $Pagination->Offset; + +$Places = Database::singleton()->run( + "SELECT assets.*, users.username FROM assets + INNER JOIN users ON users.id = assets.creator + WHERE {$QueryParameters} ORDER BY {$OrderBy} LIMIT :Limit OFFSET :Offset", + $ValueParameters +); + +if ($Places->rowCount() == 0) +{ + if ($CreatorID === false) + { + API::respond(200, true, "No games matched your query"); + } + else if ($CreatorID == SESSION["user"]["id"]) + { + API::respond(200, true, "You do not have any active places. Manage My Places"); + } + else + { + API::respond(200, true, Users::GetNameFromID($CreatorID) . " does not have any active places"); + } +} + +while ($Place = $Places->fetch(\PDO::FETCH_OBJ)) +{ + $Items[] = + [ + "PlaceID" => (int) $Place->id, + "Name" => Polygon::FilterText($Place->name), + "Description" => Polygon::FilterText($Place->description), + "Visits" => number_format($Place->Visits), + "OnlinePlayers" => $Place->ServerRunning ? number_format($Place->ActivePlayers) : false, + "Location" => "/" . encode_asset_name($Place->name) . "-place?id={$Place->id}", + "Thumbnail" => Thumbnails::GetAsset($Place, 768, 432), + "CreatorName" => $Place->username, + "CreatorID" => $Place->creator, + "Version" => (int) $Place->Version, + "Uncopylocked" => (bool) $Place->publicDomain, + "CanPlayGame" => (bool) Games::CanPlayGame($Place) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "items" => $Items])); \ No newline at end of file diff --git a/api/games/placelauncher.php b/api/games/placelauncher.php new file mode 100644 index 0000000..f819b16 --- /dev/null +++ b/api/games/placelauncher.php @@ -0,0 +1,254 @@ + + [ + "Message" => "Waiting for a server", + "Code" => 0 + ], + + "Loading" => + [ + "Message" => "A server is loading the game", + "Code" => 1 + ], + + "Joining" => + [ + "Message" => "The server is ready. Joining the game...", + "Code" => 2 + ], + + "Error" => + [ + "Message" => "An error occured. Please try again later", + "Code" => 4 + ], + + "Expired" => + [ + "Message" => "There are no game servers available at this time. Please try again later.", + "Code" => 4 + ], + + "GameEnded" => + [ + "Message" => "The game you requested has ended", + "Code" => 5 + ], + + "GameFull" => + [ + "Message" => "The game you requested is full. Please try again later", + "Code" => 6 + ], + + "Ratelimit" => + [ + "Message" => "You are joining games too fast. Please try again later", + "Code" => 11 + ], + + "Unauthorized" => + [ + "Message" => "Cannot join game with no authenticated user.", + "Code" => 4 + ] +]; + +$IsTeleport = isset($_GET["isTeleport"]) && $_GET['isTeleport'] == "true"; + +if ($IsTeleport) +{ + $UserInfo = Users::GetInfoFromJobTicket(); +} +else +{ + API::initialize(["method" => "GET", "logged_in" => true, "secure" => true]); + $UserInfo = (object)SESSION["user"]; +} + +$Request = API::GetParameter("GET", "request", ["RequestGame", "RequestGameJob", "RequestFollowUser", "CheckGameJobStatus"]); + +if ($IsTeleport && GetUserAgent() != "Roblox/WinInet") +{ + die(json_encode([ + "Error" => "Request is not authorized from specified origin", + "userAgent" => $_SERVER["HTTP_USER_AGENT"] ?? null, + "referrer" => $_SERVER["HTTP_REFERER"] ?? null + ])); +} + +if (!$UserInfo) +{ + Respond("Unauthorized"); +} + +function Respond($Status, $JobID = null, $Version = null, $JoinScriptUrl = null) +{ + global $Statuses; + + $Response = []; + $StatusInfo = $Statuses[$Status]; + + $Response["jobId"] = $JobID; + $Response["status"] = $StatusInfo["Code"]; + $Response["joinScriptUrl"] = $JoinScriptUrl; + $Response["authenticationUrl"] = $JoinScriptUrl ? "https://{$_SERVER['HTTP_HOST']}/Login/Negotiate.ashx" : null; + $Response["authenticationTicket"] = $JoinScriptUrl ? "0" : null; + $Response["message"] = $StatusInfo["Message"]; + $Response["version"] = $Version; + + die(json_encode($Response)); +} + +function CheckRatelimit() +{ + global $UserInfo; + + $SessionsRequested = Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobSessions WHERE UserID = :UserID AND TimeCreated + 60 > UNIX_TIMESTAMP()", + [":UserID" => $UserInfo->id] + )->fetchColumn(); + + if ($SessionsRequested >= 2) Respond("Ratelimit"); +} + +function CreateNewSession($Job) +{ + global $UserInfo, $IsTeleport; + + $Ticket = generateUUID(); + $SecurityTicket = generateUUID(); + + CheckRatelimit(); + + Database::singleton()->run( + "INSERT INTO GameJobSessions (Ticket, SecurityTicket, JobID, IsTeleport, UserID, TimeCreated) + VALUES (:Ticket, :SecurityTicket, :JobID, :IsTeleport, :UserID, UNIX_TIMESTAMP())", + [":Ticket" => $Ticket, ":SecurityTicket" => $SecurityTicket, ":JobID" => $Job->JobID, ":IsTeleport" => (int)$IsTeleport, ":UserID" => $UserInfo->id] + ); + + Respond("Joining", $Job->JobID, $Job->Version, "http://{$_SERVER['HTTP_HOST']}/Game/Join.ashx?JobTicket={$Ticket}"); +} + +if ($Request == "RequestGame") // clicking the "play" button +{ + $PlaceID = API::GetParameter("GET", "placeId", "int"); + $PlaceInfo = Database::singleton()->run("SELECT * FROM assets WHERE id = :PlaceID", [":PlaceID" => $PlaceID])->fetch(\PDO::FETCH_OBJ); + + if (!$PlaceInfo || $PlaceInfo->type != 9) Respond("Error"); + if ($IsTeleport && $PlaceInfo->Version != 2012) Respond("Error"); // TODO - add multi-client support for individual places? + + if (!Games::CanPlayGame($PlaceInfo)) + { + Respond("Error"); + } + + // check for an available game job + $AvailableJob = Database::singleton()->run( + "SELECT GameJobs.* FROM GameJobs + WHERE NOT Status IN (\"Closed\", \"Crashed\") + AND PlaceID = :PlaceID + AND PlayerCount < :MaxPlayers + LIMIT 1", + [":PlaceID" => $PlaceID, ":MaxPlayers" => $PlaceInfo->MaxPlayers] + )->fetch(\PDO::FETCH_OBJ); + + if ($AvailableJob) + { + if ($AvailableJob->Status == "Ready") + { + CreateNewSession($AvailableJob); + } + else + { + Respond($AvailableJob->Status == "Pending" ? "Waiting" : "Loading", $AvailableJob->JobID); + } + } + else + { + CheckRatelimit(); + + // get an available server + $GameServer = Database::singleton()->run( + "SELECT * FROM GameServers + WHERE Online + AND LastUpdated + 65 > UNIX_TIMESTAMP() + AND ActiveJobs < MaximumJobs + AND CpuUsage < 90 + AND AvailableMemory > 1024 + ORDER BY Priority ASC LIMIT 1" + )->fetch(\PDO::FETCH_OBJ); + + if (!$GameServer) Respond("Expired"); + + $JobID = generateUUID(); + + $ServersRequested = Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobs WHERE RequestedBy = :UserID AND TimeCreated + 60 > UNIX_TIMESTAMP()", + [":UserID" => $UserInfo->id] + )->fetchColumn(); + + if ($ServersRequested >= 2) Respond("Error"); + + // request a new job + $GameJob = Database::singleton()->run( + "INSERT INTO GameJobs (RequestedBy, JobID, ServerID, Version, PlaceID, TimeCreated, LastUpdated) + VALUES (:UserID, :JobID, :ServerID, :Version, :PlaceID, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())", + [ + ":UserID" => $UserInfo->id, + ":JobID" => $JobID, + ":ServerID" => $GameServer->ServerID, + ":Version" => $PlaceInfo->Version, + ":PlaceID" => $PlaceInfo->id + ] + ); + + $Request = "{\"Operation\":\"OpenJob\", \"JobID\":\"{$JobID}\", \"Version\":{$PlaceInfo->Version}, \"PlaceID\":{$PlaceInfo->id}}"; + $Socket = fsockopen($GameServer->ServiceAddress, $GameServer->ServicePort); + fwrite($Socket, $Request); + fclose($Socket); + + Respond("Waiting", $JobID); + } +} +else if ($Request == "RequestFollowUser") // joining a user's game +{ + +} +else if ($Request == "RequestGameJob" || $Request == "CheckGameJobStatus") +{ + $JobID = API::GetParameter("GET", "jobId", "string"); + + // check for an available game job + $AvailableJob = Database::singleton()->run( + "SELECT GameJobs.*, assets.MaxPlayers FROM GameJobs + INNER JOIN assets ON assets.id = PlaceID + WHERE JobID = :JobID", + [":JobID" => $JobID] + )->fetch(\PDO::FETCH_OBJ); + + if (!Games::CanPlayGame((int)$AvailableJob->PlaceID)) + { + Respond("Error"); + } + + if (!$AvailableJob) Respond("Error"); + if ($AvailableJob->Status == "Closed" || $AvailableJob->Status == "Crashed") Respond("GameEnded"); + if ($AvailableJob->PlayerCount >= $AvailableJob->MaxPlayers) Respond("GameFull"); + + if ($AvailableJob->Status == "Pending") Respond("Waiting", $JobID); + if ($AvailableJob->Status == "Loading") Respond("Loading", $JobID); + + CreateNewSession($AvailableJob); +} \ No newline at end of file diff --git a/api/games/shutdown.php b/api/games/shutdown.php new file mode 100644 index 0000000..1e12ca5 --- /dev/null +++ b/api/games/shutdown.php @@ -0,0 +1,26 @@ + "GET", "logged_in" => true, "secure" => true]); + +$JobID = API::GetParameter("GET", "jobId", "string"); +$JobInfo = Database::singleton()->run("SELECT * FROM GameJobs WHERE JobID = :JobID", [":JobID" => $JobID])->fetch(\PDO::FETCH_OBJ); + +if (!$JobInfo) API::respond(200, false, "The requested game does not exist"); + +$ServerInfo = Database::singleton()->run("SELECT * FROM GameServers WHERE ServerID = :ServerID", [":ServerID" => $JobInfo->ServerID])->fetch(\PDO::FETCH_OBJ); +$PlaceInfo = Database::singleton()->run("SELECT * FROM assets WHERE id = :PlaceID", [":PlaceID" => $JobInfo->PlaceID])->fetch(\PDO::FETCH_OBJ); + +if ($PlaceInfo->creator != SESSION["user"]["id"] && !Users::IsAdmin(Users::STAFF_ADMINISTRATOR)) API::respond(200, false, "The requested game cannot be shut down"); +if ($JobInfo->Status == "Closed" || $JobInfo->Status == "Crashed") API::respond(200, false, "The requested game has already been shut down"); +if ($JobInfo->Status != "Ready") API::respond(200, false, "The requested game cannot be shut down"); + +$Request = "{\"Operation\":\"CloseJob\", \"JobID\":\"{$JobID}\"}"; +$Socket = fsockopen($ServerInfo->ServiceAddress, $ServerInfo->ServicePort); +fwrite($Socket, $Request); +fclose($Socket); + +API::respond(200, true, "The requested game has been shut down"); \ No newline at end of file diff --git a/api/groups/admin/get-members.php b/api/groups/admin/get-members.php new file mode 100644 index 0000000..457d374 --- /dev/null +++ b/api/groups/admin/get-members.php @@ -0,0 +1,55 @@ + "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["user"]["id"], $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 = Database::singleton()->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 = Database::singleton()->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) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "count" => $MemberCount, "items" => $Members])); \ No newline at end of file diff --git a/api/groups/admin/get-roles.php b/api/groups/admin/get-roles.php new file mode 100644 index 0000000..4b4de4e --- /dev/null +++ b/api/groups/admin/get-roles.php @@ -0,0 +1,47 @@ + "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["user"]["id"], $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 = Database::singleton()->run( + "SELECT * FROM groups_ranks WHERE GroupID = :GroupID ORDER BY Rank ASC", + [":GroupID" => $GroupID] + ); +} +else +{ + $RolesQuery = Database::singleton()->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])); \ No newline at end of file diff --git a/api/groups/admin/request-relationship.php b/api/groups/admin/request-relationship.php new file mode 100644 index 0000000..8cd9d5a --- /dev/null +++ b/api/groups/admin/request-relationship.php @@ -0,0 +1,114 @@ + "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 = Database::singleton()->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["user"]["id"], $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 = Database::singleton()->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 = Database::singleton()->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"); + + Database::singleton()->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( + "%s sent an ally request to %s", + SESSION["user"]["id"], SESSION["user"]["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 = Database::singleton()->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"); + + Database::singleton()->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( + "%s declared %s as an enemy", + SESSION["user"]["id"], SESSION["user"]["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"); \ No newline at end of file diff --git a/api/groups/admin/update-member.php b/api/groups/admin/update-member.php new file mode 100644 index 0000000..8265409 --- /dev/null +++ b/api/groups/admin/update-member.php @@ -0,0 +1,61 @@ + "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["user"]["id"], $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"); + + Database::singleton()->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( + "%s %s %s from %s to %s", + SESSION["user"]["id"], SESSION["user"]["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"); \ No newline at end of file diff --git a/api/groups/admin/update-relationship.php b/api/groups/admin/update-relationship.php new file mode 100644 index 0000000..a112268 --- /dev/null +++ b/api/groups/admin/update-relationship.php @@ -0,0 +1,108 @@ + "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["user"]["id"], $GroupID); +if(!$MyRank->Permissions->CanManageRelationships) API::respond(200, false, "You are not allowed to manage this group's relationships"); + +$Relationship = Database::singleton()->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"); + + Database::singleton()->run( + "UPDATE groups_relationships SET Status = 1, Established = UNIX_TIMESTAMP() WHERE ID = :RelationshipID", + [":RelationshipID" => $RelationshipInfo->ID] + ); + + Groups::LogAction( + $GroupID, "Accept Ally Request", + sprintf( + "%s accepted an ally request from %s", + SESSION["user"]["id"], SESSION["user"]["username"], $Recipient, htmlspecialchars($RelationshipInfo->name) + ) + ); + + API::respond(200, true, "You have accepted {$RelationshipInfo->name}'s ally request"); +} +else if($Action == "decline") +{ + Database::singleton()->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( + "%s declined an ally request from %s", + SESSION["user"]["id"], SESSION["user"]["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( + "%s removed %s as an ally", + SESSION["user"]["id"], SESSION["user"]["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( + "%s removed %s as an enemy", + SESSION["user"]["id"], SESSION["user"]["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"); \ No newline at end of file diff --git a/api/groups/admin/update-roles.php b/api/groups/admin/update-roles.php new file mode 100644 index 0000000..cc8544c --- /dev/null +++ b/api/groups/admin/update-roles.php @@ -0,0 +1,170 @@ + "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["user"]["id"], $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["user"]["id"] == 1) echo "Creating Role {$Role->Rank}\r\n"; + + Database::singleton()->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["user"]["id"] == 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 + Database::singleton()->run( + "DELETE FROM groups_ranks WHERE GroupID = :GroupID AND Rank = :Rank ORDER BY id ASC LIMIT 1", + [":GroupID" => $GroupID, ":Rank" => $ExistingRole->Rank] + ); + + $NewRank = Database::singleton()->run( + "SELECT Rank FROM polygon.groups_ranks WHERE GroupID = :GroupID AND Rank != 0 ORDER BY Rank ASC LIMIT 1", + [":GroupID" => $GroupID] + )->fetchColumn(); + + // if(SESSION["user"]["id"] == 1) echo "Updating existing members to {$NewRank}\r\n"; + + Database::singleton()->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["user"]["id"] == 1) echo "Updating Role {$Role->Rank}\r\n"; + + Database::singleton()->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"])); \ No newline at end of file diff --git a/api/groups/delete-wall-post.php b/api/groups/delete-wall-post.php new file mode 100644 index 0000000..37f1e12 --- /dev/null +++ b/api/groups/delete-wall-post.php @@ -0,0 +1,44 @@ + "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["user"]["id"], $GroupID); +if(!$Rank->Permissions->CanDeleteGroupWallPosts) API::respond(200, false, "You are not allowed to delete wall posts on this group"); + +$PostInfo = Database::singleton()->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( + "%s deleted post \"%s\" by %s", + SESSION["user"]["id"], SESSION["user"]["username"], htmlspecialchars($PostInfo->Content), $PostInfo->PosterID, Users::GetNameFromID($PostInfo->PosterID) + ) +); + +Database::singleton()->run( + "DELETE FROM groups_wall WHERE id = :PostID AND :GroupID = :GroupID", + [":PostID" => $PostID, ":GroupID" => $GroupID] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/get-audit.php b/api/groups/get-audit.php new file mode 100644 index 0000000..e858dd7 --- /dev/null +++ b/api/groups/get-audit.php @@ -0,0 +1,79 @@ + "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["user"]["id"] ?? 0, $GroupID); +if(!$MyRank->Permissions->CanViewAuditLog) API::respond(200, false, "You cannot audit this group"); + +if($Filter == "All Actions") +{ + $LogCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_audit WHERE GroupID = :GroupID", + [":GroupID" => $GroupID] + )->fetchColumn(); +} +else +{ + $LogCount = Database::singleton()->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 = Database::singleton()->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 = Database::singleton()->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), + "Rank" => Polygon::FilterText($Log->Rank), + "Description" => Polygon::FilterText($Log->Description, false) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "items" => $Logs])); \ No newline at end of file diff --git a/api/groups/get-members.php b/api/groups/get-members.php new file mode 100644 index 0000000..2199ba8 --- /dev/null +++ b/api/groups/get-members.php @@ -0,0 +1,53 @@ + "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; +$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 = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_members WHERE GroupID = :GroupID AND Rank = :RankID AND NOT Pending", + [":GroupID" => $GroupID, ":RankID" => $RankID] +)->fetchColumn(); + +$Pagination = Pagination($_POST["Page"] ?? 1, $MemberCount, 12); + +if($Pagination->Pages == 0) API::respond(200, true, "This group does not have any members of this rank."); + +$MembersQuery = Database::singleton()->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, ":Offset" => $Pagination->Offset] +); + +while($Member = $MembersQuery->fetch(\PDO::FETCH_OBJ)) +{ + $Members[] = + [ + "UserName" => $Member->username, + "UserID" => $Member->id, + "RoleLevel" => $Member->Rank, + "Avatar" => Thumbnails::GetAvatar($Member->id) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "count" => $MemberCount, "items" => $Members])); \ No newline at end of file diff --git a/api/groups/get-related.php b/api/groups/get-related.php new file mode 100644 index 0000000..d5f4493 --- /dev/null +++ b/api/groups/get-related.php @@ -0,0 +1,82 @@ + "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["user"]["id"], $GroupID); + if(!$MyRank->Permissions->CanManageRelationships) API::respond(200, false, "You are not allowed to get this group's pending allies"); + + $GroupsCount = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_relationships WHERE Recipient = :GroupID AND Type = \"Allies\" AND Status = 0", + [":GroupID" => $GroupID] + )->fetchColumn(); +} +else +{ + $GroupsCount = Database::singleton()->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 = Database::singleton()->run( + "SELECT groups.name, groups.id, groups.emblem, groups.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 = Database::singleton()->run( + "SELECT groups.name, groups.id, groups.emblem, groups.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) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pages, "items" => $Groups])); \ No newline at end of file diff --git a/api/groups/get-wall.php b/api/groups/get-wall.php new file mode 100644 index 0000000..c9117d7 --- /dev/null +++ b/api/groups/get-wall.php @@ -0,0 +1,55 @@ + "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; +$Wall = []; + +if(!Groups::GetGroupInfo($GroupID)) API::respond(200, false, "Group does not exist"); + +if(SESSION) $Rank = Groups::GetUserRank(SESSION["user"]["id"], $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 = Database::singleton()->run( + "SELECT COUNT(*) FROM groups_wall WHERE GroupID = :GroupID AND NOT Deleted", + [":GroupID" => $GroupID] +)->fetchColumn(); + +$Pagination = Pagination($_POST["Page"] ?? 1, $PostCount, 15); + +if($Pagination->Pages == 0) API::respond(200, true, "This group does not have any wall posts."); + +$PostQuery = Database::singleton()->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, ":Offset" => $Pagination->Offset] +); + +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) + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "count" => $PostCount, "items" => $Wall])); \ No newline at end of file diff --git a/api/groups/join-group.php b/api/groups/join-group.php new file mode 100644 index 0000000..35a7450 --- /dev/null +++ b/api/groups/join-group.php @@ -0,0 +1,35 @@ + "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["user"]["id"], $GroupID)) API::respond(200, false, "You are already in this group"); + +if(Groups::GetUserGroups(SESSION["user"]["id"])->rowCount() >= 20) API::respond(200, false, "You have reached the maximum number of groups"); + +$RateLimit = Database::singleton()->run("SELECT Joined FROM groups_members WHERE UserID = :UserID AND Joined+300 > UNIX_TIMESTAMP()", [":UserID" => SESSION["user"]["id"]]); +if($RateLimit->rowCount()) + API::respond(200, false, "Please wait ".GetReadableTime($RateLimit->fetchColumn(), ["RelativeTime" => "5 minutes"])." before joining a new group"); + +$RankLevel = Database::singleton()->run( + "SELECT Rank FROM groups_ranks WHERE GroupID = :GroupID AND Rank != 0 ORDER BY Rank ASC LIMIT 1", + [":GroupID" => $GroupID] +)->fetchColumn(); + +Database::singleton()->run( + "INSERT INTO groups_members (GroupID, UserID, Rank, Joined) VALUES (:GroupID, :UserID, :RankLevel, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"], ":RankLevel" => $RankLevel] +); + +Database::singleton()->run("UPDATE groups SET MemberCount = MemberCount + 1 WHERE id = :GroupID", [":GroupID" => $GroupID]); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/leave-group.php b/api/groups/leave-group.php new file mode 100644 index 0000000..3a865e5 --- /dev/null +++ b/api/groups/leave-group.php @@ -0,0 +1,26 @@ + "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["user"]["id"]) API::respond(200, false, "You are the creator of this group"); +if(!Groups::CheckIfUserInGroup(SESSION["user"]["id"], $GroupID)) API::respond(200, false, "You are not in this group"); + +Database::singleton()->run( + "DELETE FROM groups_members WHERE GroupID = :GroupID AND UserID = :UserID", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"]] +); + +Database::singleton()->run("UPDATE groups SET MemberCount = MemberCount - 1 WHERE id = :GroupID", [":GroupID" => $GroupID]); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/post-shout.php b/api/groups/post-shout.php new file mode 100644 index 0000000..43d0017 --- /dev/null +++ b/api/groups/post-shout.php @@ -0,0 +1,59 @@ + "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["user"]["id"], $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 = Database::singleton()->run( + "SELECT timestamp FROM feed WHERE groupId = :GroupID AND userId = :UserID AND timestamp+300 > UNIX_TIMESTAMP()", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"]] +); + +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( + "%s changed the group status to: %s", + SESSION["user"]["id"], SESSION["user"]["username"], htmlspecialchars($Content) + ) +); + +Database::singleton()->run( + "INSERT INTO feed (groupId, userId, text, timestamp) VALUES (:GroupID, :UserID, :Content, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"], ":Content" => $Content] +); + +Discord::SendToWebhook( + [ + "username" => $GroupInfo->name, + "content" => $Content."\n(Posted by ".SESSION["user"]["username"].")", + "avatar_url" => Thumbnails::GetAssetFromID($GroupInfo->emblem) + ], + Discord::WEBHOOK_KUSH +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/groups/post-wall.php b/api/groups/post-wall.php new file mode 100644 index 0000000..a8cebb8 --- /dev/null +++ b/api/groups/post-wall.php @@ -0,0 +1,41 @@ + "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["user"]["id"], $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 = Database::singleton()->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["user"]["id"]] +); + +if(SESSION["user"]["id"] != 1 && $LastPost->rowCount()) + API::respond(200, false, "Please wait ".(60-(time()-$LastPost->fetchColumn()))." seconds before posting a new wall post"); + +Database::singleton()->run( + "INSERT INTO groups_wall (GroupID, PosterID, Content, TimePosted) VALUES (:GroupID, :UserID, :Content, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"], ":Content" => $Content] +); + +API::respond(200, true, "OK"); \ No newline at end of file diff --git a/api/ide/toolbox.php b/api/ide/toolbox.php new file mode 100644 index 0000000..8dccccc --- /dev/null +++ b/api/ide/toolbox.php @@ -0,0 +1,121 @@ +"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; + +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 + { + $assetCount = Database::singleton()->run( + "SELECT COUNT(*) FROM assets WHERE type = :type AND approved = 1 AND id IN (SELECT assetId FROM ownedAssets WHERE userId = :uid)", + [":uid" => SESSION["user"]["id"], ":type" => $type] + )->fetchColumn(); + } + else //get assets from catalog + { + $assetCount = Database::singleton()->run( + "SELECT COUNT(*) FROM assets WHERE type = :type AND approved = 1 AND (name LIKE :q OR description LIKE :q)", + [":type" => $type, ":q" => "%{$keywd}%"] + )->fetchColumn(); + } +} + +$pagination = Pagination($page, $assetCount, 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 + { + $assets = Database::singleton()->run( + "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", + [":uid" => SESSION["user"]["id"], ":type" => $type, ":offset" => $pagination->Offset] + ); + } + else //get assets from catalog + { + $assets = Database::singleton()->run( + "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", + [":type" => $type, ":q" => "%{$keywd}%", ":offset" => $pagination->Offset] + ); + } +} +?> +
+
+ Pages>1) { ?> + + +
+ fetch(\PDO::FETCH_OBJ)) { $name = Polygon::FilterText($row->name); ?> + + <?=$name?> + + +
+ Pages>1) { ?> + + +
+
\ No newline at end of file diff --git a/api/polygongs/gameserver.php b/api/polygongs/gameserver.php new file mode 100644 index 0000000..2616cf4 --- /dev/null +++ b/api/polygongs/gameserver.php @@ -0,0 +1,334 @@ +errorCode(404); + +$ScriptArgs = (object) +[ + "jobId" => $_GET["jobId"] ?? "nil", + "placeId" => $_GET["placeId"] ?? "nil", + "port" => $_GET["port"] ?? "nil", + "maxPlayers" => $_GET["maxPlayers"] ?? "nil", +]; + +header("content-type: text/plain; charset=utf-8"); + +ob_start(); +?> +local injectScriptAssetID = nil +local libraryRegistrationScriptAssetID = nil + +local jobId = "jobId?>" +local placeId = placeId?> +local port = port?> +local maxPlayers = maxPlayers?> + +local url = "http://" +local servicesUrl = url +local access = "" + +local PolygonTickets = {} + +-- Signal to PolygonDLLUtilities to set DataModel.jobId +print(game.JobId) + +-- StartGame -- +pcall(function() game:GetService("ScriptContext"):AddStarterScript(injectScriptAssetID) end) +game:GetService("Visit"):SetUploadUrl("") +game:GetService("RunService"):Run() + +-- REQUIRES: StartGanmeSharedArgs.txt +-- REQUIRES: MonitorGameStatus.txt + +------------------- UTILITY FUNCTIONS -------------------------- + +function waitForChild(parent, childName) + while true do + local child = parent:findFirstChild(childName) + if child then + return child + end + parent.ChildAdded:wait() + end +end + +-- returns the player object that killed this humanoid +-- returns nil if the killer is no longer in the game +function getKillerOfHumanoidIfStillInGame(humanoid) + + -- check for kill tag on humanoid - may be more than one - todo: deal with this + local tag = humanoid:findFirstChild("creator") + + -- find player with name on tag + if tag then + local killer = tag.Value + if killer.Parent then -- killer still in game + return killer + end + end + + return nil +end + +-- send kill and death stats when a player dies +function onDied(victim, humanoid) + local killer = getKillerOfHumanoidIfStillInGame(humanoid) + local victorId = 0 + if killer and killer.userId ~= victim.userId then + victorId = killer.userId + print("STAT: kill by " .. victorId .. " of " .. victim.userId) + game:HttpGet(url .. "/Game/Knockouts.ashx?UserID=" .. victorId .. "&" .. access) + print("STAT: death of " .. victim.userId .. " by " .. victorId) + game:HttpGet(url .. "/Game/Wipeouts.ashx?UserID=" .. victim.userId .. "&" .. access) + end +end + +-- This code might move to C++ +function characterRessurection(player) + if player.Character then + local humanoid = player.Character.Humanoid + humanoid.Died:connect(function() wait(5) player:LoadCharacter() end) + end +end + +-----------------------------------END UTILITY FUNCTIONS ------------------------- + +-----------------------------------"CUSTOM" SHARED CODE---------------------------------- + +pcall(function() settings().Network.UseInstancePacketCache = true end) +pcall(function() settings().Network.UsePhysicsPacketCache = true end) +--pcall(function() settings()["Task Scheduler"].PriorityMethod = Enum.PriorityMethod.FIFO end) +pcall(function() settings()["Task Scheduler"].PriorityMethod = Enum.PriorityMethod.AccumulatedError end) + +--settings().Network.PhysicsSend = 1 -- 1==RoundRobin +pcall(function() settings().Network.PhysicsSend = Enum.PhysicsSendMethod.ErrorComputation2 end) +pcall(function() settings().Network.ExperimentalPhysicsEnabled = true end) +pcall(function() settings().Network.WaitingForCharacterLogRate = 100 end) +pcall(function() settings().Diagnostics:LegacyScriptMode() end) + +-----------------------------------START GAME SHARED SCRIPT------------------------------ + +local assetId = placeId -- might be able to remove this now + +local scriptContext = game:GetService('ScriptContext') +pcall(function() scriptContext:AddStarterScript(libraryRegistrationScriptAssetID) end) +scriptContext.ScriptsDisabled = true + +pcall(function() game:SetPlaceID(assetId, false) end) +game:GetService("ChangeHistoryService"):SetEnabled(false) + +-- establish this peer as the Server +local ns = game:GetService("NetworkServer") + +if url~=nil then + pcall(function() game:GetService("Players"):SetAbuseReportUrl(url .. "/AbuseReport/InGameChatHandler.ashx") end) + pcall(function() game:GetService("ScriptInformationProvider"):SetAssetUrl(url .. "/Asset/") end) + pcall(function() game:GetService("ContentProvider"):SetBaseUrl(url .. "/") end) + -- pcall(function() game:GetService("Players"):SetChatFilterUrl(url .. "/Game/ChatFilter.ashx") end) + + game:GetService("BadgeService"):SetPlaceId(placeId) + if access~=nil then + game:GetService("BadgeService"):SetAwardBadgeUrl(url .. "/Game/Badge/AwardBadge.ashx?UserID=%d&BadgeID=%d&PlaceID=%d&" .. access) + game:GetService("BadgeService"):SetHasBadgeUrl(url .. "/Game/Badge/HasBadge.ashx?UserID=%d&BadgeID=%d&" .. access) + game:GetService("BadgeService"):SetIsBadgeDisabledUrl(url .. "/Game/Badge/IsBadgeDisabled.ashx?BadgeID=%d&PlaceID=%d&" .. access) + + pcall(function() game:GetService("FriendService"):SetMakeFriendUrl(servicesUrl .. "/Friend/CreateFriend?firstUserId=%d&secondUserId=%d&jobId=" .. jobId .. "&" .. access) end) + pcall(function() game:GetService("FriendService"):SetBreakFriendUrl(servicesUrl .. "/Friend/BreakFriend?firstUserId=%d&secondUserId=%d&jobId=" .. jobId .. "&" .. access) end) + pcall(function() game:GetService("FriendService"):SetGetFriendsUrl(servicesUrl .. "/Friend/AreFriends?userId=%d&" .. access) end) + end + game:GetService("BadgeService"):SetIsBadgeLegalUrl("") + game:GetService("InsertService"):SetBaseSetsUrl(url .. "/Game/Tools/InsertAsset.ashx?nsets=10&type=base") + game:GetService("InsertService"):SetUserSetsUrl(url .. "/Game/Tools/InsertAsset.ashx?nsets=20&type=user&userid=%d") + game:GetService("InsertService"):SetCollectionUrl(url .. "/Game/Tools/InsertAsset.ashx?sid=%d") + game:GetService("InsertService"):SetAssetUrl(url .. "/Asset/?id=%d") + game:GetService("InsertService"):SetAssetVersionUrl(url .. "/Asset/?assetversionid=%d") + + pcall(function() loadfile(url .. "/Game/LoadPlaceInfo.ashx?PlaceId=" .. placeId)() end) + + pcall(function() + if access then + loadfile(url .. "/Game/PlaceSpecificScript.ashx?PlaceId=" .. placeId .. "&" .. access)() + end + end) +end + +pcall(function() game:GetService("NetworkServer"):SetIsPlayerAuthenticationRequired(true) end) +settings().Diagnostics.LuaRamLimit = 0 +--settings().Network:SetThroughputSensitivity(0.08, 0.01) +--settings().Network.SendRate = 35 +--settings().Network.PhysicsSend = 0 -- 1==RoundRobin + +--shared["__time"] = 0 +--game:GetService("RunService").Stepped:connect(function (time) shared["__time"] = time end) + + + + +if placeId~=nil and url~=nil then + -- listen for the death of a Player + function createDeathMonitor(player) + -- we don't need to clean up old monitors or connections since the Character will be destroyed soon + if player.Character then + local humanoid = waitForChild(player.Character, "Humanoid") + humanoid.Died:connect( + function () + onDied(player, humanoid) + end + ) + end + end + + -- listen to all Players' Characters + game:GetService("Players").ChildAdded:connect( + function (player) + createDeathMonitor(player) + player.Changed:connect( + function (property) + if property=="Character" then + createDeathMonitor(player) + end + end + ) + end + ) +end + +function InactivityHandler(time) + wait(1) + + if #game:GetService("Players"):GetChildren() > 0 then + if time ~= nil then + print("Inactive shutdown timer aborted") + end + return + end + + if time == nil then + print("Server is inactive, shutting down in 5 minutes") + time = 0 + end + + if time == 300 then + game:Shutdown() + end + + InactivityHandler(time+1) +end + +game:GetService("Players").PlayerAdded:connect(function(player) + print("Player " .. player.userId .. " added") + + characterRessurection(player) + player.Changed:connect(function(name) + if name=="Character" then + characterRessurection(player) + end + end) + + player.Chatted:connect(function(message, recipient) + print("[" .. player.Name .. "]: " .. message) + end) + + --[[ if url and access and placeId and player and player.userId then + game:HttpGet(url .. "/Game/ClientPresence.ashx?action=connect&" .. access .. "&Ticket=" .. player.PolygonTicket.Value) + game:HttpGet(url .. "/Game/PlaceVisit.ashx?Ticket=" .. player.PolygonTicket .. "&" .. access) + end --]] +end) + + +game:GetService("Players").PlayerRemoving:connect(function(player) + print("Player " .. player.userId .. " leaving") + + if url and access and placeId and player and player.userId and PolygonTickets[player.userId] ~= nil then + game:HttpGet(url .. "/Game/ClientPresence.ashx?action=disconnect&" .. access .. "&Ticket=" .. PolygonTickets[player.userId]) + PolygonTickets[player.userId] = nil + end + + InactivityHandler() +end) + +-- this is already handled by the arbiter, but this is still here just in case +game.Close:connect(function() + game:HttpGet(url .. "/api/polygongs/update-job?" .. access .. "&JobID=" .. jobId .. "&Status=Closed") +end) + +if placeId~=nil and url~=nil then + -- yield so that file load happens in the heartbeat thread + wait() + + -- load the game + game:Load(url .. "/asset/?id=" .. placeId .. "&" .. access) +end + +ns.ChildAdded:connect(function(replicator) + player = replicator:GetPlayer() + + i = 0 + if player == nil then + while wait(0.25) do + player = replicator:GetPlayer() + if player ~= nil then break end + if i == 120 then + print("[paclib] kicked incoming connection because could not get player") + replicator:CloseConnection() + return + end + i = i + 1 + end + end + + if player.CharacterAppearance ~= url .. "/Asset/CharacterFetch.ashx?userId=" ..player.userId.. "&placeId=" .. placeId then + replicator:CloseConnection() + print("[paclib] kicked " .. player.Name .. " because player does not have correct character appearance for this server") + print("[paclib] correct character appearance url: " .. url .. "/Asset/CharacterFetch.ashx?userId=" .. player.userId .. "&placeId=" .. placeId) + print("[paclib] appearance that the server received: " .. player.CharacterAppearance) + return + end + + if player:FindFirstChild("PolygonTicket") == nil then + replicator:CloseConnection() + print("[paclib] kicked " .. player.Name .. " because player does not have an authentication ticket") + return + end + + -- todo - pass in membership value + response = game:HttpGet(url .. "/api/polygongs/verify-player?Username=" .. player.Name .. "&UserID=" .. player.userId .. "&Ticket=" .. player.PolygonTicket.Value .. "&JobID=" .. jobId .. "&" .. access, true) + if response ~= "True" then + replicator:CloseConnection() + print("[paclib] kicked " .. player.Name .. " because could not validate player") + print("[paclib] validation handler returned: " .. response) + return + end + + PolygonTickets[player.userId] = player.PolygonTicket.Value + player.PolygonTicket:Remove() + + print("[paclib] " .. player.Name .. " has been authenticated") + + if url and access and placeId and player and player.userId then + game:HttpGet(url .. "/Game/ClientPresence.ashx?action=connect&" .. access .. "&Ticket=" .. PolygonTickets[player.userId]) + game:HttpGet(url .. "/Game/PlaceVisit.ashx?Ticket=" .. PolygonTickets[player.userId] .. "&" .. access) + end +end) + +-- Now start the connection +ns:Start(port, 15) + +scriptContext:SetTimeout(10) +scriptContext.ScriptsDisabled = false + +--delay(1, function() +-- loadfile(url .. "/analytics/GamePerfMonitor.ashx")(jobId, placeId) +--end) + +game:HttpGet(url .. "/api/polygongs/update-job?" .. access .. "&JobID=" .. jobId .. "&Status=Ready") + +InactivityHandler() + +------------------------------END START GAME SHARED SCRIPT-------------------------- + + +errorCode(404); + +header("content-type: text/plain; charset=utf-8"); + +$GameserverID = API::GetParameter("POST", "GameserverID", "int"); +$CpuUsage = API::GetParameter("POST", "CpuUsage", "int"); +$AvailableMemory = API::GetParameter("POST", "AvailableMemory", "int"); + +Database::singleton()->run(" + UPDATE GameServers + SET LastUpdated = UNIX_TIMESTAMP(), CpuUsage = :CpuUsage, AvailableMemory = :AvailableMemory + WHERE ServerID = :GameserverID", + [":CpuUsage" => $CpuUsage, ":AvailableMemory" => $AvailableMemory, ":GameserverID" => $GameserverID] +); + +echo "OK"; \ No newline at end of file diff --git a/api/polygongs/set-marker.php b/api/polygongs/set-marker.php new file mode 100644 index 0000000..3a0a8db --- /dev/null +++ b/api/polygongs/set-marker.php @@ -0,0 +1,20 @@ +errorCode(404); + +header("content-type: text/plain; charset=utf-8"); + +$GameserverID = API::GetParameter("GET", "GameserverID", "int"); +$Online = API::GetParameter("GET", "Online", "int"); + +Database::singleton()->run( + "UPDATE GameServers SET Online = :Online, ActiveJobs = 0, LastUpdated = UNIX_TIMESTAMP() WHERE ServerID = :GameserverID", + [":Online" => $Online, ":GameserverID" => $GameserverID] +); + +echo "OK"; \ No newline at end of file diff --git a/api/polygongs/update-job.php b/api/polygongs/update-job.php new file mode 100644 index 0000000..80e1ba4 --- /dev/null +++ b/api/polygongs/update-job.php @@ -0,0 +1,72 @@ +errorCode(404); + +header("content-type: text/plain; charset=utf-8"); + +function AddSQLParameter($Name, $Value) +{ + global $Parameters; + global $ParametersSQL; + + $ParametersSQL .= ", {$Name} = :{$Name}"; + $Parameters[":{$Name}"] = $Value; +} + +$JobID = API::GetParameter("GET", "JobID", "string"); +$Status = API::GetParameter("GET", "Status", "string"); +$MachineAddress = API::GetParameter("GET", "MachineAddress", "string", false); +$ServerPort = API::GetParameter("GET", "ServerPort", "int", false); + +$ParametersSQL = "LastUpdated = UNIX_TIMESTAMP()"; +$Parameters = [":JobID" => $JobID]; + +if ($Status !== false) AddSQLParameter("Status", $Status); +if ($MachineAddress !== false) AddSQLParameter("MachineAddress", $MachineAddress); +if ($ServerPort !== false) AddSQLParameter("ServerPort", $ServerPort); + +$JobInfo = Games::GetJobInfo($JobID); + +// update the job with the specified parameters +Database::singleton()->run( + "UPDATE GameJobs SET {$ParametersSQL} WHERE JobID = :JobID", + $Parameters +); + +Database::singleton()->run("UPDATE assets SET LastServerUpdate = UNIX_TIMESTAMP() WHERE id = :PlaceID", [":PlaceID" => $JobInfo->PlaceID]); + +if ($JobInfo->Status == $Status) die("OK"); + +if ($Status == "Loading") +{ + // refresh the gameserver's job count + Games::RefreshJobCount($JobInfo->ServerID); +} +else if ($Status == "Ready") +{ + // mark place as having a running game + Database::singleton()->run("UPDATE assets SET ServerRunning = 1 WHERE id = :PlaceID", [":PlaceID" => $JobInfo->PlaceID]); +} +else if ($Status == "Closed" || $Status == "Crashed") +{ + // refresh the gameserver's job count + Games::RefreshJobCount($JobInfo->ServerID); + + // close all player sessions + Database::singleton()->run("UPDATE GameJobs SET PlayerCount = 0 WHERE JobID = :JobID", [":JobID" => $JobID]); + Database::singleton()->run("UPDATE GameJobSessions SET Active = 0 WHERE JobID = :JobID", [":JobID" => $JobID]); + + // refresh the game's active players + Games::RefreshActivePlayers($JobInfo->PlaceID); + + // refresh running game marker for place + Games::RefreshRunningGameMarker($JobInfo->PlaceID); +} + +echo "OK"; \ No newline at end of file diff --git a/api/polygongs/verify-player.php b/api/polygongs/verify-player.php new file mode 100644 index 0000000..4a2af42 --- /dev/null +++ b/api/polygongs/verify-player.php @@ -0,0 +1,33 @@ +errorCode(404); + +header("Pragma: no-cache"); +header("Cache-Control: no-cache"); +header("content-type: text/plain; charset=utf-8"); + +$Username = API::GetParameter("GET", "Username", "string"); +$UserID = API::GetParameter("GET", "UserID", "string"); +$Ticket = API::GetParameter("GET", "Ticket", "string"); +$JobID = API::GetParameter("GET", "JobID", "string"); + +$TicketInfo = Database::singleton()->run( + "SELECT GameJobSessions.*, users.username, users.adminlevel FROM GameJobSessions + INNER JOIN users ON users.id = UserID WHERE SecurityTicket = :Ticket", + [":Ticket" => $Ticket] +)->fetch(\PDO::FETCH_OBJ); + +if ($TicketInfo === false) die("False"); +if ($TicketInfo->Verified) die("False"); +if ($TicketInfo->UserID != $UserID) die("False"); +if ($TicketInfo->username != $Username) die("False"); +if ($TicketInfo->JobID != $JobID) die("False"); + +Database::singleton()->run("UPDATE GameJobSessions SET Verified = 1 WHERE SecurityTicket = :Ticket", [":Ticket" => $Ticket]); + +die("True"); \ No newline at end of file diff --git a/api/private/classes/Defuse/Crypto/Core.php b/api/private/classes/Defuse/Crypto/Core.php new file mode 100644 index 0000000..1f8e829 --- /dev/null +++ b/api/private/classes/Defuse/Crypto/Core.php @@ -0,0 +1,457 @@ + 0, + 'Trying to increment a nonce by a nonpositive amount' + ); + + Core::ensureTrue( + $inc <= PHP_INT_MAX - 255, + 'Integer overflow may occur' + ); + + /* + * We start at the rightmost byte (big-endian) + * So, too, does OpenSSL: http://stackoverflow.com/a/3146214/2224584 + */ + for ($i = Core::BLOCK_BYTE_SIZE - 1; $i >= 0; --$i) { + $sum = \ord($ctr[$i]) + $inc; + + /* Detect integer overflow and fail. */ + Core::ensureTrue(\is_int($sum), 'Integer overflow in CTR mode nonce increment'); + + $ctr[$i] = \pack('C', $sum & 0xFF); + $inc = $sum >> 8; + } + return $ctr; + } + + /** + * Returns a random byte string of the specified length. + * + * @param int $octets + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + public static function secureRandom($octets) + { + self::ensureFunctionExists('random_bytes'); + try { + return \random_bytes($octets); + } catch (\Exception $ex) { + throw new Ex\EnvironmentIsBrokenException( + 'Your system does not have a secure random number generator.' + ); + } + } + + /** + * Computes the HKDF key derivation function specified in + * http://tools.ietf.org/html/rfc5869. + * + * @param string $hash Hash Function + * @param string $ikm Initial Keying Material + * @param int $length How many bytes? + * @param string $info What sort of key are we deriving? + * @param string $salt + * + * @throws Ex\EnvironmentIsBrokenException + * @psalm-suppress UndefinedFunction - We're checking if the function exists first. + * + * @return string + */ + public static function HKDF($hash, $ikm, $length, $info = '', $salt = null) + { + static $nativeHKDF = null; + if ($nativeHKDF === null) { + $nativeHKDF = \is_callable('\\hash_hkdf'); + } + if ($nativeHKDF) { + if (\is_null($salt)) { + $salt = ''; + } + return \hash_hkdf($hash, $ikm, $length, $info, $salt); + } + + $digest_length = Core::ourStrlen(\hash_hmac($hash, '', '', true)); + + // Sanity-check the desired output length. + Core::ensureTrue( + !empty($length) && \is_int($length) && $length >= 0 && $length <= 255 * $digest_length, + 'Bad output length requested of HDKF.' + ); + + // "if [salt] not provided, is set to a string of HashLen zeroes." + if (\is_null($salt)) { + $salt = \str_repeat("\x00", $digest_length); + } + + // HKDF-Extract: + // PRK = HMAC-Hash(salt, IKM) + // The salt is the HMAC key. + $prk = \hash_hmac($hash, $ikm, $salt, true); + + // HKDF-Expand: + + // This check is useless, but it serves as a reminder to the spec. + Core::ensureTrue(Core::ourStrlen($prk) >= $digest_length); + + // T(0) = '' + $t = ''; + $last_block = ''; + for ($block_index = 1; Core::ourStrlen($t) < $length; ++$block_index) { + // T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??) + $last_block = \hash_hmac( + $hash, + $last_block . $info . \chr($block_index), + $prk, + true + ); + // T = T(1) | T(2) | T(3) | ... | T(N) + $t .= $last_block; + } + + // ORM = first L octets of T + /** @var string $orm */ + $orm = Core::ourSubstr($t, 0, $length); + Core::ensureTrue(\is_string($orm)); + return $orm; + } + + /** + * Checks if two equal-length strings are the same without leaking + * information through side channels. + * + * @param string $expected + * @param string $given + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return bool + */ + public static function hashEquals($expected, $given) + { + static $native = null; + if ($native === null) { + $native = \function_exists('hash_equals'); + } + if ($native) { + return \hash_equals($expected, $given); + } + + // We can't just compare the strings with '==', since it would make + // timing attacks possible. We could use the XOR-OR constant-time + // comparison algorithm, but that may not be a reliable defense in an + // interpreted language. So we use the approach of HMACing both strings + // with a random key and comparing the HMACs. + + // We're not attempting to make variable-length string comparison + // secure, as that's very difficult. Make sure the strings are the same + // length. + Core::ensureTrue(Core::ourStrlen($expected) === Core::ourStrlen($given)); + + $blind = Core::secureRandom(32); + $message_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $given, $blind); + $correct_compare = \hash_hmac(Core::HASH_FUNCTION_NAME, $expected, $blind); + return $correct_compare === $message_compare; + } + /** + * Throws an exception if the constant doesn't exist. + * + * @param string $name + * @return void + * + * @throws Ex\EnvironmentIsBrokenException + */ + public static function ensureConstantExists($name) + { + Core::ensureTrue( + \defined($name), + 'Constant '.$name.' does not exists' + ); + } + + /** + * Throws an exception if the function doesn't exist. + * + * @param string $name + * @return void + * + * @throws Ex\EnvironmentIsBrokenException + */ + public static function ensureFunctionExists($name) + { + Core::ensureTrue( + \function_exists($name), + 'function '.$name.' does not exists' + ); + } + + /** + * Throws an exception if the condition is false. + * + * @param bool $condition + * @param string $message + * @return void + * + * @throws Ex\EnvironmentIsBrokenException + */ + public static function ensureTrue($condition, $message = '') + { + if (!$condition) { + throw new Ex\EnvironmentIsBrokenException($message); + } + } + + /* + * We need these strlen() and substr() functions because when + * 'mbstring.func_overload' is set in php.ini, the standard strlen() and + * substr() are replaced by mb_strlen() and mb_substr(). + */ + + /** + * Computes the length of a string in bytes. + * + * @param string $str + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return int + */ + public static function ourStrlen($str) + { + static $exists = null; + if ($exists === null) { + $exists = \extension_loaded('mbstring') && \ini_get('mbstring.func_overload') !== false && (int)\ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING; + } + if ($exists) { + $length = \mb_strlen($str, '8bit'); + Core::ensureTrue($length !== false); + return $length; + } else { + return \strlen($str); + } + } + + /** + * Behaves roughly like the function substr() in PHP 7 does. + * + * @param string $str + * @param int $start + * @param int $length + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string|bool + */ + public static function ourSubstr($str, $start, $length = null) + { + static $exists = null; + if ($exists === null) { + $exists = \extension_loaded('mbstring') && \ini_get('mbstring.func_overload') !== false && (int)\ini_get('mbstring.func_overload') & MB_OVERLOAD_STRING; + } + + // This is required to make mb_substr behavior identical to substr. + // Without this, mb_substr() would return false, contra to what the + // PHP documentation says (it doesn't say it can return false.) + $input_len = Core::ourStrlen($str); + if ($start === $input_len && !$length) { + return ''; + } + + if ($start > $input_len) { + return false; + } + + // mb_substr($str, 0, NULL, '8bit') returns an empty string on PHP 5.3, + // so we have to find the length ourselves. Also, substr() doesn't + // accept null for the length. + if (! isset($length)) { + if ($start >= 0) { + $length = $input_len - $start; + } else { + $length = -$start; + } + } + + if ($length < 0) { + throw new \InvalidArgumentException( + "Negative lengths are not supported with ourSubstr." + ); + } + + if ($exists) { + $substr = \mb_substr($str, $start, $length, '8bit'); + // At this point there are two cases where mb_substr can + // legitimately return an empty string. Either $length is 0, or + // $start is equal to the length of the string (both mb_substr and + // substr return an empty string when this happens). It should never + // ever return a string that's longer than $length. + if (Core::ourStrlen($substr) > $length || (Core::ourStrlen($substr) === 0 && $length !== 0 && $start !== $input_len)) { + throw new Ex\EnvironmentIsBrokenException( + 'Your version of PHP has bug #66797. Its implementation of + mb_substr() is incorrect. See the details here: + https://bugs.php.net/bug.php?id=66797' + ); + } + return $substr; + } + + return \substr($str, $start, $length); + } + + /** + * Computes the PBKDF2 password-based key derivation function. + * + * The PBKDF2 function is defined in RFC 2898. Test vectors can be found in + * RFC 6070. This implementation of PBKDF2 was originally created by Taylor + * Hornby, with improvements from http://www.variations-of-shadow.com/. + * + * @param string $algorithm The hash algorithm to use. Recommended: SHA256 + * @param string $password The password. + * @param string $salt A salt that is unique to the password. + * @param int $count Iteration count. Higher is better, but slower. Recommended: At least 1000. + * @param int $key_length The length of the derived key in bytes. + * @param bool $raw_output If true, the key is returned in raw binary format. Hex encoded otherwise. + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string A $key_length-byte key derived from the password and salt. + */ + public static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false) + { + // Type checks: + if (! \is_string($algorithm)) { + throw new \InvalidArgumentException( + 'pbkdf2(): algorithm must be a string' + ); + } + if (! \is_string($password)) { + throw new \InvalidArgumentException( + 'pbkdf2(): password must be a string' + ); + } + if (! \is_string($salt)) { + throw new \InvalidArgumentException( + 'pbkdf2(): salt must be a string' + ); + } + // Coerce strings to integers with no information loss or overflow + $count += 0; + $key_length += 0; + + $algorithm = \strtolower($algorithm); + Core::ensureTrue( + \in_array($algorithm, \hash_algos(), true), + 'Invalid or unsupported hash algorithm.' + ); + + // Whitelist, or we could end up with people using CRC32. + $ok_algorithms = [ + 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', + 'ripemd160', 'ripemd256', 'ripemd320', 'whirlpool', + ]; + Core::ensureTrue( + \in_array($algorithm, $ok_algorithms, true), + 'Algorithm is not a secure cryptographic hash function.' + ); + + Core::ensureTrue($count > 0 && $key_length > 0, 'Invalid PBKDF2 parameters.'); + + if (\function_exists('hash_pbkdf2')) { + // The output length is in NIBBLES (4-bits) if $raw_output is false! + if (! $raw_output) { + $key_length = $key_length * 2; + } + return \hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output); + } + + $hash_length = Core::ourStrlen(\hash($algorithm, '', true)); + $block_count = \ceil($key_length / $hash_length); + + $output = ''; + for ($i = 1; $i <= $block_count; $i++) { + // $i encoded as 4 bytes, big endian. + $last = $salt . \pack('N', $i); + // first iteration + $last = $xorsum = \hash_hmac($algorithm, $last, $password, true); + // perform the other $count - 1 iterations + for ($j = 1; $j < $count; $j++) { + /** + * @psalm-suppress InvalidOperand + */ + $xorsum ^= ($last = \hash_hmac($algorithm, $last, $password, true)); + } + $output .= $xorsum; + } + + if ($raw_output) { + return (string) Core::ourSubstr($output, 0, $key_length); + } else { + return Encoding::binToHex((string) Core::ourSubstr($output, 0, $key_length)); + } + } +} diff --git a/api/private/classes/Defuse/Crypto/Crypto.php b/api/private/classes/Defuse/Crypto/Crypto.php new file mode 100644 index 0000000..86eb204 --- /dev/null +++ b/api/private/classes/Defuse/Crypto/Crypto.php @@ -0,0 +1,445 @@ +deriveKeys($salt); + $ekey = $keys->getEncryptionKey(); + $akey = $keys->getAuthenticationKey(); + $iv = Core::secureRandom(Core::BLOCK_BYTE_SIZE); + + $ciphertext = Core::CURRENT_VERSION . $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv); + $auth = \hash_hmac(Core::HASH_FUNCTION_NAME, $ciphertext, $akey, true); + $ciphertext = $ciphertext . $auth; + + if ($raw_binary) { + return $ciphertext; + } + return Encoding::binToHex($ciphertext); + } + + /** + * Decrypts a ciphertext to a string with either a key or a password. + * + * @param string $ciphertext + * @param KeyOrPassword $secret + * @param bool $raw_binary + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\WrongKeyOrModifiedCiphertextException + * + * @return string + */ + private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw_binary) + { + RuntimeTests::runtimeTest(); + + if (! $raw_binary) { + try { + $ciphertext = Encoding::hexToBin($ciphertext); + } catch (Ex\BadFormatException $ex) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Ciphertext has invalid hex encoding.' + ); + } + } + + if (Core::ourStrlen($ciphertext) < Core::MINIMUM_CIPHERTEXT_SIZE) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Ciphertext is too short.' + ); + } + + // Get and check the version header. + /** @var string $header */ + $header = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE); + if ($header !== Core::CURRENT_VERSION) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Bad version header.' + ); + } + + // Get the salt. + /** @var string $salt */ + $salt = Core::ourSubstr( + $ciphertext, + Core::HEADER_VERSION_SIZE, + Core::SALT_BYTE_SIZE + ); + Core::ensureTrue(\is_string($salt)); + + // Get the IV. + /** @var string $iv */ + $iv = Core::ourSubstr( + $ciphertext, + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE, + Core::BLOCK_BYTE_SIZE + ); + Core::ensureTrue(\is_string($iv)); + + // Get the HMAC. + /** @var string $hmac */ + $hmac = Core::ourSubstr( + $ciphertext, + Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE, + Core::MAC_BYTE_SIZE + ); + Core::ensureTrue(\is_string($hmac)); + + // Get the actual encrypted ciphertext. + /** @var string $encrypted */ + $encrypted = Core::ourSubstr( + $ciphertext, + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + + Core::BLOCK_BYTE_SIZE, + Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE - + Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE + ); + Core::ensureTrue(\is_string($encrypted)); + + // Derive the separate encryption and authentication keys from the key + // or password, whichever it is. + $keys = $secret->deriveKeys($salt); + + if (self::verifyHMAC($hmac, $header . $salt . $iv . $encrypted, $keys->getAuthenticationKey())) { + $plaintext = self::plainDecrypt($encrypted, $keys->getEncryptionKey(), $iv, Core::CIPHER_METHOD); + return $plaintext; + } else { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Integrity check failed.' + ); + } + } + + /** + * Raw unauthenticated encryption (insecure on its own). + * + * @param string $plaintext + * @param string $key + * @param string $iv + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + protected static function plainEncrypt($plaintext, $key, $iv) + { + Core::ensureConstantExists('OPENSSL_RAW_DATA'); + Core::ensureFunctionExists('openssl_encrypt'); + /** @var string $ciphertext */ + $ciphertext = \openssl_encrypt( + $plaintext, + Core::CIPHER_METHOD, + $key, + OPENSSL_RAW_DATA, + $iv + ); + + Core::ensureTrue(\is_string($ciphertext), 'openssl_encrypt() failed'); + + return $ciphertext; + } + + /** + * Raw unauthenticated decryption (insecure on its own). + * + * @param string $ciphertext + * @param string $key + * @param string $iv + * @param string $cipherMethod + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod) + { + Core::ensureConstantExists('OPENSSL_RAW_DATA'); + Core::ensureFunctionExists('openssl_decrypt'); + + /** @var string $plaintext */ + $plaintext = \openssl_decrypt( + $ciphertext, + $cipherMethod, + $key, + OPENSSL_RAW_DATA, + $iv + ); + Core::ensureTrue(\is_string($plaintext), 'openssl_decrypt() failed.'); + + return $plaintext; + } + + /** + * Verifies an HMAC without leaking information through side-channels. + * + * @param string $expected_hmac + * @param string $message + * @param string $key + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return bool + */ + protected static function verifyHMAC($expected_hmac, $message, $key) + { + $message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true); + return Core::hashEquals($message_hmac, $expected_hmac); + } +} diff --git a/api/private/classes/Defuse/Crypto/DerivedKeys.php b/api/private/classes/Defuse/Crypto/DerivedKeys.php new file mode 100644 index 0000000..86a48e5 --- /dev/null +++ b/api/private/classes/Defuse/Crypto/DerivedKeys.php @@ -0,0 +1,50 @@ +akey; + } + + /** + * Returns the encryption key. + * @return string + */ + public function getEncryptionKey() + { + return $this->ekey; + } + + /** + * Constructor for DerivedKeys. + * + * @param string $akey + * @param string $ekey + */ + public function __construct($akey, $ekey) + { + $this->akey = $akey; + $this->ekey = $ekey; + } +} diff --git a/api/private/classes/Defuse/Crypto/Encoding.php b/api/private/classes/Defuse/Crypto/Encoding.php new file mode 100644 index 0000000..30a60a8 --- /dev/null +++ b/api/private/classes/Defuse/Crypto/Encoding.php @@ -0,0 +1,269 @@ +> 4; + $hex .= \pack( + 'CC', + 87 + $b + ((($b - 10) >> 8) & ~38), + 87 + $c + ((($c - 10) >> 8) & ~38) + ); + } + return $hex; + } + + /** + * Converts a hexadecimal string into a byte string without leaking + * information through side channels. + * + * @param string $hex_string + * + * @throws Ex\BadFormatException + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + * @psalm-suppress TypeDoesNotContainType + */ + public static function hexToBin($hex_string) + { + $hex_pos = 0; + $bin = ''; + $hex_len = Core::ourStrlen($hex_string); + $state = 0; + $c_acc = 0; + + while ($hex_pos < $hex_len) { + $c = \ord($hex_string[$hex_pos]); + $c_num = $c ^ 48; + $c_num0 = ($c_num - 10) >> 8; + $c_alpha = ($c & ~32) - 55; + $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; + if (($c_num0 | $c_alpha0) === 0) { + throw new Ex\BadFormatException( + 'Encoding::hexToBin() input is not a hex string.' + ); + } + $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); + if ($state === 0) { + $c_acc = $c_val * 16; + } else { + $bin .= \pack('C', $c_acc | $c_val); + } + $state ^= 1; + ++$hex_pos; + } + return $bin; + } + + /** + * Remove trialing whitespace without table look-ups or branches. + * + * Calling this function may leak the length of the string as well as the + * number of trailing whitespace characters through side-channels. + * + * @param string $string + * @return string + */ + public static function trimTrailingWhitespace($string = '') + { + $length = Core::ourStrlen($string); + if ($length < 1) { + return ''; + } + do { + $prevLength = $length; + $last = $length - 1; + $chr = \ord($string[$last]); + + /* Null Byte (0x00), a.k.a. \0 */ + // if ($chr === 0x00) $length -= 1; + $sub = (($chr - 1) >> 8 ) & 1; + $length -= $sub; + $last -= $sub; + + /* Horizontal Tab (0x09) a.k.a. \t */ + $chr = \ord($string[$last]); + // if ($chr === 0x09) $length -= 1; + $sub = (((0x08 - $chr) & ($chr - 0x0a)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* New Line (0x0a), a.k.a. \n */ + $chr = \ord($string[$last]); + // if ($chr === 0x0a) $length -= 1; + $sub = (((0x09 - $chr) & ($chr - 0x0b)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* Carriage Return (0x0D), a.k.a. \r */ + $chr = \ord($string[$last]); + // if ($chr === 0x0d) $length -= 1; + $sub = (((0x0c - $chr) & ($chr - 0x0e)) >> 8) & 1; + $length -= $sub; + $last -= $sub; + + /* Space */ + $chr = \ord($string[$last]); + // if ($chr === 0x20) $length -= 1; + $sub = (((0x1f - $chr) & ($chr - 0x21)) >> 8) & 1; + $length -= $sub; + } while ($prevLength !== $length && $length > 0); + return (string) Core::ourSubstr($string, 0, $length); + } + + /* + * SECURITY NOTE ON APPLYING CHECKSUMS TO SECRETS: + * + * The checksum introduces a potential security weakness. For example, + * suppose we apply a checksum to a key, and that an adversary has an + * exploit against the process containing the key, such that they can + * overwrite an arbitrary byte of memory and then cause the checksum to + * be verified and learn the result. + * + * In this scenario, the adversary can extract the key one byte at + * a time by overwriting it with their guess of its value and then + * asking if the checksum matches. If it does, their guess was right. + * This kind of attack may be more easy to implement and more reliable + * than a remote code execution attack. + * + * This attack also applies to authenticated encryption as a whole, in + * the situation where the adversary can overwrite a byte of the key + * and then cause a valid ciphertext to be decrypted, and then + * determine whether the MAC check passed or failed. + * + * By using the full SHA256 hash instead of truncating it, I'm ensuring + * that both ways of going about the attack are equivalently difficult. + * A shorter checksum of say 32 bits might be more useful to the + * adversary as an oracle in case their writes are coarser grained. + * + * Because the scenario assumes a serious vulnerability, we don't try + * to prevent attacks of this style. + */ + + /** + * INTERNAL USE ONLY: Applies a version header, applies a checksum, and + * then encodes a byte string into a range of printable ASCII characters. + * + * @param string $header + * @param string $bytes + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + public static function saveBytesToChecksummedAsciiSafeString($header, $bytes) + { + // Headers must be a constant length to prevent one type's header from + // being a prefix of another type's header, leading to ambiguity. + Core::ensureTrue( + Core::ourStrlen($header) === self::SERIALIZE_HEADER_BYTES, + 'Header must be ' . self::SERIALIZE_HEADER_BYTES . ' bytes.' + ); + + return Encoding::binToHex( + $header . + $bytes . + \hash( + self::CHECKSUM_HASH_ALGO, + $header . $bytes, + true + ) + ); + } + + /** + * INTERNAL USE ONLY: Decodes, verifies the header and checksum, and returns + * the encoded byte string. + * + * @param string $expected_header + * @param string $string + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\BadFormatException + * + * @return string + */ + public static function loadBytesFromChecksummedAsciiSafeString($expected_header, $string) + { + // Headers must be a constant length to prevent one type's header from + // being a prefix of another type's header, leading to ambiguity. + Core::ensureTrue( + Core::ourStrlen($expected_header) === self::SERIALIZE_HEADER_BYTES, + 'Header must be 4 bytes.' + ); + + /* If you get an exception here when attempting to load from a file, first pass your + key to Encoding::trimTrailingWhitespace() to remove newline characters, etc. */ + $bytes = Encoding::hexToBin($string); + + /* Make sure we have enough bytes to get the version header and checksum. */ + if (Core::ourStrlen($bytes) < self::SERIALIZE_HEADER_BYTES + self::CHECKSUM_BYTE_SIZE) { + throw new Ex\BadFormatException( + 'Encoded data is shorter than expected.' + ); + } + + /* Grab the version header. */ + $actual_header = (string) Core::ourSubstr($bytes, 0, self::SERIALIZE_HEADER_BYTES); + + if ($actual_header !== $expected_header) { + throw new Ex\BadFormatException( + 'Invalid header.' + ); + } + + /* Grab the bytes that are part of the checksum. */ + $checked_bytes = (string) Core::ourSubstr( + $bytes, + 0, + Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE + ); + + /* Grab the included checksum. */ + $checksum_a = (string) Core::ourSubstr( + $bytes, + Core::ourStrlen($bytes) - self::CHECKSUM_BYTE_SIZE, + self::CHECKSUM_BYTE_SIZE + ); + + /* Re-compute the checksum. */ + $checksum_b = \hash(self::CHECKSUM_HASH_ALGO, $checked_bytes, true); + + /* Check if the checksum matches. */ + if (! Core::hashEquals($checksum_a, $checksum_b)) { + throw new Ex\BadFormatException( + "Data is corrupted, the checksum doesn't match" + ); + } + + return (string) Core::ourSubstr( + $bytes, + self::SERIALIZE_HEADER_BYTES, + Core::ourStrlen($bytes) - self::SERIALIZE_HEADER_BYTES - self::CHECKSUM_BYTE_SIZE + ); + } +} diff --git a/api/private/classes/Defuse/Crypto/Exception/BadFormatException.php b/api/private/classes/Defuse/Crypto/Exception/BadFormatException.php new file mode 100644 index 0000000..804d9c1 --- /dev/null +++ b/api/private/classes/Defuse/Crypto/Exception/BadFormatException.php @@ -0,0 +1,7 @@ +deriveKeys($file_salt); + $ekey = $keys->getEncryptionKey(); + $akey = $keys->getAuthenticationKey(); + + $ivsize = Core::BLOCK_BYTE_SIZE; + $iv = Core::secureRandom($ivsize); + + /* Initialize a streaming HMAC state. */ + /** @var mixed $hmac */ + $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); + Core::ensureTrue( + \is_resource($hmac) || \is_object($hmac), + 'Cannot initialize a hash context' + ); + + /* Write the header, salt, and IV. */ + self::writeBytes( + $outputHandle, + Core::CURRENT_VERSION . $file_salt . $iv, + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + $ivsize + ); + + /* Add the header, salt, and IV to the HMAC. */ + \hash_update($hmac, Core::CURRENT_VERSION); + \hash_update($hmac, $file_salt); + \hash_update($hmac, $iv); + + /* $thisIv will be incremented after each call to the encryption. */ + $thisIv = $iv; + + /* How many blocks do we encrypt at a time? We increment by this value. */ + /** + * @psalm-suppress RedundantCast + */ + $inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE); + + /* Loop until we reach the end of the input file. */ + $at_file_end = false; + while (! (\feof($inputHandle) || $at_file_end)) { + /* Find out if we can read a full buffer, or only a partial one. */ + /** @var int */ + $pos = \ftell($inputHandle); + if (!\is_int($pos)) { + throw new Ex\IOException( + 'Could not get current position in input file during encryption' + ); + } + if ($pos + Core::BUFFER_BYTE_SIZE >= $inputSize) { + /* We're at the end of the file, so we need to break out of the loop. */ + $at_file_end = true; + $read = self::readBytes( + $inputHandle, + $inputSize - $pos + ); + } else { + $read = self::readBytes( + $inputHandle, + Core::BUFFER_BYTE_SIZE + ); + } + + /* Encrypt this buffer. */ + /** @var string */ + $encrypted = \openssl_encrypt( + $read, + Core::CIPHER_METHOD, + $ekey, + OPENSSL_RAW_DATA, + $thisIv + ); + + Core::ensureTrue(\is_string($encrypted), 'OpenSSL encryption error'); + + /* Write this buffer's ciphertext. */ + self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted)); + /* Add this buffer's ciphertext to the HMAC. */ + \hash_update($hmac, $encrypted); + + /* Increment the counter by the number of blocks in a buffer. */ + $thisIv = Core::incrementCounter($thisIv, $inc); + /* WARNING: Usually, unless the file is a multiple of the buffer + * size, $thisIv will contain an incorrect value here on the last + * iteration of this loop. */ + } + + /* Get the HMAC and append it to the ciphertext. */ + $final_mac = \hash_final($hmac, true); + self::writeBytes($outputHandle, $final_mac, Core::MAC_BYTE_SIZE); + } + + /** + * Decrypts a file-backed resource with either a key or a password. + * + * @param resource $inputHandle + * @param resource $outputHandle + * @param KeyOrPassword $secret + * @return void + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\IOException + * @throws Ex\WrongKeyOrModifiedCiphertextException + * @psalm-suppress PossiblyInvalidArgument + * Fixes erroneous errors caused by PHP 7.2 switching the return value + * of hash_init from a resource to a HashContext. + */ + public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret) + { + if (! \is_resource($inputHandle)) { + throw new Ex\IOException( + 'Input handle must be a resource!' + ); + } + if (! \is_resource($outputHandle)) { + throw new Ex\IOException( + 'Output handle must be a resource!' + ); + } + + /* Make sure the file is big enough for all the reads we need to do. */ + $stat = \fstat($inputHandle); + if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Input file is too small to have been created by this library.' + ); + } + + /* Check the version header. */ + $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE); + if ($header !== Core::CURRENT_VERSION) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Bad version header.' + ); + } + + /* Get the salt. */ + $file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE); + + /* Get the IV. */ + $ivsize = Core::BLOCK_BYTE_SIZE; + $iv = self::readBytes($inputHandle, $ivsize); + + /* Derive the authentication and encryption keys. */ + $keys = $secret->deriveKeys($file_salt); + $ekey = $keys->getEncryptionKey(); + $akey = $keys->getAuthenticationKey(); + + /* We'll store the MAC of each buffer-sized chunk as we verify the + * actual MAC, so that we can check them again when decrypting. */ + $macs = []; + + /* $thisIv will be incremented after each call to the decryption. */ + $thisIv = $iv; + + /* How many blocks do we encrypt at a time? We increment by this value. */ + /** + * @psalm-suppress RedundantCast + */ + $inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE); + + /* Get the HMAC. */ + if (\fseek($inputHandle, (-1 * Core::MAC_BYTE_SIZE), SEEK_END) === -1) { + throw new Ex\IOException( + 'Cannot seek to beginning of MAC within input file' + ); + } + + /* Get the position of the last byte in the actual ciphertext. */ + /** @var int $cipher_end */ + $cipher_end = \ftell($inputHandle); + if (!\is_int($cipher_end)) { + throw new Ex\IOException( + 'Cannot read input file' + ); + } + /* We have the position of the first byte of the HMAC. Go back by one. */ + --$cipher_end; + + /* Read the HMAC. */ + /** @var string $stored_mac */ + $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE); + + /* Initialize a streaming HMAC state. */ + /** @var mixed $hmac */ + $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); + Core::ensureTrue(\is_resource($hmac) || \is_object($hmac), 'Cannot initialize a hash context'); + + /* Reset file pointer to the beginning of the file after the header */ + if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === -1) { + throw new Ex\IOException( + 'Cannot read seek within input file' + ); + } + + /* Seek to the start of the actual ciphertext. */ + if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === -1) { + throw new Ex\IOException( + 'Cannot seek input file to beginning of ciphertext' + ); + } + + /* PASS #1: Calculating the HMAC. */ + + \hash_update($hmac, $header); + \hash_update($hmac, $file_salt); + \hash_update($hmac, $iv); + /** @var mixed $hmac2 */ + $hmac2 = \hash_copy($hmac); + + $break = false; + while (! $break) { + /** @var int $pos */ + $pos = \ftell($inputHandle); + if (!\is_int($pos)) { + throw new Ex\IOException( + 'Could not get current position in input file during decryption' + ); + } + + /* Read the next buffer-sized chunk (or less). */ + if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { + $break = true; + $read = self::readBytes( + $inputHandle, + $cipher_end - $pos + 1 + ); + } else { + $read = self::readBytes( + $inputHandle, + Core::BUFFER_BYTE_SIZE + ); + } + + /* Update the HMAC. */ + \hash_update($hmac, $read); + + /* Remember this buffer-sized chunk's HMAC. */ + /** @var mixed $chunk_mac */ + $chunk_mac = \hash_copy($hmac); + Core::ensureTrue(\is_resource($chunk_mac) || \is_object($chunk_mac), 'Cannot duplicate a hash context'); + $macs []= \hash_final($chunk_mac); + } + + /* Get the final HMAC, which should match the stored one. */ + /** @var string $final_mac */ + $final_mac = \hash_final($hmac, true); + + /* Verify the HMAC. */ + if (! Core::hashEquals($final_mac, $stored_mac)) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'Integrity check failed.' + ); + } + + /* PASS #2: Decrypt and write output. */ + + /* Rewind to the start of the actual ciphertext. */ + if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === -1) { + throw new Ex\IOException( + 'Could not move the input file pointer during decryption' + ); + } + + $at_file_end = false; + while (! $at_file_end) { + /** @var int $pos */ + $pos = \ftell($inputHandle); + if (!\is_int($pos)) { + throw new Ex\IOException( + 'Could not get current position in input file during decryption' + ); + } + + /* Read the next buffer-sized chunk (or less). */ + if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) { + $at_file_end = true; + $read = self::readBytes( + $inputHandle, + $cipher_end - $pos + 1 + ); + } else { + $read = self::readBytes( + $inputHandle, + Core::BUFFER_BYTE_SIZE + ); + } + + /* Recalculate the MAC (so far) and compare it with the one we + * remembered from pass #1 to ensure attackers didn't change the + * ciphertext after MAC verification. */ + \hash_update($hmac2, $read); + /** @var mixed $calc_mac */ + $calc_mac = \hash_copy($hmac2); + Core::ensureTrue(\is_resource($calc_mac) || \is_object($calc_mac), 'Cannot duplicate a hash context'); + $calc = \hash_final($calc_mac); + + if (empty($macs)) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'File was modified after MAC verification' + ); + } elseif (! Core::hashEquals(\array_shift($macs), $calc)) { + throw new Ex\WrongKeyOrModifiedCiphertextException( + 'File was modified after MAC verification' + ); + } + + /* Decrypt this buffer-sized chunk. */ + /** @var string $decrypted */ + $decrypted = \openssl_decrypt( + $read, + Core::CIPHER_METHOD, + $ekey, + OPENSSL_RAW_DATA, + $thisIv + ); + Core::ensureTrue(\is_string($decrypted), 'OpenSSL decryption error'); + + /* Write the plaintext to the output file. */ + self::writeBytes( + $outputHandle, + $decrypted, + Core::ourStrlen($decrypted) + ); + + /* Increment the IV by the amount of blocks in a buffer. */ + /** @var string $thisIv */ + $thisIv = Core::incrementCounter($thisIv, $inc); + /* WARNING: Usually, unless the file is a multiple of the buffer + * size, $thisIv will contain an incorrect value here on the last + * iteration of this loop. */ + } + } + + /** + * Read from a stream; prevent partial reads. + * + * @param resource $stream + * @param int $num_bytes + * @return string + * + * @throws Ex\IOException + * @throws Ex\EnvironmentIsBrokenException + */ + public static function readBytes($stream, $num_bytes) + { + Core::ensureTrue($num_bytes >= 0, 'Tried to read less than 0 bytes'); + + if ($num_bytes === 0) { + return ''; + } + + $buf = ''; + $remaining = $num_bytes; + while ($remaining > 0 && ! \feof($stream)) { + /** @var string $read */ + $read = \fread($stream, $remaining); + if (!\is_string($read)) { + throw new Ex\IOException( + 'Could not read from the file' + ); + } + $buf .= $read; + $remaining -= Core::ourStrlen($read); + } + if (Core::ourStrlen($buf) !== $num_bytes) { + throw new Ex\IOException( + 'Tried to read past the end of the file' + ); + } + return $buf; + } + + /** + * Write to a stream; prevents partial writes. + * + * @param resource $stream + * @param string $buf + * @param int $num_bytes + * @return int + * + * @throws Ex\IOException + */ + public static function writeBytes($stream, $buf, $num_bytes = null) + { + $bufSize = Core::ourStrlen($buf); + if ($num_bytes === null) { + $num_bytes = $bufSize; + } + if ($num_bytes > $bufSize) { + throw new Ex\IOException( + 'Trying to write more bytes than the buffer contains.' + ); + } + if ($num_bytes < 0) { + throw new Ex\IOException( + 'Tried to write less than 0 bytes' + ); + } + $remaining = $num_bytes; + while ($remaining > 0) { + /** @var int $written */ + $written = \fwrite($stream, $buf, $remaining); + if (!\is_int($written)) { + throw new Ex\IOException( + 'Could not write to the file' + ); + } + $buf = (string) Core::ourSubstr($buf, $written, null); + $remaining -= $written; + } + return $num_bytes; + } + + /** + * Returns the last PHP error's or warning's message string. + * + * @return string + */ + private static function getLastErrorMessage() + { + $error = error_get_last(); + if ($error === null) { + return '[no PHP error]'; + } else { + return $error['message']; + } + } +} diff --git a/api/private/classes/Defuse/Crypto/Key.php b/api/private/classes/Defuse/Crypto/Key.php new file mode 100644 index 0000000..27b919f --- /dev/null +++ b/api/private/classes/Defuse/Crypto/Key.php @@ -0,0 +1,94 @@ +key_bytes + ); + } + + /** + * Gets the raw bytes of the key. + * + * @return string + */ + public function getRawBytes() + { + return $this->key_bytes; + } + + /** + * Constructs a new Key object from a string of raw bytes. + * + * @param string $bytes + * + * @throws Ex\EnvironmentIsBrokenException + */ + private function __construct($bytes) + { + Core::ensureTrue( + Core::ourStrlen($bytes) === self::KEY_BYTE_SIZE, + 'Bad key length.' + ); + $this->key_bytes = $bytes; + } + +} diff --git a/api/private/classes/Defuse/Crypto/KeyOrPassword.php b/api/private/classes/Defuse/Crypto/KeyOrPassword.php new file mode 100644 index 0000000..890b2c2 --- /dev/null +++ b/api/private/classes/Defuse/Crypto/KeyOrPassword.php @@ -0,0 +1,149 @@ +secret_type === self::SECRET_TYPE_KEY) { + Core::ensureTrue($this->secret instanceof Key); + /** + * @psalm-suppress PossiblyInvalidMethodCall + */ + $akey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $this->secret->getRawBytes(), + Core::KEY_BYTE_SIZE, + Core::AUTHENTICATION_INFO_STRING, + $salt + ); + /** + * @psalm-suppress PossiblyInvalidMethodCall + */ + $ekey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $this->secret->getRawBytes(), + Core::KEY_BYTE_SIZE, + Core::ENCRYPTION_INFO_STRING, + $salt + ); + return new DerivedKeys($akey, $ekey); + } elseif ($this->secret_type === self::SECRET_TYPE_PASSWORD) { + Core::ensureTrue(\is_string($this->secret)); + /* Our PBKDF2 polyfill is vulnerable to a DoS attack documented in + * GitHub issue #230. The fix is to pre-hash the password to ensure + * it is short. We do the prehashing here instead of in pbkdf2() so + * that pbkdf2() still computes the function as defined by the + * standard. */ + + /** + * @psalm-suppress PossiblyInvalidArgument + */ + $prehash = \hash(Core::HASH_FUNCTION_NAME, $this->secret, true); + + $prekey = Core::pbkdf2( + Core::HASH_FUNCTION_NAME, + $prehash, + $salt, + self::PBKDF2_ITERATIONS, + Core::KEY_BYTE_SIZE, + true + ); + $akey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $prekey, + Core::KEY_BYTE_SIZE, + Core::AUTHENTICATION_INFO_STRING, + $salt + ); + /* Note the cryptographic re-use of $salt here. */ + $ekey = Core::HKDF( + Core::HASH_FUNCTION_NAME, + $prekey, + Core::KEY_BYTE_SIZE, + Core::ENCRYPTION_INFO_STRING, + $salt + ); + return new DerivedKeys($akey, $ekey); + } else { + throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); + } + } + + /** + * Constructor for KeyOrPassword. + * + * @param int $secret_type + * @param mixed $secret (either a Key or a password string) + */ + private function __construct($secret_type, $secret) + { + // The constructor is private, so these should never throw. + if ($secret_type === self::SECRET_TYPE_KEY) { + Core::ensureTrue($secret instanceof Key); + } elseif ($secret_type === self::SECRET_TYPE_PASSWORD) { + Core::ensureTrue(\is_string($secret)); + } else { + throw new Ex\EnvironmentIsBrokenException('Bad secret type.'); + } + $this->secret_type = $secret_type; + $this->secret = $secret; + } +} diff --git a/api/private/classes/Defuse/Crypto/KeyProtectedByPassword.php b/api/private/classes/Defuse/Crypto/KeyProtectedByPassword.php new file mode 100644 index 0000000..347bdd9 --- /dev/null +++ b/api/private/classes/Defuse/Crypto/KeyProtectedByPassword.php @@ -0,0 +1,145 @@ +saveToAsciiSafeString(), + \hash(Core::HASH_FUNCTION_NAME, $password, true), + true + ); + + return new KeyProtectedByPassword($encrypted_key); + } + + /** + * Loads a KeyProtectedByPassword from its encoded form. + * + * @param string $saved_key_string + * + * @throws Ex\BadFormatException + * + * @return KeyProtectedByPassword + */ + public static function loadFromAsciiSafeString($saved_key_string) + { + $encrypted_key = Encoding::loadBytesFromChecksummedAsciiSafeString( + self::PASSWORD_KEY_CURRENT_VERSION, + $saved_key_string + ); + return new KeyProtectedByPassword($encrypted_key); + } + + /** + * Encodes the KeyProtectedByPassword into a string of printable ASCII + * characters. + * + * @throws Ex\EnvironmentIsBrokenException + * + * @return string + */ + public function saveToAsciiSafeString() + { + return Encoding::saveBytesToChecksummedAsciiSafeString( + self::PASSWORD_KEY_CURRENT_VERSION, + $this->encrypted_key + ); + } + + /** + * Decrypts the protected key, returning an unprotected Key object that can + * be used for encryption and decryption. + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\WrongKeyOrModifiedCiphertextException + * + * @param string $password + * @return Key + */ + public function unlockKey($password) + { + try { + $inner_key_encoded = Crypto::decryptWithPassword( + $this->encrypted_key, + \hash(Core::HASH_FUNCTION_NAME, $password, true), + true + ); + return Key::loadFromAsciiSafeString($inner_key_encoded); + } catch (Ex\BadFormatException $ex) { + /* This should never happen unless an attacker replaced the + * encrypted key ciphertext with some other ciphertext that was + * encrypted with the same password. We transform the exception type + * here in order to make the API simpler, avoiding the need to + * document that this method might throw an Ex\BadFormatException. */ + throw new Ex\WrongKeyOrModifiedCiphertextException( + "The decrypted key was found to be in an invalid format. " . + "This very likely indicates it was modified by an attacker." + ); + } + } + + /** + * Changes the password. + * + * @param string $current_password + * @param string $new_password + * + * @throws Ex\EnvironmentIsBrokenException + * @throws Ex\WrongKeyOrModifiedCiphertextException + * + * @return KeyProtectedByPassword + */ + public function changePassword($current_password, $new_password) + { + $inner_key = $this->unlockKey($current_password); + /* The password is hashed as a form of poor-man's domain separation + * between this use of encryptWithPassword() and other uses of + * encryptWithPassword() that the user may also be using as part of the + * same protocol. */ + $encrypted_key = Crypto::encryptWithPassword( + $inner_key->saveToAsciiSafeString(), + \hash(Core::HASH_FUNCTION_NAME, $new_password, true), + true + ); + + $this->encrypted_key = $encrypted_key; + + return $this; + } + + /** + * Constructor for KeyProtectedByPassword. + * + * @param string $encrypted_key + */ + private function __construct($encrypted_key) + { + $this->encrypted_key = $encrypted_key; + } +} diff --git a/api/private/classes/Defuse/Crypto/RuntimeTests.php b/api/private/classes/Defuse/Crypto/RuntimeTests.php new file mode 100644 index 0000000..65ce55d --- /dev/null +++ b/api/private/classes/Defuse/Crypto/RuntimeTests.php @@ -0,0 +1,228 @@ +getRawBytes()) === Core::KEY_BYTE_SIZE); + + Core::ensureTrue(Core::ENCRYPTION_INFO_STRING !== Core::AUTHENTICATION_INFO_STRING); + } catch (Ex\EnvironmentIsBrokenException $ex) { + // Do this, otherwise it will stay in the "tests are running" state. + $test_state = 3; + throw $ex; + } + + // Change this to '0' make the tests always re-run (for benchmarking). + $test_state = 1; + } + + /** + * High-level tests of Crypto operations. + * + * @throws Ex\EnvironmentIsBrokenException + * @return void + */ + private static function testEncryptDecrypt() + { + $key = Key::createNewRandomKey(); + $data = "EnCrYpT EvErYThInG\x00\x00"; + + // Make sure encrypting then decrypting doesn't change the message. + $ciphertext = Crypto::encrypt($data, $key, true); + try { + $decrypted = Crypto::decrypt($ciphertext, $key, true); + } catch (Ex\WrongKeyOrModifiedCiphertextException $ex) { + // It's important to catch this and change it into a + // Ex\EnvironmentIsBrokenException, otherwise a test failure could trick + // the user into thinking it's just an invalid ciphertext! + throw new Ex\EnvironmentIsBrokenException(); + } + Core::ensureTrue($decrypted === $data); + + // Modifying the ciphertext: Appending a string. + try { + Crypto::decrypt($ciphertext . 'a', $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + + // Modifying the ciphertext: Changing an HMAC byte. + $indices_to_change = [ + 0, // The header. + Core::HEADER_VERSION_SIZE + 1, // the salt + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + 1, // the IV + Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE + 1, // the ciphertext + ]; + + foreach ($indices_to_change as $index) { + try { + $ciphertext[$index] = \chr((\ord($ciphertext[$index]) + 1) % 256); + Crypto::decrypt($ciphertext, $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + } + + // Decrypting with the wrong key. + $key = Key::createNewRandomKey(); + $data = 'abcdef'; + $ciphertext = Crypto::encrypt($data, $key, true); + $wrong_key = Key::createNewRandomKey(); + try { + Crypto::decrypt($ciphertext, $wrong_key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + + // Ciphertext too small. + $key = Key::createNewRandomKey(); + $ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1); + try { + Crypto::decrypt($ciphertext, $key, true); + throw new Ex\EnvironmentIsBrokenException(); + } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ + } + } + + /** + * Test HKDF against test vectors. + * + * @throws Ex\EnvironmentIsBrokenException + * @return void + */ + private static function HKDFTestVector() + { + // HKDF test vectors from RFC 5869 + + // Test Case 1 + $ikm = \str_repeat("\x0b", 22); + $salt = Encoding::hexToBin('000102030405060708090a0b0c'); + $info = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9'); + $length = 42; + $okm = Encoding::hexToBin( + '3cb25f25faacd57a90434f64d0362f2a' . + '2d2d0a90cf1a5a4c5db02d56ecc4c5bf' . + '34007208d5b887185865' + ); + $computed_okm = Core::HKDF('sha256', $ikm, $length, $info, $salt); + Core::ensureTrue($computed_okm === $okm); + + // Test Case 7 + $ikm = \str_repeat("\x0c", 22); + $length = 42; + $okm = Encoding::hexToBin( + '2c91117204d745f3500d636a62f64f0a' . + 'b3bae548aa53d423b0d1f27ebba6f5e5' . + '673a081d70cce7acfc48' + ); + $computed_okm = Core::HKDF('sha1', $ikm, $length, '', null); + Core::ensureTrue($computed_okm === $okm); + } + + /** + * Test HMAC against test vectors. + * + * @throws Ex\EnvironmentIsBrokenException + * @return void + */ + private static function HMACTestVector() + { + // HMAC test vector From RFC 4231 (Test Case 1) + $key = \str_repeat("\x0b", 20); + $data = 'Hi There'; + $correct = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'; + Core::ensureTrue( + \hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) === $correct + ); + } + + /** + * Test AES against test vectors. + * + * @throws Ex\EnvironmentIsBrokenException + * @return void + */ + private static function AESTestVector() + { + // AES CTR mode test vector from NIST SP 800-38A + $key = Encoding::hexToBin( + '603deb1015ca71be2b73aef0857d7781' . + '1f352c073b6108d72d9810a30914dff4' + ); + $iv = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'); + $plaintext = Encoding::hexToBin( + '6bc1bee22e409f96e93d7e117393172a' . + 'ae2d8a571e03ac9c9eb76fac45af8e51' . + '30c81c46a35ce411e5fbc1191a0a52ef' . + 'f69f2445df4f9b17ad2b417be66c3710' + ); + $ciphertext = Encoding::hexToBin( + '601ec313775789a5b7a7f504bbf3d228' . + 'f443e3ca4d62b59aca84e990cacaf5c5' . + '2b0930daa23de94ce87017ba2d84988d' . + 'dfc9c58db67aada613c2dd08457941a6' + ); + + $computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv); + Core::ensureTrue($computed_ciphertext === $ciphertext); + + $computed_plaintext = Crypto::plainDecrypt($ciphertext, $key, $iv, Core::CIPHER_METHOD); + Core::ensureTrue($computed_plaintext === $plaintext); + } +} diff --git a/api/private/classes/ParagonIE/ConstantTime/Base32.php b/api/private/classes/ParagonIE/ConstantTime/Base32.php new file mode 100644 index 0000000..7784baf --- /dev/null +++ b/api/private/classes/ParagonIE/ConstantTime/Base32.php @@ -0,0 +1,471 @@ + 96 && $src < 123) $ret += $src - 97 + 1; // -64 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96); + + // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23 + $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 5-bit integers + * into 8-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return int + */ + protected static function decode5BitsUpper(int $src): int + { + $ret = -1; + + // if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23 + $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode5Bits(int $src): string + { + $diff = 0x61; + + // if ($src > 25) $ret -= 72; + $diff -= ((25 - $src) >> 8) & 73; + + return \pack('C', $src + $diff); + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return string + */ + protected static function encode5BitsUpper(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $ret -= 40; + $diff -= ((25 - $src) >> 8) & 41; + + return \pack('C', $src + $diff); + } + + + /** + * Base32 decoding + * + * @param string $src + * @param bool $upper + * @param bool $strictPadding + * @return string + * @throws \TypeError + * @psalm-suppress RedundantCondition + */ + protected static function doDecode(string $src, bool $upper = false, bool $strictPadding = false): string + { + // We do this to reduce code duplication: + $method = $upper + ? 'decode5BitsUpper' + : 'decode5Bits'; + + // Remove padding + $srcLen = Binary::safeStrlen($src); + if ($srcLen === 0) { + return ''; + } + if ($strictPadding) { + if (($srcLen & 7) === 0) { + for ($j = 0; $j < 7; ++$j) { + if ($src[$srcLen - 1] === '=') { + $srcLen--; + } else { + break; + } + } + } + if (($srcLen & 7) === 1) { + throw new \RangeException( + 'Incorrect padding' + ); + } + } else { + $src = \rtrim($src, '='); + $srcLen = Binary::safeStrlen($src); + } + + $err = 0; + $dest = ''; + // Main loop (no padding): + for ($i = 0; $i + 8 <= $srcLen; $i += 8) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 8)); + /** @var int $c0 */ + $c0 = static::$method($chunk[1]); + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + /** @var int $c6 */ + $c6 = static::$method($chunk[7]); + /** @var int $c7 */ + $c7 = static::$method($chunk[8]); + + $dest .= \pack( + 'CCCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff, + (($c6 << 5) | ($c7 ) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8; + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); + /** @var int $c0 */ + $c0 = static::$method($chunk[1]); + + if ($i + 6 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + /** @var int $c6 */ + $c6 = static::$method($chunk[7]); + + $dest .= \pack( + 'CCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8; + } elseif ($i + 5 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + + $dest .= \pack( + 'CCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8; + } elseif ($i + 4 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + + $dest .= \pack( + 'CCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8; + } elseif ($i + 3 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + + $dest .= \pack( + 'CC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3) >> 8; + } elseif ($i + 2 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + + $dest .= \pack( + 'CC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2) >> 8; + } elseif ($i + 1 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + + $dest .= \pack( + 'C', + (($c0 << 3) | ($c1 >> 2) ) & 0xff + ); + $err |= ($c0 | $c1) >> 8; + } else { + $dest .= \pack( + 'C', + (($c0 << 3) ) & 0xff + ); + $err |= ($c0) >> 8; + } + } + /** @var bool $check */ + $check = ($err === 0); + if (!$check) { + throw new \RangeException( + 'Base32::doDecode() only expects characters in the correct base32 alphabet' + ); + } + return $dest; + } + + /** + * Base32 Encoding + * + * @param string $src + * @param bool $upper + * @param bool $pad + * @return string + * @throws \TypeError + */ + protected static function doEncode(string $src, bool $upper = false, $pad = true): string + { + // We do this to reduce code duplication: + $method = $upper + ? 'encode5BitsUpper' + : 'encode5Bits'; + + $dest = ''; + $srcLen = Binary::safeStrlen($src); + + // Main loop (no padding): + for ($i = 0; $i + 5 <= $srcLen; $i += 5) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5)); + $b0 = $chunk[1]; + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $b3 = $chunk[4]; + $b4 = $chunk[5]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) | ($b3 >> 7)) & 31) . + static::$method((($b3 >> 2) ) & 31) . + static::$method((($b3 << 3) | ($b4 >> 5)) & 31) . + static::$method( $b4 & 31); + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); + $b0 = $chunk[1]; + if ($i + 3 < $srcLen) { + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $b3 = $chunk[4]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) | ($b3 >> 7)) & 31) . + static::$method((($b3 >> 2) ) & 31) . + static::$method((($b3 << 3) ) & 31); + if ($pad) { + $dest .= '='; + } + } elseif ($i + 2 < $srcLen) { + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) ) & 31); + if ($pad) { + $dest .= '==='; + } + } elseif ($i + 1 < $srcLen) { + $b1 = $chunk[2]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) ) & 31); + if ($pad) { + $dest .= '===='; + } + } else { + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method( ($b0 << 2) & 31); + if ($pad) { + $dest .= '======'; + } + } + } + return $dest; + } +} diff --git a/api/private/classes/ParagonIE/ConstantTime/Base32Hex.php b/api/private/classes/ParagonIE/ConstantTime/Base32Hex.php new file mode 100644 index 0000000..68fdad5 --- /dev/null +++ b/api/private/classes/ParagonIE/ConstantTime/Base32Hex.php @@ -0,0 +1,111 @@ + 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47); + + // if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86 + $ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 5-bit integers + * into 8-bit integers. + * + * @param int $src + * @return int + */ + protected static function decode5BitsUpper(int $src): int + { + $ret = -1; + + // if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47); + + // if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54 + $ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode5Bits(int $src): string + { + $src += 0x30; + + // if ($src > 0x39) $src += 0x61 - 0x3a; // 39 + $src += ((0x39 - $src) >> 8) & 39; + + return \pack('C', $src); + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return string + */ + protected static function encode5BitsUpper(int $src): string + { + $src += 0x30; + + // if ($src > 0x39) $src += 0x41 - 0x3a; // 7 + $src += ((0x39 - $src) >> 8) & 7; + + return \pack('C', $src); + } +} \ No newline at end of file diff --git a/api/private/classes/ParagonIE/ConstantTime/Base64.php b/api/private/classes/ParagonIE/ConstantTime/Base64.php new file mode 100644 index 0000000..4739e48 --- /dev/null +++ b/api/private/classes/ParagonIE/ConstantTime/Base64.php @@ -0,0 +1,271 @@ + $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 3)); + $b0 = $chunk[1]; + $b1 = $chunk[2]; + $b2 = $chunk[3]; + + $dest .= + static::encode6Bits( $b0 >> 2 ) . + static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . + static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) . + static::encode6Bits( $b2 & 63); + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); + $b0 = $chunk[1]; + if ($i + 1 < $srcLen) { + $b1 = $chunk[2]; + $dest .= + static::encode6Bits($b0 >> 2) . + static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . + static::encode6Bits(($b1 << 2) & 63); + if ($pad) { + $dest .= '='; + } + } else { + $dest .= + static::encode6Bits( $b0 >> 2) . + static::encode6Bits(($b0 << 4) & 63); + if ($pad) { + $dest .= '=='; + } + } + } + return $dest; + } + + /** + * decode from base64 into binary + * + * Base64 character set "./[A-Z][a-z][0-9]" + * + * @param string $encodedString + * @param bool $strictPadding + * @return string + * @throws \RangeException + * @throws \TypeError + * @psalm-suppress RedundantCondition + */ + public static function decode(string $encodedString, bool $strictPadding = false): string + { + // Remove padding + $srcLen = Binary::safeStrlen($encodedString); + if ($srcLen === 0) { + return ''; + } + + if ($strictPadding) { + if (($srcLen & 3) === 0) { + if ($encodedString[$srcLen - 1] === '=') { + $srcLen--; + if ($encodedString[$srcLen - 1] === '=') { + $srcLen--; + } + } + } + if (($srcLen & 3) === 1) { + throw new \RangeException( + 'Incorrect padding' + ); + } + if ($encodedString[$srcLen - 1] === '=') { + throw new \RangeException( + 'Incorrect padding' + ); + } + } else { + $encodedString = \rtrim($encodedString, '='); + $srcLen = Binary::safeStrlen($encodedString); + } + + $err = 0; + $dest = ''; + // Main loop (no padding): + for ($i = 0; $i + 4 <= $srcLen; $i += 4) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, 4)); + $c0 = static::decode6Bits($chunk[1]); + $c1 = static::decode6Bits($chunk[2]); + $c2 = static::decode6Bits($chunk[3]); + $c3 = static::decode6Bits($chunk[4]); + + $dest .= \pack( + 'CCC', + ((($c0 << 2) | ($c1 >> 4)) & 0xff), + ((($c1 << 4) | ($c2 >> 2)) & 0xff), + ((($c2 << 6) | $c3 ) & 0xff) + ); + $err |= ($c0 | $c1 | $c2 | $c3) >> 8; + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i)); + $c0 = static::decode6Bits($chunk[1]); + + if ($i + 2 < $srcLen) { + $c1 = static::decode6Bits($chunk[2]); + $c2 = static::decode6Bits($chunk[3]); + $dest .= \pack( + 'CC', + ((($c0 << 2) | ($c1 >> 4)) & 0xff), + ((($c1 << 4) | ($c2 >> 2)) & 0xff) + ); + $err |= ($c0 | $c1 | $c2) >> 8; + } elseif ($i + 1 < $srcLen) { + $c1 = static::decode6Bits($chunk[2]); + $dest .= \pack( + 'C', + ((($c0 << 2) | ($c1 >> 4)) & 0xff) + ); + $err |= ($c0 | $c1) >> 8; + } elseif ($i < $srcLen && $strictPadding) { + $err |= 1; + } + } + /** @var bool $check */ + $check = ($err === 0); + if (!$check) { + throw new \RangeException( + 'Base64::decode() only expects characters in the correct base64 alphabet' + ); + } + return $dest; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 6-bit integers + * into 8-bit integers. + * + * Base64 character set: + * [A-Z] [a-z] [0-9] + / + * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f + * + * @param int $src + * @return int + */ + protected static function decode6Bits(int $src): int + { + $ret = -1; + + // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); + + // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); + + // if ($src == 0x2b) $ret += 62 + 1; + $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63; + + // if ($src == 0x2f) ret += 63 + 1; + $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64; + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 + $diff += ((25 - $src) >> 8) & 6; + + // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 + $diff -= ((51 - $src) >> 8) & 75; + + // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15 + $diff -= ((61 - $src) >> 8) & 15; + + // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3 + $diff += ((62 - $src) >> 8) & 3; + + return \pack('C', $src + $diff); + } +} diff --git a/api/private/classes/ParagonIE/ConstantTime/Base64DotSlash.php b/api/private/classes/ParagonIE/ConstantTime/Base64DotSlash.php new file mode 100644 index 0000000..8ad2e2b --- /dev/null +++ b/api/private/classes/ParagonIE/ConstantTime/Base64DotSlash.php @@ -0,0 +1,88 @@ + 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45 + $ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45); + + // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62); + + // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68); + + // if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $src += 0x2e; + + // if ($src > 0x2f) $src += 0x41 - 0x30; // 17 + $src += ((0x2f - $src) >> 8) & 17; + + // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6 + $src += ((0x5a - $src) >> 8) & 6; + + // if ($src > 0x7a) $src += 0x30 - 0x7b; // -75 + $src -= ((0x7a - $src) >> 8) & 75; + + return \pack('C', $src); + } +} diff --git a/api/private/classes/ParagonIE/ConstantTime/Base64DotSlashOrdered.php b/api/private/classes/ParagonIE/ConstantTime/Base64DotSlashOrdered.php new file mode 100644 index 0000000..dd1459e --- /dev/null +++ b/api/private/classes/ParagonIE/ConstantTime/Base64DotSlashOrdered.php @@ -0,0 +1,82 @@ + 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45 + $ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45); + + // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52); + + // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $src += 0x2e; + + // if ($src > 0x39) $src += 0x41 - 0x3a; // 7 + $src += ((0x39 - $src) >> 8) & 7; + + // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6 + $src += ((0x5a - $src) >> 8) & 6; + + return \pack('C', $src); + } +} diff --git a/api/private/classes/ParagonIE/ConstantTime/Base64UrlSafe.php b/api/private/classes/ParagonIE/ConstantTime/Base64UrlSafe.php new file mode 100644 index 0000000..1a41075 --- /dev/null +++ b/api/private/classes/ParagonIE/ConstantTime/Base64UrlSafe.php @@ -0,0 +1,95 @@ + 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); + + // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); + + // if ($src == 0x2c) $ret += 62 + 1; + $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63; + + // if ($src == 0x5f) ret += 63 + 1; + $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64; + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 + $diff += ((25 - $src) >> 8) & 6; + + // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 + $diff -= ((51 - $src) >> 8) & 75; + + // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13 + $diff -= ((61 - $src) >> 8) & 13; + + // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3 + $diff += ((62 - $src) >> 8) & 49; + + return \pack('C', $src + $diff); + } +} diff --git a/api/private/classes/ParagonIE/ConstantTime/Binary.php b/api/private/classes/ParagonIE/ConstantTime/Binary.php new file mode 100644 index 0000000..38dbc4e --- /dev/null +++ b/api/private/classes/ParagonIE/ConstantTime/Binary.php @@ -0,0 +1,85 @@ + $chunk */ + $chunk = \unpack('C', Binary::safeSubstr($binString, $i, 1)); + /** @var int $c */ + $c = $chunk[1] & 0xf; + /** @var int $b */ + $b = $chunk[1] >> 4; + + $hex .= pack( + 'CC', + (87 + $b + ((($b - 10) >> 8) & ~38)), + (87 + $c + ((($c - 10) >> 8) & ~38)) + ); + } + return $hex; + } + + /** + * Convert a binary string into a hexadecimal string without cache-timing + * leaks, returning uppercase letters (as per RFC 4648) + * + * @param string $binString (raw binary) + * @return string + * @throws \TypeError + */ + public static function encodeUpper(string $binString): string + { + /** @var string $hex */ + $hex = ''; + /** @var int $len */ + $len = Binary::safeStrlen($binString); + + for ($i = 0; $i < $len; ++$i) { + /** @var array $chunk */ + $chunk = \unpack('C', Binary::safeSubstr($binString, $i, 2)); + /** @var int $c */ + $c = $chunk[1] & 0xf; + /** @var int $b */ + $b = $chunk[1] >> 4; + + $hex .= pack( + 'CC', + (55 + $b + ((($b - 10) >> 8) & ~6)), + (55 + $c + ((($c - 10) >> 8) & ~6)) + ); + } + return $hex; + } + + /** + * Convert a hexadecimal string into a binary string without cache-timing + * leaks + * + * @param string $encodedString + * @param bool $strictPadding + * @return string (raw binary) + * @throws \RangeException + */ + public static function decode(string $encodedString, bool $strictPadding = false): string + { + /** @var int $hex_pos */ + $hex_pos = 0; + /** @var string $bin */ + $bin = ''; + /** @var int $c_acc */ + $c_acc = 0; + /** @var int $hex_len */ + $hex_len = Binary::safeStrlen($encodedString); + /** @var int $state */ + $state = 0; + if (($hex_len & 1) !== 0) { + if ($strictPadding) { + throw new \RangeException( + 'Expected an even number of hexadecimal characters' + ); + } else { + $encodedString = '0' . $encodedString; + ++$hex_len; + } + } + + /** @var array $chunk */ + $chunk = \unpack('C*', $encodedString); + while ($hex_pos < $hex_len) { + ++$hex_pos; + /** @var int $c */ + $c = $chunk[$hex_pos]; + /** @var int $c_num */ + $c_num = $c ^ 48; + /** @var int $c_num0 */ + $c_num0 = ($c_num - 10) >> 8; + /** @var int $c_alpha */ + $c_alpha = ($c & ~32) - 55; + /** @var int $c_alpha0 */ + $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; + + if (($c_num0 | $c_alpha0) === 0) { + throw new \RangeException( + 'Expected hexadecimal character' + ); + } + /** @var int $c_val */ + $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); + if ($state === 0) { + $c_acc = $c_val * 16; + } else { + $bin .= \pack('C', $c_acc | $c_val); + } + $state ^= 1; + } + return $bin; + } +} diff --git a/api/private/classes/ParagonIE/ConstantTime/RFC4648.php b/api/private/classes/ParagonIE/ConstantTime/RFC4648.php new file mode 100644 index 0000000..492cad0 --- /dev/null +++ b/api/private/classes/ParagonIE/ConstantTime/RFC4648.php @@ -0,0 +1,175 @@ + "Zm9v" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base64Encode(string $str): string + { + return Base64::encode($str); + } + + /** + * RFC 4648 Base64 decoding + * + * "Zm9v" -> "foo" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base64Decode(string $str): string + { + return Base64::decode($str, true); + } + + /** + * RFC 4648 Base64 (URL Safe) encoding + * + * "foo" -> "Zm9v" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base64UrlSafeEncode(string $str): string + { + return Base64UrlSafe::encode($str); + } + + /** + * RFC 4648 Base64 (URL Safe) decoding + * + * "Zm9v" -> "foo" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base64UrlSafeDecode(string $str): string + { + return Base64UrlSafe::decode($str, true); + } + + /** + * RFC 4648 Base32 encoding + * + * "foo" -> "MZXW6===" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base32Encode(string $str): string + { + return Base32::encodeUpper($str); + } + + /** + * RFC 4648 Base32 encoding + * + * "MZXW6===" -> "foo" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base32Decode(string $str): string + { + return Base32::decodeUpper($str, true); + } + + /** + * RFC 4648 Base32-Hex encoding + * + * "foo" -> "CPNMU===" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base32HexEncode(string $str): string + { + return Base32::encodeUpper($str); + } + + /** + * RFC 4648 Base32-Hex decoding + * + * "CPNMU===" -> "foo" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base32HexDecode(string $str): string + { + return Base32::decodeUpper($str, true); + } + + /** + * RFC 4648 Base16 decoding + * + * "foo" -> "666F6F" + * + * @param string $str + * @return string + * @throws \TypeError + */ + public static function base16Encode(string $str): string + { + return Hex::encodeUpper($str); + } + + /** + * RFC 4648 Base16 decoding + * + * "666F6F" -> "foo" + * + * @param string $str + * @return string + */ + public static function base16Decode(string $str): string + { + return Hex::decode($str, true); + } +} \ No newline at end of file diff --git a/api/private/classes/ParagonIE/PasswordLock/PasswordLock.php b/api/private/classes/ParagonIE/PasswordLock/PasswordLock.php new file mode 100644 index 0000000..564ae71 --- /dev/null +++ b/api/private/classes/ParagonIE/PasswordLock/PasswordLock.php @@ -0,0 +1,221 @@ + 12]; + const OPTIONS_DEFAULT_ARGON2ID = [ + 'memory_cost' => 65536, + 'time_cost' => 4, + 'threads' => 1 + ]; + + /** + * 1. Hash password using bcrypt-base64-SHA256 + * 2. Encrypt-then-MAC the hash + * + * @param string $password + * @param Key $aesKey + * @param ?array $hashOptions + * @return string + * + * @throws EnvironmentIsBrokenException + * @throws \InvalidArgumentException + * @psalm-suppress InvalidArgument + */ + public static function hashAndEncrypt( + string $password, + Key $aesKey, + ?array $hashOptions = null + ): string { + if (is_null($hashOptions)) { + $hashOptions = static::getDefaultOptions(); + } + if (array_key_exists('salt', $hashOptions)) { + throw new \InvalidArgumentException('Explicit salts are unsupported.'); + } + /** @var string $hash */ + $hash = \password_hash( + Base64::encode( + \hash('sha384', $password, true) + ), + // PROJECT POLYGON MODIFICATION + // PASSWORD_DEFAULT, + PASSWORD_ARGON2ID, + // END PROJECT POLYGON MODIFICATION + $hashOptions + ); + if (!\is_string($hash)) { + throw new EnvironmentIsBrokenException("Unknown hashing error."); + } + return Crypto::encrypt($hash, $aesKey); + } + /** + * 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash + * 2. Verify that the password matches the hash + * + * @param string $password + * @param string $ciphertext + * @param string $aesKey - must be exactly 16 bytes + * @return bool + * + * @throws \InvalidArgumentException + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException + */ + public static function decryptAndVerifyLegacy( + string $password, + string $ciphertext, + string $aesKey + ): bool + { + if (Binary::safeStrlen($aesKey) !== 16) { + throw new \InvalidArgumentException("Encryption keys must be 16 bytes long"); + } + $hash = Crypto::legacyDecrypt( + $ciphertext, + $aesKey + ); + if (!\is_string($hash)) { + throw new EnvironmentIsBrokenException("Unknown hashing error."); + } + return \password_verify( + Base64::encode( + \hash('sha256', $password, true) + ), + $hash + ); + } + + /** + * 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash + * 2. Verify that the password matches the hash + * + * @param string $password + * @param string $ciphertext + * @param Key $aesKey + * @return bool + * + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException + */ + public static function decryptAndVerify(string $password, string $ciphertext, Key $aesKey): bool + { + $hash = Crypto::decrypt( + $ciphertext, + $aesKey + ); + if (!\is_string($hash)) { + throw new EnvironmentIsBrokenException("Unknown hashing error."); + } + return \password_verify( + Base64::encode( + \hash('sha384', $password, true) + ), + $hash + ); + } + + /** + * @return array + * + * @psalm-suppress TypeDoesNotContainType + */ + protected static function getDefaultOptions(): array + { + // PROJECT POLYGON MODIFICATION + // Future-proofing: + // if (PASSWORD_DEFAULT === PASSWORD_ARGON2ID) { + return self::OPTIONS_DEFAULT_ARGON2ID; + // } + // return self::OPTIONS_DEFAULT_BCRYPT; + // END PROJECT POLYGON MODIFICATION + } + + /** + * Decrypt the ciphertext and ascertain if the stored password needs to be rehashed? + * + * @param string $ciphertext + * @param Key $aesKey + * @param ?array $hashOptions + * @return bool + * + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException + */ + public static function needsRehash( + string $ciphertext, + Key $aesKey, + ?array $hashOptions = null + ): bool { + if (is_null($hashOptions)) { + $hashOptions = static::getDefaultOptions(); + } + $hash = Crypto::decrypt( + $ciphertext, + $aesKey + ); + if (!\is_string($hash)) { + throw new EnvironmentIsBrokenException("Unknown hashing error."); + } + /** @psalm-suppress InvalidArgument */ + return password_needs_rehash($hash, PASSWORD_DEFAULT, $hashOptions); + } + + /** + * Key rotation method -- decrypt with your old key then re-encrypt with your new key + * + * @param string $ciphertext + * @param Key $oldKey + * @param Key $newKey + * @return string + * + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException + */ + public static function rotateKey(string $ciphertext, Key $oldKey, Key $newKey): string + { + $plaintext = Crypto::decrypt($ciphertext, $oldKey); + return Crypto::encrypt($plaintext, $newKey); + } + + /** + * For migrating from an older version of the library + * + * @param string $password + * @param string $ciphertext + * @param string $oldKey + * @param Key $newKey + * @return string + * @throws \Exception + */ + public static function upgradeFromVersion1( + string $password, + string $ciphertext, + string $oldKey, + Key $newKey + ): string { + if (!self::decryptAndVerifyLegacy($password, $ciphertext, $oldKey)) { + throw new \Exception( + 'The correct password is necessary for legacy migration.' + ); + } + $plaintext = Crypto::legacyDecrypt($ciphertext, $oldKey); + return self::hashAndEncrypt($plaintext, $newKey); + } +} \ No newline at end of file diff --git a/api/private/classes/Parsedown.php b/api/private/classes/Parsedown.php new file mode 100644 index 0000000..4b6e3ee --- /dev/null +++ b/api/private/classes/Parsedown.php @@ -0,0 +1,1729 @@ +setEmbedsEnabled($embedsEnabled); + + if(!$embedsEnabled) $text = preg_replace(["/!\[(.*)\]\((.*)\)/i", "/!\[(.*)\]\((.*) \"(.*)\"\)/i"], ["$2", "[$3]($2)"], $text); + + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + $markup = $this->lines($lines); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + # + # Setters + # + + function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + function setEmbedsEnabled($embedsEnabled) + { + $this->embedsEnabled = $embedsEnabled; + + return $this; + } + + protected $embedsEnabled = false; + + function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + $CurrentBlock = null; + + foreach ($lines as $line) + { + if (chop($line) === '') + { + if (isset($CurrentBlock)) + { + $CurrentBlock['interrupted'] = true; + } + + continue; + } + + if (strpos($line, "\t") !== false) + { + $parts = explode("\t", $line); + + $line = $parts[0]; + + unset($parts[0]); + + foreach ($parts as $part) + { + $shortage = 4 - mb_strlen($line, 'utf-8') % 4; + + $line .= str_repeat(' ', $shortage); + $line .= $part; + } + } + + $indent = 0; + + while (isset($line[$indent]) and $line[$indent] === ' ') + { + $indent ++; + } + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) + { + $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); + + if (isset($Block)) + { + $CurrentBlock = $Block; + + continue; + } + else + { + if ($this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) + { + foreach ($this->BlockTypes[$marker] as $blockType) + { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) + { + $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); + + if (isset($Block)) + { + $Block['type'] = $blockType; + + if ( ! isset($Block['identified'])) + { + $Blocks []= $CurrentBlock; + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) + { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) + { + $CurrentBlock['element']['text'] .= "\n".$text; + } + else + { + $Blocks []= $CurrentBlock; + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) + { + $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + } + + # ~ + + $Blocks []= $CurrentBlock; + + unset($Blocks[0]); + + # ~ + + $markup = ''; + + foreach ($Blocks as $Block) + { + if (isset($Block['hidden'])) + { + continue; + } + + $markup .= "\n"; + $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); + } + + $markup .= "\n"; + + # ~ + + return $markup; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block'.$Type.'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block'.$Type.'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) + { + return; + } + + if ($Line['indent'] >= 4) + { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['element']['text']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['text']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') + { + $Block = array( + 'markup' => $Line['body'], + ); + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + $Block['markup'] .= "\n" . $Line['body']; + + if (preg_match('/-->$/', $Line['text'])) + { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) + { + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if (isset($matches[1])) + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r")); + + $class = 'language-'.$language; + + $Element['attributes'] = array( + 'class' => $class, + ); + } + + $Block = array( + 'char' => $Line['text'][0], + 'element' => array( + 'name' => 'pre', + 'handler' => 'element', + 'text' => $Element, + ), + ); + + return $Block; + } + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) + { + return; + } + + if (isset($Block['interrupted'])) + { + $Block['element']['text']['text'] .= "\n"; + + unset($Block['interrupted']); + } + + if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) + { + $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['text']['text'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['text']['text']; + + $Block['element']['text']['text'] = $text; + + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + if (isset($Line['text'][1])) + { + $level = 1; + + while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') + { + $level ++; + } + + if ($level > 6) + { + return; + } + + $text = trim($Line['text'], '# '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'text' => $text, + 'handler' => 'line', + ), + ); + + return $Block; + } + } + + # + # List + + protected function blockList($Line) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + + if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'element' => array( + 'name' => $name, + 'handler' => 'elements', + ), + ); + + if($name === 'ol') + { + $listStart = stristr($matches[0], '.', true); + + if($listStart !== '1') + { + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $matches[2], + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['li']['text'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => 'li', + 'text' => array( + $text, + ), + ); + + $Block['element']['text'] []= & $Block['li']; + + return $Block; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) + { + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + return $Block; + } + + if ($Line['indent'] > 0) + { + $Block['li']['text'] []= ''; + + $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + + $Block['li']['text'] []= $text; + + unset($Block['interrupted']); + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) + { + foreach ($Block['element']['text'] as &$li) + { + if (end($li['text']) !== '') + { + $li['text'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => 'lines', + 'text' => (array) $matches[1], + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + { + if (isset($Block['interrupted'])) + { + $Block['element']['text'] []= ''; + + unset($Block['interrupted']); + } + + $Block['element']['text'] []= $matches[1]; + + return $Block; + } + + if ( ! isset($Block['interrupted'])) + { + $Block['element']['text'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + { + $Block = array( + 'element' => array( + 'name' => 'hr' + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (chop($Line['text'], $Line['text'][0]) === '') + { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) + { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) + { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'markup' => $Line['text'], + ); + + $length = strlen($matches[0]); + + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + $Block['closed'] = true; + + $Block['void'] = true; + } + } + else + { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) + { + return; + } + + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) + { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) + { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open + { + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close + { + if ($Block['depth'] > 0) + { + $Block['depth'] --; + } + else + { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) + { + $Block['markup'] .= "\n"; + + unset($Block['interrupted']); + } + + $Block['markup'] .= "\n".$Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) + { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => null, + ); + + if (isset($matches[3])) + { + $Data['title'] = $matches[3]; + } + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + { + return; + } + + if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') + { + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + continue; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['text']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'text' => $headerCell, + 'handler' => 'line', + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => 'text-align: '.$alignment.';', + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'handler' => 'elements', + ), + ); + + $Block['element']['text'] []= array( + 'name' => 'thead', + 'handler' => 'elements', + ); + + $Block['element']['text'] []= array( + 'name' => 'tbody', + 'handler' => 'elements', + 'text' => array(), + ); + + $Block['element']['text'][0]['text'] []= array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $HeaderElements, + ); + + return $Block; + } + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + + foreach ($matches[0] as $index => $cell) + { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => 'line', + 'text' => $cell, + ); + + if (isset($Block['alignments'][$index])) + { + $Element['attributes'] = array( + 'style' => 'text-align: '.$Block['alignments'][$index].';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'handler' => 'elements', + 'text' => $Elements, + ); + + $Block['element']['text'][1]['text'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + $Block = array( + 'element' => array( + 'name' => 'p', + 'text' => $Line['text'], + 'handler' => 'line', + ), + ); + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!"*_&[:<>`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables=array()) + { + $markup = ''; + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) + { + $marker = $excerpt[0]; + + $markerPosition = strpos($text, $marker); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) + { + # check to see if the current inline type is nestable in the current context + + if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) + { + continue; + } + + $Inline = $this->{'inline'.$inlineType}($Excerpt); + + if ( ! isset($Inline)) + { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) + { + continue; + } + + # sets a default inline position + + if ( ! isset($Inline['position'])) + { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + foreach ($nonNestables as $non_nestable) + { + $Inline['element']['nonNestables'][] = $non_nestable; + } + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $markup .= $this->unmarkedText($unmarkedText); + + # compile the inline + $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $markup .= $this->unmarkedText($unmarkedText); + + $text = substr($text, $markerPosition + 1); + } + + $markup .= $this->unmarkedText($text); + + return $markup; + } + + # + # ~ + # + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + if ( ! isset($matches[2])) + { + $url = 'mailto:' . $url; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'strong'; + } + elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) + { + $emphasis = 'em'; + } + else + { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => 'line', + 'text' => $matches[1], + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) + { + return array( + 'markup' => $Excerpt['text'][1], + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if (! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') + { + return; + } + + $Excerpt['text'] = substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) + { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'class' => "img-fluid", + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['text'], + ), + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => 'line', + 'nonNestables' => array('Url', 'Link'), + 'text' => null, + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) + { + $Element['text'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } + else + { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) + { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) + { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } + else + { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) + { + $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } + else + { + $definition = strtolower($Element['text']); + } + + if ( ! isset($this->DefinitionData['Reference'][$definition])) + { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) + { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + { + return array( + 'markup' => $matches[0], + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) + { + return array( + 'markup' => '&', + 'extent' => 1, + ); + } + + $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); + + if (isset($SpecialCharacter[$Excerpt['text'][0]])) + { + return array( + 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', + 'extent' => 1, + ); + } + } + + protected function inlineStrikethrough($Excerpt) + { + if ( ! isset($Excerpt['text'][1])) + { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) + { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'text' => $matches[1], + 'handler' => 'line', + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') + { + return; + } + + if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) + { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + if ($this->breaksEnabled) + { + $text = preg_replace('/[ ]*\n/', "
\n", $text); + } + else + { + $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); + $text = str_replace(" \n", "\n", $text); + } + + return $text; + } + + # + # Handlers + # + + protected function element(array $Element) + { + if ($this->safeMode) + { + $Element = $this->sanitiseElement($Element); + } + + $markup = '<'.$Element['name']; + + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= ' '.$name.'="'.self::escape($value).'"'; + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) + { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) + { + $text = $Element['rawHtml']; + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + if (isset($text)) + { + $markup .= '>'; + + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (isset($Element['handler'])) + { + $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']); + } + elseif (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } + + $markup .= ''; + } + else + { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + foreach ($Elements as $Element) + { + $markup .= "\n" . $this->element($Element); + } + + $markup .= "\n"; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $markup = $this->lines($lines); + + $trimmedMarkup = trim($markup); + + if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') + { + $markup = $trimmedMarkup; + $markup = substr($markup, 3); + + $position = strpos($markup, "

"); + + $markup = substr_replace($markup, '', $position, 4); + } + + return $markup; + } + + # + # Deprecated Methods + # + + function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (isset($safeUrlNameToAtt[$Element['name']])) + { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if ( ! empty($Element['attributes'])) + { + foreach ($Element['attributes'] as $att => $val) + { + # filter out badly parsed attribute + if ( ! preg_match($goodAttribute, $att)) + { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) + { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) + { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) + { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) + { + return false; + } + else + { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + static function instance($name = 'default') + { + if (isset(self::$instances[$name])) + { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/api/private/classes/Sonata/GoogleAuthenticator/FixedBitNotation.php b/api/private/classes/Sonata/GoogleAuthenticator/FixedBitNotation.php new file mode 100644 index 0000000..c248ec0 --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/FixedBitNotation.php @@ -0,0 +1,292 @@ + + * + * 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); diff --git a/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticator.php b/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticator.php new file mode 100644 index 0000000..d3fc52f --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticator.php @@ -0,0 +1,178 @@ + + * + * 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); diff --git a/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticatorInterface.php b/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticatorInterface.php new file mode 100644 index 0000000..4dbe601 --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/GoogleAuthenticatorInterface.php @@ -0,0 +1,44 @@ + + * + * 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; +} diff --git a/api/private/classes/Sonata/GoogleAuthenticator/GoogleQrUrl.php b/api/private/classes/Sonata/GoogleAuthenticator/GoogleQrUrl.php new file mode 100644 index 0000000..e0f6e60 --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/GoogleQrUrl.php @@ -0,0 +1,93 @@ + + * + * 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 + */ +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); diff --git a/api/private/classes/Sonata/GoogleAuthenticator/RuntimeException.php b/api/private/classes/Sonata/GoogleAuthenticator/RuntimeException.php new file mode 100644 index 0000000..4d3cf5f --- /dev/null +++ b/api/private/classes/Sonata/GoogleAuthenticator/RuntimeException.php @@ -0,0 +1,46 @@ + + * + * 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 + */ +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); diff --git a/api/private/classes/Verot/Upload/Upload.php b/api/private/classes/Verot/Upload/Upload.php new file mode 100644 index 0000000..a32bb27 --- /dev/null +++ b/api/private/classes/Verot/Upload/Upload.php @@ -0,0 +1,5185 @@ + + * @license http://opensource.org/licenses/gpl-license.php GNU Public License + * @copyright Colin Verot + */ +class Upload { + + + /** + * Class version + * + * @access public + * @var string + */ + var $version; + + /** + * Uploaded file name + * + * @access public + * @var string + */ + var $file_src_name; + + /** + * Uploaded file name body (i.e. without extension) + * + * @access public + * @var string + */ + var $file_src_name_body; + + /** + * Uploaded file name extension + * + * @access public + * @var string + */ + var $file_src_name_ext; + + /** + * Uploaded file MIME type + * + * @access public + * @var string + */ + var $file_src_mime; + + /** + * Uploaded file size, in bytes + * + * @access public + * @var double + */ + var $file_src_size; + + /** + * Holds eventual PHP error code from $_FILES + * + * @access public + * @var string + */ + var $file_src_error; + + /** + * Uloaded file name, including server path + * + * @access public + * @var string + */ + var $file_src_pathname; + + /** + * Uloaded file name temporary copy + * + * @access private + * @var string + */ + var $file_src_temp; + + /** + * Destination file name + * + * @access public + * @var string + */ + var $file_dst_path; + + /** + * Destination file name + * + * @access public + * @var string + */ + var $file_dst_name; + + /** + * Destination file name body (i.e. without extension) + * + * @access public + * @var string + */ + var $file_dst_name_body; + + /** + * Destination file extension + * + * @access public + * @var string + */ + var $file_dst_name_ext; + + /** + * Destination file name, including path + * + * @access public + * @var string + */ + var $file_dst_pathname; + + /** + * Source image width + * + * @access public + * @var integer + */ + var $image_src_x; + + /** + * Source image height + * + * @access public + * @var integer + */ + var $image_src_y; + + /** + * Source image color depth + * + * @access public + * @var integer + */ + var $image_src_bits; + + /** + * Number of pixels + * + * @access public + * @var long + */ + var $image_src_pixels; + + /** + * Type of image (png, gif, jpg, webp or bmp) + * + * @access public + * @var string + */ + var $image_src_type; + + /** + * Destination image width + * + * @access public + * @var integer + */ + var $image_dst_x; + + /** + * Destination image height + * + * @access public + * @var integer + */ + var $image_dst_y; + + /** + * Destination image type (png, gif, jpg, webp or bmp) + * + * @access public + * @var integer + */ + var $image_dst_type; + + /** + * Supported image formats + * + * @access private + * @var array + */ + var $image_supported; + + /** + * Flag to determine if the source file is an image + * + * @access public + * @var boolean + */ + var $file_is_image; + + /** + * Flag set after instanciating the class + * + * Indicates if the file has been uploaded properly + * + * @access public + * @var bool + */ + var $uploaded; + + /** + * Flag stopping PHP upload checks + * + * Indicates whether we instanciated the class with a filename, in which case + * we will not check on the validity of the PHP *upload* + * + * This flag is automatically set to true when working on a local file + * + * Warning: for uploads, this flag MUST be set to false for security reason + * + * @access public + * @var bool + */ + var $no_upload_check; + + /** + * Flag set after calling a process + * + * Indicates if the processing, and copy of the resulting file went OK + * + * @access public + * @var bool + */ + var $processed; + + /** + * Holds eventual error message in plain english + * + * @access public + * @var string + */ + var $error; + + /** + * Holds an HTML formatted log + * + * @access public + * @var string + */ + var $log; + + + // overiddable processing variables + + + /** + * Set this variable to replace the name body (i.e. without extension) + * + * @access public + * @var string + */ + var $file_new_name_body; + + /** + * Set this variable to append a string to the file name body + * + * @access public + * @var string + */ + var $file_name_body_add; + + /** + * Set this variable to prepend a string to the file name body + * + * @access public + * @var string + */ + var $file_name_body_pre; + + /** + * Set this variable to change the file extension + * + * @access public + * @var string + */ + var $file_new_name_ext; + + /** + * Set this variable to format the filename (spaces changed to _) + * + * @access public + * @var boolean + */ + var $file_safe_name; + + /** + * Forces an extension if the source file doesn't have one + * + * If the file is an image, then the correct extension will be added + * Otherwise, a .txt extension will be chosen + * + * @access public + * @var boolean + */ + var $file_force_extension; + + /** + * Set this variable to false if you don't want to check the MIME against the allowed list + * + * This variable is set to true by default for security reason + * + * @access public + * @var boolean + */ + var $mime_check; + + /** + * Set this variable to false in the init() function if you don't want to check the MIME + * with Fileinfo PECL extension. On some systems, Fileinfo is known to be buggy, and you + * may want to deactivate it in the class code directly. + * + * You can also set it with the path of the magic database file. + * If set to true, the class will try to read the MAGIC environment variable + * and if it is empty, will default to the system's default + * If set to an empty string, it will call finfo_open without the path argument + * + * This variable is set to true by default for security reason + * + * @access public + * @var boolean + */ + var $mime_fileinfo; + + /** + * Set this variable to false in the init() function if you don't want to check the MIME + * with UNIX file() command + * + * This variable is set to true by default for security reason + * + * @access public + * @var boolean + */ + var $mime_file; + + /** + * Set this variable to false in the init() function if you don't want to check the MIME + * with the magic.mime file + * + * The function mime_content_type() will be deprecated, + * and this variable will be set to false in a future release + * + * This variable is set to true by default for security reason + * + * @access public + * @var boolean + */ + var $mime_magic; + + /** + * Set this variable to false in the init() function if you don't want to check the MIME + * with getimagesize() + * + * The class tries to get a MIME type from getimagesize() + * If no MIME is returned, it tries to guess the MIME type from the file type + * + * This variable is set to true by default for security reason + * + * @access public + * @var boolean + */ + var $mime_getimagesize; + + /** + * Set this variable to false if you don't want to turn dangerous scripts into simple text files + * + * @access public + * @var boolean + */ + var $no_script; + + /** + * Set this variable to true to allow automatic renaming of the file + * if the file already exists + * + * Default value is true + * + * For instance, on uploading foo.ext,
+ * if foo.ext already exists, upload will be renamed foo_1.ext
+ * and if foo_1.ext already exists, upload will be renamed foo_2.ext
+ * + * Note that this option doesn't have any effect if {@link file_overwrite} is true + * + * @access public + * @var bool + */ + var $file_auto_rename; + + /** + * Set this variable to true to allow automatic creation of the destination + * directory if it is missing (works recursively) + * + * Default value is true + * + * @access public + * @var bool + */ + var $dir_auto_create; + + /** + * Set this variable to true to allow automatic chmod of the destination + * directory if it is not writeable + * + * Default value is true + * + * @access public + * @var bool + */ + var $dir_auto_chmod; + + /** + * Set this variable to the default chmod you want the class to use + * when creating directories, or attempting to write in a directory + * + * Default value is 0755 (without quotes) + * + * @access public + * @var bool + */ + var $dir_chmod; + + /** + * Set this variable tu true to allow overwriting of an existing file + * + * Default value is false, so no files will be overwritten + * + * @access public + * @var bool + */ + var $file_overwrite; + + /** + * Set this variable to change the maximum size in bytes for an uploaded file + * + * Default value is the value upload_max_filesize from php.ini + * + * Value in bytes (integer) or shorthand byte values (string) is allowed. + * The available options are K (for Kilobytes), M (for Megabytes) and G (for Gigabytes) + * + * @access public + * @var double + */ + var $file_max_size; + + /** + * Set this variable to true to resize the file if it is an image + * + * You will probably want to set {@link image_x} and {@link image_y}, and maybe one of the ratio variables + * + * Default value is false (no resizing) + * + * @access public + * @var bool + */ + var $image_resize; + + /** + * Set this variable to convert the file if it is an image + * + * Possibles values are : ''; 'png'; 'jpeg'; 'gif'; 'webp'; 'bmp' + * + * Default value is '' (no conversion)
+ * If {@link resize} is true, {@link convert} will be set to the source file extension + * + * @access public + * @var string + */ + var $image_convert; + + /** + * Set this variable to the wanted (or maximum/minimum) width for the processed image, in pixels + * + * Default value is 150 + * + * @access public + * @var integer + */ + var $image_x; + + /** + * Set this variable to the wanted (or maximum/minimum) height for the processed image, in pixels + * + * Default value is 150 + * + * @access public + * @var integer + */ + var $image_y; + + /** + * Set this variable to keep the original size ratio to fit within {@link image_x} x {@link image_y} + * + * Default value is false + * + * @access public + * @var bool + */ + var $image_ratio; + + /** + * Set this variable to keep the original size ratio to fit within {@link image_x} x {@link image_y} + * + * The image will be resized as to fill the whole space, and excedent will be cropped + * + * Value can also be a string, one or more character from 'TBLR' (top, bottom, left and right) + * If set as a string, it determines which side of the image is kept while cropping. + * By default, the part of the image kept is in the center, i.e. it crops equally on both sides + * + * Default value is false + * + * @access public + * @var mixed + */ + var $image_ratio_crop; + + /** + * Set this variable to keep the original size ratio to fit within {@link image_x} x {@link image_y} + * + * The image will be resized to fit entirely in the space, and the rest will be colored. + * The default color is white, but can be set with {@link image_default_color} + * + * Value can also be a string, one or more character from 'TBLR' (top, bottom, left and right) + * If set as a string, it determines in which side of the space the image is displayed. + * By default, the image is displayed in the center, i.e. it fills the remaining space equally on both sides + * + * Default value is false + * + * @access public + * @var mixed + */ + var $image_ratio_fill; + + /** + * Set this variable to a number of pixels so that {@link image_x} and {@link image_y} are the best match possible + * + * The image will be resized to have approximatively the number of pixels + * The aspect ratio wil be conserved + * + * Default value is false + * + * @access public + * @var mixed + */ + var $image_ratio_pixels; + + /** + * Set this variable to calculate {@link image_x} automatically , using {@link image_y} and conserving ratio + * + * Default value is false + * + * @access public + * @var bool + */ + var $image_ratio_x; + + /** + * Set this variable to calculate {@link image_y} automatically , using {@link image_x} and conserving ratio + * + * Default value is false + * + * @access public + * @var bool + */ + var $image_ratio_y; + + /** + * (deprecated) Set this variable to keep the original size ratio to fit within {@link image_x} x {@link image_y}, + * but only if original image is bigger + * + * This setting is soon to be deprecated. Instead, use {@link image_ratio} and {@link image_no_enlarging} + * + * Default value is false + * + * @access public + * @var bool + */ + var $image_ratio_no_zoom_in; + + /** + * (deprecated) Set this variable to keep the original size ratio to fit within {@link image_x} x {@link image_y}, + * but only if original image is smaller + * + * Default value is false + * + * This setting is soon to be deprecated. Instead, use {@link image_ratio} and {@link image_no_shrinking} + * + * @access public + * @var bool + */ + var $image_ratio_no_zoom_out; + + /** + * Cancel resizing if the resized image is bigger than the original image, to prevent enlarging + * + * Default value is false + * + * @access public + * @var bool + */ + var $image_no_enlarging; + + /** + * Cancel resizing if the resized image is smaller than the original image, to prevent shrinking + * + * Default value is false + * + * @access public + * @var bool + */ + var $image_no_shrinking; + + /** + * Set this variable to set a maximum image width, above which the upload will be invalid + * + * Default value is null + * + * @access public + * @var integer + */ + var $image_max_width; + + /** + * Set this variable to set a maximum image height, above which the upload will be invalid + * + * Default value is null + * + * @access public + * @var integer + */ + var $image_max_height; + + /** + * Set this variable to set a maximum number of pixels for an image, above which the upload will be invalid + * + * Default value is null + * + * @access public + * @var long + */ + var $image_max_pixels; + + /** + * Set this variable to set a maximum image aspect ratio, above which the upload will be invalid + * + * Note that ratio = width / height + * + * Default value is null + * + * @access public + * @var float + */ + var $image_max_ratio; + + /** + * Set this variable to set a minimum image width, below which the upload will be invalid + * + * Default value is null + * + * @access public + * @var integer + */ + var $image_min_width; + + /** + * Set this variable to set a minimum image height, below which the upload will be invalid + * + * Default value is null + * + * @access public + * @var integer + */ + var $image_min_height; + + /** + * Set this variable to set a minimum number of pixels for an image, below which the upload will be invalid + * + * Default value is null + * + * @access public + * @var long + */ + var $image_min_pixels; + + /** + * Set this variable to set a minimum image aspect ratio, below which the upload will be invalid + * + * Note that ratio = width / height + * + * Default value is null + * + * @access public + * @var float + */ + var $image_min_ratio; + + /** + * Compression level for PNG images + * + * Between 1 (fast but large files) and 9 (slow but smaller files) + * + * Default value is null (Zlib default) + * + * @access public + * @var integer + */ + var $png_compression; + + /** + * Quality of JPEG created/converted destination image + * + * Default value is 85 + * + * @access public + * @var integer + */ + var $jpeg_quality; + + /** + * Quality of WebP created/converted destination image + * + * Default value is 85 + * + * @access public + * @var integer + */ + var $webp_quality; + + /** + * Determines the quality of the JPG image to fit a desired file size + * + * The JPG quality will be set between 1 and 100% + * The calculations are approximations. + * + * Value in bytes (integer) or shorthand byte values (string) is allowed. + * The available options are K (for Kilobytes), M (for Megabytes) and G (for Gigabytes) + * + * Default value is null (no calculations) + * + * @access public + * @var integer + */ + var $jpeg_size; + + /** + * Turns the interlace bit on + * + * This is actually used only for JPEG images, and defaults to false + * + * @access public + * @var boolean + */ + var $image_interlace; + + /** + * Flag set to true when the image is transparent + * + * This is actually used only for transparent GIFs + * + * @access public + * @var boolean + */ + var $image_is_transparent; + + /** + * Transparent color in a palette + * + * This is actually used only for transparent GIFs + * + * @access public + * @var boolean + */ + var $image_transparent_color; + + /** + * Background color, used to paint transparent areas with + * + * If set, it will forcibly remove transparency by painting transparent areas with the color + * This setting will fill in all transparent areas in PNG, WEPB and GIF, as opposed to {@link image_default_color} + * which will do so only in BMP, JPEG, and alpha transparent areas in transparent GIFs + * This setting overrides {@link image_default_color} + * + * Default value is null + * + * @access public + * @var string + */ + var $image_background_color; + + /** + * Default color for non alpha-transparent images + * + * This setting is to be used to define a background color for semi transparent areas + * of an alpha transparent when the output format doesn't support alpha transparency + * This is useful when, from an alpha transparent PNG or WEBP image, or an image with alpha transparent features + * if you want to output it as a transparent GIFs for instance, you can set a blending color for transparent areas + * If you output in JPEG or BMP, this color will be used to fill in the previously transparent areas + * + * The default color white + * + * @access public + * @var boolean + */ + var $image_default_color; + + /** + * Flag set to true when the image is not true color + * + * @access public + * @var boolean + */ + var $image_is_palette; + + /** + * Corrects the image brightness + * + * Value can range between -127 and 127 + * + * Default value is null + * + * @access public + * @var integer + */ + var $image_brightness; + + /** + * Corrects the image contrast + * + * Value can range between -127 and 127 + * + * Default value is null + * + * @access public + * @var integer + */ + var $image_contrast; + + /** + * Changes the image opacity + * + * Value can range between 0 and 100 + * + * Default value is null + * + * @access public + * @var integer + */ + var $image_opacity; + + /** + * Applies threshold filter + * + * Value can range between -127 and 127 + * + * Default value is null + * + * @access public + * @var integer + */ + var $image_threshold; + + /** + * Applies a tint on the image + * + * Value is an hexadecimal color, such as #FFFFFF + * + * Default value is null + * + * @access public + * @var string; + */ + var $image_tint_color; + + /** + * Applies a colored overlay on the image + * + * Value is an hexadecimal color, such as #FFFFFF + * + * To use with {@link image_overlay_opacity} + * + * Default value is null + * + * @access public + * @var string; + */ + var $image_overlay_color; + + /** + * Sets the opacity for the colored overlay + * + * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque) + * + * Unless used with {@link image_overlay_color}, this setting has no effect + * + * Default value is 50 + * + * @access public + * @var integer + */ + var $image_overlay_opacity; + + /** + * Inverts the color of an image + * + * Default value is FALSE + * + * @access public + * @var boolean; + */ + var $image_negative; + + /** + * Turns the image into greyscale + * + * Default value is FALSE + * + * @access public + * @var boolean; + */ + var $image_greyscale; + + /** + * Pixelate an image + * + * Value is integer, represents the block size + * + * Default value is null + * + * @access public + * @var integer; + */ + var $image_pixelate; + + /** + * Applies an unsharp mask, with alpha transparency support + * + * Beware that this unsharp mask is quite resource-intensive + * + * Default value is FALSE + * + * @access public + * @var boolean; + */ + var $image_unsharp; + + /** + * Sets the unsharp mask amount + * + * Value is an integer between 0 and 500, typically between 50 and 200 + * + * Unless used with {@link image_unsharp}, this setting has no effect + * + * Default value is 80 + * + * @access public + * @var integer + */ + var $image_unsharp_amount; + + /** + * Sets the unsharp mask radius + * + * Value is an integer between 0 and 50, typically between 0.5 and 1 + * It is not recommended to change it, the default works best + * + * Unless used with {@link image_unsharp}, this setting has no effect + * + * From PHP 5.1, imageconvolution is used, and this setting has no effect + * + * Default value is 0.5 + * + * @access public + * @var integer + */ + var $image_unsharp_radius; + + /** + * Sets the unsharp mask threshold + * + * Value is an integer between 0 and 255, typically between 0 and 5 + * + * Unless used with {@link image_unsharp}, this setting has no effect + * + * Default value is 1 + * + * @access public + * @var integer + */ + var $image_unsharp_threshold; + + /** + * Adds a text label on the image + * + * Value is a string, any text. Text will not word-wrap, although you can use breaklines in your text "\n" + * + * If set, this setting allow the use of all other settings starting with image_text_ + * + * Replacement tokens can be used in the string: + *
+     * gd_version    src_name       src_name_body src_name_ext
+     * src_pathname  src_mime       src_x         src_y
+     * src_type      src_bits       src_pixels
+     * src_size      src_size_kb    src_size_mb   src_size_human
+     * dst_path      dst_name_body  dst_pathname
+     * dst_name      dst_name_ext   dst_x         dst_y
+     * date          time           host          server        ip
+     * 
+ * The tokens must be enclosed in square brackets: [dst_x] will be replaced by the width of the picture + * + * Default value is null + * + * @access public + * @var string; + */ + var $image_text; + + /** + * Sets the text direction for the text label + * + * Value is either 'h' or 'v', as in horizontal and vertical + * + * Note that if you use a TrueType font, you can use {@link image_text_angle} instead + * + * Default value is h (horizontal) + * + * @access public + * @var string; + */ + var $image_text_direction; + + /** + * Sets the text color for the text label + * + * Value is an hexadecimal color, such as #FFFFFF + * + * Default value is #FFFFFF (white) + * + * @access public + * @var string; + */ + var $image_text_color; + + /** + * Sets the text opacity in the text label + * + * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque) + * + * Default value is 100 + * + * @access public + * @var integer + */ + var $image_text_opacity; + + /** + * Sets the text background color for the text label + * + * Value is an hexadecimal color, such as #FFFFFF + * + * Default value is null (no background) + * + * @access public + * @var string; + */ + var $image_text_background; + + /** + * Sets the text background opacity in the text label + * + * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque) + * + * Default value is 100 + * + * @access public + * @var integer + */ + var $image_text_background_opacity; + + /** + * Sets the text font in the text label + * + * Value is a an integer between 1 and 5 for GD built-in fonts. 1 is the smallest font, 5 the biggest + * Value can also be a string, which represents the path to a GDF or TTF font (TrueType). + * + * Default value is 5 + * + * @access public + * @var mixed; + */ + var $image_text_font; + + /** + * Sets the text font size for TrueType fonts + * + * Value is a an integer, and represents the font size in pixels (GD1) or points (GD1) + * + * Note that this setting is only applicable to TrueType fonts, and has no effects with GD fonts + * + * Default value is 16 + * + * @access public + * @var integer; + */ + var $image_text_size; + + /** + * Sets the text angle for TrueType fonts + * + * Value is a an integer between 0 and 360, in degrees, with 0 degrees being left-to-right reading text. + * + * Note that this setting is only applicable to TrueType fonts, and has no effects with GD fonts + * For GD fonts, you can use {@link image_text_direction} instead + * + * Default value is null (so it is determined by the value of {@link image_text_direction}) + * + * @access public + * @var integer; + */ + var $image_text_angle; + + /** + * Sets the text label position within the image + * + * Value is one or two out of 'TBLR' (top, bottom, left, right) + * + * The positions are as following: + *
+     *                        TL  T  TR
+     *                        L       R
+     *                        BL  B  BR
+     * 
+ * + * Default value is null (centered, horizontal and vertical) + * + * Note that is {@link image_text_x} and {@link image_text_y} are used, this setting has no effect + * + * @access public + * @var string; + */ + var $image_text_position; + + /** + * Sets the text label absolute X position within the image + * + * Value is in pixels, representing the distance between the left of the image and the label + * If a negative value is used, it will represent the distance between the right of the image and the label + * + * Default value is null (so {@link image_text_position} is used) + * + * @access public + * @var integer + */ + var $image_text_x; + + /** + * Sets the text label absolute Y position within the image + * + * Value is in pixels, representing the distance between the top of the image and the label + * If a negative value is used, it will represent the distance between the bottom of the image and the label + * + * Default value is null (so {@link image_text_position} is used) + * + * @access public + * @var integer + */ + var $image_text_y; + + /** + * Sets the text label padding + * + * Value is in pixels, representing the distance between the text and the label background border + * + * Default value is 0 + * + * This setting can be overriden by {@link image_text_padding_x} and {@link image_text_padding_y} + * + * @access public + * @var integer + */ + var $image_text_padding; + + /** + * Sets the text label horizontal padding + * + * Value is in pixels, representing the distance between the text and the left and right label background borders + * + * Default value is null + * + * If set, this setting overrides the horizontal part of {@link image_text_padding} + * + * @access public + * @var integer + */ + var $image_text_padding_x; + + /** + * Sets the text label vertical padding + * + * Value is in pixels, representing the distance between the text and the top and bottom label background borders + * + * Default value is null + * + * If set, his setting overrides the vertical part of {@link image_text_padding} + * + * @access public + * @var integer + */ + var $image_text_padding_y; + + /** + * Sets the text alignment + * + * Value is a string, which can be either 'L', 'C' or 'R' + * + * Default value is 'C' + * + * This setting is relevant only if the text has several lines. + * + * Note that this setting is only applicable to GD fonts, and has no effects with TrueType fonts + * + * @access public + * @var string; + */ + var $image_text_alignment; + + /** + * Sets the text line spacing + * + * Value is an integer, in pixels + * + * Default value is 0 + * + * This setting is relevant only if the text has several lines. + * + * Note that this setting is only applicable to GD fonts, and has no effects with TrueType fonts + * + * @access public + * @var integer + */ + var $image_text_line_spacing; + + /** + * Sets the height of the reflection + * + * Value is an integer in pixels, or a string which format can be in pixels or percentage. + * For instance, values can be : 40, '40', '40px' or '40%' + * + * Default value is null, no reflection + * + * @access public + * @var mixed; + */ + var $image_reflection_height; + + /** + * Sets the space between the source image and its relection + * + * Value is an integer in pixels, which can be negative + * + * Default value is 2 + * + * This setting is relevant only if {@link image_reflection_height} is set + * + * @access public + * @var integer + */ + var $image_reflection_space; + + /** + * Sets the initial opacity of the reflection + * + * Value is an integer between 0 (no opacity) and 100 (full opacity). + * The reflection will start from {@link image_reflection_opacity} and end up at 0 + * + * Default value is 60 + * + * This setting is relevant only if {@link image_reflection_height} is set + * + * @access public + * @var integer + */ + var $image_reflection_opacity; + + /** + * Automatically rotates the image according to EXIF data (JPEG only) + * + * Default value is true + * + * @access public + * @var boolean; + */ + var $image_auto_rotate; + + /** + * Flips the image vertically or horizontally + * + * Value is either 'h' or 'v', as in horizontal and vertical + * + * Default value is null (no flip) + * + * @access public + * @var string; + */ + var $image_flip; + + /** + * Rotates the image by increments of 45 degrees + * + * Value is either 90, 180 or 270 + * + * Default value is null (no rotation) + * + * @access public + * @var string; + */ + var $image_rotate; + + /** + * Crops an image + * + * Values are four dimensions, or two, or one (CSS style) + * They represent the amount cropped top, right, bottom and left. + * These values can either be in an array, or a space separated string. + * Each value can be in pixels (with or without 'px'), or percentage (of the source image) + * + * For instance, are valid: + *
+     * $foo->image_crop = 20                  OR array(20);
+     * $foo->image_crop = '20px'              OR array('20px');
+     * $foo->image_crop = '20 40'             OR array('20', 40);
+     * $foo->image_crop = '-20 25%'           OR array(-20, '25%');
+     * $foo->image_crop = '20px 25%'          OR array('20px', '25%');
+     * $foo->image_crop = '20% 25%'           OR array('20%', '25%');
+     * $foo->image_crop = '20% 25% 10% 30%'   OR array('20%', '25%', '10%', '30%');
+     * $foo->image_crop = '20px 25px 2px 2px' OR array('20px', '25%px', '2px', '2px');
+     * $foo->image_crop = '20 25% 40px 10%'   OR array(20, '25%', '40px', '10%');
+     * 
+ * + * If a value is negative, the image will be expanded, and the extra parts will be filled with black + * + * Default value is null (no cropping) + * + * @access public + * @var string OR array; + */ + var $image_crop; + + /** + * Crops an image, before an eventual resizing + * + * See {@link image_crop} for valid formats + * + * Default value is null (no cropping) + * + * @access public + * @var string OR array; + */ + var $image_precrop; + + /** + * Adds a bevel border on the image + * + * Value is a positive integer, representing the thickness of the bevel + * + * If the bevel colors are the same as the background, it makes a fade out effect + * + * Default value is null (no bevel) + * + * @access public + * @var integer + */ + var $image_bevel; + + /** + * Top and left bevel color + * + * Value is a color, in hexadecimal format + * This setting is used only if {@link image_bevel} is set + * + * Default value is #FFFFFF + * + * @access public + * @var string; + */ + var $image_bevel_color1; + + /** + * Right and bottom bevel color + * + * Value is a color, in hexadecimal format + * This setting is used only if {@link image_bevel} is set + * + * Default value is #000000 + * + * @access public + * @var string; + */ + var $image_bevel_color2; + + /** + * Adds a single-color border on the outer of the image + * + * Values are four dimensions, or two, or one (CSS style) + * They represent the border thickness top, right, bottom and left. + * These values can either be in an array, or a space separated string. + * Each value can be in pixels (with or without 'px'), or percentage (of the source image) + * + * See {@link image_crop} for valid formats + * + * If a value is negative, the image will be cropped. + * Note that the dimensions of the picture will be increased by the borders' thickness + * + * Default value is null (no border) + * + * @access public + * @var integer + */ + var $image_border; + + /** + * Border color + * + * Value is a color, in hexadecimal format. + * This setting is used only if {@link image_border} is set + * + * Default value is #FFFFFF + * + * @access public + * @var string; + */ + var $image_border_color; + + /** + * Sets the opacity for the borders + * + * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque) + * + * Unless used with {@link image_border}, this setting has no effect + * + * Default value is 100 + * + * @access public + * @var integer + */ + var $image_border_opacity; + + /** + * Adds a fading-to-transparent border on the image + * + * Values are four dimensions, or two, or one (CSS style) + * They represent the border thickness top, right, bottom and left. + * These values can either be in an array, or a space separated string. + * Each value can be in pixels (with or without 'px'), or percentage (of the source image) + * + * See {@link image_crop} for valid formats + * + * Note that the dimensions of the picture will not be increased by the borders' thickness + * + * Default value is null (no border) + * + * @access public + * @var integer + */ + var $image_border_transparent; + + /** + * Adds a multi-color frame on the outer of the image + * + * Value is an integer. Two values are possible for now: + * 1 for flat border, meaning that the frame is mirrored horizontally and vertically + * 2 for crossed border, meaning that the frame will be inversed, as in a bevel effect + * + * The frame will be composed of colored lines set in {@link image_frame_colors} + * + * Note that the dimensions of the picture will be increased by the borders' thickness + * + * Default value is null (no frame) + * + * @access public + * @var integer + */ + var $image_frame; + + /** + * Sets the colors used to draw a frame + * + * Values is a list of n colors in hexadecimal format. + * These values can either be in an array, or a space separated string. + * + * The colors are listed in the following order: from the outset of the image to its center + * + * For instance, are valid: + *
+     * $foo->image_frame_colors = '#FFFFFF #999999 #666666 #000000';
+     * $foo->image_frame_colors = array('#FFFFFF', '#999999', '#666666', '#000000');
+     * 
+ * + * This setting is used only if {@link image_frame} is set + * + * Default value is '#FFFFFF #999999 #666666 #000000' + * + * @access public + * @var string OR array; + */ + var $image_frame_colors; + + /** + * Sets the opacity for the frame + * + * Value is a percentage, as an integer between 0 (transparent) and 100 (opaque) + * + * Unless used with {@link image_frame}, this setting has no effect + * + * Default value is 100 + * + * @access public + * @var integer + */ + var $image_frame_opacity; + + /** + * Adds a watermark on the image + * + * Value is a local image filename, relative or absolute. GIF, JPG, BMP, WEBP and PNG are supported, as well as PNG and WEBP alpha. + * + * If set, this setting allow the use of all other settings starting with image_watermark_ + * + * Default value is null + * + * @access public + * @var string; + */ + var $image_watermark; + + /** + * Sets the watermarkposition within the image + * + * Value is one or two out of 'TBLR' (top, bottom, left, right) + * + * The positions are as following: TL T TR + * L R + * BL B BR + * + * Default value is null (centered, horizontal and vertical) + * + * Note that is {@link image_watermark_x} and {@link image_watermark_y} are used, this setting has no effect + * + * @access public + * @var string; + */ + var $image_watermark_position; + + /** + * Sets the watermark absolute X position within the image + * + * Value is in pixels, representing the distance between the top of the image and the watermark + * If a negative value is used, it will represent the distance between the bottom of the image and the watermark + * + * Default value is null (so {@link image_watermark_position} is used) + * + * @access public + * @var integer + */ + var $image_watermark_x; + + /** + * Sets the twatermark absolute Y position within the image + * + * Value is in pixels, representing the distance between the left of the image and the watermark + * If a negative value is used, it will represent the distance between the right of the image and the watermark + * + * Default value is null (so {@link image_watermark_position} is used) + * + * @access public + * @var integer + */ + var $image_watermark_y; + + /** + * Prevents the watermark to be resized up if it is smaller than the image + * + * If the watermark if smaller than the destination image, taking in account the desired watermark position + * then it will be resized up to fill in the image (minus the {@link image_watermark_x} or {@link image_watermark_y} values) + * + * If you don't want your watermark to be resized in any way, then + * set {@link image_watermark_no_zoom_in} and {@link image_watermark_no_zoom_out} to true + * If you want your watermark to be resized up or doan to fill in the image better, then + * set {@link image_watermark_no_zoom_in} and {@link image_watermark_no_zoom_out} to false + * + * Default value is true (so the watermark will not be resized up, which is the behaviour most people expect) + * + * @access public + * @var integer + */ + var $image_watermark_no_zoom_in; + + /** + * Prevents the watermark to be resized down if it is bigger than the image + * + * If the watermark if bigger than the destination image, taking in account the desired watermark position + * then it will be resized down to fit in the image (minus the {@link image_watermark_x} or {@link image_watermark_y} values) + * + * If you don't want your watermark to be resized in any way, then + * set {@link image_watermark_no_zoom_in} and {@link image_watermark_no_zoom_out} to true + * If you want your watermark to be resized up or doan to fill in the image better, then + * set {@link image_watermark_no_zoom_in} and {@link image_watermark_no_zoom_out} to false + * + * Default value is false (so the watermark may be shrinked to fit in the image) + * + * @access public + * @var integer + */ + var $image_watermark_no_zoom_out; + + /** + * List of MIME types per extension + * + * @access private + * @var array + */ + var $mime_types; + + /** + * Allowed MIME types + * + * Default is a selection of safe mime-types, but you might want to change it + * + * Simple wildcards are allowed, such as image/* or application/* + * If there is only one MIME type allowed, then it can be a string instead of an array + * + * @access public + * @var array OR string + */ + var $allowed; + + /** + * Forbidden MIME types + * + * Default is a selection of safe mime-types, but you might want to change it + * To only check for forbidden MIME types, and allow everything else, set {@link allowed} to array('* / *') without the spaces + * + * Simple wildcards are allowed, such as image/* or application/* + * If there is only one MIME type forbidden, then it can be a string instead of an array + * + * @access public + * @var array OR string + */ + var $forbidden; + + /** + * Blacklisted file extensions + * + * List of blacklisted extensions, that are enforced if {@link no_script} is true + * + * @access public + * @var array + */ + var $blacklist; + + + /** + * Array of translated error messages + * + * By default, the language is english (en_GB) + * Translations can be in separate files, in a lang/ subdirectory + * + * @access public + * @var array + */ + var $translation; + + /** + * Language selected for the translations + * + * By default, the language is english ("en_GB") + * + * @access public + * @var array + */ + var $lang; + + /** + * Init or re-init all the processing variables to their default values + * + * This function is called in the constructor, and after each call of {@link process} + * + * @access private + */ + function init() { + + // overiddable variables + $this->file_new_name_body = null; // replace the name body + $this->file_name_body_add = null; // append to the name body + $this->file_name_body_pre = null; // prepend to the name body + $this->file_new_name_ext = null; // replace the file extension + $this->file_safe_name = true; // format safely the filename + $this->file_force_extension = true; // forces extension if there isn't one + $this->file_overwrite = false; // allows overwritting if the file already exists + $this->file_auto_rename = true; // auto-rename if the file already exists + $this->dir_auto_create = true; // auto-creates directory if missing + $this->dir_auto_chmod = true; // auto-chmod directory if not writeable + $this->dir_chmod = 0755; // default chmod to use + + $this->no_script = true; // turns scripts into test files + $this->mime_check = true; // checks the mime type against the allowed list + + // these are the different MIME detection methods. if one of these method doesn't work on your + // system, you can deactivate it here; just set it to false + $this->mime_fileinfo = true; // MIME detection with Fileinfo PECL extension + $this->mime_file = true; // MIME detection with UNIX file() command + $this->mime_magic = true; // MIME detection with mime_magic (mime_content_type()) + $this->mime_getimagesize = true; // MIME detection with getimagesize() + + // get the default max size from php.ini + $this->file_max_size_raw = trim(ini_get('upload_max_filesize')); + $this->file_max_size = $this->getsize($this->file_max_size_raw); + + $this->image_resize = false; // resize the image + $this->image_convert = ''; // convert. values :''; 'png'; 'jpeg'; 'gif'; 'bmp'; 'webp' + + $this->image_x = 150; + $this->image_y = 150; + $this->image_ratio = false; // keeps aspect ratio within x and y dimensions + $this->image_ratio_crop = false; // keeps aspect ratio within x and y dimensions, filling the space + $this->image_ratio_fill = false; // keeps aspect ratio within x and y dimensions, fitting the image in the space + $this->image_ratio_pixels = false; // keeps aspect ratio, calculating x and y to reach the number of pixels + $this->image_ratio_x = false; // calculate the $image_x if true + $this->image_ratio_y = false; // calculate the $image_y if true + $this->image_ratio_no_zoom_in = false; + $this->image_ratio_no_zoom_out = false; + $this->image_no_enlarging = false; + $this->image_no_shrinking = false; + + $this->png_compression = null; + $this->webp_quality = 85; + $this->jpeg_quality = 85; + $this->jpeg_size = null; + $this->image_interlace = false; + $this->image_is_transparent = false; + $this->image_transparent_color = null; + $this->image_background_color = null; + $this->image_default_color = '#ffffff'; + $this->image_is_palette = false; + + $this->image_max_width = null; + $this->image_max_height = null; + $this->image_max_pixels = null; + $this->image_max_ratio = null; + $this->image_min_width = null; + $this->image_min_height = null; + $this->image_min_pixels = null; + $this->image_min_ratio = null; + + $this->image_brightness = null; + $this->image_contrast = null; + $this->image_opacity = null; + $this->image_threshold = null; + $this->image_tint_color = null; + $this->image_overlay_color = null; + $this->image_overlay_opacity = null; + $this->image_negative = false; + $this->image_greyscale = false; + $this->image_pixelate = null; + $this->image_unsharp = false; + $this->image_unsharp_amount = 80; + $this->image_unsharp_radius = 0.5; + $this->image_unsharp_threshold = 1; + + $this->image_text = null; + $this->image_text_direction = null; + $this->image_text_color = '#FFFFFF'; + $this->image_text_opacity = 100; + $this->image_text_background = null; + $this->image_text_background_opacity = 100; + $this->image_text_font = 5; + $this->image_text_size = 16; + $this->image_text_angle = null; + $this->image_text_x = null; + $this->image_text_y = null; + $this->image_text_position = null; + $this->image_text_padding = 0; + $this->image_text_padding_x = null; + $this->image_text_padding_y = null; + $this->image_text_alignment = 'C'; + $this->image_text_line_spacing = 0; + + $this->image_reflection_height = null; + $this->image_reflection_space = 2; + $this->image_reflection_opacity = 60; + + $this->image_watermark = null; + $this->image_watermark_x = null; + $this->image_watermark_y = null; + $this->image_watermark_position = null; + $this->image_watermark_no_zoom_in = true; + $this->image_watermark_no_zoom_out = false; + + $this->image_flip = null; + $this->image_auto_rotate = true; + $this->image_rotate = null; + $this->image_crop = null; + $this->image_precrop = null; + + $this->image_bevel = null; + $this->image_bevel_color1 = '#FFFFFF'; + $this->image_bevel_color2 = '#000000'; + $this->image_border = null; + $this->image_border_color = '#FFFFFF'; + $this->image_border_opacity = 100; + $this->image_border_transparent = null; + $this->image_frame = null; + $this->image_frame_colors = '#FFFFFF #999999 #666666 #000000'; + $this->image_frame_opacity = 100; + + $this->forbidden = array(); + $this->allowed = array( + 'application/arj', + 'application/excel', + 'application/gnutar', + 'application/mspowerpoint', + 'application/msword', + 'application/octet-stream', + 'application/onenote', + 'application/pdf', + 'application/plain', + 'application/postscript', + 'application/powerpoint', + 'application/rar', + 'application/rtf', + 'application/vnd.ms-excel', + 'application/vnd.ms-excel.addin.macroEnabled.12', + 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'application/vnd.ms-excel.template.macroEnabled.12', + 'application/vnd.ms-office', + 'application/vnd.ms-officetheme', + 'application/vnd.ms-powerpoint', + 'application/vnd.ms-powerpoint.addin.macroEnabled.12', + 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'application/vnd.ms-powerpoint.slide.macroEnabled.12', + 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', + 'application/vnd.ms-powerpoint.template.macroEnabled.12', + 'application/vnd.ms-word', + 'application/vnd.ms-word.document.macroEnabled.12', + 'application/vnd.ms-word.template.macroEnabled.12', + 'application/vnd.oasis.opendocument.chart', + 'application/vnd.oasis.opendocument.database', + 'application/vnd.oasis.opendocument.formula', + 'application/vnd.oasis.opendocument.graphics', + 'application/vnd.oasis.opendocument.graphics-template', + 'application/vnd.oasis.opendocument.image', + 'application/vnd.oasis.opendocument.presentation', + 'application/vnd.oasis.opendocument.presentation-template', + 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.spreadsheet-template', + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.text-master', + 'application/vnd.oasis.opendocument.text-template', + 'application/vnd.oasis.opendocument.text-web', + 'application/vnd.openofficeorg.extension', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'application/vocaltec-media-file', + 'application/wordperfect', + 'application/haansoftxlsx', + 'application/x-bittorrent', + 'application/x-bzip', + 'application/x-bzip2', + 'application/x-compressed', + 'application/x-excel', + 'application/x-gzip', + 'application/x-latex', + 'application/x-midi', + 'application/xml', + 'application/x-msexcel', + 'application/x-rar', + 'application/x-rar-compressed', + 'application/x-rtf', + 'application/x-shockwave-flash', + 'application/x-sit', + 'application/x-stuffit', + 'application/x-troff-msvideo', + 'application/x-zip', + 'application/x-zip-compressed', + 'application/zip', + 'audio/*', + 'image/*', + 'multipart/x-gzip', + 'multipart/x-zip', + 'text/plain', + 'text/rtf', + 'text/richtext', + 'text/xml', + 'video/*', + 'text/csv', + 'text/x-c', + 'text/x-csv', + 'text/comma-separated-values', + 'text/x-comma-separated-values', + 'application/csv', + 'application/x-csv', + ); + + $this->mime_types = array( + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'png' => 'image/png', + 'bmp' => 'image/bmp', + 'flif' => 'image/flif', + 'flv' => 'video/x-flv', + 'js' => 'application/x-javascript', + 'json' => 'application/json', + 'tiff' => 'image/tiff', + 'css' => 'text/css', + 'xml' => 'application/xml', + 'doc' => 'application/msword', + 'xls' => 'application/vnd.ms-excel', + 'xlt' => 'application/vnd.ms-excel', + 'xlm' => 'application/vnd.ms-excel', + 'xld' => 'application/vnd.ms-excel', + 'xla' => 'application/vnd.ms-excel', + 'xlc' => 'application/vnd.ms-excel', + 'xlw' => 'application/vnd.ms-excel', + 'xll' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pps' => 'application/vnd.ms-powerpoint', + 'rtf' => 'application/rtf', + 'pdf' => 'application/pdf', + 'html' => 'text/html', + 'htm' => 'text/html', + 'php' => 'text/html', + 'txt' => 'text/plain', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mp3' => 'audio/mpeg3', + 'wav' => 'audio/wav', + 'aiff' => 'audio/aiff', + 'aif' => 'audio/aiff', + 'avi' => 'video/msvideo', + 'wmv' => 'video/x-ms-wmv', + 'mov' => 'video/quicktime', + 'zip' => 'application/zip', + 'tar' => 'application/x-tar', + 'swf' => 'application/x-shockwave-flash', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'oxt' => 'application/vnd.openofficeorg.extension', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'docm' => 'application/vnd.ms-word.document.macroEnabled.12', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'sldm' => 'application/vnd.ms-powerpoint.slide.macroEnabled.12', + 'thmx' => 'application/vnd.ms-officetheme', + 'onetoc' => 'application/onenote', + 'onetoc2' => 'application/onenote', + 'onetmp' => 'application/onenote', + 'onepkg' => 'application/onenote', + 'csv' => 'text/csv', + ); + + $this->blacklist = array( + 'php', + 'php7', + 'php6', + 'php5', + 'php4', + 'php3', + 'phtml', + 'pht', + 'phpt', + 'phtm', + 'phps', + 'inc', + 'pl', + 'py', + 'cgi', + 'asp', + 'js', + 'sh', + 'phar', + ); + + } + + /** + * Constructor, for PHP5+ + */ + function __construct($file, $lang = 'en_GB') { + $this->upload($file, $lang); + } + + /** + * Constructor, for PHP4. Checks if the file has been uploaded + * + * The constructor takes $_FILES['form_field'] array as argument + * where form_field is the form field name + * + * The constructor will check if the file has been uploaded in its temporary location, and + * accordingly will set {@link uploaded} (and {@link error} is an error occurred) + * + * If the file has been uploaded, the constructor will populate all the variables holding the upload + * information (none of the processing class variables are used here). + * You can have access to information about the file (name, size, MIME type...). + * + * + * Alternatively, you can set the first argument to be a local filename (string) + * This allows processing of a local file, as if the file was uploaded + * + * The optional second argument allows you to set the language for the error messages + * + * @access private + * @param array $file $_FILES['form_field'] + * or string $file Local filename + * @param string $lang Optional language code + */ + function upload($file, $lang = 'en_GB') { + + $this->version = '05/10/2021'; + + $this->file_src_name = ''; + $this->file_src_name_body = ''; + $this->file_src_name_ext = ''; + $this->file_src_mime = ''; + $this->file_src_size = ''; + $this->file_src_error = ''; + $this->file_src_pathname = ''; + $this->file_src_temp = ''; + + $this->file_dst_path = ''; + $this->file_dst_name = ''; + $this->file_dst_name_body = ''; + $this->file_dst_name_ext = ''; + $this->file_dst_pathname = ''; + + $this->image_src_x = null; + $this->image_src_y = null; + $this->image_src_bits = null; + $this->image_src_type = null; + $this->image_src_pixels = null; + $this->image_dst_x = 0; + $this->image_dst_y = 0; + $this->image_dst_type = ''; + + $this->uploaded = true; + $this->no_upload_check = false; + $this->processed = false; + $this->error = ''; + $this->log = ''; + $this->allowed = array(); + $this->forbidden = array(); + $this->file_is_image = false; + $this->init(); + $info = null; + $mime_from_browser = null; + + // sets default language + $this->translation = array(); + $this->translation['file_error'] = 'File error. Please try again.'; + $this->translation['local_file_missing'] = 'Local file doesn\'t exist.'; + $this->translation['local_file_not_readable'] = 'Local file is not readable.'; + $this->translation['uploaded_too_big_ini'] = 'File upload error (the uploaded file exceeds the upload_max_filesize directive in php.ini).'; + $this->translation['uploaded_too_big_html'] = 'File upload error (the uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the html form).'; + $this->translation['uploaded_partial'] = 'File upload error (the uploaded file was only partially uploaded).'; + $this->translation['uploaded_missing'] = 'File upload error (no file was uploaded).'; + $this->translation['uploaded_no_tmp_dir'] = 'File upload error (missing a temporary folder).'; + $this->translation['uploaded_cant_write'] = 'File upload error (failed to write file to disk).'; + $this->translation['uploaded_err_extension'] = 'File upload error (file upload stopped by extension).'; + $this->translation['uploaded_unknown'] = 'File upload error (unknown error code).'; + $this->translation['try_again'] = 'File upload error. Please try again.'; + $this->translation['file_too_big'] = 'File too big.'; + $this->translation['no_mime'] = 'MIME type can\'t be detected.'; + $this->translation['incorrect_file'] = 'Incorrect type of file.'; + $this->translation['image_too_wide'] = 'Image too wide.'; + $this->translation['image_too_narrow'] = 'Image too narrow.'; + $this->translation['image_too_high'] = 'Image too tall.'; + $this->translation['image_too_short'] = 'Image too short.'; + $this->translation['ratio_too_high'] = 'Image ratio too high (image too wide).'; + $this->translation['ratio_too_low'] = 'Image ratio too low (image too high).'; + $this->translation['too_many_pixels'] = 'Image has too many pixels.'; + $this->translation['not_enough_pixels'] = 'Image has not enough pixels.'; + $this->translation['file_not_uploaded'] = 'File not uploaded. Can\'t carry on a process.'; + $this->translation['already_exists'] = '%s already exists. Please change the file name.'; + $this->translation['temp_file_missing'] = 'No correct temp source file. Can\'t carry on a process.'; + $this->translation['source_missing'] = 'No correct uploaded source file. Can\'t carry on a process.'; + $this->translation['destination_dir'] = 'Destination directory can\'t be created. Can\'t carry on a process.'; + $this->translation['destination_dir_missing'] = 'Destination directory doesn\'t exist. Can\'t carry on a process.'; + $this->translation['destination_path_not_dir'] = 'Destination path is not a directory. Can\'t carry on a process.'; + $this->translation['destination_dir_write'] = 'Destination directory can\'t be made writeable. Can\'t carry on a process.'; + $this->translation['destination_path_write'] = 'Destination path is not a writeable. Can\'t carry on a process.'; + $this->translation['temp_file'] = 'Can\'t create the temporary file. Can\'t carry on a process.'; + $this->translation['source_not_readable'] = 'Source file is not readable. Can\'t carry on a process.'; + $this->translation['no_create_support'] = 'No create from %s support.'; + $this->translation['create_error'] = 'Error in creating %s image from source.'; + $this->translation['source_invalid'] = 'Can\'t read image source. Not an image?.'; + $this->translation['gd_missing'] = 'GD doesn\'t seem to be present.'; + $this->translation['watermark_no_create_support'] = 'No create from %s support, can\'t read watermark.'; + $this->translation['watermark_create_error'] = 'No %s read support, can\'t create watermark.'; + $this->translation['watermark_invalid'] = 'Unknown image format, can\'t read watermark.'; + $this->translation['file_create'] = 'No %s create support.'; + $this->translation['no_conversion_type'] = 'No conversion type defined.'; + $this->translation['copy_failed'] = 'Error copying file on the server. copy() failed.'; + $this->translation['reading_failed'] = 'Error reading the file.'; + + // determines the language + $this->lang = $lang; + if ($this->lang != 'en_GB' && file_exists(dirname(__FILE__).'/lang') && file_exists(dirname(__FILE__).'/lang/class.upload.' . $lang . '.php')) { + $translation = null; + include(dirname(__FILE__).'/lang/class.upload.' . $lang . '.php'); + if (is_array($translation)) { + $this->translation = array_merge($this->translation, $translation); + } else { + $this->lang = 'en_GB'; + } + } + + + // determines the supported MIME types, and matching image format + $this->image_supported = array(); + if ($this->gdversion()) { + if (imagetypes() & IMG_GIF) { + $this->image_supported['image/gif'] = 'gif'; + } + if (imagetypes() & IMG_JPG) { + $this->image_supported['image/jpg'] = 'jpg'; + $this->image_supported['image/jpeg'] = 'jpg'; + $this->image_supported['image/pjpeg'] = 'jpg'; + } + if (imagetypes() & IMG_PNG) { + $this->image_supported['image/png'] = 'png'; + $this->image_supported['image/x-png'] = 'png'; + } + if (imagetypes() & IMG_WEBP) { + $this->image_supported['image/webp'] = 'webp'; + $this->image_supported['image/x-webp'] = 'webp'; + } + if (imagetypes() & IMG_WBMP) { + $this->image_supported['image/bmp'] = 'bmp'; + $this->image_supported['image/x-ms-bmp'] = 'bmp'; + $this->image_supported['image/x-windows-bmp'] = 'bmp'; + } + } + + // display some system information + if (empty($this->log)) { + $this->log .= 'system information
'; + if ($this->function_enabled('ini_get_all')) { + $inis = ini_get_all(); + $open_basedir = (array_key_exists('open_basedir', $inis) && array_key_exists('local_value', $inis['open_basedir']) && !empty($inis['open_basedir']['local_value'])) ? $inis['open_basedir']['local_value'] : false; + } else { + $open_basedir = false; + } + $gd = $this->gdversion() ? $this->gdversion(true) : 'GD not present'; + $supported = trim((in_array('png', $this->image_supported) ? 'png' : '') . ' ' . + (in_array('webp', $this->image_supported) ? 'webp' : '') . ' ' . + (in_array('jpg', $this->image_supported) ? 'jpg' : '') . ' ' . + (in_array('gif', $this->image_supported) ? 'gif' : '') . ' ' . + (in_array('bmp', $this->image_supported) ? 'bmp' : '')); + $this->log .= '- class version : ' . $this->version . '
'; + $this->log .= '- operating system : ' . PHP_OS . '
'; + $this->log .= '- PHP version : ' . PHP_VERSION . '
'; + $this->log .= '- GD version : ' . $gd . '
'; + $this->log .= '- supported image types : ' . (!empty($supported) ? $supported : 'none') . '
'; + $this->log .= '- open_basedir : ' . (!empty($open_basedir) ? $open_basedir : 'no restriction') . '
'; + $this->log .= '- upload_max_filesize : ' . $this->file_max_size_raw . ' (' . $this->file_max_size . ' bytes)
'; + $this->log .= '- language : ' . $this->lang . '
'; + } + + if (!$file) { + $this->uploaded = false; + $this->error = $this->translate('file_error'); + } + + // check if we sent a local filename or a PHP stream rather than a $_FILE element + if (!is_array($file)) { + if (empty($file)) { + $this->uploaded = false; + $this->error = $this->translate('file_error'); + } else { + $file = (string) $file; + if (substr($file, 0, 4) == 'php:' || substr($file, 0, 5) == 'data:' || substr($file, 0, 7) == 'base64:') { + $data = null; + + // this is a PHP stream, i.e.not uploaded + if (substr($file, 0, 4) == 'php:') { + $file = preg_replace('/^php:(.*)/i', '$1', $file); + if (!$file) $file = $_SERVER['HTTP_X_FILE_NAME']; + if (!$file) $file = 'unknown'; + $data = file_get_contents('php://input'); + $this->log .= 'source is a PHP stream ' . $file . ' of length ' . strlen($data) . '
'; + + // this is the raw file data, base64-encoded, i.e.not uploaded + } else if (substr($file, 0, 7) == 'base64:') { + $data = base64_decode(preg_replace('/^base64:(.*)/i', '$1', $file)); + $file = 'base64'; + $this->log .= 'source is a base64 string of length ' . strlen($data) . '
'; + + // this is the raw file data, base64-encoded, i.e.not uploaded + } else if (substr($file, 0, 5) == 'data:' && strpos($file, 'base64,') !== false) { + $data = base64_decode(preg_replace('/^data:.*base64,(.*)/i', '$1', $file)); + $file = 'base64'; + $this->log .= 'source is a base64 data string of length ' . strlen($data) . '
'; + + // this is the raw file data, i.e.not uploaded + } else if (substr($file, 0, 5) == 'data:') { + $data = preg_replace('/^data:(.*)/i', '$1', $file); + $file = 'data'; + $this->log .= 'source is a data string of length ' . strlen($data) . '
'; + } + + if (!$data) { + $this->log .= '- source is empty!
'; + $this->uploaded = false; + $this->error = $this->translate('source_invalid'); + } + + $this->no_upload_check = true; + + if ($this->uploaded) { + $this->log .= '- requires a temp file ... '; + $hash = $this->temp_dir() . md5($file . rand(1, 1000)); + if ($data && file_put_contents($hash, $data)) { + $this->file_src_pathname = $hash; + $this->log .= ' file created
'; + $this->log .= '    temp file is: ' . $this->file_src_pathname . '
'; + } else { + $this->log .= ' failed
'; + $this->uploaded = false; + $this->error = $this->translate('temp_file'); + } + } + + if ($this->uploaded) { + $this->file_src_name = $file; + $this->log .= '- local file OK
'; + preg_match('/\.([^\.]*$)/', $this->file_src_name, $extension); + if (is_array($extension) && sizeof($extension) > 0) { + $this->file_src_name_ext = strtolower($extension[1]); + $this->file_src_name_body = substr($this->file_src_name, 0, ((strlen($this->file_src_name) - strlen($this->file_src_name_ext)))-1); + } else { + $this->file_src_name_ext = ''; + $this->file_src_name_body = $this->file_src_name; + } + $this->file_src_size = (file_exists($this->file_src_pathname) ? filesize($this->file_src_pathname) : 0); + } + $this->file_src_error = 0; + + } else { + // this is a local filename, i.e.not uploaded + $this->log .= 'source is a local file ' . $file . '
'; + $this->no_upload_check = true; + + if ($this->uploaded && !file_exists($file)) { + $this->uploaded = false; + $this->error = $this->translate('local_file_missing'); + } + + if ($this->uploaded && !is_readable($file)) { + $this->uploaded = false; + $this->error = $this->translate('local_file_not_readable'); + } + + if ($this->uploaded) { + $this->file_src_pathname = $file; + $this->file_src_name = basename($file); + $this->log .= '- local file OK
'; + preg_match('/\.([^\.]*$)/', $this->file_src_name, $extension); + if (is_array($extension) && sizeof($extension) > 0) { + $this->file_src_name_ext = strtolower($extension[1]); + $this->file_src_name_body = substr($this->file_src_name, 0, ((strlen($this->file_src_name) - strlen($this->file_src_name_ext)))-1); + } else { + $this->file_src_name_ext = ''; + $this->file_src_name_body = $this->file_src_name; + } + $this->file_src_size = (file_exists($this->file_src_pathname) ? filesize($this->file_src_pathname) : 0); + } + $this->file_src_error = 0; + } + } + } else { + // this is an element from $_FILE, i.e. an uploaded file + $this->log .= 'source is an uploaded file
'; + if ($this->uploaded) { + $this->file_src_error = trim((int) $file['error']); + switch($this->file_src_error) { + case UPLOAD_ERR_OK: + // all is OK + $this->log .= '- upload OK
'; + break; + case UPLOAD_ERR_INI_SIZE: + $this->uploaded = false; + $this->error = $this->translate('uploaded_too_big_ini'); + break; + case UPLOAD_ERR_FORM_SIZE: + $this->uploaded = false; + $this->error = $this->translate('uploaded_too_big_html'); + break; + case UPLOAD_ERR_PARTIAL: + $this->uploaded = false; + $this->error = $this->translate('uploaded_partial'); + break; + case UPLOAD_ERR_NO_FILE: + $this->uploaded = false; + $this->error = $this->translate('uploaded_missing'); + break; + case @UPLOAD_ERR_NO_TMP_DIR: + $this->uploaded = false; + $this->error = $this->translate('uploaded_no_tmp_dir'); + break; + case @UPLOAD_ERR_CANT_WRITE: + $this->uploaded = false; + $this->error = $this->translate('uploaded_cant_write'); + break; + case @UPLOAD_ERR_EXTENSION: + $this->uploaded = false; + $this->error = $this->translate('uploaded_err_extension'); + break; + default: + $this->uploaded = false; + $this->error = $this->translate('uploaded_unknown') . ' ('.$this->file_src_error.')'; + } + } + + if ($this->uploaded) { + $this->file_src_pathname = (string) $file['tmp_name']; + $this->file_src_name = (string) $file['name']; + if ($this->file_src_name == '') { + $this->uploaded = false; + $this->error = $this->translate('try_again'); + } + } + + if ($this->uploaded) { + $this->log .= '- file name OK
'; + preg_match('/\.([^\.]*$)/', $this->file_src_name, $extension); + if (is_array($extension) && sizeof($extension) > 0) { + $this->file_src_name_ext = strtolower($extension[1]); + $this->file_src_name_body = substr($this->file_src_name, 0, ((strlen($this->file_src_name) - strlen($this->file_src_name_ext)))-1); + } else { + $this->file_src_name_ext = ''; + $this->file_src_name_body = $this->file_src_name; + } + $this->file_src_size = (int) $file['size']; + $mime_from_browser = (string) $file['type']; + } + } + + if ($this->uploaded) { + $this->log .= 'determining MIME type
'; + $this->file_src_mime = null; + + // checks MIME type with Fileinfo PECL extension + if (!$this->file_src_mime || !is_string($this->file_src_mime) || empty($this->file_src_mime) || strpos($this->file_src_mime, '/') === false) { + if ($this->mime_fileinfo) { + $this->log .= '- Checking MIME type with Fileinfo PECL extension
'; + if ($this->function_enabled('finfo_open')) { + $path = null; + if ($this->mime_fileinfo !== '') { + if ($this->mime_fileinfo === true) { + if (getenv('MAGIC') === false) { + if (substr(PHP_OS, 0, 3) == 'WIN') { + $path = realpath(ini_get('extension_dir') . '/../') . '/extras/magic'; + $this->log .= '    MAGIC path defaults to ' . $path . '
'; + } + } else { + $path = getenv('MAGIC'); + $this->log .= '    MAGIC path is set to ' . $path . ' from MAGIC variable
'; + } + } else { + $path = $this->mime_fileinfo; + $this->log .= '    MAGIC path is set to ' . $path . '
'; + } + } + if ($path) { + $f = @finfo_open(FILEINFO_MIME, $path); + } else { + $this->log .= '    MAGIC path will not be used
'; + $f = @finfo_open(FILEINFO_MIME); + } + if (is_resource($f)) { + $mime = finfo_file($f, realpath($this->file_src_pathname)); + finfo_close($f); + $this->file_src_mime = $mime; + $this->log .= '    MIME type detected as ' . $this->file_src_mime . ' by Fileinfo PECL extension
'; + if (preg_match("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", $this->file_src_mime)) { + $this->file_src_mime = preg_replace("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", '$1/$2', $this->file_src_mime); + $this->log .= '- MIME validated as ' . $this->file_src_mime . '
'; + } else { + $this->file_src_mime = null; + } + } else { + $this->log .= '    Fileinfo PECL extension failed (finfo_open)
'; + } + } elseif (@class_exists('finfo', false)) { + $f = new finfo( FILEINFO_MIME ); + if ($f) { + $this->file_src_mime = $f->file(realpath($this->file_src_pathname)); + $this->log .= '- MIME type detected as ' . $this->file_src_mime . ' by Fileinfo PECL extension
'; + if (preg_match("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", $this->file_src_mime)) { + $this->file_src_mime = preg_replace("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", '$1/$2', $this->file_src_mime); + $this->log .= '- MIME validated as ' . $this->file_src_mime . '
'; + } else { + $this->file_src_mime = null; + } + } else { + $this->log .= '    Fileinfo PECL extension failed (finfo)
'; + } + } else { + $this->log .= '    Fileinfo PECL extension not available
'; + } + } else { + $this->log .= '- Fileinfo PECL extension deactivated
'; + } + } + + // checks MIME type with shell if unix access is authorized + if (!$this->file_src_mime || !is_string($this->file_src_mime) || empty($this->file_src_mime) || strpos($this->file_src_mime, '/') === false) { + if ($this->mime_file) { + $this->log .= '- Checking MIME type with UNIX file() command
'; + if (substr(PHP_OS, 0, 3) != 'WIN') { + if ($this->function_enabled('exec') && $this->function_enabled('escapeshellarg')) { + if (strlen($mime = @exec("file -bi ".escapeshellarg($this->file_src_pathname))) != 0) { + $this->file_src_mime = trim($mime); + $this->log .= '    MIME type detected as ' . $this->file_src_mime . ' by UNIX file() command
'; + if (preg_match("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", $this->file_src_mime)) { + $this->file_src_mime = preg_replace("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", '$1/$2', $this->file_src_mime); + $this->log .= '- MIME validated as ' . $this->file_src_mime . '
'; + } else { + $this->file_src_mime = null; + } + } else { + $this->log .= '    UNIX file() command failed
'; + } + } else { + $this->log .= '    PHP exec() function is disabled
'; + } + } else { + $this->log .= '    UNIX file() command not availabled
'; + } + } else { + $this->log .= '- UNIX file() command is deactivated
'; + } + } + + // checks MIME type with mime_magic + if (!$this->file_src_mime || !is_string($this->file_src_mime) || empty($this->file_src_mime) || strpos($this->file_src_mime, '/') === false) { + if ($this->mime_magic) { + $this->log .= '- Checking MIME type with mime.magic file (mime_content_type())
'; + if ($this->function_enabled('mime_content_type')) { + $this->file_src_mime = mime_content_type($this->file_src_pathname); + $this->log .= '    MIME type detected as ' . $this->file_src_mime . ' by mime_content_type()
'; + if (preg_match("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", $this->file_src_mime)) { + $this->file_src_mime = preg_replace("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", '$1/$2', $this->file_src_mime); + $this->log .= '- MIME validated as ' . $this->file_src_mime . '
'; + } else { + $this->file_src_mime = null; + } + } else { + $this->log .= '    mime_content_type() is not available
'; + } + } else { + $this->log .= '- mime.magic file (mime_content_type()) is deactivated
'; + } + } + + // checks MIME type with getimagesize() + if (!$this->file_src_mime || !is_string($this->file_src_mime) || empty($this->file_src_mime) || strpos($this->file_src_mime, '/') === false) { + if ($this->mime_getimagesize) { + $this->log .= '- Checking MIME type with getimagesize()
'; + $info = getimagesize($this->file_src_pathname); + if (is_array($info) && array_key_exists('mime', $info)) { + $this->file_src_mime = trim($info['mime']); + if (empty($this->file_src_mime)) { + $this->log .= '    MIME empty, guessing from type
'; + $mime = (is_array($info) && array_key_exists(2, $info) ? $info[2] : null); // 1 = GIF, 2 = JPG, 3 = PNG + $this->file_src_mime = ($mime==IMAGETYPE_GIF ? 'image/gif' : + ($mime==IMAGETYPE_JPEG ? 'image/jpeg' : + ($mime==IMAGETYPE_PNG ? 'image/png' : + ($mime==IMAGETYPE_WEBP ? 'image/webp' : + ($mime==IMAGETYPE_BMP ? 'image/bmp' : null))))); + } + $this->log .= '    MIME type detected as ' . $this->file_src_mime . ' by PHP getimagesize() function
'; + if (preg_match("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", $this->file_src_mime)) { + $this->file_src_mime = preg_replace("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", '$1/$2', $this->file_src_mime); + $this->log .= '- MIME validated as ' . $this->file_src_mime . '
'; + } else { + $this->file_src_mime = null; + } + } else { + $this->log .= '    getimagesize() failed
'; + } + } else { + $this->log .= '- getimagesize() is deactivated
'; + } + } + + // default to MIME from browser (or Flash) + if (!empty($mime_from_browser) && !$this->file_src_mime || !is_string($this->file_src_mime) || empty($this->file_src_mime)) { + $this->file_src_mime =$mime_from_browser; + $this->log .= '- MIME type detected as ' . $this->file_src_mime . ' by browser
'; + if (preg_match("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", $this->file_src_mime)) { + $this->file_src_mime = preg_replace("/^([\.\w-]+)\/([\.\w-]+)(.*)$/i", '$1/$2', $this->file_src_mime); + $this->log .= '- MIME validated as ' . $this->file_src_mime . '
'; + } else { + $this->file_src_mime = null; + } + } + + // we need to work some magic if we upload via Flash + if ($this->file_src_mime == 'application/octet-stream' || !$this->file_src_mime || !is_string($this->file_src_mime) || empty($this->file_src_mime) || strpos($this->file_src_mime, '/') === false) { + if ($this->file_src_mime == 'application/octet-stream') $this->log .= '- Flash may be rewriting MIME as application/octet-stream
'; + $this->log .= '- Try to guess MIME type from file extension (' . $this->file_src_name_ext . '): '; + if (array_key_exists($this->file_src_name_ext, $this->mime_types)) $this->file_src_mime = $this->mime_types[$this->file_src_name_ext]; + if ($this->file_src_mime == 'application/octet-stream') { + $this->log .= 'doesn\'t look like anything known
'; + } else { + $this->log .= 'MIME type set to ' . $this->file_src_mime . '
'; + } + } + + if (!$this->file_src_mime || !is_string($this->file_src_mime) || empty($this->file_src_mime) || strpos($this->file_src_mime, '/') === false) { + $this->log .= '- MIME type couldn\'t be detected! (' . (string) $this->file_src_mime . ')
'; + } + + // determine whether the file is an image + if ($this->file_src_mime && is_string($this->file_src_mime) && !empty($this->file_src_mime) && array_key_exists($this->file_src_mime, $this->image_supported)) { + $this->file_is_image = true; + $this->image_src_type = $this->image_supported[$this->file_src_mime]; + } + + // if the file is an image, we gather some useful data + if ($this->file_is_image) { + if ($h = fopen($this->file_src_pathname, 'r')) { + fclose($h); + $info = getimagesize($this->file_src_pathname); + if (is_array($info)) { + $this->image_src_x = $info[0]; + $this->image_src_y = $info[1]; + $this->image_dst_x = $this->image_src_x; + $this->image_dst_y = $this->image_src_y; + $this->image_src_pixels = $this->image_src_x * $this->image_src_y; + $this->image_src_bits = array_key_exists('bits', $info) ? $info['bits'] : null; + } else { + $this->file_is_image = false; + $this->uploaded = false; + $this->log .= '- can\'t retrieve image information, image may have been tampered with
'; + $this->error = $this->translate('source_invalid'); + } + } else { + $this->log .= '- can\'t read source file directly. open_basedir restriction in place?
'; + } + } + + $this->log .= 'source variables
'; + $this->log .= '- You can use all these before calling process()
'; + $this->log .= '    file_src_name : ' . $this->file_src_name . '
'; + $this->log .= '    file_src_name_body : ' . $this->file_src_name_body . '
'; + $this->log .= '    file_src_name_ext : ' . $this->file_src_name_ext . '
'; + $this->log .= '    file_src_pathname : ' . $this->file_src_pathname . '
'; + $this->log .= '    file_src_mime : ' . $this->file_src_mime . '
'; + $this->log .= '    file_src_size : ' . $this->file_src_size . ' (max= ' . $this->file_max_size . ')
'; + $this->log .= '    file_src_error : ' . $this->file_src_error . '
'; + + if ($this->file_is_image) { + $this->log .= '- source file is an image
'; + $this->log .= '    image_src_x : ' . $this->image_src_x . '
'; + $this->log .= '    image_src_y : ' . $this->image_src_y . '
'; + $this->log .= '    image_src_pixels : ' . $this->image_src_pixels . '
'; + $this->log .= '    image_src_type : ' . $this->image_src_type . '
'; + $this->log .= '    image_src_bits : ' . $this->image_src_bits . '
'; + } + } + + } + + /** + * Returns the version of GD + * + * @access public + * @param boolean $full Optional flag to get precise version + * @return float GD version + */ + function gdversion($full = false) { + static $gd_version = null; + static $gd_full_version = null; + if ($gd_version === null) { + if ($this->function_enabled('gd_info')) { + $gd = gd_info(); + $gd = $gd["GD Version"]; + $regex = "/([\d\.]+)/i"; + } else { + ob_start(); + phpinfo(8); + $gd = ob_get_contents(); + ob_end_clean(); + $regex = "/\bgd\s+version\b[^\d\n\r]+?([\d\.]+)/i"; + } + if (preg_match($regex, $gd, $m)) { + $gd_full_version = (string) $m[1]; + $gd_version = (float) $m[1]; + } else { + $gd_full_version = 'none'; + $gd_version = 0; + } + } + if ($full) { + return $gd_full_version; + } else { + return $gd_version; + } + } + + /** + * Checks if a function is available + * + * @access private + * @param string $func Function name + * @return boolean Success + */ + function function_enabled($func) { + // cache the list of disabled functions + static $disabled = null; + if ($disabled === null) $disabled = array_map('trim', array_map('strtolower', explode(',', ini_get('disable_functions')))); + // cache the list of functions blacklisted by suhosin + static $blacklist = null; + if ($blacklist === null) $blacklist = extension_loaded('suhosin') ? array_map('trim', array_map('strtolower', explode(',', ini_get(' suhosin.executor.func.blacklist')))) : array(); + // checks if the function is really enabled + return (function_exists($func) && !in_array($func, $disabled) && !in_array($func, $blacklist)); + } + + /** + * Creates directories recursively + * + * @access private + * @param string $path Path to create + * @param integer $mode Optional permissions + * @return boolean Success + */ + function rmkdir($path, $mode = 0755) { + return is_dir($path) || ( $this->rmkdir(dirname($path), $mode) && $this->_mkdir($path, $mode) ); + } + + /** + * Creates directory + * + * @access private + * @param string $path Path to create + * @param integer $mode Optional permissions + * @return boolean Success + */ + function _mkdir($path, $mode = 0755) { + $old = umask(0); + $res = @mkdir($path, $mode); + umask($old); + return $res; + } + + /** + * Translate error messages + * + * @access private + * @param string $str Message to translate + * @param array $tokens Optional token values + * @return string Translated string + */ + function translate($str, $tokens = array()) { + if (array_key_exists($str, $this->translation)) $str = $this->translation[$str]; + if (is_array($tokens) && sizeof($tokens) > 0) $str = vsprintf($str, $tokens); + return $str; + } + + /** + * Returns the temp directory + * + * @access private + * @return string Temp directory string + */ + function temp_dir() { + $dir = ''; + if ($this->function_enabled('sys_get_temp_dir')) $dir = sys_get_temp_dir(); + if (!$dir && $tmp=getenv('TMP')) $dir = $tmp; + if (!$dir && $tmp=getenv('TEMP')) $dir = $tmp; + if (!$dir && $tmp=getenv('TMPDIR')) $dir = $tmp; + if (!$dir) { + $tmp = tempnam(__FILE__,''); + if (file_exists($tmp)) { + unlink($tmp); + $dir = dirname($tmp); + } + } + if (!$dir) return ''; + $slash = (strtolower(substr(PHP_OS, 0, 3)) === 'win' ? '\\' : '/'); + if (substr($dir, -1) != $slash) $dir = $dir . $slash; + return $dir; + } + + /** + * Sanitize a file name + * + * @access private + * @param string $filename File name + * @return string Sanitized file name + */ + function sanitize($filename) { + // remove HTML tags + $filename = strip_tags($filename); + // remove non-breaking spaces + $filename = preg_replace("#\x{00a0}#siu", ' ', $filename); + // remove illegal file system characters + $filename = str_replace(array_map('chr', range(0, 31)), '', $filename); + // remove dangerous characters for file names + $chars = array("?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "’", "%20", + "+", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}", "%", "+", "^", chr(0)); + $filename = str_replace($chars, '-', $filename); + // remove break/tabs/return carriage + $filename = preg_replace('/[\r\n\t -]+/', '-', $filename); + // convert some special letters + $convert = array('Þ' => 'TH', 'þ' => 'th', 'Ð' => 'DH', 'ð' => 'dh', 'ß' => 'ss', + 'Œ' => 'OE', 'œ' => 'oe', 'Æ' => 'AE', 'æ' => 'ae', 'µ' => 'u'); + $filename = strtr($filename, $convert); + // remove foreign accents by converting to HTML entities, and then remove the code + $filename = html_entity_decode( $filename, ENT_QUOTES, "utf-8" ); + $filename = htmlentities($filename, ENT_QUOTES, "utf-8"); + $filename = preg_replace("/(&)([a-z])([a-z]+;)/i", '$2', $filename); + // clean up, and remove repetitions + $filename = preg_replace('/_+/', '_', $filename); + $filename = preg_replace(array('/ +/', '/-+/'), '-', $filename); + $filename = preg_replace(array('/-*\.-*/', '/\.{2,}/'), '.', $filename); + // cut to 255 characters + $length = 255 - strlen($this->file_dst_name_ext) + 1; + $filename = extension_loaded('mbstring') ? mb_strcut($filename, 0, $length, mb_detect_encoding($filename)) : substr($filename, 0, $length); + // remove bad characters at start and end + $filename = trim($filename, '.-_'); + return $filename; + } + + /** + * Decodes colors + * + * @access private + * @param string $color Color string + * @return array RGB colors + */ + function getcolors($color) { + $color = str_replace('#', '', $color); + if (strlen($color) == 3) $color = str_repeat(substr($color, 0, 1), 2) . str_repeat(substr($color, 1, 1), 2) . str_repeat(substr($color, 2, 1), 2); + $r = sscanf($color, "%2x%2x%2x"); + $red = (is_array($r) && array_key_exists(0, $r) && is_numeric($r[0]) ? $r[0] : 0); + $green = (is_array($r) && array_key_exists(1, $r) && is_numeric($r[1]) ? $r[1] : 0); + $blue = (is_array($r) && array_key_exists(2, $r) && is_numeric($r[2]) ? $r[2] : 0); + return array($red, $green, $blue); + } + + /** + * Decodes sizes + * + * @access private + * @param string $size Size in bytes, or shorthand byte options + * @return integer Size in bytes + */ + function getsize($size) { + if ($size === null) return null; + $last = is_string($size) ? strtolower(substr($size, -1)) : null; + $size = (int) $size; + switch($last) { + case 'g': + $size *= 1024; + case 'm': + $size *= 1024; + case 'k': + $size *= 1024; + } + return $size; + } + + /** + * Decodes offsets + * + * @access private + * @param misc $offsets Offsets, as an integer, a string or an array + * @param integer $x Reference picture width + * @param integer $y Reference picture height + * @param boolean $round Round offsets before returning them + * @param boolean $negative Allow negative offsets to be returned + * @return array Array of four offsets (TRBL) + */ + function getoffsets($offsets, $x, $y, $round = true, $negative = true) { + if (!is_array($offsets)) $offsets = explode(' ', $offsets); + if (sizeof($offsets) == 4) { + $ct = $offsets[0]; $cr = $offsets[1]; $cb = $offsets[2]; $cl = $offsets[3]; + } else if (sizeof($offsets) == 2) { + $ct = $offsets[0]; $cr = $offsets[1]; $cb = $offsets[0]; $cl = $offsets[1]; + } else { + $ct = $offsets[0]; $cr = $offsets[0]; $cb = $offsets[0]; $cl = $offsets[0]; + } + if (strpos($ct, '%')>0) $ct = $y * (str_replace('%','',$ct) / 100); + if (strpos($cr, '%')>0) $cr = $x * (str_replace('%','',$cr) / 100); + if (strpos($cb, '%')>0) $cb = $y * (str_replace('%','',$cb) / 100); + if (strpos($cl, '%')>0) $cl = $x * (str_replace('%','',$cl) / 100); + if (strpos($ct, 'px')>0) $ct = str_replace('px','',$ct); + if (strpos($cr, 'px')>0) $cr = str_replace('px','',$cr); + if (strpos($cb, 'px')>0) $cb = str_replace('px','',$cb); + if (strpos($cl, 'px')>0) $cl = str_replace('px','',$cl); + $ct = (int) $ct; $cr = (int) $cr; $cb = (int) $cb; $cl = (int) $cl; + if ($round) { + $ct = round($ct); + $cr = round($cr); + $cb = round($cb); + $cl = round($cl); + } + if (!$negative) { + if ($ct < 0) $ct = 0; + if ($cr < 0) $cr = 0; + if ($cb < 0) $cb = 0; + if ($cl < 0) $cl = 0; + } + return array($ct, $cr, $cb, $cl); + } + + /** + * Creates a container image + * + * @access private + * @param integer $x Width + * @param integer $y Height + * @param boolean $fill Optional flag to draw the background color or not + * @param boolean $trsp Optional flag to set the background to be transparent + * @return resource Container image + */ + function imagecreatenew($x, $y, $fill = true, $trsp = false) { + if ($x < 1) $x = 1; if ($y < 1) $y = 1; + if ($this->gdversion() >= 2 && !$this->image_is_palette) { + // create a true color image + $dst_im = imagecreatetruecolor($x, $y); + // this preserves transparency in PNG and WEBP, in true color + if (empty($this->image_background_color) || $trsp) { + imagealphablending($dst_im, false ); + imagefilledrectangle($dst_im, 0, 0, $x, $y, imagecolorallocatealpha($dst_im, 0, 0, 0, 127)); + } + } else { + // creates a palette image + $dst_im = imagecreate($x, $y); + // preserves transparency for palette images, if the original image has transparency + if (($fill && $this->image_is_transparent && empty($this->image_background_color)) || $trsp) { + imagefilledrectangle($dst_im, 0, 0, $x, $y, $this->image_transparent_color); + imagecolortransparent($dst_im, $this->image_transparent_color); + } + } + // fills with background color if any is set + if ($fill && !empty($this->image_background_color) && !$trsp) { + list($red, $green, $blue) = $this->getcolors($this->image_background_color); + $background_color = imagecolorallocate($dst_im, $red, $green, $blue); + imagefilledrectangle($dst_im, 0, 0, $x, $y, $background_color); + } + return $dst_im; + } + + + /** + * Transfers an image from the container to the destination image + * + * @access private + * @param resource $src_im Container image + * @param resource $dst_im Destination image + * @return resource Destination image + */ + function imagetransfer($src_im, $dst_im) { + $this->imageunset($dst_im); + $dst_im = & $src_im; + return $dst_im; + } + + /** + * Destroy GD ressource + * + * @access private + * @param resource $im Image + */ + function imageunset($im) { + if (is_resource($im)) { + imagedestroy($im); + } else if (is_object($im) && $im instanceOf \GdImage) { + unset($im); + } + } + + /** + * Merges two images + * + * If the output format is PNG or WEBP, then we do it pixel per pixel to retain the alpha channel + * + * @access private + * @param resource $dst_img Destination image + * @param resource $src_img Overlay image + * @param int $dst_x x-coordinate of destination point + * @param int $dst_y y-coordinate of destination point + * @param int $src_x x-coordinate of source point + * @param int $src_y y-coordinate of source point + * @param int $src_w Source width + * @param int $src_h Source height + * @param int $pct Optional percentage of the overlay, between 0 and 100 (default: 100) + * @return resource Destination image + */ + function imagecopymergealpha(&$dst_im, &$src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct = 0) { + $dst_x = (int) $dst_x; + $dst_y = (int) $dst_y; + $src_x = (int) $src_x; + $src_y = (int) $src_y; + $src_w = (int) $src_w; + $src_h = (int) $src_h; + $pct = (int) $pct; + $dst_w = imagesx($dst_im); + $dst_h = imagesy($dst_im); + + for ($y = $src_y; $y < $src_h; $y++) { + for ($x = $src_x; $x < $src_w; $x++) { + + if ($x + $dst_x >= 0 && $x + $dst_x < $dst_w && $x + $src_x >= 0 && $x + $src_x < $src_w + && $y + $dst_y >= 0 && $y + $dst_y < $dst_h && $y + $src_y >= 0 && $y + $src_y < $src_h) { + + $dst_pixel = imagecolorsforindex($dst_im, imagecolorat($dst_im, $x + $dst_x, $y + $dst_y)); + $src_pixel = imagecolorsforindex($src_im, imagecolorat($src_im, $x + $src_x, $y + $src_y)); + + $src_alpha = 1 - ($src_pixel['alpha'] / 127); + $dst_alpha = 1 - ($dst_pixel['alpha'] / 127); + $opacity = $src_alpha * $pct / 100; + if ($dst_alpha >= $opacity) $alpha = $dst_alpha; + if ($dst_alpha < $opacity) $alpha = $opacity; + if ($alpha > 1) $alpha = 1; + + if ($opacity > 0) { + $dst_red = round(( ($dst_pixel['red'] * $dst_alpha * (1 - $opacity)) ) ); + $dst_green = round(( ($dst_pixel['green'] * $dst_alpha * (1 - $opacity)) ) ); + $dst_blue = round(( ($dst_pixel['blue'] * $dst_alpha * (1 - $opacity)) ) ); + $src_red = round((($src_pixel['red'] * $opacity)) ); + $src_green = round((($src_pixel['green'] * $opacity)) ); + $src_blue = round((($src_pixel['blue'] * $opacity)) ); + $red = round(($dst_red + $src_red ) / ($dst_alpha * (1 - $opacity) + $opacity)); + $green = round(($dst_green + $src_green) / ($dst_alpha * (1 - $opacity) + $opacity)); + $blue = round(($dst_blue + $src_blue ) / ($dst_alpha * (1 - $opacity) + $opacity)); + if ($red > 255) $red = 255; + if ($green > 255) $green = 255; + if ($blue > 255) $blue = 255; + $alpha = round((1 - $alpha) * 127); + $color = imagecolorallocatealpha($dst_im, $red, $green, $blue, $alpha); + imagesetpixel($dst_im, $x + $dst_x, $y + $dst_y, $color); + } + } + } + } + return true; + } + + + + /** + * Actually uploads the file, and act on it according to the set processing class variables + * + * This function copies the uploaded file to the given location, eventually performing actions on it. + * Typically, you can call {@link process} several times for the same file, + * for instance to create a resized image and a thumbnail of the same file. + * The original uploaded file remains intact in its temporary location, so you can use {@link process} several times. + * You will be able to delete the uploaded file with {@link clean} when you have finished all your {@link process} calls. + * + * According to the processing class variables set in the calling file, the file can be renamed, + * and if it is an image, can be resized or converted. + * + * When the processing is completed, and the file copied to its new location, the + * processing class variables will be reset to their default value. + * This allows you to set new properties, and perform another {@link process} on the same uploaded file + * + * If the function is called with a null or empty argument, then it will return the content of the picture + * + * It will set {@link processed} (and {@link error} is an error occurred) + * + * @access public + * @param string $server_path Optional path location of the uploaded file, with an ending slash + * @return string Optional content of the image + */ + function process($server_path = null) { + $this->error = ''; + $this->processed = true; + $return_mode = false; + $return_content = null; + + // clean up dst variables + $this->file_dst_path = ''; + $this->file_dst_pathname = ''; + $this->file_dst_name = ''; + $this->file_dst_name_body = ''; + $this->file_dst_name_ext = ''; + + // clean up some parameters + $this->file_max_size = $this->getsize($this->file_max_size); + $this->jpeg_size = $this->getsize($this->jpeg_size); + + // copy some variables as we need to keep them clean + $file_src_name = $this->file_src_name; + $file_src_name_body = $this->file_src_name_body; + $file_src_name_ext = $this->file_src_name_ext; + + if (!$this->uploaded) { + $this->error = $this->translate('file_not_uploaded'); + $this->processed = false; + } + + if ($this->processed) { + if (empty($server_path) || is_null($server_path)) { + $this->log .= 'process file and return the content
'; + $return_mode = true; + } else { + if(strtolower(substr(PHP_OS, 0, 3)) === 'win') { + if (substr($server_path, -1, 1) != '\\') $server_path = $server_path . '\\'; + } else { + if (substr($server_path, -1, 1) != '/') $server_path = $server_path . '/'; + } + $this->log .= 'process file to ' . $server_path . '
'; + } + } + + if ($this->processed) { + // checks file max size + if ($this->file_src_size > $this->file_max_size) { + $this->processed = false; + $this->error = $this->translate('file_too_big') . ' : ' . $this->file_src_size . ' > ' . $this->file_max_size; + } else { + $this->log .= '- file size OK
'; + } + } + + if ($this->processed) { + // if we have an image without extension, set it + if ($this->file_force_extension && $this->file_is_image && !$this->file_src_name_ext) $file_src_name_ext = $this->image_src_type; + // turn dangerous scripts into text files + if ($this->no_script) { + // if the file has no extension, we try to guess it from the MIME type + if ($this->file_force_extension && empty($file_src_name_ext)) { + if ($key = array_search($this->file_src_mime, $this->mime_types)) { + $file_src_name_ext = $key; + $file_src_name = $file_src_name_body . '.' . $file_src_name_ext; + $this->log .= '- file renamed as ' . $file_src_name_body . '.' . $file_src_name_ext . '!
'; + } + } + // if the file is text based, or has a dangerous extension, we rename it as .txt + if ((((substr($this->file_src_mime, 0, 5) == 'text/' && $this->file_src_mime != 'text/rtf') || strpos($this->file_src_mime, 'javascript') !== false) && (substr($file_src_name, -4) != '.txt')) + || preg_match('/\.(' . implode('|', $this->blacklist) . ')$/i', $this->file_src_name) + || $this->file_force_extension && empty($file_src_name_ext)) { + $this->file_src_mime = 'text/plain'; + if ($this->file_src_name_ext) $file_src_name_body = $file_src_name_body . '.' . $this->file_src_name_ext; + $file_src_name_ext = 'txt'; + $file_src_name = $file_src_name_body . '.' . $file_src_name_ext; + $this->log .= '- script renamed as ' . $file_src_name_body . '.' . $file_src_name_ext . '!
'; + } + } + + if ($this->mime_check && empty($this->file_src_mime)) { + $this->processed = false; + $this->error = $this->translate('no_mime'); + } else if ($this->mime_check && !empty($this->file_src_mime) && strpos($this->file_src_mime, '/') !== false) { + list($m1, $m2) = explode('/', $this->file_src_mime); + $allowed = false; + // check wether the mime type is allowed + if (!is_array($this->allowed)) $this->allowed = array($this->allowed); + foreach($this->allowed as $k => $v) { + list($v1, $v2) = explode('/', $v); + if (($v1 == '*' && $v2 == '*') || ($v1 == $m1 && ($v2 == $m2 || $v2 == '*'))) { + $allowed = true; + break; + } + } + // check wether the mime type is forbidden + if (!is_array($this->forbidden)) $this->forbidden = array($this->forbidden); + foreach($this->forbidden as $k => $v) { + list($v1, $v2) = explode('/', $v); + if (($v1 == '*' && $v2 == '*') || ($v1 == $m1 && ($v2 == $m2 || $v2 == '*'))) { + $allowed = false; + break; + } + } + if (!$allowed) { + $this->processed = false; + $this->error = $this->translate('incorrect_file'); + } else { + $this->log .= '- file mime OK : ' . $this->file_src_mime . '
'; + } + } else { + $this->log .= '- file mime (not checked) : ' . $this->file_src_mime . '
'; + } + + // if the file is an image, we can check on its dimensions + // these checks are not available if open_basedir restrictions are in place + if ($this->file_is_image) { + if (is_numeric($this->image_src_x) && is_numeric($this->image_src_y)) { + $ratio = $this->image_src_x / $this->image_src_y; + if (!is_null($this->image_max_width) && $this->image_src_x > $this->image_max_width) { + $this->processed = false; + $this->error = $this->translate('image_too_wide'); + } + if (!is_null($this->image_min_width) && $this->image_src_x < $this->image_min_width) { + $this->processed = false; + $this->error = $this->translate('image_too_narrow'); + } + if (!is_null($this->image_max_height) && $this->image_src_y > $this->image_max_height) { + $this->processed = false; + $this->error = $this->translate('image_too_high'); + } + if (!is_null($this->image_min_height) && $this->image_src_y < $this->image_min_height) { + $this->processed = false; + $this->error = $this->translate('image_too_short'); + } + if (!is_null($this->image_max_ratio) && $ratio > $this->image_max_ratio) { + $this->processed = false; + $this->error = $this->translate('ratio_too_high'); + } + if (!is_null($this->image_min_ratio) && $ratio < $this->image_min_ratio) { + $this->processed = false; + $this->error = $this->translate('ratio_too_low'); + } + if (!is_null($this->image_max_pixels) && $this->image_src_pixels > $this->image_max_pixels) { + $this->processed = false; + $this->error = $this->translate('too_many_pixels'); + } + if (!is_null($this->image_min_pixels) && $this->image_src_pixels < $this->image_min_pixels) { + $this->processed = false; + $this->error = $this->translate('not_enough_pixels'); + } + } else { + $this->log .= '- no image properties available, can\'t enforce dimension checks : ' . $this->file_src_mime . '
'; + } + } + } + + if ($this->processed) { + $this->file_dst_path = $server_path; + + // repopulate dst variables from src + $this->file_dst_name = $file_src_name; + $this->file_dst_name_body = $file_src_name_body; + $this->file_dst_name_ext = $file_src_name_ext; + if ($this->file_overwrite) $this->file_auto_rename = false; + + if ($this->image_convert && $this->file_is_image) { // if we convert as an image + $this->file_dst_name_ext = $this->image_convert; + $this->log .= '- new file name ext : ' . $this->file_dst_name_ext . '
'; + } + if (!is_null($this->file_new_name_body)) { // rename file body + $this->file_dst_name_body = $this->file_new_name_body; + $this->log .= '- new file name body : ' . $this->file_new_name_body . '
'; + } + if (!is_null($this->file_new_name_ext)) { // rename file ext + $this->file_dst_name_ext = $this->file_new_name_ext; + $this->log .= '- new file name ext : ' . $this->file_new_name_ext . '
'; + } + if (!is_null($this->file_name_body_add)) { // append a string to the name + $this->file_dst_name_body = $this->file_dst_name_body . $this->file_name_body_add; + $this->log .= '- file name body append : ' . $this->file_name_body_add . '
'; + } + if (!is_null($this->file_name_body_pre)) { // prepend a string to the name + $this->file_dst_name_body = $this->file_name_body_pre . $this->file_dst_name_body; + $this->log .= '- file name body prepend : ' . $this->file_name_body_pre . '
'; + } + if ($this->file_safe_name) { // sanitize the name + $this->file_dst_name_body = $this->sanitize($this->file_dst_name_body); + $this->log .= '- file name safe format
'; + } + + $this->log .= '- destination variables
'; + if (empty($this->file_dst_path) || is_null($this->file_dst_path)) { + $this->log .= '    file_dst_path : n/a
'; + } else { + $this->log .= '    file_dst_path : ' . $this->file_dst_path . '
'; + } + $this->log .= '    file_dst_name_body : ' . $this->file_dst_name_body . '
'; + $this->log .= '    file_dst_name_ext : ' . $this->file_dst_name_ext . '
'; + + // set the destination file name + $this->file_dst_name = $this->file_dst_name_body . (!empty($this->file_dst_name_ext) ? '.' . $this->file_dst_name_ext : ''); + + if (!$return_mode) { + if (!$this->file_auto_rename) { + $this->log .= '- no auto_rename if same filename exists
'; + $this->file_dst_pathname = $this->file_dst_path . $this->file_dst_name; + } else { + $this->log .= '- checking for auto_rename
'; + $this->file_dst_pathname = $this->file_dst_path . $this->file_dst_name; + $body = $this->file_dst_name_body; + $ext = ''; + // if we have changed the extension, then we add our increment before + if ($file_src_name_ext != $this->file_src_name_ext) { + if (substr($this->file_dst_name_body, -1 - strlen($this->file_src_name_ext)) == '.' . $this->file_src_name_ext) { + $body = substr($this->file_dst_name_body, 0, strlen($this->file_dst_name_body) - 1 - strlen($this->file_src_name_ext)); + $ext = '.' . $this->file_src_name_ext; + } + } + $cpt = 1; + while (@file_exists($this->file_dst_pathname)) { + $this->file_dst_name_body = $body . '_' . $cpt . $ext; + $this->file_dst_name = $this->file_dst_name_body . (!empty($this->file_dst_name_ext) ? '.' . $this->file_dst_name_ext : ''); + $cpt++; + $this->file_dst_pathname = $this->file_dst_path . $this->file_dst_name; + } + if ($cpt>1) $this->log .= '    auto_rename to ' . $this->file_dst_name . '
'; + } + + $this->log .= '- destination file details
'; + $this->log .= '    file_dst_name : ' . $this->file_dst_name . '
'; + $this->log .= '    file_dst_pathname : ' . $this->file_dst_pathname . '
'; + + if ($this->file_overwrite) { + $this->log .= '- no overwrite checking
'; + } else { + if (@file_exists($this->file_dst_pathname)) { + $this->processed = false; + $this->error = $this->translate('already_exists', array($this->file_dst_name)); + } else { + $this->log .= '- ' . $this->file_dst_name . ' doesn\'t exist already
'; + } + } + } + } + + if ($this->processed) { + // if we have already moved the uploaded file, we use the temporary copy as source file, and check if it exists + if (!empty($this->file_src_temp)) { + $this->log .= '- use the temp file instead of the original file since it is a second process
'; + $this->file_src_pathname = $this->file_src_temp; + if (!file_exists($this->file_src_pathname)) { + $this->processed = false; + $this->error = $this->translate('temp_file_missing'); + } + // if we haven't a temp file, and that we do check on uploads, we use is_uploaded_file() + } else if (!$this->no_upload_check) { + if (!is_uploaded_file($this->file_src_pathname)) { + $this->processed = false; + $this->error = $this->translate('source_missing'); + } + // otherwise, if we don't check on uploaded files (local file for instance), we use file_exists() + } else { + if (!file_exists($this->file_src_pathname)) { + $this->processed = false; + $this->error = $this->translate('source_missing'); + } + } + + // checks if the destination directory exists, and attempt to create it + if (!$return_mode) { + if ($this->processed && !file_exists($this->file_dst_path)) { + if ($this->dir_auto_create) { + $this->log .= '- ' . $this->file_dst_path . ' doesn\'t exist. Attempting creation:'; + if (!$this->rmkdir($this->file_dst_path, $this->dir_chmod)) { + $this->log .= ' failed
'; + $this->processed = false; + $this->error = $this->translate('destination_dir'); + } else { + $this->log .= ' success
'; + } + } else { + $this->error = $this->translate('destination_dir_missing'); + } + } + + if ($this->processed && !is_dir($this->file_dst_path)) { + $this->processed = false; + $this->error = $this->translate('destination_path_not_dir'); + } + + // checks if the destination directory is writeable, and attempt to make it writeable + $hash = md5($this->file_dst_name_body . rand(1, 1000)); + if ($this->processed && !($f = @fopen($this->file_dst_path . $hash . (!empty($this->file_dst_name_ext) ? '.' . $this->file_dst_name_ext : ''), 'a+'))) { + if ($this->dir_auto_chmod) { + $this->log .= '- ' . $this->file_dst_path . ' is not writeable. Attempting chmod:'; + if (!@chmod($this->file_dst_path, $this->dir_chmod)) { + $this->log .= ' failed
'; + $this->processed = false; + $this->error = $this->translate('destination_dir_write'); + } else { + $this->log .= ' success
'; + if (!($f = @fopen($this->file_dst_path . $hash . (!empty($this->file_dst_name_ext) ? '.' . $this->file_dst_name_ext : ''), 'a+'))) { // we re-check + $this->processed = false; + $this->error = $this->translate('destination_dir_write'); + } else { + @fclose($f); + } + } + } else { + $this->processed = false; + $this->error = $this->translate('destination_path_write'); + } + } else { + if ($this->processed) @fclose($f); + @unlink($this->file_dst_path . $hash . (!empty($this->file_dst_name_ext) ? '.' . $this->file_dst_name_ext : '')); + } + + + // if we have an uploaded file, and if it is the first process, and if we can't access the file directly (open_basedir restriction) + // then we create a temp file that will be used as the source file in subsequent processes + // the third condition is there to check if the file is not accessible *directly* (it already has positively gone through is_uploaded_file(), so it exists) + if (!$this->no_upload_check && empty($this->file_src_temp) && !@file_exists($this->file_src_pathname)) { + $this->log .= '- attempting to use a temp file:'; + $hash = md5($this->file_dst_name_body . rand(1, 1000)); + if (move_uploaded_file($this->file_src_pathname, $this->file_dst_path . $hash . (!empty($this->file_dst_name_ext) ? '.' . $this->file_dst_name_ext : ''))) { + $this->file_src_pathname = $this->file_dst_path . $hash . (!empty($this->file_dst_name_ext) ? '.' . $this->file_dst_name_ext : ''); + $this->file_src_temp = $this->file_src_pathname; + $this->log .= ' file created
'; + $this->log .= '    temp file is: ' . $this->file_src_temp . '
'; + } else { + $this->log .= ' failed
'; + $this->processed = false; + $this->error = $this->translate('temp_file'); + } + } + } + } + + if ($this->processed) { + + // check if we need to autorotate, to automatically pre-rotates the image according to EXIF data (JPEG only) + $auto_flip = false; + $auto_rotate = 0; + if ($this->file_is_image && $this->image_auto_rotate && $this->image_src_type == 'jpg' && $this->function_enabled('exif_read_data')) { + $exif = @exif_read_data($this->file_src_pathname); + if (is_array($exif) && isset($exif['Orientation'])) { + $orientation = $exif['Orientation']; + switch($orientation) { + case 1: + $this->log .= '- EXIF orientation = 1 : default
'; + break; + case 2: + $auto_flip = 'v'; + $this->log .= '- EXIF orientation = 2 : vertical flip
'; + break; + case 3: + $auto_rotate = 180; + $this->log .= '- EXIF orientation = 3 : 180 rotate left
'; + break; + case 4: + $auto_flip = 'h'; + $this->log .= '- EXIF orientation = 4 : horizontal flip
'; + break; + case 5: + $auto_flip = 'h'; + $auto_rotate = 90; + $this->log .= '- EXIF orientation = 5 : horizontal flip + 90 rotate right
'; + break; + case 6: + $auto_rotate = 90; + $this->log .= '- EXIF orientation = 6 : 90 rotate right
'; + break; + case 7: + $auto_flip = 'v'; + $auto_rotate = 90; + $this->log .= '- EXIF orientation = 7 : vertical flip + 90 rotate right
'; + break; + case 8: + $auto_rotate = 270; + $this->log .= '- EXIF orientation = 8 : 90 rotate left
'; + break; + default: + $this->log .= '- EXIF orientation = '.$orientation.' : unknown
'; + break; + } + } else { + $this->log .= '- EXIF data is invalid or missing
'; + } + } else { + if (!$this->image_auto_rotate) { + $this->log .= '- auto-rotate deactivated
'; + } else if (!$this->image_src_type == 'jpg') { + $this->log .= '- auto-rotate applies only to JPEG images
'; + } else if (!$this->function_enabled('exif_read_data')) { + $this->log .= '- auto-rotate requires function exif_read_data to be enabled
'; + } + } + + // do we do some image manipulation? + $image_manipulation = ($this->file_is_image && ( + $this->image_resize + || $this->image_convert != '' + || is_numeric($this->image_brightness) + || is_numeric($this->image_contrast) + || is_numeric($this->image_opacity) + || is_numeric($this->image_threshold) + || !empty($this->image_tint_color) + || !empty($this->image_overlay_color) + || $this->image_pixelate + || $this->image_unsharp + || !empty($this->image_text) + || $this->image_greyscale + || $this->image_negative + || !empty($this->image_watermark) + || $auto_rotate || $auto_flip + || is_numeric($this->image_rotate) + || is_numeric($this->jpeg_size) + || !empty($this->image_flip) + || !empty($this->image_crop) + || !empty($this->image_precrop) + || !empty($this->image_border) + || !empty($this->image_border_transparent) + || $this->image_frame > 0 + || $this->image_bevel > 0 + || $this->image_reflection_height)); + + // we do a quick check to ensure the file is really an image + // we can do this only now, as it would have failed before in case of open_basedir + if ($image_manipulation && !@getimagesize($this->file_src_pathname)) { + $this->log .= '- the file is not an image!
'; + $image_manipulation = false; + } + + if ($image_manipulation) { + + // make sure GD doesn't complain too much + @ini_set("gd.jpeg_ignore_warning", 1); + + // checks if the source file is readable + if ($this->processed && !($f = @fopen($this->file_src_pathname, 'r'))) { + $this->processed = false; + $this->error = $this->translate('source_not_readable'); + } else { + @fclose($f); + } + + // we now do all the image manipulations + $this->log .= '- image resizing or conversion wanted
'; + if ($this->gdversion()) { + switch($this->image_src_type) { + case 'jpg': + if (!$this->function_enabled('imagecreatefromjpeg')) { + $this->processed = false; + $this->error = $this->translate('no_create_support', array('JPEG')); + } else { + $image_src = @imagecreatefromjpeg($this->file_src_pathname); + if (!$image_src) { + $this->processed = false; + $this->error = $this->translate('create_error', array('JPEG')); + } else { + $this->log .= '- source image is JPEG
'; + } + } + break; + case 'png': + if (!$this->function_enabled('imagecreatefrompng')) { + $this->processed = false; + $this->error = $this->translate('no_create_support', array('PNG')); + } else { + $image_src = @imagecreatefrompng($this->file_src_pathname); + if (!$image_src) { + $this->processed = false; + $this->error = $this->translate('create_error', array('PNG')); + } else { + $this->log .= '- source image is PNG
'; + } + } + break; + case 'webp': + if (!$this->function_enabled('imagecreatefromwebp')) { + $this->processed = false; + $this->error = $this->translate('no_create_support', array('WEBP')); + } else { + $image_src = @imagecreatefromwebp($this->file_src_pathname); + if (!$image_src) { + $this->processed = false; + $this->error = $this->translate('create_error', array('WEBP')); + } else { + $this->log .= '- source image is WEBP
'; + } + } + break; + case 'gif': + if (!$this->function_enabled('imagecreatefromgif')) { + $this->processed = false; + $this->error = $this->translate('no_create_support', array('GIF')); + } else { + $image_src = @imagecreatefromgif($this->file_src_pathname); + if (!$image_src) { + $this->processed = false; + $this->error = $this->translate('create_error', array('GIF')); + } else { + $this->log .= '- source image is GIF
'; + } + } + break; + case 'bmp': + if (!method_exists($this, 'imagecreatefrombmp')) { + $this->processed = false; + $this->error = $this->translate('no_create_support', array('BMP')); + } else { + $image_src = @$this->imagecreatefrombmp($this->file_src_pathname); + if (!$image_src) { + $this->processed = false; + $this->error = $this->translate('create_error', array('BMP')); + } else { + $this->log .= '- source image is BMP
'; + } + } + break; + default: + $this->processed = false; + $this->error = $this->translate('source_invalid'); + } + } else { + $this->processed = false; + $this->error = $this->translate('gd_missing'); + } + + if ($this->processed && $image_src) { + + // we have to set image_convert if it is not already + if (empty($this->image_convert)) { + $this->log .= '- setting destination file type to ' . $this->image_src_type . '
'; + $this->image_convert = $this->image_src_type; + } else { + $this->log .= '- requested destination file type is ' . $this->image_convert . '
'; + } + + if (!in_array($this->image_convert, $this->image_supported)) { + $this->log .= '- destination file type ' . $this->image_convert . ' is not supported; switching to jpg
'; + $this->image_convert = 'jpg'; + } + + // we set the default color to be the background color if we don't output in a transparent format + if ($this->image_convert != 'png' && $this->image_convert != 'webp' && $this->image_convert != 'gif' && !empty($this->image_default_color) && empty($this->image_background_color)) $this->image_background_color = $this->image_default_color; + if (!empty($this->image_background_color)) $this->image_default_color = $this->image_background_color; + if (empty($this->image_default_color)) $this->image_default_color = '#FFFFFF'; + + $this->image_src_x = imagesx($image_src); + $this->image_src_y = imagesy($image_src); + $gd_version = $this->gdversion(); + $ratio_crop = null; + + if (!imageistruecolor($image_src)) { // $this->image_src_type == 'gif' + $this->log .= '- image is detected as having a palette
'; + $this->image_is_palette = true; + $this->image_transparent_color = imagecolortransparent($image_src); + if ($this->image_transparent_color >= 0 && imagecolorstotal($image_src) > $this->image_transparent_color) { + $this->image_is_transparent = true; + $this->log .= '    palette image is detected as transparent
'; + } + // if the image has a palette (GIF), we convert it to true color, preserving transparency + $this->log .= '    convert palette image to true color
'; + $true_color = imagecreatetruecolor($this->image_src_x, $this->image_src_y); + imagealphablending($true_color, false); + imagesavealpha($true_color, true); + for ($x = 0; $x < $this->image_src_x; $x++) { + for ($y = 0; $y < $this->image_src_y; $y++) { + if ($this->image_transparent_color >= 0 && imagecolorat($image_src, $x, $y) == $this->image_transparent_color) { + imagesetpixel($true_color, $x, $y, 127 << 24); + } else { + $rgb = imagecolorsforindex($image_src, imagecolorat($image_src, $x, $y)); + imagesetpixel($true_color, $x, $y, ($rgb['alpha'] << 24) | ($rgb['red'] << 16) | ($rgb['green'] << 8) | $rgb['blue']); + } + } + } + $image_src = $this->imagetransfer($true_color, $image_src); + imagealphablending($image_src, false); + imagesavealpha($image_src, true); + $this->image_is_palette = false; + } + + $image_dst = & $image_src; + + // auto-flip image, according to EXIF data (JPEG only) + if ($gd_version >= 2 && !empty($auto_flip)) { + $this->log .= '- auto-flip image : ' . ($auto_flip == 'v' ? 'vertical' : 'horizontal') . '
'; + $tmp = $this->imagecreatenew($this->image_src_x, $this->image_src_y); + for ($x = 0; $x < $this->image_src_x; $x++) { + for ($y = 0; $y < $this->image_src_y; $y++){ + if (strpos($auto_flip, 'v') !== false) { + imagecopy($tmp, $image_dst, $this->image_src_x - $x - 1, $y, $x, $y, 1, 1); + } else { + imagecopy($tmp, $image_dst, $x, $this->image_src_y - $y - 1, $x, $y, 1, 1); + } + } + } + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // auto-rotate image, according to EXIF data (JPEG only) + if ($gd_version >= 2 && is_numeric($auto_rotate)) { + if (!in_array($auto_rotate, array(0, 90, 180, 270))) $auto_rotate = 0; + if ($auto_rotate != 0) { + if ($auto_rotate == 90 || $auto_rotate == 270) { + $tmp = $this->imagecreatenew($this->image_src_y, $this->image_src_x); + } else { + $tmp = $this->imagecreatenew($this->image_src_x, $this->image_src_y); + } + $this->log .= '- auto-rotate image : ' . $auto_rotate . '
'; + for ($x = 0; $x < $this->image_src_x; $x++) { + for ($y = 0; $y < $this->image_src_y; $y++){ + if ($auto_rotate == 90) { + imagecopy($tmp, $image_dst, $y, $x, $x, $this->image_src_y - $y - 1, 1, 1); + } else if ($auto_rotate == 180) { + imagecopy($tmp, $image_dst, $x, $y, $this->image_src_x - $x - 1, $this->image_src_y - $y - 1, 1, 1); + } else if ($auto_rotate == 270) { + imagecopy($tmp, $image_dst, $y, $x, $this->image_src_x - $x - 1, $y, 1, 1); + } else { + imagecopy($tmp, $image_dst, $x, $y, $x, $y, 1, 1); + } + } + } + if ($auto_rotate == 90 || $auto_rotate == 270) { + $t = $this->image_src_y; + $this->image_src_y = $this->image_src_x; + $this->image_src_x = $t; + } + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + } + + // pre-crop image, before resizing + if ((!empty($this->image_precrop))) { + list($ct, $cr, $cb, $cl) = $this->getoffsets($this->image_precrop, $this->image_src_x, $this->image_src_y, true, true); + $this->log .= '- pre-crop image : ' . $ct . ' ' . $cr . ' ' . $cb . ' ' . $cl . '
'; + $this->image_src_x = $this->image_src_x - $cl - $cr; + $this->image_src_y = $this->image_src_y - $ct - $cb; + if ($this->image_src_x < 1) $this->image_src_x = 1; + if ($this->image_src_y < 1) $this->image_src_y = 1; + $tmp = $this->imagecreatenew($this->image_src_x, $this->image_src_y); + + // we copy the image into the recieving image + imagecopy($tmp, $image_dst, 0, 0, $cl, $ct, $this->image_src_x, $this->image_src_y); + + // if we crop with negative margins, we have to make sure the extra bits are the right color, or transparent + if ($ct < 0 || $cr < 0 || $cb < 0 || $cl < 0 ) { + // use the background color if present + if (!empty($this->image_background_color)) { + list($red, $green, $blue) = $this->getcolors($this->image_background_color); + $fill = imagecolorallocate($tmp, $red, $green, $blue); + } else { + $fill = imagecolorallocatealpha($tmp, 0, 0, 0, 127); + } + // fills eventual negative margins + if ($ct < 0) imagefilledrectangle($tmp, 0, 0, $this->image_src_x, -$ct, $fill); + if ($cr < 0) imagefilledrectangle($tmp, $this->image_src_x + $cr, 0, $this->image_src_x, $this->image_src_y, $fill); + if ($cb < 0) imagefilledrectangle($tmp, 0, $this->image_src_y + $cb, $this->image_src_x, $this->image_src_y, $fill); + if ($cl < 0) imagefilledrectangle($tmp, 0, 0, -$cl, $this->image_src_y, $fill); + } + + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // resize image (and move image_src_x, image_src_y dimensions into image_dst_x, image_dst_y) + if ($this->image_resize) { + $this->log .= '- resizing...
'; + $this->image_dst_x = $this->image_x; + $this->image_dst_y = $this->image_y; + + // backward compatibility for soon to be deprecated settings + if ($this->image_ratio_no_zoom_in) { + $this->image_ratio = true; + $this->image_no_enlarging = true; + } else if ($this->image_ratio_no_zoom_out) { + $this->image_ratio = true; + $this->image_no_shrinking = true; + } + + // keeps aspect ratio with x calculated from y + if ($this->image_ratio_x) { + $this->log .= '    calculate x size
'; + $this->image_dst_x = round(($this->image_src_x * $this->image_y) / $this->image_src_y); + $this->image_dst_y = $this->image_y; + + // keeps aspect ratio with y calculated from x + } else if ($this->image_ratio_y) { + $this->log .= '    calculate y size
'; + $this->image_dst_x = $this->image_x; + $this->image_dst_y = round(($this->image_src_y * $this->image_x) / $this->image_src_x); + + // keeps aspect ratio, calculating x and y so that the image is approx the set number of pixels + } else if (is_numeric($this->image_ratio_pixels)) { + $this->log .= '    calculate x/y size to match a number of pixels
'; + $pixels = $this->image_src_y * $this->image_src_x; + $diff = sqrt($this->image_ratio_pixels / $pixels); + $this->image_dst_x = round($this->image_src_x * $diff); + $this->image_dst_y = round($this->image_src_y * $diff); + + // keeps aspect ratio with x and y dimensions, filling the space + } else if ($this->image_ratio_crop) { + if (!is_string($this->image_ratio_crop)) $this->image_ratio_crop = ''; + $this->image_ratio_crop = strtolower($this->image_ratio_crop); + if (($this->image_src_x/$this->image_x) > ($this->image_src_y/$this->image_y)) { + $this->image_dst_y = $this->image_y; + $this->image_dst_x = intval($this->image_src_x*($this->image_y / $this->image_src_y)); + $ratio_crop = array(); + $ratio_crop['x'] = $this->image_dst_x - $this->image_x; + if (strpos($this->image_ratio_crop, 'l') !== false) { + $ratio_crop['l'] = 0; + $ratio_crop['r'] = $ratio_crop['x']; + } else if (strpos($this->image_ratio_crop, 'r') !== false) { + $ratio_crop['l'] = $ratio_crop['x']; + $ratio_crop['r'] = 0; + } else { + $ratio_crop['l'] = round($ratio_crop['x']/2); + $ratio_crop['r'] = $ratio_crop['x'] - $ratio_crop['l']; + } + $this->log .= '    ratio_crop_x : ' . $ratio_crop['x'] . ' (' . $ratio_crop['l'] . ';' . $ratio_crop['r'] . ')
'; + if (is_null($this->image_crop)) $this->image_crop = array(0, 0, 0, 0); + } else { + $this->image_dst_x = $this->image_x; + $this->image_dst_y = intval($this->image_src_y*($this->image_x / $this->image_src_x)); + $ratio_crop = array(); + $ratio_crop['y'] = $this->image_dst_y - $this->image_y; + if (strpos($this->image_ratio_crop, 't') !== false) { + $ratio_crop['t'] = 0; + $ratio_crop['b'] = $ratio_crop['y']; + } else if (strpos($this->image_ratio_crop, 'b') !== false) { + $ratio_crop['t'] = $ratio_crop['y']; + $ratio_crop['b'] = 0; + } else { + $ratio_crop['t'] = round($ratio_crop['y']/2); + $ratio_crop['b'] = $ratio_crop['y'] - $ratio_crop['t']; + } + $this->log .= '    ratio_crop_y : ' . $ratio_crop['y'] . ' (' . $ratio_crop['t'] . ';' . $ratio_crop['b'] . ')
'; + if (is_null($this->image_crop)) $this->image_crop = array(0, 0, 0, 0); + } + + // keeps aspect ratio with x and y dimensions, fitting the image in the space, and coloring the rest + } else if ($this->image_ratio_fill) { + if (!is_string($this->image_ratio_fill)) $this->image_ratio_fill = ''; + $this->image_ratio_fill = strtolower($this->image_ratio_fill); + if (($this->image_src_x/$this->image_x) < ($this->image_src_y/$this->image_y)) { + $this->image_dst_y = $this->image_y; + $this->image_dst_x = intval($this->image_src_x*($this->image_y / $this->image_src_y)); + $ratio_crop = array(); + $ratio_crop['x'] = $this->image_dst_x - $this->image_x; + if (strpos($this->image_ratio_fill, 'l') !== false) { + $ratio_crop['l'] = 0; + $ratio_crop['r'] = $ratio_crop['x']; + } else if (strpos($this->image_ratio_fill, 'r') !== false) { + $ratio_crop['l'] = $ratio_crop['x']; + $ratio_crop['r'] = 0; + } else { + $ratio_crop['l'] = round($ratio_crop['x']/2); + $ratio_crop['r'] = $ratio_crop['x'] - $ratio_crop['l']; + } + $this->log .= '    ratio_fill_x : ' . $ratio_crop['x'] . ' (' . $ratio_crop['l'] . ';' . $ratio_crop['r'] . ')
'; + if (is_null($this->image_crop)) $this->image_crop = array(0, 0, 0, 0); + } else { + $this->image_dst_x = $this->image_x; + $this->image_dst_y = intval($this->image_src_y*($this->image_x / $this->image_src_x)); + $ratio_crop = array(); + $ratio_crop['y'] = $this->image_dst_y - $this->image_y; + if (strpos($this->image_ratio_fill, 't') !== false) { + $ratio_crop['t'] = 0; + $ratio_crop['b'] = $ratio_crop['y']; + } else if (strpos($this->image_ratio_fill, 'b') !== false) { + $ratio_crop['t'] = $ratio_crop['y']; + $ratio_crop['b'] = 0; + } else { + $ratio_crop['t'] = round($ratio_crop['y']/2); + $ratio_crop['b'] = $ratio_crop['y'] - $ratio_crop['t']; + } + $this->log .= '    ratio_fill_y : ' . $ratio_crop['y'] . ' (' . $ratio_crop['t'] . ';' . $ratio_crop['b'] . ')
'; + if (is_null($this->image_crop)) $this->image_crop = array(0, 0, 0, 0); + } + + // keeps aspect ratio with x and y dimensions + } else if ($this->image_ratio) { + if (($this->image_src_x/$this->image_x) > ($this->image_src_y/$this->image_y)) { + $this->image_dst_x = $this->image_x; + $this->image_dst_y = intval($this->image_src_y*($this->image_x / $this->image_src_x)); + } else { + $this->image_dst_y = $this->image_y; + $this->image_dst_x = intval($this->image_src_x*($this->image_y / $this->image_src_y)); + } + + // resize to provided exact dimensions + } else { + $this->log .= '    use plain sizes
'; + $this->image_dst_x = $this->image_x; + $this->image_dst_y = $this->image_y; + } + + if ($this->image_dst_x < 1) $this->image_dst_x = 1; + if ($this->image_dst_y < 1) $this->image_dst_y = 1; + $this->log .= '    image_src_x y : ' . $this->image_src_x . ' x ' . $this->image_src_y . '
'; + $this->log .= '    image_dst_x y : ' . $this->image_dst_x . ' x ' . $this->image_dst_y . '
'; + + // make sure we don't enlarge the image if we don't want to + if ($this->image_no_enlarging && ($this->image_src_x < $this->image_dst_x || $this->image_src_y < $this->image_dst_y)) { + $this->log .= '    cancel resizing, as it would enlarge the image!
'; + $this->image_dst_x = $this->image_src_x; + $this->image_dst_y = $this->image_src_y; + $ratio_crop = null; + } + + // make sure we don't shrink the image if we don't want to + if ($this->image_no_shrinking && ($this->image_src_x > $this->image_dst_x || $this->image_src_y > $this->image_dst_y)) { + $this->log .= '    cancel resizing, as it would shrink the image!
'; + $this->image_dst_x = $this->image_src_x; + $this->image_dst_y = $this->image_src_y; + $ratio_crop = null; + } + + // resize the image + if ($this->image_dst_x != $this->image_src_x || $this->image_dst_y != $this->image_src_y) { + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + + if ($gd_version >= 2) { + $res = imagecopyresampled($tmp, $image_src, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y, $this->image_src_x, $this->image_src_y); + } else { + $res = imagecopyresized($tmp, $image_src, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y, $this->image_src_x, $this->image_src_y); + } + + $this->log .= '    resized image object created
'; + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + } else { + $this->image_dst_x = $this->image_src_x; + $this->image_dst_y = $this->image_src_y; + } + + // crop image (and also crops if image_ratio_crop is used) + if ((!empty($this->image_crop) || !is_null($ratio_crop))) { + list($ct, $cr, $cb, $cl) = $this->getoffsets($this->image_crop, $this->image_dst_x, $this->image_dst_y, true, true); + // we adjust the cropping if we use image_ratio_crop + if (!is_null($ratio_crop)) { + if (array_key_exists('t', $ratio_crop)) $ct += $ratio_crop['t']; + if (array_key_exists('r', $ratio_crop)) $cr += $ratio_crop['r']; + if (array_key_exists('b', $ratio_crop)) $cb += $ratio_crop['b']; + if (array_key_exists('l', $ratio_crop)) $cl += $ratio_crop['l']; + } + if ($ct != 0 || $cr != 0 || $cb != 0 || $cl != 0) { + $this->log .= '- crop image : ' . $ct . ' ' . $cr . ' ' . $cb . ' ' . $cl . '
'; + $this->image_dst_x = $this->image_dst_x - $cl - $cr; + $this->image_dst_y = $this->image_dst_y - $ct - $cb; + if ($this->image_dst_x < 1) $this->image_dst_x = 1; + if ($this->image_dst_y < 1) $this->image_dst_y = 1; + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + + // we copy the image into the recieving image + imagecopy($tmp, $image_dst, 0, 0, $cl, $ct, $this->image_dst_x, $this->image_dst_y); + + // if we crop with negative margins, we have to make sure the extra bits are the right color, or transparent + if ($ct < 0 || $cr < 0 || $cb < 0 || $cl < 0 ) { + // use the background color if present + if (!empty($this->image_background_color)) { + list($red, $green, $blue) = $this->getcolors($this->image_background_color); + $fill = imagecolorallocate($tmp, $red, $green, $blue); + } else { + $fill = imagecolorallocatealpha($tmp, 0, 0, 0, 127); + } + // fills eventual negative margins + if ($ct < 0) imagefilledrectangle($tmp, 0, 0, $this->image_dst_x, -$ct-1, $fill); + if ($cr < 0) imagefilledrectangle($tmp, $this->image_dst_x + $cr, 0, $this->image_dst_x, $this->image_dst_y, $fill); + if ($cb < 0) imagefilledrectangle($tmp, 0, $this->image_dst_y + $cb, $this->image_dst_x, $this->image_dst_y, $fill); + if ($cl < 0) imagefilledrectangle($tmp, 0, 0, -$cl-1, $this->image_dst_y, $fill); + } + + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + } + + // flip image + if ($gd_version >= 2 && !empty($this->image_flip)) { + $this->image_flip = strtolower($this->image_flip); + $this->log .= '- flip image : ' . $this->image_flip . '
'; + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + for ($x = 0; $x < $this->image_dst_x; $x++) { + for ($y = 0; $y < $this->image_dst_y; $y++){ + if (strpos($this->image_flip, 'v') !== false) { + imagecopy($tmp, $image_dst, $this->image_dst_x - $x - 1, $y, $x, $y, 1, 1); + } else { + imagecopy($tmp, $image_dst, $x, $this->image_dst_y - $y - 1, $x, $y, 1, 1); + } + } + } + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // rotate image + if ($gd_version >= 2 && is_numeric($this->image_rotate)) { + if (!in_array($this->image_rotate, array(0, 90, 180, 270))) $this->image_rotate = 0; + if ($this->image_rotate != 0) { + if ($this->image_rotate == 90 || $this->image_rotate == 270) { + $tmp = $this->imagecreatenew($this->image_dst_y, $this->image_dst_x); + } else { + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + } + $this->log .= '- rotate image : ' . $this->image_rotate . '
'; + for ($x = 0; $x < $this->image_dst_x; $x++) { + for ($y = 0; $y < $this->image_dst_y; $y++){ + if ($this->image_rotate == 90) { + imagecopy($tmp, $image_dst, $y, $x, $x, $this->image_dst_y - $y - 1, 1, 1); + } else if ($this->image_rotate == 180) { + imagecopy($tmp, $image_dst, $x, $y, $this->image_dst_x - $x - 1, $this->image_dst_y - $y - 1, 1, 1); + } else if ($this->image_rotate == 270) { + imagecopy($tmp, $image_dst, $y, $x, $this->image_dst_x - $x - 1, $y, 1, 1); + } else { + imagecopy($tmp, $image_dst, $x, $y, $x, $y, 1, 1); + } + } + } + if ($this->image_rotate == 90 || $this->image_rotate == 270) { + $t = $this->image_dst_y; + $this->image_dst_y = $this->image_dst_x; + $this->image_dst_x = $t; + } + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + } + + // pixelate image + if ((is_numeric($this->image_pixelate) && $this->image_pixelate > 0)) { + $this->log .= '- pixelate image (' . $this->image_pixelate . 'px)
'; + $filter = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + if ($gd_version >= 2) { + imagecopyresampled($filter, $image_dst, 0, 0, 0, 0, round($this->image_dst_x / $this->image_pixelate), round($this->image_dst_y / $this->image_pixelate), $this->image_dst_x, $this->image_dst_y); + imagecopyresampled($image_dst, $filter, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y, round($this->image_dst_x / $this->image_pixelate), round($this->image_dst_y / $this->image_pixelate)); + } else { + imagecopyresized($filter, $image_dst, 0, 0, 0, 0, round($this->image_dst_x / $this->image_pixelate), round($this->image_dst_y / $this->image_pixelate), $this->image_dst_x, $this->image_dst_y); + imagecopyresized($image_dst, $filter, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y, round($this->image_dst_x / $this->image_pixelate), round($this->image_dst_y / $this->image_pixelate)); + } + $this->imageunset($filter); + } + + // unsharp mask + if ($gd_version >= 2 && $this->image_unsharp && is_numeric($this->image_unsharp_amount) && is_numeric($this->image_unsharp_radius) && is_numeric($this->image_unsharp_threshold)) { + // Unsharp Mask for PHP - version 2.1.1 + // Unsharp mask algorithm by Torstein Hønsi 2003-07. + // Used with permission + // Modified to support alpha transparency + if ($this->image_unsharp_amount > 500) $this->image_unsharp_amount = 500; + $this->image_unsharp_amount = $this->image_unsharp_amount * 0.016; + if ($this->image_unsharp_radius > 50) $this->image_unsharp_radius = 50; + $this->image_unsharp_radius = $this->image_unsharp_radius * 2; + if ($this->image_unsharp_threshold > 255) $this->image_unsharp_threshold = 255; + $this->image_unsharp_radius = abs(round($this->image_unsharp_radius)); + if ($this->image_unsharp_radius != 0) { + $this->image_dst_x = imagesx($image_dst); $this->image_dst_y = imagesy($image_dst); + $canvas = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y, false, true); + $blur = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y, false, true); + if ($this->function_enabled('imageconvolution')) { // PHP >= 5.1 + $matrix = array(array( 1, 2, 1 ), array( 2, 4, 2 ), array( 1, 2, 1 )); + imagecopy($blur, $image_dst, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y); + imageconvolution($blur, $matrix, 16, 0); + } else { + for ($i = 0; $i < $this->image_unsharp_radius; $i++) { + imagecopy($blur, $image_dst, 0, 0, 1, 0, $this->image_dst_x - 1, $this->image_dst_y); // left + $this->imagecopymergealpha($blur, $image_dst, 1, 0, 0, 0, $this->image_dst_x, $this->image_dst_y, 50); // right + $this->imagecopymergealpha($blur, $image_dst, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y, 50); // center + imagecopy($canvas, $blur, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y); + $this->imagecopymergealpha($blur, $canvas, 0, 0, 0, 1, $this->image_dst_x, $this->image_dst_y - 1, 33.33333 ); // up + $this->imagecopymergealpha($blur, $canvas, 0, 1, 0, 0, $this->image_dst_x, $this->image_dst_y, 25); // down + } + } + $p_new = array(); + if($this->image_unsharp_threshold>0) { + for ($x = 0; $x < $this->image_dst_x-1; $x++) { + for ($y = 0; $y < $this->image_dst_y; $y++) { + $p_orig = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $p_blur = imagecolorsforindex($blur, imagecolorat($blur, $x, $y)); + $p_new['red'] = (abs($p_orig['red'] - $p_blur['red']) >= $this->image_unsharp_threshold) ? max(0, min(255, ($this->image_unsharp_amount * ($p_orig['red'] - $p_blur['red'])) + $p_orig['red'])) : $p_orig['red']; + $p_new['green'] = (abs($p_orig['green'] - $p_blur['green']) >= $this->image_unsharp_threshold) ? max(0, min(255, ($this->image_unsharp_amount * ($p_orig['green'] - $p_blur['green'])) + $p_orig['green'])) : $p_orig['green']; + $p_new['blue'] = (abs($p_orig['blue'] - $p_blur['blue']) >= $this->image_unsharp_threshold) ? max(0, min(255, ($this->image_unsharp_amount * ($p_orig['blue'] - $p_blur['blue'])) + $p_orig['blue'])) : $p_orig['blue']; + if (($p_orig['red'] != $p_new['red']) || ($p_orig['green'] != $p_new['green']) || ($p_orig['blue'] != $p_new['blue'])) { + $color = imagecolorallocatealpha($image_dst, $p_new['red'], $p_new['green'], $p_new['blue'], $p_orig['alpha']); + imagesetpixel($image_dst, $x, $y, $color); + } + } + } + } else { + for ($x = 0; $x < $this->image_dst_x; $x++) { + for ($y = 0; $y < $this->image_dst_y; $y++) { + $p_orig = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $p_blur = imagecolorsforindex($blur, imagecolorat($blur, $x, $y)); + $p_new['red'] = ($this->image_unsharp_amount * ($p_orig['red'] - $p_blur['red'])) + $p_orig['red']; + if ($p_new['red']>255) { $p_new['red']=255; } elseif ($p_new['red']<0) { $p_new['red']=0; } + $p_new['green'] = ($this->image_unsharp_amount * ($p_orig['green'] - $p_blur['green'])) + $p_orig['green']; + if ($p_new['green']>255) { $p_new['green']=255; } elseif ($p_new['green']<0) { $p_new['green']=0; } + $p_new['blue'] = ($this->image_unsharp_amount * ($p_orig['blue'] - $p_blur['blue'])) + $p_orig['blue']; + if ($p_new['blue']>255) { $p_new['blue']=255; } elseif ($p_new['blue']<0) { $p_new['blue']=0; } + $color = imagecolorallocatealpha($image_dst, $p_new['red'], $p_new['green'], $p_new['blue'], $p_orig['alpha']); + imagesetpixel($image_dst, $x, $y, $color); + } + } + } + $this->imageunset($canvas); + $this->imageunset($blur); + } + } + + // add color overlay + if ($gd_version >= 2 && (is_numeric($this->image_overlay_opacity) && $this->image_overlay_opacity > 0 && !empty($this->image_overlay_color))) { + $this->log .= '- apply color overlay
'; + list($red, $green, $blue) = $this->getcolors($this->image_overlay_color); + $filter = imagecreatetruecolor($this->image_dst_x, $this->image_dst_y); + $color = imagecolorallocate($filter, $red, $green, $blue); + imagefilledrectangle($filter, 0, 0, $this->image_dst_x, $this->image_dst_y, $color); + $this->imagecopymergealpha($image_dst, $filter, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y, $this->image_overlay_opacity); + $this->imageunset($filter); + } + + // add brightness, contrast and tint, turns to greyscale and inverts colors + if ($gd_version >= 2 && ($this->image_negative || $this->image_greyscale || is_numeric($this->image_threshold)|| is_numeric($this->image_brightness) || is_numeric($this->image_contrast) || !empty($this->image_tint_color))) { + $this->log .= '- apply tint, light, contrast correction, negative, greyscale and threshold
'; + if (!empty($this->image_tint_color)) list($tint_red, $tint_green, $tint_blue) = $this->getcolors($this->image_tint_color); + //imagealphablending($image_dst, true); + for($y=0; $y < $this->image_dst_y; $y++) { + for($x=0; $x < $this->image_dst_x; $x++) { + if ($this->image_greyscale) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $r = $g = $b = round((0.2125 * $pixel['red']) + (0.7154 * $pixel['green']) + (0.0721 * $pixel['blue'])); + $color = imagecolorallocatealpha($image_dst, $r, $g, $b, $pixel['alpha']); + imagesetpixel($image_dst, $x, $y, $color); + unset($color); unset($pixel); + } + if (is_numeric($this->image_threshold)) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $c = (round($pixel['red'] + $pixel['green'] + $pixel['blue']) / 3) - 127; + $r = $g = $b = ($c > $this->image_threshold ? 255 : 0); + $color = imagecolorallocatealpha($image_dst, $r, $g, $b, $pixel['alpha']); + imagesetpixel($image_dst, $x, $y, $color); + unset($color); unset($pixel); + } + if (is_numeric($this->image_brightness)) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $r = max(min(round($pixel['red'] + (($this->image_brightness * 2))), 255), 0); + $g = max(min(round($pixel['green'] + (($this->image_brightness * 2))), 255), 0); + $b = max(min(round($pixel['blue'] + (($this->image_brightness * 2))), 255), 0); + $color = imagecolorallocatealpha($image_dst, $r, $g, $b, $pixel['alpha']); + imagesetpixel($image_dst, $x, $y, $color); + unset($color); unset($pixel); + } + if (is_numeric($this->image_contrast)) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $r = max(min(round(($this->image_contrast + 128) * $pixel['red'] / 128), 255), 0); + $g = max(min(round(($this->image_contrast + 128) * $pixel['green'] / 128), 255), 0); + $b = max(min(round(($this->image_contrast + 128) * $pixel['blue'] / 128), 255), 0); + $color = imagecolorallocatealpha($image_dst, $r, $g, $b, $pixel['alpha']); + imagesetpixel($image_dst, $x, $y, $color); + unset($color); unset($pixel); + } + if (!empty($this->image_tint_color)) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $r = min(round($tint_red * $pixel['red'] / 169), 255); + $g = min(round($tint_green * $pixel['green'] / 169), 255); + $b = min(round($tint_blue * $pixel['blue'] / 169), 255); + $color = imagecolorallocatealpha($image_dst, $r, $g, $b, $pixel['alpha']); + imagesetpixel($image_dst, $x, $y, $color); + unset($color); unset($pixel); + } + if (!empty($this->image_negative)) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $r = round(255 - $pixel['red']); + $g = round(255 - $pixel['green']); + $b = round(255 - $pixel['blue']); + $color = imagecolorallocatealpha($image_dst, $r, $g, $b, $pixel['alpha']); + imagesetpixel($image_dst, $x, $y, $color); + unset($color); unset($pixel); + } + } + } + } + + // adds a border + if ($gd_version >= 2 && !empty($this->image_border)) { + list($ct, $cr, $cb, $cl) = $this->getoffsets($this->image_border, $this->image_dst_x, $this->image_dst_y, true, false); + $this->log .= '- add border : ' . $ct . ' ' . $cr . ' ' . $cb . ' ' . $cl . '
'; + $this->image_dst_x = $this->image_dst_x + $cl + $cr; + $this->image_dst_y = $this->image_dst_y + $ct + $cb; + if (!empty($this->image_border_color)) list($red, $green, $blue) = $this->getcolors($this->image_border_color); + $opacity = (is_numeric($this->image_border_opacity) ? (int) (127 - $this->image_border_opacity / 100 * 127): 0); + // we now create an image, that we fill with the border color + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + $background = imagecolorallocatealpha($tmp, $red, $green, $blue, $opacity); + imagefilledrectangle($tmp, 0, 0, $this->image_dst_x, $this->image_dst_y, $background); + // we then copy the source image into the new image, without merging so that only the border is actually kept + imagecopy($tmp, $image_dst, $cl, $ct, 0, 0, $this->image_dst_x - $cr - $cl, $this->image_dst_y - $cb - $ct); + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // adds a fading-to-transparent border + if ($gd_version >= 2 && !empty($this->image_border_transparent)) { + list($ct, $cr, $cb, $cl) = $this->getoffsets($this->image_border_transparent, $this->image_dst_x, $this->image_dst_y, true, false); + $this->log .= '- add transparent border : ' . $ct . ' ' . $cr . ' ' . $cb . ' ' . $cl . '
'; + // we now create an image, that we fill with the border color + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + // we then copy the source image into the new image, without the borders + imagecopy($tmp, $image_dst, $cl, $ct, $cl, $ct, $this->image_dst_x - $cr - $cl, $this->image_dst_y - $cb - $ct); + // we now add the top border + $opacity = 100; + for ($y = $ct - 1; $y >= 0; $y--) { + $il = (int) ($ct > 0 ? ($cl * ($y / $ct)) : 0); + $ir = (int) ($ct > 0 ? ($cr * ($y / $ct)) : 0); + for ($x = $il; $x < $this->image_dst_x - $ir; $x++) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $alpha = (1 - ($pixel['alpha'] / 127)) * $opacity / 100; + if ($alpha > 0) { + if ($alpha > 1) $alpha = 1; + $color = imagecolorallocatealpha($tmp, $pixel['red'] , $pixel['green'], $pixel['blue'], round((1 - $alpha) * 127)); + imagesetpixel($tmp, $x, $y, $color); + } + } + if ($opacity > 0) $opacity = $opacity - (100 / $ct); + } + // we now add the right border + $opacity = 100; + for ($x = $this->image_dst_x - $cr; $x < $this->image_dst_x; $x++) { + $it = (int) ($cr > 0 ? ($ct * (($this->image_dst_x - $x - 1) / $cr)) : 0); + $ib = (int) ($cr > 0 ? ($cb * (($this->image_dst_x - $x - 1) / $cr)) : 0); + for ($y = $it; $y < $this->image_dst_y - $ib; $y++) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $alpha = (1 - ($pixel['alpha'] / 127)) * $opacity / 100; + if ($alpha > 0) { + if ($alpha > 1) $alpha = 1; + $color = imagecolorallocatealpha($tmp, $pixel['red'] , $pixel['green'], $pixel['blue'], round((1 - $alpha) * 127)); + imagesetpixel($tmp, $x, $y, $color); + } + } + if ($opacity > 0) $opacity = $opacity - (100 / $cr); + } + // we now add the bottom border + $opacity = 100; + for ($y = $this->image_dst_y - $cb; $y < $this->image_dst_y; $y++) { + $il = (int) ($cb > 0 ? ($cl * (($this->image_dst_y - $y - 1) / $cb)) : 0); + $ir = (int) ($cb > 0 ? ($cr * (($this->image_dst_y - $y - 1) / $cb)) : 0); + for ($x = $il; $x < $this->image_dst_x - $ir; $x++) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $alpha = (1 - ($pixel['alpha'] / 127)) * $opacity / 100; + if ($alpha > 0) { + if ($alpha > 1) $alpha = 1; + $color = imagecolorallocatealpha($tmp, $pixel['red'] , $pixel['green'], $pixel['blue'], round((1 - $alpha) * 127)); + imagesetpixel($tmp, $x, $y, $color); + } + } + if ($opacity > 0) $opacity = $opacity - (100 / $cb); + } + // we now add the left border + $opacity = 100; + for ($x = $cl - 1; $x >= 0; $x--) { + $it = (int) ($cl > 0 ? ($ct * ($x / $cl)) : 0); + $ib = (int) ($cl > 0 ? ($cb * ($x / $cl)) : 0); + for ($y = $it; $y < $this->image_dst_y - $ib; $y++) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $alpha = (1 - ($pixel['alpha'] / 127)) * $opacity / 100; + if ($alpha > 0) { + if ($alpha > 1) $alpha = 1; + $color = imagecolorallocatealpha($tmp, $pixel['red'] , $pixel['green'], $pixel['blue'], round((1 - $alpha) * 127)); + imagesetpixel($tmp, $x, $y, $color); + } + } + if ($opacity > 0) $opacity = $opacity - (100 / $cl); + } + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // add frame border + if ($gd_version >= 2 && is_numeric($this->image_frame)) { + if (is_array($this->image_frame_colors)) { + $vars = $this->image_frame_colors; + $this->log .= '- add frame : ' . implode(' ', $this->image_frame_colors) . '
'; + } else { + $this->log .= '- add frame : ' . $this->image_frame_colors . '
'; + $vars = explode(' ', $this->image_frame_colors); + } + $nb = sizeof($vars); + $this->image_dst_x = $this->image_dst_x + ($nb * 2); + $this->image_dst_y = $this->image_dst_y + ($nb * 2); + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + imagecopy($tmp, $image_dst, $nb, $nb, 0, 0, $this->image_dst_x - ($nb * 2), $this->image_dst_y - ($nb * 2)); + $opacity = (is_numeric($this->image_frame_opacity) ? (int) (127 - $this->image_frame_opacity / 100 * 127): 0); + for ($i=0; $i<$nb; $i++) { + list($red, $green, $blue) = $this->getcolors($vars[$i]); + $c = imagecolorallocatealpha($tmp, $red, $green, $blue, $opacity); + if ($this->image_frame == 1) { + imageline($tmp, $i, $i, $this->image_dst_x - $i -1, $i, $c); + imageline($tmp, $this->image_dst_x - $i -1, $this->image_dst_y - $i -1, $this->image_dst_x - $i -1, $i, $c); + imageline($tmp, $this->image_dst_x - $i -1, $this->image_dst_y - $i -1, $i, $this->image_dst_y - $i -1, $c); + imageline($tmp, $i, $i, $i, $this->image_dst_y - $i -1, $c); + } else { + imageline($tmp, $i, $i, $this->image_dst_x - $i -1, $i, $c); + imageline($tmp, $this->image_dst_x - $nb + $i, $this->image_dst_y - $nb + $i, $this->image_dst_x - $nb + $i, $nb - $i, $c); + imageline($tmp, $this->image_dst_x - $nb + $i, $this->image_dst_y - $nb + $i, $nb - $i, $this->image_dst_y - $nb + $i, $c); + imageline($tmp, $i, $i, $i, $this->image_dst_y - $i -1, $c); + } + } + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // add bevel border + if ($gd_version >= 2 && $this->image_bevel > 0) { + if (empty($this->image_bevel_color1)) $this->image_bevel_color1 = '#FFFFFF'; + if (empty($this->image_bevel_color2)) $this->image_bevel_color2 = '#000000'; + list($red1, $green1, $blue1) = $this->getcolors($this->image_bevel_color1); + list($red2, $green2, $blue2) = $this->getcolors($this->image_bevel_color2); + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y); + imagecopy($tmp, $image_dst, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y); + imagealphablending($tmp, true); + for ($i=0; $i<$this->image_bevel; $i++) { + $alpha = round(($i / $this->image_bevel) * 127); + $c1 = imagecolorallocatealpha($tmp, $red1, $green1, $blue1, $alpha); + $c2 = imagecolorallocatealpha($tmp, $red2, $green2, $blue2, $alpha); + imageline($tmp, $i, $i, $this->image_dst_x - $i -1, $i, $c1); + imageline($tmp, $this->image_dst_x - $i -1, $this->image_dst_y - $i, $this->image_dst_x - $i -1, $i, $c2); + imageline($tmp, $this->image_dst_x - $i -1, $this->image_dst_y - $i -1, $i, $this->image_dst_y - $i -1, $c2); + imageline($tmp, $i, $i, $i, $this->image_dst_y - $i -1, $c1); + } + // we transfert tmp into image_dst + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // add watermark image + if ($this->image_watermark!='' && file_exists($this->image_watermark)) { + $this->log .= '- add watermark
'; + $this->image_watermark_position = strtolower($this->image_watermark_position); + $watermark_info = getimagesize($this->image_watermark); + $watermark_type = (array_key_exists(2, $watermark_info) ? $watermark_info[2] : null); // 1 = GIF, 2 = JPG, 3 = PNG + $watermark_checked = false; + if ($watermark_type == IMAGETYPE_GIF) { + if (!$this->function_enabled('imagecreatefromgif')) { + $this->error = $this->translate('watermark_no_create_support', array('GIF')); + } else { + $filter = @imagecreatefromgif($this->image_watermark); + if (!$filter) { + $this->error = $this->translate('watermark_create_error', array('GIF')); + } else { + $this->log .= '    watermark source image is GIF
'; + $watermark_checked = true; + } + } + } else if ($watermark_type == IMAGETYPE_JPEG) { + if (!$this->function_enabled('imagecreatefromjpeg')) { + $this->error = $this->translate('watermark_no_create_support', array('JPEG')); + } else { + $filter = @imagecreatefromjpeg($this->image_watermark); + if (!$filter) { + $this->error = $this->translate('watermark_create_error', array('JPEG')); + } else { + $this->log .= '    watermark source image is JPEG
'; + $watermark_checked = true; + } + } + } else if ($watermark_type == IMAGETYPE_PNG) { + if (!$this->function_enabled('imagecreatefrompng')) { + $this->error = $this->translate('watermark_no_create_support', array('PNG')); + } else { + $filter = @imagecreatefrompng($this->image_watermark); + if (!$filter) { + $this->error = $this->translate('watermark_create_error', array('PNG')); + } else { + $this->log .= '    watermark source image is PNG
'; + $watermark_checked = true; + } + } + } else if ($watermark_type == IMAGETYPE_WEBP) { + if (!$this->function_enabled('imagecreatefromwebp')) { + $this->error = $this->translate('watermark_no_create_support', array('WEBP')); + } else { + $filter = @imagecreatefromwebp($this->image_watermark); + if (!$filter) { + $this->error = $this->translate('watermark_create_error', array('WEBP')); + } else { + $this->log .= '    watermark source image is WEBP
'; + $watermark_checked = true; + } + } + } else if ($watermark_type == IMAGETYPE_BMP) { + if (!method_exists($this, 'imagecreatefrombmp')) { + $this->error = $this->translate('watermark_no_create_support', array('BMP')); + } else { + $filter = @$this->imagecreatefrombmp($this->image_watermark); + if (!$filter) { + $this->error = $this->translate('watermark_create_error', array('BMP')); + } else { + $this->log .= '    watermark source image is BMP
'; + $watermark_checked = true; + } + } + } else { + $this->error = $this->translate('watermark_invalid'); + } + if ($watermark_checked) { + $watermark_dst_width = $watermark_src_width = imagesx($filter); + $watermark_dst_height = $watermark_src_height = imagesy($filter); + + // if watermark is too large/tall, resize it first + if ((!$this->image_watermark_no_zoom_out && ($watermark_dst_width > $this->image_dst_x || $watermark_dst_height > $this->image_dst_y)) + || (!$this->image_watermark_no_zoom_in && $watermark_dst_width < $this->image_dst_x && $watermark_dst_height < $this->image_dst_y)) { + $canvas_width = $this->image_dst_x - abs($this->image_watermark_x); + $canvas_height = $this->image_dst_y - abs($this->image_watermark_y); + if (($watermark_src_width/$canvas_width) > ($watermark_src_height/$canvas_height)) { + $watermark_dst_width = $canvas_width; + $watermark_dst_height = intval($watermark_src_height*($canvas_width / $watermark_src_width)); + } else { + $watermark_dst_height = $canvas_height; + $watermark_dst_width = intval($watermark_src_width*($canvas_height / $watermark_src_height)); + } + $this->log .= '    watermark resized from '.$watermark_src_width.'x'.$watermark_src_height.' to '.$watermark_dst_width.'x'.$watermark_dst_height.'
'; + + } + // determine watermark position + $watermark_x = 0; + $watermark_y = 0; + if (is_numeric($this->image_watermark_x)) { + if ($this->image_watermark_x < 0) { + $watermark_x = $this->image_dst_x - $watermark_dst_width + $this->image_watermark_x; + } else { + $watermark_x = $this->image_watermark_x; + } + } else { + if (strpos($this->image_watermark_position, 'r') !== false) { + $watermark_x = $this->image_dst_x - $watermark_dst_width; + } else if (strpos($this->image_watermark_position, 'l') !== false) { + $watermark_x = 0; + } else { + $watermark_x = ($this->image_dst_x - $watermark_dst_width) / 2; + } + } + if (is_numeric($this->image_watermark_y)) { + if ($this->image_watermark_y < 0) { + $watermark_y = $this->image_dst_y - $watermark_dst_height + $this->image_watermark_y; + } else { + $watermark_y = $this->image_watermark_y; + } + } else { + if (strpos($this->image_watermark_position, 'b') !== false) { + $watermark_y = $this->image_dst_y - $watermark_dst_height; + } else if (strpos($this->image_watermark_position, 't') !== false) { + $watermark_y = 0; + } else { + $watermark_y = ($this->image_dst_y - $watermark_dst_height) / 2; + } + } + imagealphablending($image_dst, true); + imagecopyresampled($image_dst, $filter, $watermark_x, $watermark_y, 0, 0, $watermark_dst_width, $watermark_dst_height, $watermark_src_width, $watermark_src_height); + } else { + $this->error = $this->translate('watermark_invalid'); + } + } + + // add text + if (!empty($this->image_text)) { + $this->log .= '- add text
'; + + // calculate sizes in human readable format + $src_size = $this->file_src_size / 1024; + $src_size_mb = number_format($src_size / 1024, 1, ".", " "); + $src_size_kb = number_format($src_size, 1, ".", " "); + $src_size_human = ($src_size > 1024 ? $src_size_mb . " MB" : $src_size_kb . " kb"); + + $this->image_text = str_replace( + array('[src_name]', + '[src_name_body]', + '[src_name_ext]', + '[src_pathname]', + '[src_mime]', + '[src_size]', + '[src_size_kb]', + '[src_size_mb]', + '[src_size_human]', + '[src_x]', + '[src_y]', + '[src_pixels]', + '[src_type]', + '[src_bits]', + '[dst_path]', + '[dst_name_body]', + '[dst_name_ext]', + '[dst_name]', + '[dst_pathname]', + '[dst_x]', + '[dst_y]', + '[date]', + '[time]', + '[host]', + '[server]', + '[ip]', + '[gd_version]'), + array($this->file_src_name, + $this->file_src_name_body, + $this->file_src_name_ext, + $this->file_src_pathname, + $this->file_src_mime, + $this->file_src_size, + $src_size_kb, + $src_size_mb, + $src_size_human, + $this->image_src_x, + $this->image_src_y, + $this->image_src_pixels, + $this->image_src_type, + $this->image_src_bits, + $this->file_dst_path, + $this->file_dst_name_body, + $this->file_dst_name_ext, + $this->file_dst_name, + $this->file_dst_pathname, + $this->image_dst_x, + $this->image_dst_y, + date('Y-m-d'), + date('H:i:s'), + (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'n/a'), + (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'n/a'), + (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'n/a'), + $this->gdversion(true)), + $this->image_text); + + if (!is_numeric($this->image_text_padding)) $this->image_text_padding = 0; + if (!is_numeric($this->image_text_line_spacing)) $this->image_text_line_spacing = 0; + if (!is_numeric($this->image_text_padding_x)) $this->image_text_padding_x = $this->image_text_padding; + if (!is_numeric($this->image_text_padding_y)) $this->image_text_padding_y = $this->image_text_padding; + $this->image_text_position = strtolower($this->image_text_position); + $this->image_text_direction = strtolower($this->image_text_direction); + $this->image_text_alignment = strtolower($this->image_text_alignment); + + $font_type = 'gd'; + + // if the font is a string with a GDF font path, we assume that we might want to load a font + if (!is_numeric($this->image_text_font) && strlen($this->image_text_font) > 4 && substr(strtolower($this->image_text_font), -4) == '.gdf') { + if (strpos($this->image_text_font, '/') === false) $this->image_text_font = "./" . $this->image_text_font; + $this->log .= '    try to load font ' . $this->image_text_font . '... '; + if ($this->image_text_font = @imageloadfont($this->image_text_font)) { + $this->log .= 'success
'; + } else { + $this->log .= 'error
'; + $this->image_text_font = 5; + } + } + + // if the font is a string with a TTF font path, we check if we can access the font file + if (!is_numeric($this->image_text_font) && strlen($this->image_text_font) > 4 && substr(strtolower($this->image_text_font), -4) == '.ttf') { + $this->log .= '    try to load font ' . $this->image_text_font . '... '; + if (strpos($this->image_text_font, '/') === false) $this->image_text_font = "./" . $this->image_text_font; + if (file_exists($this->image_text_font) && is_readable($this->image_text_font)) { + $this->log .= 'success
'; + $font_type = 'tt'; + } else { + $this->log .= 'error
'; + $this->image_text_font = 5; + } + } + + // get the text bounding box (GD fonts) + if ($font_type == 'gd') { + $text = explode("\n", $this->image_text); + $char_width = imagefontwidth($this->image_text_font); + $char_height = imagefontheight($this->image_text_font); + $text_height = 0; + $text_width = 0; + $line_height = 0; + $line_width = 0; + foreach ($text as $k => $v) { + if ($this->image_text_direction == 'v') { + $h = ($char_width * strlen($v)); + if ($h > $text_height) $text_height = $h; + $line_width = $char_height; + $text_width += $line_width + ($k < (sizeof($text)-1) ? $this->image_text_line_spacing : 0); + } else { + $w = ($char_width * strlen($v)); + if ($w > $text_width) $text_width = $w; + $line_height = $char_height; + $text_height += $line_height + ($k < (sizeof($text)-1) ? $this->image_text_line_spacing : 0); + } + } + $text_width += (2 * $this->image_text_padding_x); + $text_height += (2 * $this->image_text_padding_y); + + // get the text bounding box (TrueType fonts) + } else if ($font_type == 'tt') { + $text = $this->image_text; + if (!$this->image_text_angle) $this->image_text_angle = $this->image_text_direction == 'v' ? 90 : 0; + $text_height = 0; + $text_width = 0; + $text_offset_x = 0; + $text_offset_y = 0; + $rect = imagettfbbox($this->image_text_size, $this->image_text_angle, $this->image_text_font, $text ); + if ($rect) { + $minX = min(array($rect[0],$rect[2],$rect[4],$rect[6])); + $maxX = max(array($rect[0],$rect[2],$rect[4],$rect[6])); + $minY = min(array($rect[1],$rect[3],$rect[5],$rect[7])); + $maxY = max(array($rect[1],$rect[3],$rect[5],$rect[7])); + $text_offset_x = abs($minX) - 1; + $text_offset_y = abs($minY) - 1; + $text_width = $maxX - $minX + (2 * $this->image_text_padding_x); + $text_height = $maxY - $minY + (2 * $this->image_text_padding_y); + } + } + + // position the text block + $text_x = 0; + $text_y = 0; + if (is_numeric($this->image_text_x)) { + if ($this->image_text_x < 0) { + $text_x = $this->image_dst_x - $text_width + $this->image_text_x; + } else { + $text_x = $this->image_text_x; + } + } else { + if (strpos($this->image_text_position, 'r') !== false) { + $text_x = $this->image_dst_x - $text_width; + } else if (strpos($this->image_text_position, 'l') !== false) { + $text_x = 0; + } else { + $text_x = ($this->image_dst_x - $text_width) / 2; + } + } + if (is_numeric($this->image_text_y)) { + if ($this->image_text_y < 0) { + $text_y = $this->image_dst_y - $text_height + $this->image_text_y; + } else { + $text_y = $this->image_text_y; + } + } else { + if (strpos($this->image_text_position, 'b') !== false) { + $text_y = $this->image_dst_y - $text_height; + } else if (strpos($this->image_text_position, 't') !== false) { + $text_y = 0; + } else { + $text_y = ($this->image_dst_y - $text_height) / 2; + } + } + + // add a background, maybe transparent + if (!empty($this->image_text_background)) { + list($red, $green, $blue) = $this->getcolors($this->image_text_background); + if ($gd_version >= 2 && (is_numeric($this->image_text_background_opacity)) && $this->image_text_background_opacity >= 0 && $this->image_text_background_opacity <= 100) { + $filter = imagecreatetruecolor($text_width, $text_height); + $background_color = imagecolorallocate($filter, $red, $green, $blue); + imagefilledrectangle($filter, 0, 0, $text_width, $text_height, $background_color); + $this->imagecopymergealpha($image_dst, $filter, $text_x, $text_y, 0, 0, $text_width, $text_height, $this->image_text_background_opacity); + $this->imageunset($filter); + } else { + $background_color = imagecolorallocate($image_dst ,$red, $green, $blue); + imagefilledrectangle($image_dst, $text_x, $text_y, $text_x + $text_width, $text_y + $text_height, $background_color); + } + } + + $text_x += $this->image_text_padding_x; + $text_y += $this->image_text_padding_y; + $t_width = $text_width - (2 * $this->image_text_padding_x); + $t_height = $text_height - (2 * $this->image_text_padding_y); + list($red, $green, $blue) = $this->getcolors($this->image_text_color); + + // add the text, maybe transparent + if ($gd_version >= 2 && (is_numeric($this->image_text_opacity)) && $this->image_text_opacity >= 0 && $this->image_text_opacity <= 100) { + if ($t_width < 0) $t_width = 0; + if ($t_height < 0) $t_height = 0; + $filter = $this->imagecreatenew($t_width, $t_height, false, true); + $text_color = imagecolorallocate($filter ,$red, $green, $blue); + + if ($font_type == 'gd') { + foreach ($text as $k => $v) { + if ($this->image_text_direction == 'v') { + imagestringup($filter, + $this->image_text_font, + $k * ($line_width + ($k > 0 && $k < (sizeof($text)) ? $this->image_text_line_spacing : 0)), + $text_height - (2 * $this->image_text_padding_y) - ($this->image_text_alignment == 'l' ? 0 : (($t_height - strlen($v) * $char_width) / ($this->image_text_alignment == 'r' ? 1 : 2))) , + $v, + $text_color); + } else { + imagestring($filter, + $this->image_text_font, + ($this->image_text_alignment == 'l' ? 0 : (($t_width - strlen($v) * $char_width) / ($this->image_text_alignment == 'r' ? 1 : 2))), + $k * ($line_height + ($k > 0 && $k < (sizeof($text)) ? $this->image_text_line_spacing : 0)), + $v, + $text_color); + } + } + } else if ($font_type == 'tt') { + imagettftext($filter, + $this->image_text_size, + $this->image_text_angle, + $text_offset_x, + $text_offset_y, + $text_color, + $this->image_text_font, + $text); + } + $this->imagecopymergealpha($image_dst, $filter, $text_x, $text_y, 0, 0, $t_width, $t_height, $this->image_text_opacity); + $this->imageunset($filter); + + } else { + $text_color = imagecolorallocate($image_dst ,$red, $green, $blue); + if ($font_type == 'gd') { + foreach ($text as $k => $v) { + if ($this->image_text_direction == 'v') { + imagestringup($image_dst, + $this->image_text_font, + $text_x + $k * ($line_width + ($k > 0 && $k < (sizeof($text)) ? $this->image_text_line_spacing : 0)), + $text_y + $text_height - (2 * $this->image_text_padding_y) - ($this->image_text_alignment == 'l' ? 0 : (($t_height - strlen($v) * $char_width) / ($this->image_text_alignment == 'r' ? 1 : 2))), + $v, + $text_color); + } else { + imagestring($image_dst, + $this->image_text_font, + $text_x + ($this->image_text_alignment == 'l' ? 0 : (($t_width - strlen($v) * $char_width) / ($this->image_text_alignment == 'r' ? 1 : 2))), + $text_y + $k * ($line_height + ($k > 0 && $k < (sizeof($text)) ? $this->image_text_line_spacing : 0)), + $v, + $text_color); + } + } + } else if ($font_type == 'tt') { + imagettftext($image_dst, + $this->image_text_size, + $this->image_text_angle, + $text_offset_x + ($this->image_dst_x / 2) - ($text_width / 2) + $this->image_text_padding_x, + $text_offset_y + ($this->image_dst_y / 2) - ($text_height / 2) + $this->image_text_padding_y, + $text_color, + $this->image_text_font, + $text); + } + } + } + + // add a reflection + if ($this->image_reflection_height) { + $this->log .= '- add reflection : ' . $this->image_reflection_height . '
'; + // we decode image_reflection_height, which can be a integer, a string in pixels or percentage + $image_reflection_height = $this->image_reflection_height; + if (strpos($image_reflection_height, '%')>0) $image_reflection_height = $this->image_dst_y * ((int) str_replace('%','',$image_reflection_height) / 100); + if (strpos($image_reflection_height, 'px')>0) $image_reflection_height = (int) str_replace('px','',$image_reflection_height); + $image_reflection_height = (int) $image_reflection_height; + if ($image_reflection_height > $this->image_dst_y) $image_reflection_height = $this->image_dst_y; + if (empty($this->image_reflection_opacity)) $this->image_reflection_opacity = 60; + // create the new destination image + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y + $image_reflection_height + $this->image_reflection_space, true); + $transparency = $this->image_reflection_opacity; + + // copy the original image + imagecopy($tmp, $image_dst, 0, 0, 0, 0, $this->image_dst_x, $this->image_dst_y + ($this->image_reflection_space < 0 ? $this->image_reflection_space : 0)); + + // we have to make sure the extra bit is the right color, or transparent + if ($image_reflection_height + $this->image_reflection_space > 0) { + // use the background color if present + if (!empty($this->image_background_color)) { + list($red, $green, $blue) = $this->getcolors($this->image_background_color); + $fill = imagecolorallocate($tmp, $red, $green, $blue); + } else { + $fill = imagecolorallocatealpha($tmp, 0, 0, 0, 127); + } + // fill in from the edge of the extra bit + imagefill($tmp, round($this->image_dst_x / 2), $this->image_dst_y + $image_reflection_height + $this->image_reflection_space - 1, $fill); + } + + // copy the reflection + for ($y = 0; $y < $image_reflection_height; $y++) { + for ($x = 0; $x < $this->image_dst_x; $x++) { + $pixel_b = imagecolorsforindex($tmp, imagecolorat($tmp, $x, $y + $this->image_dst_y + $this->image_reflection_space)); + $pixel_o = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $this->image_dst_y - $y - 1 + ($this->image_reflection_space < 0 ? $this->image_reflection_space : 0))); + $alpha_o = 1 - ($pixel_o['alpha'] / 127); + $alpha_b = 1 - ($pixel_b['alpha'] / 127); + $opacity = $alpha_o * $transparency / 100; + if ($opacity > 0) { + $red = round((($pixel_o['red'] * $opacity) + ($pixel_b['red'] ) * $alpha_b) / ($alpha_b + $opacity)); + $green = round((($pixel_o['green'] * $opacity) + ($pixel_b['green']) * $alpha_b) / ($alpha_b + $opacity)); + $blue = round((($pixel_o['blue'] * $opacity) + ($pixel_b['blue'] ) * $alpha_b) / ($alpha_b + $opacity)); + $alpha = ($opacity + $alpha_b); + if ($alpha > 1) $alpha = 1; + $alpha = round((1 - $alpha) * 127); + $color = imagecolorallocatealpha($tmp, $red, $green, $blue, $alpha); + imagesetpixel($tmp, $x, $y + $this->image_dst_y + $this->image_reflection_space, $color); + } + } + if ($transparency > 0) $transparency = $transparency - ($this->image_reflection_opacity / $image_reflection_height); + } + + // copy the resulting image into the destination image + $this->image_dst_y = $this->image_dst_y + $image_reflection_height + $this->image_reflection_space; + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // change opacity + if ($gd_version >= 2 && is_numeric($this->image_opacity) && $this->image_opacity < 100) { + $this->log .= '- change opacity
'; + // create the new destination image + $tmp = $this->imagecreatenew($this->image_dst_x, $this->image_dst_y, true); + for($y=0; $y < $this->image_dst_y; $y++) { + for($x=0; $x < $this->image_dst_x; $x++) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $alpha = $pixel['alpha'] + round((127 - $pixel['alpha']) * (100 - $this->image_opacity) / 100); + if ($alpha > 127) $alpha = 127; + if ($alpha > 0) { + $color = imagecolorallocatealpha($tmp, $pixel['red'] , $pixel['green'], $pixel['blue'], $alpha); + imagesetpixel($tmp, $x, $y, $color); + } + } + } + // copy the resulting image into the destination image + $image_dst = $this->imagetransfer($tmp, $image_dst); + } + + // reduce the JPEG image to a set desired size + if (is_numeric($this->jpeg_size) && $this->jpeg_size > 0 && ($this->image_convert == 'jpeg' || $this->image_convert == 'jpg')) { + // inspired by: JPEGReducer class version 1, 25 November 2004, Author: Huda M ElMatsani, justhuda at netscape dot net + $this->log .= '- JPEG desired file size : ' . $this->jpeg_size . '
'; + // calculate size of each image. 75%, 50%, and 25% quality + ob_start(); imagejpeg($image_dst,null,75); $buffer = ob_get_contents(); ob_end_clean(); + $size75 = strlen($buffer); + ob_start(); imagejpeg($image_dst,null,50); $buffer = ob_get_contents(); ob_end_clean(); + $size50 = strlen($buffer); + ob_start(); imagejpeg($image_dst,null,25); $buffer = ob_get_contents(); ob_end_clean(); + $size25 = strlen($buffer); + + // make sure we won't divide by 0 + if ($size50 == $size25) $size50++; + if ($size75 == $size50 || $size75 == $size25) $size75++; + + // calculate gradient of size reduction by quality + $mgrad1 = 25 / ($size50-$size25); + $mgrad2 = 25 / ($size75-$size50); + $mgrad3 = 50 / ($size75-$size25); + $mgrad = ($mgrad1 + $mgrad2 + $mgrad3) / 3; + // result of approx. quality factor for expected size + $q_factor = round($mgrad * ($this->jpeg_size - $size50) + 50); + + if ($q_factor<1) { + $this->jpeg_quality=1; + } elseif ($q_factor>100) { + $this->jpeg_quality=100; + } else { + $this->jpeg_quality=$q_factor; + } + $this->log .= '    JPEG quality factor set to ' . $this->jpeg_quality . '
'; + } + + // converts image from true color, and fix transparency if needed + $this->log .= '- converting...
'; + $this->image_dst_type = $this->image_convert; + switch($this->image_convert) { + case 'gif': + // if the image is true color, we convert it to a palette + if (imageistruecolor($image_dst)) { + $this->log .= '    true color to palette
'; + // creates a black and white mask + $mask = array(array()); + for ($x = 0; $x < $this->image_dst_x; $x++) { + for ($y = 0; $y < $this->image_dst_y; $y++) { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $mask[$x][$y] = $pixel['alpha']; + } + } + list($red, $green, $blue) = $this->getcolors($this->image_default_color); + // first, we merge the image with the background color, so we know which colors we will have + for ($x = 0; $x < $this->image_dst_x; $x++) { + for ($y = 0; $y < $this->image_dst_y; $y++) { + if ($mask[$x][$y] > 0){ + // we have some transparency. we combine the color with the default color + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + $alpha = ($mask[$x][$y] / 127); + $pixel['red'] = round(($pixel['red'] * (1 -$alpha) + $red * ($alpha))); + $pixel['green'] = round(($pixel['green'] * (1 -$alpha) + $green * ($alpha))); + $pixel['blue'] = round(($pixel['blue'] * (1 -$alpha) + $blue * ($alpha))); + $color = imagecolorallocate($image_dst, $pixel['red'], $pixel['green'], $pixel['blue']); + imagesetpixel($image_dst, $x, $y, $color); + } + } + } + // transforms the true color image into palette, with its merged default color + if (empty($this->image_background_color)) { + imagetruecolortopalette($image_dst, true, 255); + $transparency = imagecolorallocate($image_dst, 254, 1, 253); + imagecolortransparent($image_dst, $transparency); + // make the transparent areas transparent + for ($x = 0; $x < $this->image_dst_x; $x++) { + for ($y = 0; $y < $this->image_dst_y; $y++) { + // we test wether we have enough opacity to justify keeping the color + if ($mask[$x][$y] > 120) imagesetpixel($image_dst, $x, $y, $transparency); + } + } + } + unset($mask); + } + break; + case 'jpg': + case 'bmp': + // if the image doesn't support any transparency, then we merge it with the default color + $this->log .= '    fills in transparency with default color
'; + list($red, $green, $blue) = $this->getcolors($this->image_default_color); + $transparency = imagecolorallocate($image_dst, $red, $green, $blue); + // make the transaparent areas transparent + for ($x = 0; $x < $this->image_dst_x; $x++) { + for ($y = 0; $y < $this->image_dst_y; $y++) { + // we test wether we have some transparency, in which case we will merge the colors + if (imageistruecolor($image_dst)) { + $rgba = imagecolorat($image_dst, $x, $y); + $pixel = array('red' => ($rgba >> 16) & 0xFF, + 'green' => ($rgba >> 8) & 0xFF, + 'blue' => $rgba & 0xFF, + 'alpha' => ($rgba & 0x7F000000) >> 24); + } else { + $pixel = imagecolorsforindex($image_dst, imagecolorat($image_dst, $x, $y)); + } + if ($pixel['alpha'] == 127) { + // we have full transparency. we make the pixel transparent + imagesetpixel($image_dst, $x, $y, $transparency); + } else if ($pixel['alpha'] > 0) { + // we have some transparency. we combine the color with the default color + $alpha = ($pixel['alpha'] / 127); + $pixel['red'] = round(($pixel['red'] * (1 -$alpha) + $red * ($alpha))); + $pixel['green'] = round(($pixel['green'] * (1 -$alpha) + $green * ($alpha))); + $pixel['blue'] = round(($pixel['blue'] * (1 -$alpha) + $blue * ($alpha))); + $color = imagecolorclosest($image_dst, $pixel['red'], $pixel['green'], $pixel['blue']); + imagesetpixel($image_dst, $x, $y, $color); + } + } + } + + break; + default: + break; + } + + // interlace options + if($this->image_interlace) imageinterlace($image_dst, true); + + // outputs image + $this->log .= '- saving image...
'; + switch($this->image_convert) { + case 'jpeg': + case 'jpg': + if (!$return_mode) { + $result = @imagejpeg($image_dst, $this->file_dst_pathname, $this->jpeg_quality); + } else { + ob_start(); + $result = @imagejpeg($image_dst, null, $this->jpeg_quality); + $return_content = ob_get_contents(); + ob_end_clean(); + } + if (!$result) { + $this->processed = false; + $this->error = $this->translate('file_create', array('JPEG')); + } else { + $this->log .= '    JPEG image created
'; + } + break; + case 'png': + imagealphablending( $image_dst, false ); + imagesavealpha( $image_dst, true ); + if (!$return_mode) { + if (is_numeric($this->png_compression) && version_compare(PHP_VERSION, '5.1.2') >= 0) { + $result = @imagepng($image_dst, $this->file_dst_pathname, $this->png_compression); + } else { + $result = @imagepng($image_dst, $this->file_dst_pathname); + } + } else { + ob_start(); + if (is_numeric($this->png_compression) && version_compare(PHP_VERSION, '5.1.2') >= 0) { + $result = @imagepng($image_dst, null, $this->png_compression); + } else { + $result = @imagepng($image_dst); + } + $return_content = ob_get_contents(); + ob_end_clean(); + } + if (!$result) { + $this->processed = false; + $this->error = $this->translate('file_create', array('PNG')); + } else { + $this->log .= '    PNG image created
'; + } + break; + case 'webp': + imagealphablending( $image_dst, false ); + imagesavealpha( $image_dst, true ); + if (!$return_mode) { + $result = @imagewebp($image_dst, $this->file_dst_pathname, $this->webp_quality); + } else { + ob_start(); + $result = @imagewebp($image_dst, null, $this->webp_quality); + $return_content = ob_get_contents(); + ob_end_clean(); + } + if (!$result) { + $this->processed = false; + $this->error = $this->translate('file_create', array('WEBP')); + } else { + $this->log .= '    WEBP image created
'; + } + break; + case 'gif': + if (!$return_mode) { + $result = @imagegif($image_dst, $this->file_dst_pathname); + } else { + ob_start(); + $result = @imagegif($image_dst); + $return_content = ob_get_contents(); + ob_end_clean(); + } + if (!$result) { + $this->processed = false; + $this->error = $this->translate('file_create', array('GIF')); + } else { + $this->log .= '    GIF image created
'; + } + break; + case 'bmp': + if (!$return_mode) { + $result = $this->imagebmp($image_dst, $this->file_dst_pathname); + } else { + ob_start(); + $result = $this->imagebmp($image_dst); + $return_content = ob_get_contents(); + ob_end_clean(); + } + if (!$result) { + $this->processed = false; + $this->error = $this->translate('file_create', array('BMP')); + } else { + $this->log .= '    BMP image created
'; + } + break; + + default: + $this->processed = false; + $this->error = $this->translate('no_conversion_type'); + } + if ($this->processed) { + $this->imageunset($image_src); + $this->imageunset($image_dst); + $this->log .= '    image objects destroyed
'; + } + } + + } else { + $this->log .= '- no image processing wanted
'; + + if (!$return_mode) { + // copy the file to its final destination. we don't use move_uploaded_file here + // if we happen to have open_basedir restrictions, it is a temp file that we copy, not the original uploaded file + if (!copy($this->file_src_pathname, $this->file_dst_pathname)) { + $this->processed = false; + $this->error = $this->translate('copy_failed'); + } + } else { + // returns the file, so that its content can be received by the caller + $return_content = @file_get_contents($this->file_src_pathname); + if ($return_content === false) { + $this->processed = false; + $this->error = $this->translate('reading_failed'); + } + } + } + } + + if ($this->processed) { + $this->log .= '- process OK
'; + } else { + $this->log .= '- error: ' . $this->error . '
'; + } + + // we reinit all the vars + $this->init(); + + // we may return the image content + if ($return_mode) return $return_content; + + } + + /** + * Deletes the uploaded file from its temporary location + * + * When PHP uploads a file, it stores it in a temporary location. + * When you {@link process} the file, you actually copy the resulting file to the given location, it doesn't alter the original file. + * Once you have processed the file as many times as you wanted, you can delete the uploaded file. + * If there is open_basedir restrictions, the uploaded file is in fact a temporary file + * + * You might want not to use this function if you work on local files, as it will delete the source file + * + * @access public + */ + function clean() { + $this->log .= 'cleanup
'; + $this->log .= '- delete temp file ' . $this->file_src_pathname . '
'; + @unlink($this->file_src_pathname); + } + + + /** + * Opens a BMP image + * + * This function has been written by DHKold, and is used with permission of the author + * + * @access public + */ + function imagecreatefrombmp($filename) { + if (! $f1 = fopen($filename,"rb")) return false; + + $file = unpack("vfile_type/Vfile_size/Vreserved/Vbitmap_offset", fread($f1,14)); + if ($file['file_type'] != 19778) return false; + + $bmp = unpack('Vheader_size/Vwidth/Vheight/vplanes/vbits_per_pixel'. + '/Vcompression/Vsize_bitmap/Vhoriz_resolution'. + '/Vvert_resolution/Vcolors_used/Vcolors_important', fread($f1,40)); + $bmp['colors'] = pow(2,$bmp['bits_per_pixel']); + if ($bmp['size_bitmap'] == 0) $bmp['size_bitmap'] = $file['file_size'] - $file['bitmap_offset']; + $bmp['bytes_per_pixel'] = $bmp['bits_per_pixel']/8; + $bmp['bytes_per_pixel2'] = ceil($bmp['bytes_per_pixel']); + $bmp['decal'] = ($bmp['width']*$bmp['bytes_per_pixel']/4); + $bmp['decal'] -= floor($bmp['width']*$bmp['bytes_per_pixel']/4); + $bmp['decal'] = 4-(4*$bmp['decal']); + if ($bmp['decal'] == 4) $bmp['decal'] = 0; + + $palette = array(); + if ($bmp['colors'] < 16777216) { + $palette = unpack('V'.$bmp['colors'], fread($f1,$bmp['colors']*4)); + } + + $im = fread($f1,$bmp['size_bitmap']); + $vide = chr(0); + + $res = imagecreatetruecolor($bmp['width'],$bmp['height']); + $P = 0; + $Y = $bmp['height']-1; + while ($Y >= 0) { + $X=0; + while ($X < $bmp['width']) { + if ($bmp['bits_per_pixel'] == 24) + $color = unpack("V",substr($im,$P,3).$vide); + elseif ($bmp['bits_per_pixel'] == 16) { + $color = unpack("n",substr($im,$P,2)); + $color[1] = $palette[$color[1]+1]; + } elseif ($bmp['bits_per_pixel'] == 8) { + $color = unpack("n",$vide.substr($im,$P,1)); + $color[1] = $palette[$color[1]+1]; + } elseif ($bmp['bits_per_pixel'] == 4) { + $color = unpack("n",$vide.substr($im,floor($P),1)); + if (($P*2)%2 == 0) $color[1] = ($color[1] >> 4) ; else $color[1] = ($color[1] & 0x0F); + $color[1] = $palette[$color[1]+1]; + } elseif ($bmp['bits_per_pixel'] == 1) { + $color = unpack("n",$vide.substr($im,floor($P),1)); + if (($P*8)%8 == 0) $color[1] = $color[1] >>7; + elseif (($P*8)%8 == 1) $color[1] = ($color[1] & 0x40)>>6; + elseif (($P*8)%8 == 2) $color[1] = ($color[1] & 0x20)>>5; + elseif (($P*8)%8 == 3) $color[1] = ($color[1] & 0x10)>>4; + elseif (($P*8)%8 == 4) $color[1] = ($color[1] & 0x8)>>3; + elseif (($P*8)%8 == 5) $color[1] = ($color[1] & 0x4)>>2; + elseif (($P*8)%8 == 6) $color[1] = ($color[1] & 0x2)>>1; + elseif (($P*8)%8 == 7) $color[1] = ($color[1] & 0x1); + $color[1] = $palette[$color[1]+1]; + } else + return false; + imagesetpixel($res,$X,$Y,$color[1]); + $X++; + $P += $bmp['bytes_per_pixel']; + } + $Y--; + $P+=$bmp['decal']; + } + fclose($f1); + return $res; + } + + /** + * Saves a BMP image + * + * This function has been published on the PHP website, and can be used freely + * + * @access public + */ + function imagebmp(&$im, $filename = "") { + + if (!$im) return false; + $w = imagesx($im); + $h = imagesy($im); + $result = ''; + + // if the image is not true color, we convert it first + if (!imageistruecolor($im)) { + $tmp = imagecreatetruecolor($w, $h); + imagecopy($tmp, $im, 0, 0, 0, 0, $w, $h); + $this->imageunset($im); + $im = & $tmp; + } + + $biBPLine = $w * 3; + $biStride = ($biBPLine + 3) & ~3; + $biSizeImage = $biStride * $h; + $bfOffBits = 54; + $bfSize = $bfOffBits + $biSizeImage; + + $result .= substr('BM', 0, 2); + $result .= pack ('VvvV', $bfSize, 0, 0, $bfOffBits); + $result .= pack ('VVVvvVVVVVV', 40, $w, $h, 1, 24, 0, $biSizeImage, 0, 0, 0, 0); + + $numpad = $biStride - $biBPLine; + for ($y = $h - 1; $y >= 0; --$y) { + for ($x = 0; $x < $w; ++$x) { + $col = imagecolorat ($im, $x, $y); + $result .= substr(pack ('V', $col), 0, 3); + } + for ($i = 0; $i < $numpad; ++$i) + $result .= pack ('C', 0); + } + + if($filename==""){ + echo $result; + } else { + $file = fopen($filename, "wb"); + fwrite($file, $result); + fclose($file); + } + return true; + } +} + +?> diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/API.php b/api/private/classes/pizzaboxer/ProjectPolygon/API.php new file mode 100644 index 0000000..e100293 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/API.php @@ -0,0 +1,125 @@ + $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["user"]["adminlevel"])) PageBuilder::instance()->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["user"]["twofa"] && !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["user"]["twofa"]) self::respond(403, false, "Your account must have two-factor authentication enabled before you can do any administrative actions"); + if (!$admin_ratelimit) return; + + $lastAction = Database::singleton()->run("SELECT time FROM stafflogs WHERE adminId = :uid AND time + 2 > UNIX_TIMESTAMP()", [":uid" => SESSION["user"]["id"]]); + if ($lastAction->rowCount()) self::respond(429, false, "Please wait ".(($lastAction->fetchColumn()+2)-time())." seconds before doing another administrative action"); + } + } + + static function getParameter($Method, $Name, $Type, $DefaultValue = NULL) + { + if ($Method === "GET") + { + $Parameters = $_GET; + } + else if ($Method === "POST") + { + $Parameters = $_POST; + } + else + { + throw new \Exception("Invalid method \"$Method\" specified in API::getParameter"); + } + + if (!isset($Parameters[$Name])) + { + if ($DefaultValue === NULL) self::respond(400, false, "$Method parameter \"$Name\" must be set"); + return $DefaultValue; + } + + $Parameter = $Parameters[$Name]; + + if (is_array($Type)) + { + if (!in_array($Parameter, $Type)) + { + self::respond(400, false, "$Method parameter \"$Name\" must be an enumeration of [" . implode(", ", $Type) . "]"); + } + + return $Parameter; + } + else if ($Type === "int" || $Type === "integer") + { + if (!is_numeric($Parameter)) + { + self::respond(400, false, "$Method parameter \"$Name\" must be an integer"); + } + + return (int) $Parameter; + } + else if ($Type === "bool" || $Type === "boolean") + { + $Parameter = strtolower($Parameter); + + if ($Parameter !== "true" && $Parameter !== "false") + { + self::respond(400, false, "$Method parameter \"$Name\" must be a boolean"); + } + + if ($Parameter == "true") return true; + return false; + } + else if ($Type === "string") + { + return $Parameter; + } + else + { + throw new \Exception("Invalid type \"$Type\" specified in API::getParameter"); + } + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Catalog.php b/api/private/classes/pizzaboxer/ProjectPolygon/Catalog.php new file mode 100644 index 0000000..d6cb4c0 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Catalog.php @@ -0,0 +1,193 @@ + "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) + 22 => "Group Emblem", // (internal use only - these are basically just images really) + 24 => "Animation", + 25 => "Arms", + 26 => "Legs", + 27 => "Torso", + 28 => "Right Arm", + 29 => "Left Arm", + 30 => "Left Leg", + 31 => "Right Leg", + 32 => "Package", + 33 => "YoutubeVideo", + 34 => "Gamepass", + 35 => "App", + 37 => "Code", + 38 => "Plugin", // (ignore everything beyond this point) + 39 => "SolidModel", + 40 => "MeshPart", + 41 => "Hair Accessory", + 42 => "Face Accessory", + 43 => "Neck Accessory", + 44 => "Shoulder Accessory", + 45 => "Front Accessory", + 46 => "Back Accessory", + 47 => "Waist Accessory", + 48 => "Climb Animation", + 49 => "Death Animation", + 50 => "Fall Animation", + 51 => "Idle Animation", + 52 => "Jump Animation", + 53 => "Run Animation", + 54 => "Swim Animation", + 55 => "Walk Animation", + 56 => "Pose Animation", + 59 => "LocalizationTableManifest", + 60 => "LocalizationTableTranslation", + 61 => "Emote Animation", + 62 => "Video", + 63 => "TexturePack", + 64 => "T-Shirt Accessory", + 65 => "Shirt Accessory", + 66 => "Pants Accessory", + 67 => "Jacket Accessory", + 68 => "Sweater Accessory", + 69 => "Shorts Accessory", + 70 => "Left Shoe Accessory", + 71 => "Right Shoe Accessory", + 72 => "Dress Skirt Accessory", + 73 => "Font Family", + 74 => "Font Face", + 75 => "MeshHiddenSurfaceRemoval" + ]; + + 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" => "Navigation", "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" => "Building", "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 Database::singleton()->run( + "SELECT assets.*, users.username, users.jointime FROM assets + INNER JOIN users ON creator = users.id WHERE assets.id = :id", + [":id" => $id])->fetch(\PDO::FETCH_OBJ); + } + + static function CreateAsset($options) + { + $columns = array_keys($options); + + // is this safe? + $querystring = "INSERT INTO assets (".implode(", ", $columns).", created, updated) "; + array_walk($columns, function(&$value, $_){ $value = ":{$value}"; }); + $querystring .= "VALUES (".implode(", ", $columns).", UNIX_TIMESTAMP(), UNIX_TIMESTAMP())"; + + Database::singleton()->run($querystring, $options); + + $aid = Database::singleton()->lastInsertId(); + $uid = $options["creator"] ?? SESSION["user"]["id"]; + + Database::singleton()->run("INSERT INTO ownedAssets (assetId, userId, timestamp) VALUES (:aid, :uid, UNIX_TIMESTAMP())", [":aid" => $aid, ":uid" => $uid]); + + return $aid; + } + + static function DeleteAsset($AssetID) + { + $Location = SITE_CONFIG["paths"]["assets"].$AssetID; + if (file_exists($Location)) unlink($Location); + } + + static function OwnsAsset($uid, $aid) + { + return Database::singleton()->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(); ?> + + null + nil + " referent="RBX0"> + + + 5 + + 20 + 0 + + %ASSETURL% + + + "> + %ASSETURL% + + + + true + + + + pdo = new \PDO( + "mysql:host=" . \SITE_CONFIG["database"]["host"] . "; + dbname=" . \SITE_CONFIG["database"]["schema"] . "; + charset=utf8mb4", + \SITE_CONFIG["database"]["username"], + \SITE_CONFIG["database"]["password"] + ); + + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->pdo->setAttribute(\PDO::ATTR_PERSISTENT, true); + } + + /** + * Executes an SQL query. + * + * @param string $sql + * @param array $args + * + * @return \PDOStatement + */ + function run($sql, $args = null) + { + if (!$args) return $this->pdo->query($sql); + + $stmt = $this->pdo->prepare($sql); + + foreach ($args as $param => $value) + { + $stmt->bindValue($param, $value, is_int($value) ? \PDO::PARAM_INT : \PDO::PARAM_STR); + } + + $stmt->execute(); + + return $stmt; + } + + /** + * Gets the unique ID of the row that was last inserted. + * + * @return string|false + */ + function lastInsertId() + { + return $this->pdo->lastInsertId(); + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Discord.php b/api/private/classes/pizzaboxer/ProjectPolygon/Discord.php new file mode 100644 index 0000000..5ec2c22 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Discord.php @@ -0,0 +1,74 @@ +run("SELECT discordID FROM users WHERE id = :UserID", [":UserID" => $UserID])->fetchColumn(); + if ($IsVerified === NULL) return false; + return true; + } + + 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; + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/ErrorHandler.php b/api/private/classes/pizzaboxer/ProjectPolygon/ErrorHandler.php new file mode 100644 index 0000000..9176e8b --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/ErrorHandler.php @@ -0,0 +1,128 @@ +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); + $message = $this->getVerboseMessage(); + $parameters = $_SERVER["REQUEST_URI"]; + + $log[$logID] = + [ + "Timestamp" => time(), + // "GETParameters" => $_GET, + "GETParameters" => $parameters, + "Message" => $message + ]; + + file_put_contents($logFile, json_encode($log)); + + Discord::SendToWebhook( + ["content" => "<@194171603049775113> An unexpected error occurred\nError ID: `{$logID}`\nTime: `".date('d/m/Y h:i:s A')."`\nParameters: `{$parameters}`\nMessage:\n```{$message}```"], + Discord::WEBHOOK_POLYGON_ERRORLOG, + false + ); + + return $logID; + } + + private function logAndRedirect() + { + $logID = $this->writeLog(); + + if (headers_sent()) + { + die("An unexpected error occurred! More info: $logID"); + } + else if (defined("SESSION") && isset(SESSION["user"]["adminlevel"]) && SESSION["user"]["adminlevel"] != 0) + { + redirect("/error?id=$logID&verbose=true"); + } + else + { + redirect("/error?id=$logID"); + } + } + + public function handleError($type, $string, $file, $line) + { + $this->type = $this->getType($type); + $this->string = $string; + $this->file = $file; + $this->line = $line; + + $this->logAndRedirect(); + } + + public function handleException($exception) + { + $this->type = "Exception"; + $this->exception = $exception; + + $this->logAndRedirect(); + } + + public function register() + { + 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; + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Forum.php b/api/private/classes/pizzaboxer/ProjectPolygon/Forum.php new file mode 100644 index 0000000..32debcc --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Forum.php @@ -0,0 +1,39 @@ +run("SELECT * FROM forum_threads WHERE id = :id", [":id" => $id])->fetch(\PDO::FETCH_OBJ); + } + + static function GetReplyInfo($id) + { + return Database::singleton()->run("SELECT * FROM forum_replies WHERE id = :id", [":id" => $id])->fetch(\PDO::FETCH_OBJ); + } + + static function GetThreadReplies($id) + { + return Database::singleton()->run("SELECT COUNT(*) FROM forum_replies WHERE threadId = :id AND NOT deleted", [":id" => $id])->fetchColumn() ?: "-"; + } + + static function GetSubforumInfo($id) + { + return Database::singleton()->run("SELECT * FROM forum_subforums WHERE id = :id", [":id" => $id])->fetch(\PDO::FETCH_OBJ); + } + + static function GetSubforumThreadCount($id, $includeReplies = false) + { + $threads = Database::singleton()->run("SELECT COUNT(*) FROM forum_threads WHERE subforumid = :id", [":id" => $id])->fetchColumn(); + if(!$includeReplies) return $threads ?: '-'; + + $replies = Database::singleton()->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 ?: '-'; + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Games.php b/api/private/classes/pizzaboxer/ProjectPolygon/Games.php new file mode 100644 index 0000000..4763d37 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Games.php @@ -0,0 +1,161 @@ +run( + "SELECT selfhosted_servers.*, + users.username, users.jointime, + (SELECT COUNT(DISTINCT uid) FROM client_sessions WHERE ping+35 > UNIX_TIMESTAMP() AND serverID = :id AND valid AND verified) AS players, + (ping+35 > UNIX_TIMESTAMP()) AS online + FROM selfhosted_servers + INNER JOIN users ON users.id = hoster + WHERE selfhosted_servers.id = :id AND (Privacy = \"Public\" OR hoster = :UserID OR JSON_CONTAINS(PrivacyWhitelist, :UserID, \"$\"))", + [":id" => $id, ":UserID" => $UserID] + )->fetch(\PDO::FETCH_OBJ); + } + else + { + return Database::singleton()->run( + "SELECT selfhosted_servers.*, + users.username, users.jointime, + (SELECT COUNT(DISTINCT uid) FROM client_sessions WHERE ping+35 > UNIX_TIMESTAMP() AND serverID = :id AND valid AND verified) 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 Database::singleton()->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]); + } + + static function GetPlayerCountInServer($ServerID) + { + $PlayerCount = Database::singleton()->run( + "SELECT COUNT(DISTINCT uid) FROM client_sessions WHERE ping+35 > UNIX_TIMESTAMP() AND serverID = :ServerID AND valid AND verified)", + [":ServerID" => $ServerID] + )->fetchColumn(); + + return (int) $PlayerCount; + } + + static function GetPlayersInGame($JobID) + { + $Players = Database::singleton()->run( + "SELECT GameJobSessions.UserID, users.username AS Username FROM GameJobSessions + INNER JOIN users ON users.id = UserID + WHERE JobID = :JobID AND Active", + [":JobID" => $JobID] + )->fetchAll(); + + foreach ($Players as &$Player) + { + $Player["Thumbnail"] = Thumbnails::GetAvatar($Player["UserID"]); + } + + return $Players; + } + + static function CheckIfPlayerInGame($UserID, $JobID) + { + return Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobSessions WHERE Active AND Verified AND UserID = :UserID AND JobID = :JobID", + [":UserID" => $UserID, ":JobID" => $JobID] + )->fetchColumn(); + } + + static function GetJobSession($Ticket) + { + // used for clientpresence and placevisit + // we just want to make sure that the ticket exists and the game job is open + return Database::singleton()->run( + "SELECT GameJobSessions.*, GameJobs.PlaceID FROM GameJobSessions + INNER JOIN GameJobs ON GameJobs.JobID = GameJobSessions.JobID + WHERE SecurityTicket = :Ticket AND Status = \"Ready\"", + [":Ticket" => $Ticket] + )->fetch(\PDO::FETCH_OBJ); + } + + static function GetJobInfo($JobID) + { + return Database::singleton()->run( + "SELECT * FROM GameJobs WHERE JobID = :JobID", + [":JobID" => $JobID] + )->fetch(\PDO::FETCH_OBJ); + } + + static function RefreshJobCount($ServerID) + { + $JobCount = Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobs WHERE ServerID = :ServerID AND Status IN (\"Loading\", \"Ready\")", + [":ServerID" => $ServerID] + )->fetchColumn(); + + Database::singleton()->run( + "UPDATE GameServers SET ActiveJobs = :JobCount WHERE ServerID = :ServerID", + [":JobCount" => $JobCount, ":ServerID" => $ServerID] + ); + } + + static function RefreshActivePlayers($PlaceID) + { + $ActivePlayers = Database::singleton()->run( + "SELECT COUNT(*) FROM GameJobSessions WHERE Active AND JobID IN ( + SELECT JobID FROM GameJobs WHERE PlaceID = :PlaceID AND Status = \"Ready\" + )", + [":PlaceID" => $PlaceID] + )->fetchColumn(); + + Database::singleton()->run( + "UPDATE assets SET ActivePlayers = :ActivePlayers WHERE id = :PlaceID", + [":ActivePlayers" => $ActivePlayers, ":PlaceID" => $PlaceID] + ); + } + + static function RefreshRunningGameMarker($PlaceID) + { + Database::singleton()->run( + "UPDATE assets SET ServerRunning = ( + SELECT COUNT(*) FROM GameJobs WHERE PlaceID = :PlaceID AND Status = \"Ready\" + ) > 0 WHERE id = :PlaceID", + [":PlaceID" => $PlaceID] + ); + } + + static function CanPlayGame($PlaceInfo) + { + if (is_int($PlaceInfo)) $PlaceInfo = Database::singleton()->run("SELECT * FROM assets WHERE id = :PlaceID", [":PlaceID" => $PlaceInfo])->fetch(\PDO::FETCH_OBJ); + + if ($PlaceInfo->Access == "Everyone") return true; + if (!SESSION) return false; + + if ($PlaceInfo->creator == SESSION["user"]["id"]) return true; + + if ($PlaceInfo->Access == "Friends") + { + $isFriends = Database::singleton()->run( + "SELECT COUNT(*) FROM friends WHERE :UserID IN (requesterId, receiverId) AND :OtherID IN (requesterId, receiverId) AND status = 1", + [":UserID" => SESSION["user"]["id"], ":OtherID" => $PlaceInfo->creator] + )->fetchColumn(); + + if ($isFriends) return true; + } + + return false; + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Groups.php b/api/private/classes/pizzaboxer/ProjectPolygon/Groups.php new file mode 100644 index 0000000..f0be93f --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Groups.php @@ -0,0 +1,164 @@ +run("SELECT * FROM groups WHERE groups.id = :id", [":id" => $GroupID])->fetch(\PDO::FETCH_OBJ); + } + else + { + $GroupInfo = Database::singleton()->run( + "SELECT groups.*, 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 Database::singleton()->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 = Database::singleton()->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 = Database::singleton()->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 Database::singleton()->run("SELECT * FROM groups_ranks WHERE GroupID = :id ORDER BY Rank ASC", [":id" => $GroupID]); + else + return Database::singleton()->run("SELECT * FROM groups_ranks WHERE GroupID = :id AND Rank != 0 ORDER BY Rank ASC", [":id" => $GroupID]); + } + + static function CheckIfUserInGroup($UserID, $GroupID) + { + return Database::singleton()->run( + "SELECT * FROM groups_members WHERE UserID = :UserID AND GroupID = :GroupID", + [":UserID" => $UserID, ":GroupID" => $GroupID] + )->rowCount(); + } + + static function GetUserRank($UserID, $GroupID) + { + $RankLevel = Database::singleton()->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 Database::singleton()->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["user"]["id"], $GroupID); + + Database::singleton()->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["user"]["id"], ":Rank" => $MyRank->Name, ":Description" => $Description] + ); + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Gzip.php b/api/private/classes/pizzaboxer/ProjectPolygon/Gzip.php new file mode 100644 index 0000000..d870b47 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Gzip.php @@ -0,0 +1,58 @@ +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"]; + } + } + + $directory = Polygon::GetSharedResource($options["dir"]); + + if(strlen($options["name"]) && file_exists($directory.$options["name"])) + unlink($directory.$options["name"]); + + $handle->process($directory); + 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(SITE_CONFIG['paths']['thumbs']."/$img.png", 420, 420, SITE_CONFIG['paths']['thumbs_assets']."/$assetID-420x420.png"); + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/PageBuilder.php b/api/private/classes/pizzaboxer/ProjectPolygon/PageBuilder.php new file mode 100644 index 0000000..473c0cf --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/PageBuilder.php @@ -0,0 +1,186 @@ + false, + "Theme" => "light", + "ShowNavbar" => true, + "ShowFooter" => true + ]; + + private array $appAttributes = + [ + "class" => "app container py-4 nav-content" + ]; + + private array $metaTags = + [ + "viewport" => "width=device-width, initial-scale=1", + "polygon-csrf" => SESSION ? SESSION["csrfToken"] : "false", + "theme-color" => "#eb4034", + "og:type" => "Website", + "og:url" => "https://polygon.pizzaboxer.xyz", + "og:site_name" => SITE_CONFIG["site"]["name"], + "og:description" => "yeah", + "og:image" => "https://polygon.pizzaboxer.xyz/img/ProjectPolygon.png" + ]; + + private array $templateVariables = + [ + "Announcements" => [], + "Markdown" => false, + "PendingAssets" => 0, + "ErrorTitle" => "", + "ErrorMessage" => "" + ]; + + private function importTemplate($template) + { + if (!file_exists(ROOT . "/api/private/templates/{$template}.php")) return false; + + require ROOT . "/api/private/templates/{$template}.php"; + } + + function addResource($resourceList, $resource, $cache = true, $pushToFirst = false) + { + if (substr($resource, 0, 1) != "/") $cache = false; + + if ($cache) + { + $resource .= "?id=" . sha1_file(ROOT . $resource); + } + + if ($pushToFirst) + { + array_unshift($this->$resourceList, $resource); + } + else + { + $this->$resourceList[] = $resource; + } + } + + function addMetaTag($property, $content) + { + $this->metaTags[$property] = $content; + } + + function addAppAttribute($attribute, $value) + { + $this->appAttributes[$attribute] = $value; + } + + function showStaticModal($options) + { + $this->footerAdditions .= ''; + } + + function __construct($config = null) + { + if (!is_null($config)) + { + $this->config = array_merge($this->config, $config); + } + } + + static function instance($config = null) + { + return new PageBuilder($config); + } + + function buildHeader() + { + $this->addMetaTag("og:title", $this->config["title"]); + $this->addResource("polygonScripts", "/js/polygon/core.js", true, true); + $this->addResource("stylesheets", "/css/polygon.css"); + + global $announcements, $markdown; + + $this->templateVariables["Announcements"] = $announcements; + $this->templateVariables["Markdown"] = $markdown; + + if (SESSION) + { + if (SESSION["user"]["adminlevel"]) + { + $this->templateVariables["PendingAssets"] = Database::singleton()->run("SELECT COUNT(*) FROM assets WHERE NOT approved AND type != 1")->fetchColumn(); + } + + $this->config["Theme"] = SESSION["user"]["theme"]; + } + + $this->addResource("stylesheets", "/css/polygon-" . $this->config["Theme"] . ".css"); + + if ($this->config["Theme"] == "2014") + { + $this->addResource("scripts", "/js/polygon/Navigation2014.js"); + $this->appAttributes["id"] = "navContent"; + + $this->importTemplate("Head"); + $this->importTemplate("Body2014"); + } + else + { + $this->importTemplate("Head"); + $this->importTemplate("Body"); + } + + ob_start(); + } + + function buildFooter() + { + $this->importTemplate("Footer"); + + ob_end_flush(); + } + + function errorCode($httpCode, $customMessage = false) + { + http_response_code($httpCode); + + $messages = + [ + 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($messages[$httpCode])) $code = 500; + if (is_array($customMessage) && count($customMessage)) $messages[$httpCode] = $customMessage; + + $this->templateVariables["ErrorTitle"] = $messages[$httpCode]["title"]; + $this->templateVariables["ErrorMessage"] = $messages[$httpCode]["text"]; + + $this->buildHeader(); + $this->importTemplate("Error"); + $this->buildFooter(); + + die(); + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Pagination.php b/api/private/classes/pizzaboxer/ProjectPolygon/Pagination.php new file mode 100644 index 0000000..dea5069 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Pagination.php @@ -0,0 +1,57 @@ + 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; + ?> + + plaintext, $this->key); + } + + function verify($passwordHash) + { + if (strpos($passwordHash, "$2y$10") !== false) //standard bcrypt - used since 04/09/2020 + { + return password_verify($this->plaintext, $passwordHash); + } + else if (strpos($passwordHash, "def50200") !== false) //argon2id w/ encryption - used since 26/02/2021 + { + return PasswordLock::decryptAndVerify($this->plaintext, $passwordHash, $this->key); + } + } + + function update($userID) + { + $passwordHash = $this->create(); + Database::singleton()->run("UPDATE users SET password = :hash, lastpwdchange = UNIX_TIMESTAMP() WHERE id = :id", [":hash" => $passwordHash, ":id" => $userID]); + } + + function __construct($plaintext) + { + $this->plaintext = $plaintext; + $this->key = Key::loadFromAsciiSafeString(SITE_CONFIG["keys"]["passwordEncryption"]); + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Polygon.php b/api/private/classes/pizzaboxer/ProjectPolygon/Polygon.php new file mode 100644 index 0000000..caa1fa6 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Polygon.php @@ -0,0 +1,215 @@ +$filters" : $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); + // before we add more invisible characters this entire filter should be on json + 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 RequestRender($Type, $AssetID, $Async = true) + { + $PendingRender = Database::singleton()->run( + "SELECT * FROM renderqueue WHERE renderType = :Type AND assetID = :AssetID AND renderStatus IN (0, 1)", + [":Type" => $Type, ":AssetID" => $AssetID] + )->fetch(); + + if ($PendingRender) + { + if ($PendingRender["timestampRequested"] + 60 > time()) + { + return; + } + else + { + Database::singleton()->run( + "UPDATE renderqueue SET renderStatus = 3 WHERE jobID = :JobID", + [":JobID" => $PendingRender["jobID"]] + ); + } + } + + $JobID = generateUUID(); + + Database::singleton()->run( + "INSERT INTO renderqueue (jobID, renderType, assetID, timestampRequested) VALUES (:JobID, :Type, :AssetID, UNIX_TIMESTAMP())", + [":JobID" => $JobID, ":Type" => $Type, ":AssetID" => $AssetID] + ); + + if (SITE_CONFIG["site"]["thumbserver"] == "RCCService2015") + { + $Variables = + [ + "{JobID}" => $JobID, + "{BaseURL}" => "https://polygon.pizzaboxer.xyz", + "{ThumbnailKey}" => SITE_CONFIG["keys"]["RenderServer"], + "{RenderType}" => $Type, + "{AssetID}" => $AssetID, + "{Synchronous}" => $Async ? "false" : "true" + ]; + + $SOAPBody = file_get_contents($_SERVER["DOCUMENT_ROOT"] . "/api/private/soap/{$Type}.xml"); + $SOAPBody = str_replace(array_keys($Variables), array_values($Variables), $SOAPBody); + + if ($Async) + { + $Request = "POST / HTTP/1.1 + Host: 127.0.0.1:64989 + Content-type: text/xml; charset=UTF-8 + SOAPAction: http://roblox.com/OpenJobEx + + {$SOAPBody}"; + + $Socket = fsockopen("127.0.0.1", 64989); + fwrite($Socket, $Request); + fclose($Socket); + } + else + { + /* $StreamContext = stream_context_create([ + "http" => [ + "method" => "POST", + "header" => "Content-type: text/xml; charset=UTF-8\r\nSOAPAction: http://roblox.com/OpenJobEx", + "content" => $SOAPBody + ] + ]); + + $SOAPResponse = file_get_contents("http://127.0.0.1:64989", false, $StreamContext); */ + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:64989"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $SOAPBody); + curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: text/xml; charset=UTF-8", "SOAPAction: http://roblox.com/OpenJobEx"]); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); + $SOAPResponse = curl_exec($ch); + } + } + } + + static function GetPendingRenders() + { + return Database::singleton()->run("SELECT COUNT(*) FROM renderqueue WHERE renderStatus IN (0, 1)")->fetchColumn(); + } + + static function GetServerPing($id) + { + return Database::singleton()->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 = Database::singleton()->run("SELECT * FROM announcements WHERE activated ORDER BY id DESC")->fetchAll(); + + if (!SITE_CONFIG["site"]["thumbserver"]) + { + array_unshift($announcements, + [ + "text" => "Avatar and asset rendering has been temporarily disabled for maintenance", + "textcolor" => "light", + "bgcolor" => "#F76E19" + ]); + } + + if (self::IsDevSite()) + { + array_unshift($announcements, + [ + "text" => "You are currently on the Project Polygon development branch. Click [here](https://polygon.pizzaboxer.xyz) to go back to the main website \n Note: Asset, user and group statistics may not match up with the main website right now.", + "textcolor" => "light", + "bgcolor" => "#F76E19" + ]); + } + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/RBXClient.php b/api/private/classes/pizzaboxer/ProjectPolygon/RBXClient.php new file mode 100644 index 0000000..ccd2e79 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/RBXClient.php @@ -0,0 +1,44 @@ + 1, "A1A5A2" => 2, "F9E999" => 3, "D7C59A" => 5, "C2DAB8" => 6, "E8BAC8" => 9, "80BBDC" => 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 CryptGetSignature($data) + { + $KeyLocation = sprintf("file://%s", Polygon::GetSharedResource("polygon_private.pem")); + openssl_sign($data, $signature, openssl_pkey_get_private($KeyLocation)); + return base64_encode($signature); + } + + static function CryptVerifySignature($data, $inputSignature) + { + $trustedSignature = self::CryptGetSignature($data); + return hash_equals($trustedSignature, $inputSignature); + } + + static function CryptSignScript($data, $assetID = false) + { + if($assetID) $data = "%{$assetID}%\n{$data}"; + else $data = "\n{$data}"; + $signedScript = "%" . self::CryptGetSignature($data) . "%{$data}"; + return $signedScript; + } + + static function HexToBrickColor($hex) + { + return self::$brickcolors[$hex] ?? false; + } + + static function BrickColorToHex($brickcolor) + { + return array_flip(self::$brickcolors)[$brickcolor] ?? false; + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Session.php b/api/private/classes/pizzaboxer/ProjectPolygon/Session.php new file mode 100644 index 0000000..47b5023 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Session.php @@ -0,0 +1,80 @@ +run( + "INSERT INTO sessions (`sessionKey`, `userAgent`, `userId`, `loginIp`, `lastIp`, `created`, `lastonline`, `csrf`, `twofaVerified`, `IsGameClient`) + VALUES (:SessionKey, :UserAgent, :UserID, :IPAddress, :IPAddress, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :CSRFToken, :isGameClient, :isGameClient)", + [ + ":SessionKey" => $SessionKey, + ":UserAgent" => GetUserAgent(), + ":UserID" => $UserID, + ":IPAddress" => GetIPAddress(), + ":CSRFToken" => bin2hex(random_bytes(32)), + ":isGameClient" => (int)$isGameClient + ] + ); + + setcookie( + "polygon_session", // name + $SessionKey, // value + time()+(157700000*3), // expires (5 years) + "/", // path + "", // domain + true, // secure + true // httponly + ); + + return $SessionKey; + } + + // these two functions are sorta ambiguous + // especially cause they're named so similarly + static function Destroy($SessionKey) + { + Database::singleton()->run("UPDATE sessions SET valid = 0 WHERE sessionKey = :key", [":key" => $SessionKey]); + } + + static function Clear($SessionKey = "", $Refresh = true) + { + setcookie("polygon_session", "", 1, "/"); + if (strlen($SessionKey)) self::Destroy($SessionKey); + if ($Refresh) die(header("Refresh: 0")); + } + + static function Get($SessionKey) + { + $SessionInfo = Database::singleton()->run( + "SELECT * FROM sessions WHERE sessionKey = :sesskey AND valid AND lastonline + 432000 > UNIX_TIMESTAMP()", + [":sesskey" => $SessionKey] + )->fetch(); + + if (!$SessionInfo) return false; + if (Polygon::IsDevSite() && !in_array($SessionInfo["userId"], SITE_CONFIG["DevWhitelist"])) return false; + if ($SessionInfo["created"] + (157700000*3) < time()) return false; // todo - figure out "remember me" cookies instead of just making the session 5 years long + if ($SessionInfo["lastIp"] != GetIPAddress()) + { + Database::singleton()->run( + "UPDATE sessions SET lastIp = :IPAddress WHERE sessionKey = :SessionKey", + [":IPAddress" => GetIPAddress(), ":SessionKey" => $SessionKey] + ); + + if ($SessionInfo["twofaVerified"] && !$SessionInfo["IsGameClient"]) + { + Database::singleton()->run("UPDATE sessions SET twofaVerified = 0 WHERE sessionKey = :SessionKey", [":SessionKey" => $SessionKey]); + $SessionInfo["twofaVerified"] = 0; + } + } + + return $SessionInfo; + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/System.php b/api/private/classes/pizzaboxer/ProjectPolygon/System.php new file mode 100644 index 0000000..d2372a6 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/System.php @@ -0,0 +1,32 @@ + $total*1024, "free" => $free*1024]; + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Thumbnails.php b/api/private/classes/pizzaboxer/ProjectPolygon/Thumbnails.php new file mode 100644 index 0000000..d9c50eb --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Thumbnails.php @@ -0,0 +1,150 @@ + "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-768x432.png" => "ed084f98f4b5855e9e0d95d6801753ba7357ca18", + "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-768x432.png" => "ebb176be0c34a9c0e18a3da139a81fa6cf2d8f10", + "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" + ]; + + static function GetCDNLocation($Location, $Extension = "png") + { + $ThumbnailHash = sha1_file($Location) . ".{$Extension}"; + $CDNLocation = $_SERVER["DOCUMENT_ROOT"]."/../polygoncdn/{$ThumbnailHash}"; + + if (!file_exists($CDNLocation)) self::UploadToCDN($Location); + return self::$BaseURL.$ThumbnailHash; + } + + static function GetStatus($status, $x = 420, $y = 420) + { + $ImageName = "{$status}-{$x}x{$y}.png"; + if (!isset(self::$StatusThumbnails[$ImageName])) $ImageName = "{$status}-420x420.png"; + + return self::$BaseURL.self::$StatusThumbnails[$ImageName].".png"; + } + + static function UploadToCDN($Location, $Extension = "png") + { + $Hash = sha1_file($Location); + file_put_contents($_SERVER["DOCUMENT_ROOT"]."/../polygoncdn/{$Hash}.{$Extension}", file_get_contents($Location)); + } + + static function DeleteFromCDN($Location) + { + $ThumbnailHash = sha1_file($Location); + $CDNLocation = $_SERVER["DOCUMENT_ROOT"]."/../polygoncdn/{$ThumbnailHash}.png"; + + if (file_exists($CDNLocation)) unlink($CDNLocation); + } + + static function GetAsset($SQLResult, $x = 420, $y = 420, $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; + $Location = SITE_CONFIG['paths']['thumbs_assets']."{$AssetID}-{$x}x{$y}.png"; + + if ($Force) $SQLResult->approved = 1; + + if ($SQLResult->approved == 0) return self::GetStatus("pending", $x, $y); + if ($SQLResult->approved == 2) return self::GetStatus("unapproved", $x, $y); + + if (!file_exists($Location)) return self::GetStatus("rendering", $x, $y); + if ($SQLResult->approved == 1) return self::GetCDNLocation($Location); + + return self::GetStatus("rendering", $x, $y); + } + + static function GetAssetFromID($AssetID, $x = 420, $y = 420, $force = false) + { + // primarily used for fetching group emblems + // we dont need to block this as group emblems are fine to show publicly + + $AssetInfo = Database::singleton()->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 = 420, $y = 420, $force = false) + { + if(!$force && !SESSION && GetUserAgent() != "Roblox/WinInet") + return self::GetStatus("rendering", $x, $y); + + $Location = SITE_CONFIG['paths']['thumbs_avatars']."{$avatarID}-{$x}x{$y}.png"; + + if(!file_exists($Location)) return self::GetStatus("rendering", $x, $y); + return self::GetCDNLocation($Location); + } + + static function UploadAsset($handle, $assetID, $x, $y, $additionalOptions = []) + { + $options = ["name" => "{$assetID}-{$x}x{$y}.png", "x" => $x, "y" => $y, "dir" => "thumbs/assets/"]; + $options = array_merge($options, $additionalOptions); + + $Processed = Image::Process($handle, $options); + if ($Processed !== true) throw new Exception($Processed); + + self::UploadToCDN(SITE_CONFIG['paths']['thumbs_assets']."{$assetID}-{$x}x{$y}.png"); + } + + static function DeleteAsset($AssetID) + { + $Thumbnails = glob(SITE_CONFIG["paths"]["thumbs_assets"]."{$AssetID}-*.png"); + + foreach ($Thumbnails as $Thumbnail) + { + self::DeleteFromCDN($Thumbnail); + unlink($Thumbnail); + } + } + + static function UploadAvatar($handle, $avatarID, $x, $y) + { + 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"); + } +} \ No newline at end of file diff --git a/api/private/classes/pizzaboxer/ProjectPolygon/Users.php b/api/private/classes/pizzaboxer/ProjectPolygon/Users.php new file mode 100644 index 0000000..4e4ef55 --- /dev/null +++ b/api/private/classes/pizzaboxer/ProjectPolygon/Users.php @@ -0,0 +1,277 @@ +run("SELECT id FROM users WHERE username = :username", [":username" => $username])->fetchColumn(); + } + + static function GetNameFromID($userId) + { + return Database::singleton()->run("SELECT username FROM users WHERE id = :uid", [":uid" => $userId])->fetchColumn(); + } + + static function GetInfoFromName($username) + { + return Database::singleton()->run("SELECT * FROM users WHERE username = :username", [":username" => $username])->fetch(\PDO::FETCH_OBJ); + } + + static function GetInfoFromID($userId) + { + return Database::singleton()->run("SELECT * FROM users WHERE id = :uid", [":uid" => $userId])->fetch(\PDO::FETCH_OBJ); + } + + static function GetInfoFromJobTicket() + { + if (!isset($_COOKIE['GameJobTicket'])) return false; + + return Database::singleton()->run( + "SELECT users.* FROM GameJobSessions + INNER JOIN users ON users.id = GameJobSessions.UserID + WHERE Ticket = :Ticket AND Verified AND TimeCreated + 86400 > UNIX_TIMESTAMP()", + [":Ticket" => $_COOKIE['GameJobTicket']] + )->fetch(\PDO::FETCH_OBJ); + } + + static function CheckIfFriends($userId1, $userId2, $status = false) + { + if ($status === false) + { + return Database::singleton()->run( + "SELECT * FROM friends WHERE :uid1 IN (requesterId, receiverId) AND :uid2 IN (requesterId, receiverId) AND NOT status = 2", + [":uid1" => $userId1, ":uid2" => $userId2] + )->fetch(\PDO::FETCH_OBJ); + } + else + { + return Database::singleton()->run( + "SELECT * FROM friends WHERE :uid1 IN (requesterId, receiverId) AND :uid2 IN (requesterId, receiverId) AND status = :status", + [":uid1" => $userId1, ":uid2" => $userId2, ":status" => $status] + )->fetch(\PDO::FETCH_OBJ); + } + } + + static function GetFriendCount($userId) + { + return Database::singleton()->run("SELECT COUNT(*) FROM friends WHERE :uid IN (requesterId, receiverId) AND status = 1", [":uid" => $userId])->fetchColumn(); + } + + static function GetFriendRequestCount($userId) + { + return Database::singleton()->run("SELECT COUNT(*) FROM friends WHERE receiverId = :uid AND status = 0", [":uid" => $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["user"]["nextCurrencyStipend"] <= time()) + { + $days = floor((time() - SESSION["user"]["nextCurrencyStipend"]) / 86400); + if(!$days) $days = 1; + $stipend = $days * 10; + $nextstipend = SESSION["user"]["nextCurrencyStipend"] + (($days + 1) * 86400); + + Database::singleton()->run( + "UPDATE users SET currency = currency + :stipend, nextCurrencyStipend = :nextstipend WHERE id = :uid", + [":stipend" => $stipend, ":nextstipend" => $nextstipend, ":uid" => SESSION["user"]["id"]] + ); + } + + // update presence + Database::singleton()->run( + "UPDATE users SET lastonline = UNIX_TIMESTAMP() WHERE id = :id; UPDATE sessions SET lastonline = UNIX_TIMESTAMP() WHERE sessionKey = :key", + [":id" => SESSION["user"]["id"], ":key" => SESSION["sessionKey"]] + ); + } + + static function GetOnlineStatus($UserID, $IsProfile) + { + // this is also a mess + + $Status = (object) + [ + "Online" => false, + "Text" => false, + "Attributes" => false + ]; + + $Presence = Database::singleton()->run( + "SELECT lastonline, name AS PlaceName, assets.id AS PlaceID, ClientPresenceType FROM users + LEFT JOIN assets ON assets.id = ClientPresenceLocation AND ClientPresencePing + 65 > UNIX_TIMESTAMP() + WHERE users.id = :UserID", + [":UserID" => $UserID] + )->fetch(); + + if (SESSION && $Presence["PlaceID"] != null) + { + $Status->Online = true; + + $Status->Text = sprintf( + ($Presence["ClientPresenceType"] == "Visit" ? "Playing" : "Editing") . " %s", + encode_asset_name($Presence["PlaceName"]), + $Presence["PlaceID"], + Polygon::FilterText($Presence["PlaceName"]) + ); + + $Status->Attributes = " class=\"text-" . ($Presence["ClientPresenceType"] == "Visit" ? "success" : "danger") . " mb-0\""; + } + else if ($Presence["lastonline"] + 65 > time()) + { + $Status->Online = true; + $Status->Text = "Website"; + $Status->Attributes = " class=\"text-primary mb-0\""; + } + else + { + if ($IsProfile) + { + $Status->Text = "Offline"; + } + else + { + $Status->Text = timeSince($Presence["lastonline"]); + } + + $Status->Attributes = " class=\"text-muted mb-0\" data-toggle=\"tooltip\" data-placement=\"right\" title=\"".date('j/n/Y g:i A', $Presence["lastonline"])."\""; + } + + if ($Status->Online && $IsProfile) + { + $Status->Text = "Online: {$Status->Text}"; + } + + return $Status; + } + + static function GetUsersOnline() + { + return Database::singleton()->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["user"]["adminlevel"] == 0) return false; + if($level === self::STAFF) return true; + + if(gettype($level) == "array") + { + if(in_array(SESSION["user"]["adminlevel"], $level)) return true; + } + else + { + if(SESSION["user"]["adminlevel"] == $level) return true; + } + + return false; + } + + static function RequireAdmin($level = self::STAFF) + { + if(!self::IsAdmin($level)) + PageBuilder::instance()->errorCode(404); + + if(!SESSION["user"]["twofa"]) + PageBuilder::instance()->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 Database::singleton()->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) + { + $banInfo = self::GetUserModeration($userId); + + if (!$banInfo) return false; + if (!$admin && ($banInfo->banType == 3 || ($banInfo->banType != 1 && $banInfo->timeEnds < time()))) return false; + + Database::singleton()->run( + "UPDATE bans SET isDismissed = 1 WHERE userId = :id AND NOT isDismissed; + UPDATE users SET Banned = 0 WHERE id = :id", + [":id" => $userId] + ); + + return true; + } + + static function LogStaffAction($action) + { + if(!SESSION || !SESSION["user"]["adminlevel"]) return false; + Database::singleton()->run("INSERT INTO stafflogs (time, adminId, action) VALUES (UNIX_TIMESTAMP(), :uid, :action)", [":uid" => SESSION["user"]["id"], ":action" => $action]); + } + + static function GetAlternateAccounts($data) + { + $alts = []; + $usedIPs = []; + $usedIDs = []; + + if(is_numeric($data)) // user id + { + $ips = Database::singleton()->run("SELECT loginIp FROM sessions WHERE userId = :uid GROUP BY loginIp", [":uid" => $data]); + } + else // ip address + { + $ips = Database::singleton()->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 = Database::singleton()->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; + } +} \ No newline at end of file diff --git a/api/private/core.php b/api/private/core.php new file mode 100644 index 0000000..7ece531 --- /dev/null +++ b/api/private/core.php @@ -0,0 +1,478 @@ + + [ + "/directory_login/2fa.php", + "/logout.php", + + "/rbxclient/asset/fetch.php", + "/rbxclient/asset/bodycolors.php", + "/rbxclient/asset/characterfetch.php", + "/rbxclient/friend/arefriends.php", + + "/rbxclient/game/studio.php", + "/rbxclient/game/join.php", + "/rbxclient/game/visit.php", + "/rbxclient/game/gameserver.php", + "/rbxclient/game/machineconfiguration.php", + + "/rbxclient/game/luawebservice/handlesocialrequest.php", + "/rbxclient/game/tools/insertasset.php", + + "/game/clientpresence.php", + "/game/join.php", + "/game/server.php", + "/game/serverpresence.php", + "/game/verifyplayer.php", + + "/asset/index.php" + ], + + "Moderation" => + [ + "/moderation.php", + "/info/terms-of-service.php", + "/info/privacy.php", + "/info/selfhosting.php", + "/directory_login/2fa.php", + "/logout.php", + "/rbxclient/game/machineconfiguration.php" + ], + + "HTTPS" => + [ + "/error.php" + ], + + "Negotiate" => + [ + "/rbxclient/game/join.php", + "/rbxclient/game/visit.php", + "/rbxclient/game/edit.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(!Polygon::CanBypass("HTTPS") && isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == "http") +{ + 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 = preg_replace("![^a-z0-9]+!i", "-", $string); + if (str_ends_with($string, "-")) $string = substr($string, 0, -1); + return $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 Pagination($Page, $Count, $Limit) +{ + $Page = intval($Page); + + $Pages = ceil($Count/$Limit); + + if ($Page < 1) $Page = 1; + else if ($Page > $Pages) $Page = $Pages; + + $Offset = ($Page - 1)*$Limit; + + if ($Offset < 0) $Offset = 0; + + return (object)["Page" => (int)$Page, "Pages" => (int)$Pages, "Offset" => (int)$Offset]; +} + +function GetIPAddress() +{ + return $_SERVER["REMOTE_ADDR"]; +} + +function GetUserAgent() +{ + return $_SERVER["HTTP_USER_AGENT"] ?? "Unknown"; +} + +function VerifyReCAPTCHA() +{ + $fields = http_build_query([ + 'secret' => SITE_CONFIG["keys"]["captcha"]["secret"], + 'response' => $_POST['g-recaptcha-response'] ?? "", + 'remoteip' => GetIPAddress() + ]); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify"); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $fields); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + $result = json_decode($response); + + if ($result === false || !$result->success) return false; + return true; +} + +function GetIPInfo($IPAddress) +{ + if (!filter_var($IPAddress, FILTER_VALIDATE_IP)) + throw new Exception("Invalid IP Address {$IPAddress}"); + + $response = @file_get_contents("https://proxycheck.io/v2/{$IPAddress}?key=03l192-455797-m9n0w8-0wz258"); + + if($response === false) + throw new Exception(error_get_last()["message"]); + + $result = json_decode($response); + + if($result->status != "ok") + throw new Exception("CheckIPAddress failed: response didnt return ok: \"".var_export($result, true)."\""); + + if(!isset($result->{$IPAddress})) + throw new Exception("CheckIPAddress failed: bad response \"".var_export($result, true)."\""); + + return $result->{$IPAddress}; +} + +function GetASNumber($IPAddress) +{ + if (!filter_var($IPAddress, FILTER_VALIDATE_IP)) + throw new Exception("Invalid IP Address {$IPAddress}"); + + $whois = shell_exec("whois -h whois.cymru.com \" -f {$IPAddress}\""); + $asn = trim(explode('|', $whois)[0]); + + if ($asn == "NA") + return false; // no asn record available + + if (!is_numeric($asn)) + throw new Exception("GetASNumber failed: invalid ASN: \"{$whois}\""); + + return intval($asn); +} + +// 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; +} + +require Polygon::GetSharedResource("config.php"); + +$errorHandler = new ErrorHandler(); +$errorHandler->register(); + +$markdown = new Parsedown(); +$markdown->setMarkupEscaped(true); +$markdown->setBreaksEnabled(true); +$markdown->setSafeMode(true); +$markdown->setUrlsLinked(true); + +Polygon::GetAnnouncements(); + +// TEMPORARY HACK for negotiate.ashx on 2010 and 2011 +// later on this should just be moved to /rbxclient/login/negotiate.php +if (isset($_GET["suggest"]) && !isset($_COOKIE['polygon_session']) && Polygon::CanBypass("Negotiate")) +{ + // the ticket is formatted as [{username}:{id}:{timestamp}]:{signature} + // the part in square brackets is what the signature represents + + $ticket = explode(":", $_GET["suggest"]); + + if (count($ticket) == 4) + { + $username = $ticket[0]; + $userid = $ticket[1]; + $timestamp = (int)$ticket[2]; + $signature = $ticket[3]; + + // reconstruct the signed message + $ticketRecon = sprintf("%s:%s:%d", $username, $userid, $timestamp); + + // check if signature matches and if ticket is 3 minutes old max + if (RBXClient::CryptVerifySignature($ticketRecon, $signature) && $timestamp + 180 > time()) + { + // before we create the session, let's just quickly check to make sure we don't create any duplicate sessions + $lastSession = Database::singleton()->run( + "SELECT created FROM sessions + WHERE userId = :UserID AND IsGameClient + ORDER BY created DESC LIMIT 1", + [":UserID" => $userid] + )->fetchColumn(); + + if ($lastSession + 180 < $timestamp) + { + $session = Session::Create($userid, true); + + // this might be a war crime + $_COOKIE["polygon_session"] = $session; + } + } + } +} + +if (isset($_COOKIE['polygon_session'])) +{ + $Session = Session::Get($_COOKIE['polygon_session']); + + if ($Session) + { + $userInfo = Users::GetInfoFromID($Session["userId"]); + define("SESSION", + [ + "2faVerified" => $Session["twofaVerified"], + "sessionKey" => $Session["sessionKey"], + "csrfToken" => $Session["csrf"], + "user" => (array)$userInfo + ]); + + if (SESSION["user"]["twofa"] && !SESSION["2faVerified"] && !Polygon::CanBypass("2FA")) + { + die(header("Location: /login/2fa")); + } + else if (SESSION["user"]["Banned"] && !Polygon::CanBypass("Moderation")) + { + die(header("Location: /moderation")); + } + else + { + Users::UpdatePing(); + } + } + else + { + // do not reload the page if we're doing a negotiate ticket + Session::Clear($_COOKIE['polygon_session'], Polygon::CanBypass("Negotiate")); + define('SESSION', false); + } +} +else +{ + define('SESSION', false); +} \ No newline at end of file diff --git a/api/private/soap/Avatar.xml b/api/private/soap/Avatar.xml new file mode 100644 index 0000000..6cf979f --- /dev/null +++ b/api/private/soap/Avatar.xml @@ -0,0 +1,85 @@ + + + + + + {JobID} + 30 + 1 + 1 + + + + + LUA_TSTRING + {BaseURL} + + + LUA_TSTRING + {ThumbnailKey} + + + LUA_TSTRING + {RenderType} + + + LUA_TNUMBER + {AssetID} + + + LUA_TBOOLEAN + {Synchronous} + + + + + + \ No newline at end of file diff --git a/api/private/soap/Clothing.xml b/api/private/soap/Clothing.xml new file mode 100644 index 0000000..a219d90 --- /dev/null +++ b/api/private/soap/Clothing.xml @@ -0,0 +1,84 @@ + + + + + + {JobID} + 30 + 1 + 1 + + + + + LUA_TSTRING + {BaseURL} + + + LUA_TSTRING + {ThumbnailKey} + + + LUA_TSTRING + {RenderType} + + + LUA_TNUMBER + {AssetID} + + + LUA_TBOOLEAN + {Synchronous} + + + + + + \ No newline at end of file diff --git a/api/private/soap/Head.xml b/api/private/soap/Head.xml new file mode 100644 index 0000000..8b511ea --- /dev/null +++ b/api/private/soap/Head.xml @@ -0,0 +1,75 @@ + + + + + + {JobID} + 30 + 1 + 1 + + + + + LUA_TSTRING + {BaseURL} + + + LUA_TSTRING + {ThumbnailKey} + + + LUA_TSTRING + {RenderType} + + + LUA_TNUMBER + {AssetID} + + + LUA_TBOOLEAN + {Synchronous} + + + + + + \ No newline at end of file diff --git a/api/private/soap/Mesh.xml b/api/private/soap/Mesh.xml new file mode 100644 index 0000000..118712d --- /dev/null +++ b/api/private/soap/Mesh.xml @@ -0,0 +1,79 @@ + + + + + + {JobID} + 30 + 1 + 1 + + + + + LUA_TSTRING + {BaseURL} + + + LUA_TSTRING + {ThumbnailKey} + + + LUA_TSTRING + {RenderType} + + + LUA_TNUMBER + {AssetID} + + + LUA_TBOOLEAN + {Synchronous} + + + + + + \ No newline at end of file diff --git a/api/private/soap/Model.xml b/api/private/soap/Model.xml new file mode 100644 index 0000000..e2e6a80 --- /dev/null +++ b/api/private/soap/Model.xml @@ -0,0 +1,76 @@ + + + + + + {JobID} + 30 + 1 + 1 + + + + + LUA_TSTRING + {BaseURL} + + + LUA_TSTRING + {ThumbnailKey} + + + LUA_TSTRING + {RenderType} + + + LUA_TNUMBER + {AssetID} + + + LUA_TBOOLEAN + {Synchronous} + + + + + + \ No newline at end of file diff --git a/api/private/soap/Place.xml b/api/private/soap/Place.xml new file mode 100644 index 0000000..16e6c92 --- /dev/null +++ b/api/private/soap/Place.xml @@ -0,0 +1,86 @@ + + + + + + {JobID} + 120 + 1 + 1 + + + + + LUA_TSTRING + {BaseURL} + + + LUA_TSTRING + {ThumbnailKey} + + + LUA_TSTRING + {RenderType} + + + LUA_TNUMBER + {AssetID} + + + LUA_TBOOLEAN + {Synchronous} + + + + + + \ No newline at end of file diff --git a/api/private/soap/UserModel.xml b/api/private/soap/UserModel.xml new file mode 100644 index 0000000..f61b7ae --- /dev/null +++ b/api/private/soap/UserModel.xml @@ -0,0 +1,93 @@ + + + + + + {JobID} + 60 + 1 + 1 + + + + + LUA_TSTRING + {BaseURL} + + + LUA_TSTRING + {ThumbnailKey} + + + LUA_TSTRING + {RenderType} + + + LUA_TNUMBER + {AssetID} + + + LUA_TBOOLEAN + {Synchronous} + + + + + + \ No newline at end of file diff --git a/api/private/templates/Body.php b/api/private/templates/Body.php new file mode 100644 index 0000000..0cbfac0 --- /dev/null +++ b/api/private/templates/Body.php @@ -0,0 +1,120 @@ + +config["ShowNavbar"]) { ?> + + + + + +templateVariables["Announcements"] as $announcement) { ?> +
" role="alert" style="background-color: "> + templateVariables["Markdown"]->text($announcement["text"], true) ?> +
+ +config["ShowNavbar"]) */ ?> + appAttributes as $attribute => $value) echo " {$attribute}=\"{$value}\""; ?>> \ No newline at end of file diff --git a/api/private/templates/Body2014.php b/api/private/templates/Body2014.php new file mode 100644 index 0000000..9c8b210 --- /dev/null +++ b/api/private/templates/Body2014.php @@ -0,0 +1,172 @@ +config["ShowNavbar"]) { ?> + + + +templateVariables["Announcements"] as $announcement) { ?> +
+
"> +
+
templateVariables["Markdown"]->line($announcement["text"]) ?>
+
+
+
+ +config["ShowNavbar"]) */ ?> + +config["ShowNavbar"]) */ ?> + appAttributes as $attribute => $value) echo " {$attribute}=\"{$value}\""; ?>> \ No newline at end of file diff --git a/api/private/templates/Error.php b/api/private/templates/Error.php new file mode 100644 index 0000000..3757974 --- /dev/null +++ b/api/private/templates/Error.php @@ -0,0 +1,10 @@ +
+
+ +

templateVariables["ErrorTitle"] ?>

+ templateVariables["ErrorMessage"] ?> +
+ Go to Previous Page + Return Home +
+
\ No newline at end of file diff --git a/api/private/templates/Footer.php b/api/private/templates/Footer.php new file mode 100644 index 0000000..f2d15a6 --- /dev/null +++ b/api/private/templates/Footer.php @@ -0,0 +1,151 @@ + +config["ShowFooter"]) { ?> +
+ +
+ + + + + + + + + + +polygonScripts as $url) { ?> + + + + footerAdditions?> + + \ No newline at end of file diff --git a/api/private/templates/Head.php b/api/private/templates/Head.php new file mode 100644 index 0000000..bb0e25c --- /dev/null +++ b/api/private/templates/Head.php @@ -0,0 +1,33 @@ + + + + <?= ($this->config["title"] ? $this->config["title"] . " - " : "") . SITE_CONFIG["site"]["name"] ?> + + +metaTags as $property => $content) { ?> + ="" content=""> + + +stylesheets as $url) { ?> + +scripts as $url) { ?> + + + + + diff --git a/api/register/NameValidator.php b/api/register/NameValidator.php new file mode 100644 index 0000000..5b57484 --- /dev/null +++ b/api/register/NameValidator.php @@ -0,0 +1,22 @@ + 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)); \ No newline at end of file diff --git a/api/render/character.xml b/api/render/character.xml new file mode 100644 index 0000000..1038bcd --- /dev/null +++ b/api/render/character.xml @@ -0,0 +1,17 @@ + + + null + nil + + + 1 + 1 + 1 + Body Colors + 1 + 1 + 1 + true + + + \ No newline at end of file diff --git a/api/render/characterasset.php b/api/render/characterasset.php new file mode 100644 index 0000000..2f00445 --- /dev/null +++ b/api/render/characterasset.php @@ -0,0 +1,2 @@ + +https:///api/render/character.xml;https:///asset/?id=&t=; diff --git a/api/render/ping.php b/api/render/ping.php new file mode 100644 index 0000000..ef65cc1 --- /dev/null +++ b/api/render/ping.php @@ -0,0 +1,9 @@ +run("UPDATE servers SET ping = UNIX_TIMESTAMP() WHERE id = 2"); \ No newline at end of file diff --git a/api/render/update.php b/api/render/update.php new file mode 100644 index 0000000..b15e057 --- /dev/null +++ b/api/render/update.php @@ -0,0 +1,162 @@ +run("SELECT * FROM renderqueue WHERE jobID = :JobID", [":JobID" => $_GET["RenderJobID"]])->fetch(\PDO::FETCH_OBJ); + + debuglog("creating new upload handle from base64 string"); + $image = new Upload("base64:{$Response['Click']}"); + $image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; + $image->image_convert = 'png'; + + if ($RenderInfo->renderType == "Avatar") + { + debuglog("uploading image as avatar"); + $ThumbsDir = SITE_CONFIG["paths"]["thumbs_avatars"]; + Thumbnails::UploadAvatar($image, $RenderInfo->assetID, 420, 420); + } + else + { + debuglog("uploading image as asset"); + $ThumbsDir = SITE_CONFIG["paths"]["thumbs_assets"]; + Thumbnails::UploadAsset($image, $RenderInfo->assetID, 420, 420); + + if (isset($Response['ClickWidescreen'])) + { + debuglog("creating new upload handle from base64 string (for widescreen"); + $image = new Upload("base64:{$Response['ClickWidescreen']}"); + $image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; + $image->image_convert = 'png'; + debuglog("uploading image as asset (for widescreen"); + Thumbnails::UploadAsset($image, $RenderInfo->assetID, 768, 432); + } + } + + // this is a 3d object! this contains an obj file, an mtl file and multiple texture files + if (isset($Response['ClickObject']) && isset($Response["ClickObject"][0])) + { + $ObjectInfo = $Response["ClickObject"][0]; + $MtlAssets = []; + $Manifest = []; + + $Manifest["camera"] = $ObjectInfo["camera"]; + $Manifest["camera"]["fov"] = 70; + $Manifest["aabb"] = $ObjectInfo["AABB"]; + + // the object asset list is ordered as obj, mtl, textures(...) + // we need it to be reversed so that mtl comes AFTER textures(...) to help make replacing the texture names easier + $ObjectInfo["files"] = array_reverse($ObjectInfo["files"], true); + + foreach ($ObjectInfo["files"] as $FileName => $File) + { + $FileContent = base64_decode($File["content"]); + $FileExtension = explode(".", $FileName)[1]; + + if ($FileExtension == "obj") + { + $FileHash = sha1($FileContent) . "." . $FileExtension; + $Manifest["obj"] = $FileHash; + } + else if ($FileExtension == "mtl") + { + $FileContent = str_replace(array_keys($MtlAssets), array_values($MtlAssets), $FileContent); + $FileHash = sha1($FileContent) . "." . $FileExtension; + $Manifest["mtl"] = $FileHash; + } + else + { + $FileHash = sha1($FileContent) . "." . $FileExtension; + $Manifest["textures"][] = $FileHash; + $MtlAssets[$FileName] = $FileHash; + } + + file_put_contents("{$ThumbsDir}/{$RenderInfo->assetID}-{$FileName}", $FileContent); + Thumbnails::UploadToCDN("{$ThumbsDir}/{$RenderInfo->assetID}-{$FileName}", $FileExtension); + } + + $ManifestContent = json_encode($Manifest); + $ManifestHash = sha1($ManifestContent); + + // this manifest file is used by three.js for camera positioning, asset location, etc + file_put_contents("{$ThumbsDir}/{$RenderInfo->assetID}-3DManifest.json", $ManifestContent); + Thumbnails::UploadToCDN("{$ThumbsDir}/{$RenderInfo->assetID}-3DManifest.json", "json"); + } + + debuglog("updating render status as completed"); + + Database::singleton()->run( + "UPDATE renderqueue SET renderStatus = :Status, timestampCompleted = UNIX_TIMESTAMP() WHERE jobID = :JobID", + [":Status" => $Response["Status"], ":JobID" => $_GET["RenderJobID"]] + ); + + debuglog("upload finished..."); +} +else if ($Response["Status"] == 1) +{ + debuglog("updating render status as pending"); + Database::singleton()->run( + "UPDATE renderqueue SET renderStatus = :Status, timestampAcknowledged = UNIX_TIMESTAMP() WHERE jobID = :JobID", + [":Status" => $Response["Status"], ":JobID" => $_GET["RenderJobID"]] + ); +} +else if ($Response["Status"] == 3) +{ + debuglog("updating render status as error"); + Database::singleton()->run( + "UPDATE renderqueue SET renderStatus = :Status, timestampCompleted = UNIX_TIMESTAMP(), additionalInfo = :Message WHERE jobID = :JobID", + [":Status" => $Response["Status"], ":Message" => $Response["Message"], ":JobID" => $_GET["RenderJobID"]] + ); +} + +// file_put_contents("tests/{$JobID}.png", base64_decode($Response->click)); +// echo "Render uploaded!"; + +debuglog("script end"); + +echo "OK"; \ No newline at end of file diff --git a/api/users/get-badges.php b/api/users/get-badges.php new file mode 100644 index 0000000..962cf71 --- /dev/null +++ b/api/users/get-badges.php @@ -0,0 +1,72 @@ + "POST"]); + +$UserID = API::GetParameter("POST", "UserID", "int"); +$BadgeType = API::GetParameter("POST", "BadgeType", ["Place", "Polygon"]); +$selfProfile = isset($_SERVER['HTTP_REFERER']) && str_ends_with($_SERVER['HTTP_REFERER'], "/user"); + +$userinfo = Users::GetInfoFromID($UserID); +if(!$userinfo) API::respond(400, false, "User does not exist"); + +$badges = []; + +if($BadgeType == "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 .= $BadgeType == "Polygon" ? SITE_CONFIG["site"]["name"]." badges" : "player badges"; + API::respond(200, true, $responsemsg); +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "pages" => 1, "items" => $badges])); \ No newline at end of file diff --git a/api/users/get-groups.php b/api/users/get-groups.php new file mode 100644 index 0000000..fd06927 --- /dev/null +++ b/api/users/get-groups.php @@ -0,0 +1,54 @@ + "POST"]); + +$UserID = API::GetParameter("POST", "UserID", "int"); +$UserInfo = Users::GetInfoFromID($UserID); +$SelfProfile = isset($_SERVER['HTTP_REFERER']) && str_ends_with($_SERVER['HTTP_REFERER'], "/user"); + +if (!$UserInfo) API::respond(400, false, "User does not exist"); + +$GroupsCount = Database::singleton()->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") . " not in any groups."); +} + +$Pagination = Pagination(API::GetParameter("POST", "Page", "int", 1), $GroupsCount, 8); + +$GroupsQuery = Database::singleton()->run( + "SELECT groups.name, groups.id, groups.emblem, groups.MemberCount, groups_ranks.Name AS Role 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, ":Offset" => $Pagination->Offset] +); + + +$Groups = []; +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) + ]; +} + +API::respondCustom(["status" => 200, "success" => true, "message" => "OK", "pages" => $Pagination->Pages, "items" => $Groups]); \ No newline at end of file diff --git a/api/users/get-inventory.php b/api/users/get-inventory.php new file mode 100644 index 0000000..9cd2aa0 --- /dev/null +++ b/api/users/get-inventory.php @@ -0,0 +1,62 @@ + "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, 9])) API::respond(400, false, "Invalid asset type"); + +$type_str = Catalog::GetTypeByNum($type); + +$assetCount = Database::singleton()->run( + "SELECT COUNT(*) FROM ownedAssets + INNER JOIN assets ON assets.id = assetId + WHERE userId = :uid AND assets.type = :type", + [":uid" => $userId, ":type" => $type] +)->fetchColumn(); + +$pagination = Pagination($page, $assetCount, 18); + +if (!$pagination->Pages) API::respond(200, true, ($self?'You do':Users::GetNameFromID($userId).' does').' not have any '.plural($type_str)); + +$assets = Database::singleton()->run( + "SELECT assets.*, users.username 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", + [":uid" => $userId, ":type" => $type, ":offset" => $pagination->Offset] +); + +$items = []; + +while ($asset = $assets->fetch(\PDO::FETCH_OBJ)) +{ + $price = ''; + if($asset->sale) $price .= $asset->price ? ' '.$asset->price : 'Free'; + $price .= ''; + + $items[] = + [ + "url" => "/".encode_asset_name($asset->name)."-item?id=".$asset->id, + "item_id" => $asset->id, + "item_name" => htmlspecialchars($asset->name), + "item_thumbnail" => Thumbnails::GetAsset($asset), + "creator_id" => $asset->creator, + "creator_name" => $asset->username, + "price" => $price + ]; +} + +die(json_encode(["status" => 200, "success" => true, "message" => "OK", "items" => $items, "pages" => $pagination->Pages])); \ No newline at end of file diff --git a/browse.php b/browse.php new file mode 100644 index 0000000..ea14f9a --- /dev/null +++ b/browse.php @@ -0,0 +1,130 @@ + "Browse Groups"]); +} +else +{ + // WHERE MATCH (username) AGAINST (:keywd IN NATURAL LANGUAGE MODE) + + $querycount = + "SELECT COUNT(*) FROM users WHERE username LIKE :keywd AND NOT Banned"; + + $querystring = + "SELECT * FROM users WHERE username LIKE :keywd AND NOT Banned + ORDER BY lastonline DESC LIMIT 15 OFFSET :Offset"; + + $pageBuilder = new PageBuilder(["title" => "Browse People"]); +} + +$count = Database::singleton()->run($querycount, [":keywd" => $keyword_sql])->fetchColumn(); + +$Pagination = Pagination($page, $count, 15); + +$results = Database::singleton()->run($querystring, [":keywd" => $keyword_sql, ":Offset" => $Pagination->Offset]); + +function buildURL($page) +{ + global $keyword; + global $category; + + $url = "?"; + if($keyword) $url .= "SearchTextBox=$keyword&"; + $url .= "Category=$category&"; + $url .= "PageNumber=$page"; + return $url; +} + +$pageBuilder->buildHeader(); +?> +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+rowCount()) { ?> + + + + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { $Status = Users::GetOnlineStatus($row->id, false); ?> + + + + + + + + + + + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + + + + + + + +
AvatarNameBlurbLocation / Last Seen
" data-src="id)?>" title="username?>" alt="username?>" width="63" height="63">username?>blurb)?>Attributes?>>Text?>
GroupDescriptionMembers
<?=$row->name?>name)?>description)?>MemberCount?>
+ +

No results matched your search query

+Pages > 1) { ?> + +buildFooter(); ?> diff --git a/catalog.php b/catalog.php new file mode 100644 index 0000000..2415237 --- /dev/null +++ b/catalog.php @@ -0,0 +1,249 @@ + ["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'] ?? ""; +$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"]) || !is_numeric($subcat) || !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 = Database::singleton()->run( + "SELECT COUNT(*) FROM assets WHERE type != 1 AND name LIKE :keywd AND approved != 2 AND sale $queryparam", + [":keywd" => "%{$keyword}%"] +)->fetchColumn(); + +$pagination = Pagination($page, $results, 18); + +$query = Database::singleton()->run( + "SELECT assets.*, users.username 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", + [":keywd" => "%{$keyword}%", ":offset" => $pagination->Offset] +); + +$pageBuilder = new PageBuilder(["title" => "Avatar Items, Virtual Avatars, Virtual Goods"]); +$pageBuilder->addResource("polygonScripts", "/js/polygon/catalog.js"); +$pageBuilder->buildHeader(); +?> + +
+
+

Catalog

+
+
+
+ +
+ + +
+
+
+
+
+
+ + +

Filters

+
+ +
Category
+ +
+ +
Type
+ +
+ +
Currency / Price
+
+

All Currency

+ +

Pizzas

+ +

Free

+
+ > + +
+
+
+ +
+
+ +
+ rowCount()) { ?> +
+
+

Showing Offset+1)?> - Offset+$query->rowCount())?> of results

+
+
+ + +
+
+ +

No results matched your criteria

+ +
+ fetch(\PDO::FETCH_OBJ)) { ?> +
+
+ " data-src="" class="card-img-top img-fluid p-2" title="name)?>" alt="name)?>"> +
+

name)?>

+ sale) { ?>

price ? ' '.number_format($item->price):'Free'?>

+
+
+
+
+
+

Creator: username?>

+

Updated: updated)?>

+

Sales: Sales)?>

+
+
+
+
+ +
+ Pages > 1) { ?> + + +
+
+
+buildFooter(); ?> diff --git a/css/bootstrap-colorpicker.min.css b/css/bootstrap-colorpicker.min.css new file mode 100644 index 0000000..52be690 --- /dev/null +++ b/css/bootstrap-colorpicker.min.css @@ -0,0 +1,10 @@ +/*! + * Bootstrap Colorpicker - Bootstrap Colorpicker is a modular color picker plugin for Bootstrap 4. + * @package bootstrap-colorpicker + * @version v3.1.2 + * @license MIT + * @link https://farbelous.github.io/bootstrap-colorpicker/ + * @link https://github.com/farbelous/bootstrap-colorpicker.git + */ +.colorpicker{position:relative;display:none;font-size:inherit;color:inherit;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);padding:.75rem .75rem;width:148px;border-radius:4px;-webkit-box-sizing:content-box;box-sizing:content-box}.colorpicker.colorpicker-disabled,.colorpicker.colorpicker-disabled *{cursor:default!important}.colorpicker div{position:relative}.colorpicker-popup{position:absolute;top:100%;left:0;float:left;margin-top:1px;z-index:1060}.colorpicker-popup.colorpicker-bs-popover-content{position:relative;top:auto;left:auto;float:none;margin:0;z-index:initial;border:none;padding:.25rem 0;border-radius:0;background:0 0;-webkit-box-shadow:none;box-shadow:none}.colorpicker:after,.colorpicker:before{content:"";display:table;clear:both;line-height:0}.colorpicker-clear{clear:both;display:block}.colorpicker:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,.2);position:absolute;top:-7px;left:auto;right:6px}.colorpicker:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;top:-6px;left:auto;right:7px}.colorpicker.colorpicker-with-alpha{width:170px}.colorpicker.colorpicker-with-alpha .colorpicker-alpha{display:block}.colorpicker-saturation{position:relative;width:126px;height:126px;background:-webkit-gradient(linear,left top,left bottom,from(transparent),to(black)),-webkit-gradient(linear,left top,right top,from(white),to(rgba(255,255,255,0)));background:linear-gradient(to bottom,transparent 0,#000 100%),linear-gradient(to right,#fff 0,rgba(255,255,255,0) 100%);cursor:crosshair;float:left;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.2);box-shadow:0 0 0 1px rgba(0,0,0,.2);margin-bottom:6px}.colorpicker-saturation .colorpicker-guide{display:block;height:6px;width:6px;border-radius:6px;border:1px solid #000;-webkit-box-shadow:0 0 0 1px rgba(255,255,255,.8);box-shadow:0 0 0 1px rgba(255,255,255,.8);position:absolute;top:0;left:0;margin:-3px 0 0 -3px}.colorpicker-alpha,.colorpicker-hue{position:relative;width:16px;height:126px;float:left;cursor:row-resize;margin-left:6px;margin-bottom:6px}.colorpicker-alpha-color{position:absolute;top:0;left:0;width:100%;height:100%}.colorpicker-alpha-color,.colorpicker-hue{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.2);box-shadow:0 0 0 1px rgba(0,0,0,.2)}.colorpicker-alpha .colorpicker-guide,.colorpicker-hue .colorpicker-guide{display:block;height:4px;background:rgba(255,255,255,.8);border:1px solid rgba(0,0,0,.4);position:absolute;top:0;left:0;margin-left:-2px;margin-top:-2px;right:-2px;z-index:1}.colorpicker-hue{background:-webkit-gradient(linear,left bottom,left top,from(red),color-stop(8%,#ff8000),color-stop(17%,#ff0),color-stop(25%,#80ff00),color-stop(33%,#0f0),color-stop(42%,#00ff80),color-stop(50%,#0ff),color-stop(58%,#0080ff),color-stop(67%,#00f),color-stop(75%,#8000ff),color-stop(83%,#ff00ff),color-stop(92%,#ff0080),to(red));background:linear-gradient(to top,red 0,#ff8000 8%,#ff0 17%,#80ff00 25%,#0f0 33%,#00ff80 42%,#0ff 50%,#0080ff 58%,#00f 67%,#8000ff 75%,#ff00ff 83%,#ff0080 92%,red 100%)}.colorpicker-alpha{background:linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),#fff;background-size:10px 10px;background-position:0 0,5px 5px;display:none}.colorpicker-bar{min-height:16px;margin:6px 0 0 0;clear:both;text-align:center;font-size:10px;line-height:normal;max-width:100%;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.2);box-shadow:0 0 0 1px rgba(0,0,0,.2)}.colorpicker-bar:before{content:"";display:table;clear:both}.colorpicker-bar.colorpicker-bar-horizontal{height:126px;width:16px;margin:0 0 6px 0;float:left}.colorpicker-input-addon{position:relative}.colorpicker-input-addon i{display:inline-block;cursor:pointer;vertical-align:text-top;height:16px;width:16px;position:relative}.colorpicker-input-addon:before{content:"";position:absolute;width:16px;height:16px;display:inline-block;vertical-align:text-top;background:linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),#fff;background-size:10px 10px;background-position:0 0,5px 5px}.colorpicker.colorpicker-inline{position:relative;display:inline-block;float:none;z-index:auto;vertical-align:text-bottom}.colorpicker.colorpicker-horizontal{width:126px;height:auto}.colorpicker.colorpicker-horizontal .colorpicker-bar{width:126px}.colorpicker.colorpicker-horizontal .colorpicker-saturation{float:none;margin-bottom:0}.colorpicker.colorpicker-horizontal .colorpicker-alpha,.colorpicker.colorpicker-horizontal .colorpicker-hue{float:none;width:126px;height:16px;cursor:col-resize;margin-left:0;margin-top:6px;margin-bottom:0}.colorpicker.colorpicker-horizontal .colorpicker-alpha .colorpicker-guide,.colorpicker.colorpicker-horizontal .colorpicker-hue .colorpicker-guide{position:absolute;display:block;bottom:-2px;left:0;right:auto;height:auto;width:4px}.colorpicker.colorpicker-horizontal .colorpicker-hue{background:-webkit-gradient(linear,right top,left top,from(red),color-stop(8%,#ff8000),color-stop(17%,#ff0),color-stop(25%,#80ff00),color-stop(33%,#0f0),color-stop(42%,#00ff80),color-stop(50%,#0ff),color-stop(58%,#0080ff),color-stop(67%,#00f),color-stop(75%,#8000ff),color-stop(83%,#ff00ff),color-stop(92%,#ff0080),to(red));background:linear-gradient(to left,red 0,#ff8000 8%,#ff0 17%,#80ff00 25%,#0f0 33%,#00ff80 42%,#0ff 50%,#0080ff 58%,#00f 67%,#8000ff 75%,#ff00ff 83%,#ff0080 92%,red 100%)}.colorpicker.colorpicker-horizontal .colorpicker-alpha{background:linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),#fff;background-size:10px 10px;background-position:0 0,5px 5px}.colorpicker-inline:before,.colorpicker-no-arrow:before,.colorpicker-popup.colorpicker-bs-popover-content:before{content:none;display:none}.colorpicker-inline:after,.colorpicker-no-arrow:after,.colorpicker-popup.colorpicker-bs-popover-content:after{content:none;display:none}.colorpicker-alpha,.colorpicker-hue,.colorpicker-saturation{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.colorpicker-alpha.colorpicker-visible,.colorpicker-bar.colorpicker-visible,.colorpicker-hue.colorpicker-visible,.colorpicker-saturation.colorpicker-visible,.colorpicker.colorpicker-visible{display:block}.colorpicker-alpha.colorpicker-hidden,.colorpicker-bar.colorpicker-hidden,.colorpicker-hue.colorpicker-hidden,.colorpicker-saturation.colorpicker-hidden,.colorpicker.colorpicker-hidden{display:none}.colorpicker-inline.colorpicker-visible{display:inline-block}.colorpicker.colorpicker-disabled:after{border:none;content:'';display:block;width:100%;height:100%;background:rgba(233,236,239,.33);top:0;left:0;right:auto;z-index:2;position:absolute}.colorpicker.colorpicker-disabled .colorpicker-guide{display:none}.colorpicker-preview{background:linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),#fff;background-size:10px 10px;background-position:0 0,5px 5px}.colorpicker-preview>div{position:absolute;left:0;top:0;width:100%;height:100%}.colorpicker-bar.colorpicker-swatches{-webkit-box-shadow:none;box-shadow:none;height:auto}.colorpicker-swatches--inner{clear:both;margin-top:-6px}.colorpicker-swatch{position:relative;cursor:pointer;float:left;height:16px;width:16px;margin-right:6px;margin-top:6px;margin-left:0;display:block;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.2);box-shadow:0 0 0 1px rgba(0,0,0,.2);background:linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),linear-gradient(45deg,rgba(0,0,0,.1) 25%,transparent 25%,transparent 75%,rgba(0,0,0,.1) 75%,rgba(0,0,0,.1) 0),#fff;background-size:10px 10px;background-position:0 0,5px 5px}.colorpicker-swatch--inner{position:absolute;top:0;left:0;width:100%;height:100%}.colorpicker-swatch:nth-of-type(7n+0){margin-right:0}.colorpicker-with-alpha .colorpicker-swatch:nth-of-type(7n+0){margin-right:6px}.colorpicker-with-alpha .colorpicker-swatch:nth-of-type(8n+0){margin-right:0}.colorpicker-horizontal .colorpicker-swatch:nth-of-type(6n+0){margin-right:0}.colorpicker-horizontal .colorpicker-swatch:nth-of-type(7n+0){margin-right:6px}.colorpicker-horizontal .colorpicker-swatch:nth-of-type(8n+0){margin-right:6px}.colorpicker-swatch:last-of-type:after{content:"";display:table;clear:both}.colorpicker-element input[dir=rtl],.colorpicker-element[dir=rtl] input,[dir=rtl] .colorpicker-element input{direction:ltr;text-align:right} +/*# sourceMappingURL=bootstrap-colorpicker.min.css.map */ diff --git a/css/bootstrap-datepicker.min.css b/css/bootstrap-datepicker.min.css new file mode 100644 index 0000000..eb68151 --- /dev/null +++ b/css/bootstrap-datepicker.min.css @@ -0,0 +1,7 @@ +/*! + * Datepicker for Bootstrap v1.9.0 (https://github.com/uxsolutions/bootstrap-datepicker) + * + * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +.datepicker{padding:4px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;direction:ltr}.datepicker-inline{width:220px}.datepicker-rtl{direction:rtl}.datepicker-rtl.dropdown-menu{left:auto}.datepicker-rtl table tr td span{float:right}.datepicker-dropdown{top:0;left:0}.datepicker-dropdown:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #999;border-top:0;border-bottom-color:rgba(0,0,0,.2);position:absolute}.datepicker-dropdown:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;border-top:0;position:absolute}.datepicker-dropdown.datepicker-orient-left:before{left:6px}.datepicker-dropdown.datepicker-orient-left:after{left:7px}.datepicker-dropdown.datepicker-orient-right:before{right:6px}.datepicker-dropdown.datepicker-orient-right:after{right:7px}.datepicker-dropdown.datepicker-orient-bottom:before{top:-7px}.datepicker-dropdown.datepicker-orient-bottom:after{top:-6px}.datepicker-dropdown.datepicker-orient-top:before{bottom:-7px;border-bottom:0;border-top:7px solid #999}.datepicker-dropdown.datepicker-orient-top:after{bottom:-6px;border-bottom:0;border-top:6px solid #fff}.datepicker table{margin:0;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.datepicker td,.datepicker th{text-align:center;width:20px;height:20px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:none}.table-striped .datepicker table tr td,.table-striped .datepicker table tr th{background-color:transparent}.datepicker table tr td.day.focused,.datepicker table tr td.day:hover{background:#eee;cursor:pointer}.datepicker table tr td.new,.datepicker table tr td.old{color:#999}.datepicker table tr td.disabled,.datepicker table tr td.disabled:hover{background:0 0;color:#999;cursor:default}.datepicker table tr td.highlighted{background:#d9edf7;border-radius:0}.datepicker table tr td.today,.datepicker table tr td.today.disabled,.datepicker table tr td.today.disabled:hover,.datepicker table tr td.today:hover{background-color:#fde19a;background-image:-moz-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-ms-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fdd49a),to(#fdf59a));background-image:-webkit-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-o-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:linear-gradient(to bottom,#fdd49a,#fdf59a);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);border-color:#fdf59a #fdf59a #fbed50;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#000}.datepicker table tr td.today.active,.datepicker table tr td.today.disabled,.datepicker table tr td.today.disabled.active,.datepicker table tr td.today.disabled.disabled,.datepicker table tr td.today.disabled:active,.datepicker table tr td.today.disabled:hover,.datepicker table tr td.today.disabled:hover.active,.datepicker table tr td.today.disabled:hover.disabled,.datepicker table tr td.today.disabled:hover:active,.datepicker table tr td.today.disabled:hover:hover,.datepicker table tr td.today.disabled:hover[disabled],.datepicker table tr td.today.disabled[disabled],.datepicker table tr td.today:active,.datepicker table tr td.today:hover,.datepicker table tr td.today:hover.active,.datepicker table tr td.today:hover.disabled,.datepicker table tr td.today:hover:active,.datepicker table tr td.today:hover:hover,.datepicker table tr td.today:hover[disabled],.datepicker table tr td.today[disabled]{background-color:#fdf59a}.datepicker table tr td.today.active,.datepicker table tr td.today.disabled.active,.datepicker table tr td.today.disabled:active,.datepicker table tr td.today.disabled:hover.active,.datepicker table tr td.today.disabled:hover:active,.datepicker table tr td.today:active,.datepicker table tr td.today:hover.active,.datepicker table tr td.today:hover:active{background-color:#fbf069\9}.datepicker table tr td.today:hover:hover{color:#000}.datepicker table tr td.today.active:hover{color:#fff}.datepicker table tr td.range,.datepicker table tr td.range.disabled,.datepicker table tr td.range.disabled:hover,.datepicker table tr td.range:hover{background:#eee;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.datepicker table tr td.range.today,.datepicker table tr td.range.today.disabled,.datepicker table tr td.range.today.disabled:hover,.datepicker table tr td.range.today:hover{background-color:#f3d17a;background-image:-moz-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-ms-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f3c17a),to(#f3e97a));background-image:-webkit-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-o-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:linear-gradient(to bottom,#f3c17a,#f3e97a);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);border-color:#f3e97a #f3e97a #edde34;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.datepicker table tr td.range.today.active,.datepicker table tr td.range.today.disabled,.datepicker table tr td.range.today.disabled.active,.datepicker table tr td.range.today.disabled.disabled,.datepicker table tr td.range.today.disabled:active,.datepicker table tr td.range.today.disabled:hover,.datepicker table tr td.range.today.disabled:hover.active,.datepicker table tr td.range.today.disabled:hover.disabled,.datepicker table tr td.range.today.disabled:hover:active,.datepicker table tr td.range.today.disabled:hover:hover,.datepicker table tr td.range.today.disabled:hover[disabled],.datepicker table tr td.range.today.disabled[disabled],.datepicker table tr td.range.today:active,.datepicker table tr td.range.today:hover,.datepicker table tr td.range.today:hover.active,.datepicker table tr td.range.today:hover.disabled,.datepicker table tr td.range.today:hover:active,.datepicker table tr td.range.today:hover:hover,.datepicker table tr td.range.today:hover[disabled],.datepicker table tr td.range.today[disabled]{background-color:#f3e97a}.datepicker table tr td.range.today.active,.datepicker table tr td.range.today.disabled.active,.datepicker table tr td.range.today.disabled:active,.datepicker table tr td.range.today.disabled:hover.active,.datepicker table tr td.range.today.disabled:hover:active,.datepicker table tr td.range.today:active,.datepicker table tr td.range.today:hover.active,.datepicker table tr td.range.today:hover:active{background-color:#efe24b\9}.datepicker table tr td.selected,.datepicker table tr td.selected.disabled,.datepicker table tr td.selected.disabled:hover,.datepicker table tr td.selected:hover{background-color:#9e9e9e;background-image:-moz-linear-gradient(to bottom,#b3b3b3,grey);background-image:-ms-linear-gradient(to bottom,#b3b3b3,grey);background-image:-webkit-gradient(linear,0 0,0 100%,from(#b3b3b3),to(grey));background-image:-webkit-linear-gradient(to bottom,#b3b3b3,grey);background-image:-o-linear-gradient(to bottom,#b3b3b3,grey);background-image:linear-gradient(to bottom,#b3b3b3,grey);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);border-color:grey grey #595959;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td.selected.active,.datepicker table tr td.selected.disabled,.datepicker table tr td.selected.disabled.active,.datepicker table tr td.selected.disabled.disabled,.datepicker table tr td.selected.disabled:active,.datepicker table tr td.selected.disabled:hover,.datepicker table tr td.selected.disabled:hover.active,.datepicker table tr td.selected.disabled:hover.disabled,.datepicker table tr td.selected.disabled:hover:active,.datepicker table tr td.selected.disabled:hover:hover,.datepicker table tr td.selected.disabled:hover[disabled],.datepicker table tr td.selected.disabled[disabled],.datepicker table tr td.selected:active,.datepicker table tr td.selected:hover,.datepicker table tr td.selected:hover.active,.datepicker table tr td.selected:hover.disabled,.datepicker table tr td.selected:hover:active,.datepicker table tr td.selected:hover:hover,.datepicker table tr td.selected:hover[disabled],.datepicker table tr td.selected[disabled]{background-color:grey}.datepicker table tr td.selected.active,.datepicker table tr td.selected.disabled.active,.datepicker table tr td.selected.disabled:active,.datepicker table tr td.selected.disabled:hover.active,.datepicker table tr td.selected.disabled:hover:active,.datepicker table tr td.selected:active,.datepicker table tr td.selected:hover.active,.datepicker table tr td.selected:hover:active{background-color:#666\9}.datepicker table tr td.active,.datepicker table tr td.active.disabled,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active:hover{background-color:#006dcc;background-image:-moz-linear-gradient(to bottom,#08c,#04c);background-image:-ms-linear-gradient(to bottom,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(to bottom,#08c,#04c);background-image:-o-linear-gradient(to bottom,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td.active.active,.datepicker table tr td.active.disabled,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled.disabled,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover.active,.datepicker table tr td.active.disabled:hover.disabled,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active.disabled:hover[disabled],.datepicker table tr td.active.disabled[disabled],.datepicker table tr td.active:active,.datepicker table tr td.active:hover,.datepicker table tr td.active:hover.active,.datepicker table tr td.active:hover.disabled,.datepicker table tr td.active:hover:active,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active:hover[disabled],.datepicker table tr td.active[disabled]{background-color:#04c}.datepicker table tr td.active.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover.active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active:active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active:hover:active{background-color:#039\9}.datepicker table tr td span{display:block;width:23%;height:54px;line-height:54px;float:left;margin:1%;cursor:pointer;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.datepicker table tr td span.focused,.datepicker table tr td span:hover{background:#eee}.datepicker table tr td span.disabled,.datepicker table tr td span.disabled:hover{background:0 0;color:#999;cursor:default}.datepicker table tr td span.active,.datepicker table tr td span.active.disabled,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active:hover{background-color:#006dcc;background-image:-moz-linear-gradient(to bottom,#08c,#04c);background-image:-ms-linear-gradient(to bottom,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(to bottom,#08c,#04c);background-image:-o-linear-gradient(to bottom,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td span.active.active,.datepicker table tr td span.active.disabled,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled.disabled,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover.active,.datepicker table tr td span.active.disabled:hover.disabled,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active.disabled:hover[disabled],.datepicker table tr td span.active.disabled[disabled],.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active:hover.disabled,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active:hover[disabled],.datepicker table tr td span.active[disabled]{background-color:#04c}.datepicker table tr td span.active.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover.active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active:hover:active{background-color:#039\9}.datepicker table tr td span.new,.datepicker table tr td span.old{color:#999}.datepicker .datepicker-switch{width:145px}.datepicker .datepicker-switch,.datepicker .next,.datepicker .prev,.datepicker tfoot tr th{cursor:pointer}.datepicker .datepicker-switch:hover,.datepicker .next:hover,.datepicker .prev:hover,.datepicker tfoot tr th:hover{background:#eee}.datepicker .next.disabled,.datepicker .prev.disabled{visibility:hidden}.datepicker .cw{font-size:10px;width:12px;padding:0 2px 0 5px;vertical-align:middle}.input-append.date .add-on,.input-prepend.date .add-on{cursor:pointer}.input-append.date .add-on i,.input-prepend.date .add-on i{margin-top:3px}.input-daterange input{text-align:center}.input-daterange input:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-daterange input:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-daterange .add-on{display:inline-block;width:auto;min-width:16px;height:18px;padding:4px 5px;font-weight:400;line-height:18px;text-align:center;text-shadow:0 1px 0 #fff;vertical-align:middle;background-color:#eee;border:1px solid #ccc;margin-left:-5px;margin-right:-5px} \ No newline at end of file diff --git a/css/bootstrap.min.css b/css/bootstrap.min.css new file mode 100644 index 0000000..21d10ba --- /dev/null +++ b/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.5.2 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item{display:-ms-flexbox;display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{-ms-flex-preferred-size:350px;flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;-ms-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/css/bootstrap.min.css.map b/css/bootstrap.min.css.map new file mode 100644 index 0000000..3c23c17 --- /dev/null +++ b/css/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","bootstrap.css","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/utilities/_interactions.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_stretched-link.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"AAAA;;;;;ACCA,MAGI,OAAA,QAAA,SAAA,QAAA,SAAA,QAAA,OAAA,QAAA,MAAA,QAAA,SAAA,QAAA,SAAA,QAAA,QAAA,QAAA,OAAA,QAAA,OAAA,QAAA,QAAA,KAAA,OAAA,QAAA,YAAA,QAIA,UAAA,QAAA,YAAA,QAAA,UAAA,QAAA,OAAA,QAAA,UAAA,QAAA,SAAA,QAAA,QAAA,QAAA,OAAA,QAIA,gBAAA,EAAA,gBAAA,MAAA,gBAAA,MAAA,gBAAA,MAAA,gBAAA,OAKF,yBAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBACA,wBAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UCAF,ECqBA,QADA,SDjBE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,4BAAA,YAMF,QAAA,MAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAUF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBEgFI,UAAA,KF9EJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KGYF,0CHCE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KChBF,0BD2BA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EACA,iCAAA,KAAA,yBAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCrBF,GDwBA,GCzBA,GD4BE,WAAA,EACA,cAAA,KAGF,MCxBA,MACA,MAFA,MD6BE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,ECzBA,OD2BE,YAAA,OAGF,MExFI,UAAA,IFiGJ,IC9BA,IDgCE,SAAA,SEnGE,UAAA,IFqGF,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YIhLA,QJmLE,MAAA,QACA,gBAAA,UASJ,2BACE,MAAA,QACA,gBAAA,KI/LA,iCJkME,MAAA,QACA,gBAAA,KC/BJ,KACA,IDuCA,ICtCA,KD0CE,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,UEpJE,UAAA,IFwJJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAGA,mBAAA,UAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,IAGE,SAAA,OACA,eAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OAEE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBC5EF,OD+EA,MC7EA,SADA,OAEA,SDiFE,OAAA,EACA,YAAA,QExPE,UAAA,QF0PF,YAAA,QAGF,OC/EA,MDiFE,SAAA,QAGF,OC/EA,ODiFE,eAAA,KG/EF,cHsFE,OAAA,QAMF,OACE,UAAA,OClFF,cACA,aACA,cDuFA,OAIE,mBAAA,OCtFF,6BACA,4BACA,6BDyFE,sBAKI,OAAA,QCzFN,gCACA,+BACA,gCD6FA,yBAIE,QAAA,EACA,aAAA,KC5FF,qBD+FA,kBAEE,WAAA,WACA,QAAA,EAIF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,ME/RI,UAAA,OFiSJ,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SGzGF,yCFGA,yCD4GE,OAAA,KG1GF,cHkHE,eAAA,KACA,mBAAA,KG9GF,yCHsHE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KG3HF,SHiIE,QAAA,eC1HF,IAAK,IAAK,IAAK,IAAK,IAAK,II9VzB,GAAA,GAAA,GAAA,GAAA,GAAA,GAEE,cAAA,MAEA,YAAA,IACA,YAAA,IAIF,IAAA,GHgHM,UAAA,OG/GN,IAAA,GH+GM,UAAA,KG9GN,IAAA,GH8GM,UAAA,QG7GN,IAAA,GH6GM,UAAA,OG5GN,IAAA,GH4GM,UAAA,QG3GN,IAAA,GH2GM,UAAA,KGzGN,MHyGM,UAAA,QGvGJ,YAAA,IAIF,WHmGM,UAAA,KGjGJ,YAAA,IACA,YAAA,IAEF,WH8FM,UAAA,OG5FJ,YAAA,IACA,YAAA,IAEF,WHyFM,UAAA,OGvFJ,YAAA,IACA,YAAA,IAEF,WHoFM,UAAA,OGlFJ,YAAA,IACA,YAAA,IL6BF,GKpBE,WAAA,KACA,cAAA,KACA,OAAA,EACA,WAAA,IAAA,MAAA,eJ6WF,OIrWA,MHMI,UAAA,IGHF,YAAA,IJwWF,MIrWA,KAEE,QAAA,KACA,iBAAA,QAQF,eC/EE,aAAA,EACA,WAAA,KDmFF,aCpFE,aAAA,EACA,WAAA,KDsFF,kBACE,QAAA,aADF,mCAII,aAAA,MAUJ,YHjCI,UAAA,IGmCF,eAAA,UAIF,YACE,cAAA,KHeI,UAAA,QGXN,mBACE,QAAA,MH7CE,UAAA,IG+CF,MAAA,QAHF,2BAMI,QAAA,aEnHJ,WCIE,UAAA,KAGA,OAAA,KDDF,eACE,QAAA,OACA,iBAAA,KACA,OAAA,IAAA,MAAA,QEEE,cAAA,ODPF,UAAA,KAGA,OAAA,KDcF,QAEE,QAAA,aAGF,YACE,cAAA,MACA,YAAA,EAGF,gBLkCI,UAAA,IKhCF,MAAA,QGvCF,KRuEI,UAAA,MQrEF,MAAA,QACA,UAAA,WAGA,OACE,MAAA,QAKJ,IACE,QAAA,MAAA,MR0DE,UAAA,MQxDF,MAAA,KACA,iBAAA,QDCE,cAAA,MCLJ,QASI,QAAA,ERkDA,UAAA,KQhDA,YAAA,IVwMJ,IUjME,QAAA,MRyCE,UAAA,MQvCF,MAAA,QAHF,SR0CI,UAAA,QQlCA,MAAA,QACA,WAAA,OAKJ,gBACE,WAAA,MACA,WAAA,OCxCA,WVwhBF,iBAGA,cADA,cADA,cAGA,cW7hBE,MAAA,KACA,cAAA,KACA,aAAA,KACA,aAAA,KACA,YAAA,KCmDE,yBFzCE,WAAA,cACE,UAAA,OEwCJ,yBFzCE,WAAA,cAAA,cACE,UAAA,OEwCJ,yBFzCE,WAAA,cAAA,cAAA,cACE,UAAA,OEwCJ,0BFzCE,WAAA,cAAA,cAAA,cAAA,cACE,UAAA,QA4BN,KCnCA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,MACA,YAAA,MDsCA,YACE,aAAA,EACA,YAAA,EAFF,iBV2hBF,0BUrhBM,cAAA,EACA,aAAA,EGtDJ,KAAA,OAAA,QAAA,QAAA,QAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,OAAA,ObglBF,UAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aAFkJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACnG,aAEqJ,QAAvI,UAAmG,WAAY,WAAY,WAAhH,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UAAW,UACtG,aanlBI,SAAA,SACA,MAAA,KACA,cAAA,KACA,aAAA,KAsBE,KACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,cFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,cFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,cFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,cFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,cFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,cFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,UFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,OFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,OFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,QFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,QFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,QFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,aAAwB,eAAA,GAAA,MAAA,GAExB,YAAuB,eAAA,GAAA,MAAA,GAGrB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,SAAwB,eAAA,EAAA,MAAA,EAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAAxB,UAAwB,eAAA,GAAA,MAAA,GAOpB,UFhBV,YAAA,UEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,IEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,IEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,WEgBU,UFhBV,YAAA,IEgBU,WFhBV,YAAA,WEgBU,WFhBV,YAAA,WCKE,yBC3BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,iBFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,aFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,UFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAOpB,aFhBV,YAAA,EEgBU,aFhBV,YAAA,UEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,cFhBV,YAAA,WEgBU,cFhBV,YAAA,YCKE,yBC3BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,iBFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,aFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,UFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAOpB,aFhBV,YAAA,EEgBU,aFhBV,YAAA,UEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,cFhBV,YAAA,WEgBU,cFhBV,YAAA,YCKE,yBC3BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,iBFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,aFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,UFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAOpB,aFhBV,YAAA,EEgBU,aFhBV,YAAA,UEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,cFhBV,YAAA,WEgBU,cFhBV,YAAA,YCKE,0BC3BE,QACE,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,UAAA,KAKE,iBFwBN,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,UAAA,KEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IACA,UAAA,IEzBM,iBFwBN,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WACA,UAAA,WEnBE,aFCJ,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,UAAA,KEGQ,UFbR,SAAA,EAAA,EAAA,UAAA,KAAA,EAAA,EAAA,UAIA,UAAA,UESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,UFbR,SAAA,EAAA,EAAA,IAAA,KAAA,EAAA,EAAA,IAIA,UAAA,IESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,WAAA,KAAA,EAAA,EAAA,WAIA,UAAA,WESQ,WFbR,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAIA,UAAA,KEeI,gBAAwB,eAAA,GAAA,MAAA,GAExB,eAAuB,eAAA,GAAA,MAAA,GAGrB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,YAAwB,eAAA,EAAA,MAAA,EAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAAxB,aAAwB,eAAA,GAAA,MAAA,GAOpB,aFhBV,YAAA,EEgBU,aFhBV,YAAA,UEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,WEgBU,aFhBV,YAAA,IEgBU,cFhBV,YAAA,WEgBU,cFhBV,YAAA,YGnDF,OACE,MAAA,KACA,cAAA,KACA,MAAA,Qd4nDF,Uc/nDA,UAQI,QAAA,OACA,eAAA,IACA,WAAA,IAAA,MAAA,QAVJ,gBAcI,eAAA,OACA,cAAA,IAAA,MAAA,QAfJ,mBAmBI,WAAA,IAAA,MAAA,Qd4nDJ,acnnDA,aAGI,QAAA,MASJ,gBACE,OAAA,IAAA,MAAA,Qd+mDF,mBchnDA,mBAKI,OAAA,IAAA,MAAA,QdgnDJ,yBcrnDA,yBAWM,oBAAA,IdinDN,8BAFA,qBc1mDA,qBd2mDA,2BctmDI,OAAA,EAQJ,yCAEI,iBAAA,gBX/DF,4BW2EI,MAAA,QACA,iBAAA,iBCnFJ,efkrDF,kBADA,kBe7qDM,iBAAA,QfqrDN,2BAFA,kBevrDE,kBfwrDF,wBe5qDQ,aAAA,QZLN,kCYiBM,iBAAA,QALN,qCf+qDF,qCetqDU,iBAAA,QA5BR,iBfwsDF,oBADA,oBensDM,iBAAA,Qf2sDN,6BAFA,oBe7sDE,oBf8sDF,0BelsDQ,aAAA,QZLN,oCYiBM,iBAAA,QALN,uCfqsDF,uCe5rDU,iBAAA,QA5BR,ef8tDF,kBADA,kBeztDM,iBAAA,QfiuDN,2BAFA,kBenuDE,kBfouDF,wBextDQ,aAAA,QZLN,kCYiBM,iBAAA,QALN,qCf2tDF,qCeltDU,iBAAA,QA5BR,YfovDF,eADA,ee/uDM,iBAAA,QfuvDN,wBAFA,eezvDE,ef0vDF,qBe9uDQ,aAAA,QZLN,+BYiBM,iBAAA,QALN,kCfivDF,kCexuDU,iBAAA,QA5BR,ef0wDF,kBADA,kBerwDM,iBAAA,Qf6wDN,2BAFA,kBe/wDE,kBfgxDF,wBepwDQ,aAAA,QZLN,kCYiBM,iBAAA,QALN,qCfuwDF,qCe9vDU,iBAAA,QA5BR,cfgyDF,iBADA,iBe3xDM,iBAAA,QfmyDN,0BAFA,iBeryDE,iBfsyDF,uBe1xDQ,aAAA,QZLN,iCYiBM,iBAAA,QALN,oCf6xDF,oCepxDU,iBAAA,QA5BR,afszDF,gBADA,gBejzDM,iBAAA,QfyzDN,yBAFA,gBe3zDE,gBf4zDF,sBehzDQ,aAAA,QZLN,gCYiBM,iBAAA,QALN,mCfmzDF,mCe1yDU,iBAAA,QA5BR,Yf40DF,eADA,eev0DM,iBAAA,Qf+0DN,wBAFA,eej1DE,efk1DF,qBet0DQ,aAAA,QZLN,+BYiBM,iBAAA,QALN,kCfy0DF,kCeh0DU,iBAAA,QA5BR,cfk2DF,iBADA,iBe71DM,iBAAA,iBZGJ,iCYiBM,iBAAA,iBALN,oCfw1DF,oCe/0DU,iBAAA,iBD8EV,sBAGM,MAAA,KACA,iBAAA,QACA,aAAA,QALN,uBAWM,MAAA,QACA,iBAAA,QACA,aAAA,QAKN,YACE,MAAA,KACA,iBAAA,QdmwDF,ecrwDA,edswDA,qBc/vDI,aAAA,QAPJ,2BAWI,OAAA,EAXJ,oDAgBM,iBAAA,sBXrIJ,uCW4IM,MAAA,KACA,iBAAA,uBFhFJ,4BEiGA,qBAEI,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MALH,qCASK,OAAA,GF1GN,4BEiGA,qBAEI,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MALH,qCASK,OAAA,GF1GN,4BEiGA,qBAEI,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MALH,qCASK,OAAA,GF1GN,6BEiGA,qBAEI,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MALH,qCASK,OAAA,GAdV,kBAOQ,QAAA,MACA,MAAA,KACA,WAAA,KACA,2BAAA,MAVR,kCAcU,OAAA,EE7KV,cACE,QAAA,MACA,MAAA,KACA,OAAA,2BACA,QAAA,QAAA,OfqHI,UAAA,KelHJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,QRAE,cAAA,OSFE,WAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCDdN,cCeQ,WAAA,MDfR,0BAsBI,iBAAA,YACA,OAAA,EAvBJ,6BA4BI,MAAA,YACA,YAAA,EAAA,EAAA,EAAA,QEtBF,oBACE,MAAA,QACA,iBAAA,KACA,aAAA,QACA,QAAA,EAKE,WAAA,EAAA,EAAA,EAAA,MAAA,oBFhBN,yCAqCI,MAAA,QAEA,QAAA,EAvCJ,gCAqCI,MAAA,QAEA,QAAA,EAvCJ,oCAqCI,MAAA,QAEA,QAAA,EAvCJ,qCAqCI,MAAA,QAEA,QAAA,EAvCJ,2BAqCI,MAAA,QAEA,QAAA,EAvCJ,uBAAA,wBAiDI,iBAAA,QAEA,QAAA,EAIJ,8BhB89DA,wCACA,+BAFA,8BgBx9DI,mBAAA,KAAA,gBAAA,KAAA,WAAA,KAIJ,qCAOI,MAAA,QACA,iBAAA,KAKJ,mBhBq9DA,oBgBn9DE,QAAA,MACA,MAAA,KAUF,gBACE,YAAA,oBACA,eAAA,oBACA,cAAA,Ef3BE,UAAA,Qe6BF,YAAA,IAGF,mBACE,YAAA,kBACA,eAAA,kBfqBI,UAAA,QenBJ,YAAA,IAGF,mBACE,YAAA,mBACA,eAAA,mBfcI,UAAA,QeZJ,YAAA,IASF,wBACE,QAAA,MACA,MAAA,KACA,QAAA,QAAA,EACA,cAAA,EfDI,UAAA,KeGJ,YAAA,IACA,MAAA,QACA,iBAAA,YACA,OAAA,MAAA,YACA,aAAA,IAAA,EAVF,wCAAA,wCAcI,cAAA,EACA,aAAA,EAYJ,iBACE,OAAA,0BACA,QAAA,OAAA,Mf1BI,UAAA,Qe4BJ,YAAA,IRzIE,cAAA,MQ6IJ,iBACE,OAAA,yBACA,QAAA,MAAA,KflCI,UAAA,QeoCJ,YAAA,IRjJE,cAAA,MQsJJ,8BAAA,0BAGI,OAAA,KAIJ,sBACE,OAAA,KAQF,YACE,cAAA,KAGF,WACE,QAAA,MACA,WAAA,OAQF,UACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,KACA,YAAA,KAJF,ehB07DA,wBgBl7DI,cAAA,IACA,aAAA,IASJ,YACE,SAAA,SACA,QAAA,MACA,aAAA,QAGF,kBACE,SAAA,SACA,WAAA,MACA,YAAA,ShBi7DF,6CgBp7DA,8CAQI,MAAA,QAIJ,kBACE,cAAA,EAGF,mBACE,QAAA,mBAAA,QAAA,YACA,eAAA,OAAA,YAAA,OACA,aAAA,EACA,aAAA,OAJF,qCAQI,SAAA,OACA,WAAA,EACA,aAAA,SACA,YAAA,EE7MF,gBACE,QAAA,KACA,MAAA,KACA,WAAA,OjByBA,UAAA,IiBvBA,MAAA,QAGF,eACE,SAAA,SACA,IAAA,KACA,KAAA,EACA,QAAA,EACA,QAAA,KACA,UAAA,KACA,QAAA,OAAA,MACA,WAAA,MjBmEE,UAAA,QiBjEF,YAAA,IACA,MAAA,KACA,iBAAA,mBV9CA,cAAA,ORkrEJ,0BACA,yBkBrqEI,sClBmqEJ,qCkB5nEM,QAAA,MAvCF,uBAAA,mCA6CE,aAAA,QAGE,cAAA,qBACA,iBAAA,gQACA,kBAAA,UACA,oBAAA,MAAA,wBAAA,OACA,gBAAA,sBAAA,sBApDJ,6BAAA,yCAwDI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAzDJ,2CAAA,+BAkEI,cAAA,qBACA,oBAAA,IAAA,wBAAA,MAAA,wBAnEJ,wBAAA,oCA0EE,aAAA,QAGE,cAAA,wBACA,WAAA,+KAAA,UAAA,MAAA,OAAA,MAAA,CAAA,IAAA,IAAA,CAAA,gQAAA,KAAA,UAAA,OAAA,MAAA,OAAA,CAAA,sBAAA,sBA9EJ,8BAAA,0CAkFI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAnFJ,6CAAA,yDA2FI,MAAA,QlBinEiD,2CACzD,0CkB7sEI,uDlB4sEJ,sDkB5mEQ,QAAA,MAhGJ,qDAAA,iEAwGI,MAAA,QAxGJ,6DAAA,yEA2GM,aAAA,QA3GN,qEAAA,iFAiHM,aAAA,QC3IN,iBAAA,QD0BA,mEAAA,+EAwHM,WAAA,EAAA,EAAA,EAAA,MAAA,oBAxHN,iFAAA,6FA4HM,aAAA,QA5HN,+CAAA,2DAsII,aAAA,QAtIJ,qDAAA,iEA2IM,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAhIR,kBACE,QAAA,KACA,MAAA,KACA,WAAA,OjByBA,UAAA,IiBvBA,MAAA,QAGF,iBACE,SAAA,SACA,IAAA,KACA,KAAA,EACA,QAAA,EACA,QAAA,KACA,UAAA,KACA,QAAA,OAAA,MACA,WAAA,MjBmEE,UAAA,QiBjEF,YAAA,IACA,MAAA,KACA,iBAAA,mBV9CA,cAAA,ORuxEJ,8BACA,6BkB1wEI,0ClBwwEJ,yCkBjuEM,QAAA,MAvCF,yBAAA,qCA6CE,aAAA,QAGE,cAAA,qBACA,iBAAA,2TACA,kBAAA,UACA,oBAAA,MAAA,wBAAA,OACA,gBAAA,sBAAA,sBApDJ,+BAAA,2CAwDI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAzDJ,6CAAA,iCAkEI,cAAA,qBACA,oBAAA,IAAA,wBAAA,MAAA,wBAnEJ,0BAAA,sCA0EE,aAAA,QAGE,cAAA,wBACA,WAAA,+KAAA,UAAA,MAAA,OAAA,MAAA,CAAA,IAAA,IAAA,CAAA,2TAAA,KAAA,UAAA,OAAA,MAAA,OAAA,CAAA,sBAAA,sBA9EJ,gCAAA,4CAkFI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAnFJ,+CAAA,2DA2FI,MAAA,QlBstEqD,+CAC7D,8CkBlzEI,2DlBizEJ,0DkBjtEQ,QAAA,MAhGJ,uDAAA,mEAwGI,MAAA,QAxGJ,+DAAA,2EA2GM,aAAA,QA3GN,uEAAA,mFAiHM,aAAA,QC3IN,iBAAA,QD0BA,qEAAA,iFAwHM,WAAA,EAAA,EAAA,EAAA,MAAA,oBAxHN,mFAAA,+FA4HM,aAAA,QA5HN,iDAAA,6DAsII,aAAA,QAtIJ,uDAAA,mEA2IM,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBFsGV,aACE,QAAA,YAAA,QAAA,KACA,cAAA,IAAA,KAAA,UAAA,IAAA,KACA,eAAA,OAAA,YAAA,OAHF,yBASI,MAAA,KJ/NA,yBIsNJ,mBAeM,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,OAAA,gBAAA,OACA,cAAA,EAlBN,yBAuBM,QAAA,YAAA,QAAA,KACA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,cAAA,IAAA,KAAA,UAAA,IAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,EA3BN,2BAgCM,QAAA,aACA,MAAA,KACA,eAAA,OAlCN,qCAuCM,QAAA,ahBsmEJ,4BgB7oEF,0BA4CM,MAAA,KA5CN,yBAkDM,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,OAAA,gBAAA,OACA,MAAA,KACA,aAAA,EAtDN,+BAyDM,SAAA,SACA,kBAAA,EAAA,YAAA,EACA,WAAA,EACA,aAAA,OACA,YAAA,EA7DN,6BAiEM,eAAA,OAAA,YAAA,OACA,cAAA,OAAA,gBAAA,OAlEN,mCAqEM,cAAA,GIjVN,KACE,QAAA,aAEA,YAAA,IACA,MAAA,QACA,WAAA,OAGA,eAAA,OACA,oBAAA,KAAA,iBAAA,KAAA,gBAAA,KAAA,YAAA,KACA,iBAAA,YACA,OAAA,IAAA,MAAA,YCuFA,QAAA,QAAA,OpBuBI,UAAA,KoBrBJ,YAAA,IbxFE,cAAA,OSFE,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCGdN,KHeQ,WAAA,MdTN,WiBUE,MAAA,QACA,gBAAA,KAjBJ,WAAA,WAsBI,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAvBJ,cAAA,cA6BI,QAAA,IA7BJ,mCAkCI,OAAA,QAcJ,epBq7EA,wBoBn7EE,eAAA,KASA,aC3DA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,mBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,mBAAA,mBAEE,MAAA,KFbA,iBAAA,QEeA,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,MAAA,oBAKJ,sBAAA,sBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,kDAAA,kDrB+9EF,mCqB59EI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,wDAAA,wDrB49EJ,yCqBv9EQ,WAAA,EAAA,EAAA,EAAA,MAAA,oBDQN,eC3DA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,qBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,qBAAA,qBAEE,MAAA,KFbA,iBAAA,QEeA,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,MAAA,qBAKJ,wBAAA,wBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,oDAAA,oDrBogFF,qCqBjgFI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,0DAAA,0DrBigFJ,2CqB5/EQ,WAAA,EAAA,EAAA,EAAA,MAAA,qBDQN,aC3DA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,mBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,mBAAA,mBAEE,MAAA,KFbA,iBAAA,QEeA,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAKJ,sBAAA,sBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,kDAAA,kDrByiFF,mCqBtiFI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,wDAAA,wDrBsiFJ,yCqBjiFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBDQN,UC3DA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,gBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,gBAAA,gBAEE,MAAA,KFbA,iBAAA,QEeA,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,MAAA,oBAKJ,mBAAA,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,+CAAA,+CrB8kFF,gCqB3kFI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,qDAAA,qDrB2kFJ,sCqBtkFQ,WAAA,EAAA,EAAA,EAAA,MAAA,oBDQN,aC3DA,MAAA,QFAE,iBAAA,QEEF,aAAA,QlBIA,mBkBAE,MAAA,QFNA,iBAAA,QEQA,aAAA,QAGF,mBAAA,mBAEE,MAAA,QFbA,iBAAA,QEeA,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,MAAA,oBAKJ,sBAAA,sBAEE,MAAA,QACA,iBAAA,QACA,aAAA,QAOF,kDAAA,kDrBmnFF,mCqBhnFI,MAAA,QACA,iBAAA,QAIA,aAAA,QAEA,wDAAA,wDrBgnFJ,yCqB3mFQ,WAAA,EAAA,EAAA,EAAA,MAAA,oBDQN,YC3DA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,kBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,kBAAA,kBAEE,MAAA,KFbA,iBAAA,QEeA,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAKJ,qBAAA,qBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,iDAAA,iDrBwpFF,kCqBrpFI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,uDAAA,uDrBqpFJ,wCqBhpFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBDQN,WC3DA,MAAA,QFAE,iBAAA,QEEF,aAAA,QlBIA,iBkBAE,MAAA,QFNA,iBAAA,QEQA,aAAA,QAGF,iBAAA,iBAEE,MAAA,QFbA,iBAAA,QEeA,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,MAAA,qBAKJ,oBAAA,oBAEE,MAAA,QACA,iBAAA,QACA,aAAA,QAOF,gDAAA,gDrB6rFF,iCqB1rFI,MAAA,QACA,iBAAA,QAIA,aAAA,QAEA,sDAAA,sDrB0rFJ,uCqBrrFQ,WAAA,EAAA,EAAA,EAAA,MAAA,qBDQN,UC3DA,MAAA,KFAE,iBAAA,QEEF,aAAA,QlBIA,gBkBAE,MAAA,KFNA,iBAAA,QEQA,aAAA,QAGF,gBAAA,gBAEE,MAAA,KFbA,iBAAA,QEeA,aAAA,QAKE,WAAA,EAAA,EAAA,EAAA,MAAA,kBAKJ,mBAAA,mBAEE,MAAA,KACA,iBAAA,QACA,aAAA,QAOF,+CAAA,+CrBkuFF,gCqB/tFI,MAAA,KACA,iBAAA,QAIA,aAAA,QAEA,qDAAA,qDrB+tFJ,sCqB1tFQ,WAAA,EAAA,EAAA,EAAA,MAAA,kBDcN,qBCPA,MAAA,QACA,aAAA,QlBrDA,2BkBwDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,2BAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAGF,8BAAA,8BAEE,MAAA,QACA,iBAAA,YAGF,0DAAA,0DrBwtFF,2CqBrtFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,gEAAA,gErBwtFJ,iDqBntFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBDzBN,uBCPA,MAAA,QACA,aAAA,QlBrDA,6BkBwDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,6BAAA,6BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,qBAGF,gCAAA,gCAEE,MAAA,QACA,iBAAA,YAGF,4DAAA,4DrBwvFF,6CqBrvFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,kEAAA,kErBwvFJ,mDqBnvFQ,WAAA,EAAA,EAAA,EAAA,MAAA,qBDzBN,qBCPA,MAAA,QACA,aAAA,QlBrDA,2BkBwDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,2BAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAGF,8BAAA,8BAEE,MAAA,QACA,iBAAA,YAGF,0DAAA,0DrBwxFF,2CqBrxFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,gEAAA,gErBwxFJ,iDqBnxFQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBDzBN,kBCPA,MAAA,QACA,aAAA,QlBrDA,wBkBwDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,wBAAA,wBAEE,WAAA,EAAA,EAAA,EAAA,MAAA,oBAGF,2BAAA,2BAEE,MAAA,QACA,iBAAA,YAGF,uDAAA,uDrBwzFF,wCqBrzFI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,6DAAA,6DrBwzFJ,8CqBnzFQ,WAAA,EAAA,EAAA,EAAA,MAAA,oBDzBN,qBCPA,MAAA,QACA,aAAA,QlBrDA,2BkBwDE,MAAA,QACA,iBAAA,QACA,aAAA,QAGF,2BAAA,2BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAGF,8BAAA,8BAEE,MAAA,QACA,iBAAA,YAGF,0DAAA,0DrBw1FF,2CqBr1FI,MAAA,QACA,iBAAA,QACA,aAAA,QAEA,gEAAA,gErBw1FJ,iDqBn1FQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBDzBN,oBCPA,MAAA,QACA,aAAA,QlBrDA,0BkBwDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,0BAAA,0BAEE,WAAA,EAAA,EAAA,EAAA,MAAA,mBAGF,6BAAA,6BAEE,MAAA,QACA,iBAAA,YAGF,yDAAA,yDrBw3FF,0CqBr3FI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,+DAAA,+DrBw3FJ,gDqBn3FQ,WAAA,EAAA,EAAA,EAAA,MAAA,mBDzBN,mBCPA,MAAA,QACA,aAAA,QlBrDA,yBkBwDE,MAAA,QACA,iBAAA,QACA,aAAA,QAGF,yBAAA,yBAEE,WAAA,EAAA,EAAA,EAAA,MAAA,qBAGF,4BAAA,4BAEE,MAAA,QACA,iBAAA,YAGF,wDAAA,wDrBw5FF,yCqBr5FI,MAAA,QACA,iBAAA,QACA,aAAA,QAEA,8DAAA,8DrBw5FJ,+CqBn5FQ,WAAA,EAAA,EAAA,EAAA,MAAA,qBDzBN,kBCPA,MAAA,QACA,aAAA,QlBrDA,wBkBwDE,MAAA,KACA,iBAAA,QACA,aAAA,QAGF,wBAAA,wBAEE,WAAA,EAAA,EAAA,EAAA,MAAA,kBAGF,2BAAA,2BAEE,MAAA,QACA,iBAAA,YAGF,uDAAA,uDrBw7FF,wCqBr7FI,MAAA,KACA,iBAAA,QACA,aAAA,QAEA,6DAAA,6DrBw7FJ,8CqBn7FQ,WAAA,EAAA,EAAA,EAAA,MAAA,kBDdR,UACE,YAAA,IACA,MAAA,QACA,gBAAA,KjBzEA,gBiB4EE,MAAA,QACA,gBAAA,UAPJ,gBAAA,gBAYI,gBAAA,UAZJ,mBAAA,mBAiBI,MAAA,QACA,eAAA,KAWJ,mBAAA,QCPE,QAAA,MAAA,KpBuBI,UAAA,QoBrBJ,YAAA,IbxFE,cAAA,MYiGJ,mBAAA,QCXE,QAAA,OAAA,MpBuBI,UAAA,QoBrBJ,YAAA,IbxFE,cAAA,MY0GJ,WACE,QAAA,MACA,MAAA,KAFF,sBAMI,WAAA,MpBk8FJ,6BADA,4BoB57FA,6BAII,MAAA,KE3IJ,MLgBM,WAAA,QAAA,KAAA,OAIA,uCKpBN,MLqBQ,WAAA,MKrBR,iBAII,QAAA,EAIJ,qBAEI,QAAA,KAIJ,YACE,SAAA,SACA,OAAA,EACA,SAAA,OLDI,WAAA,OAAA,KAAA,KAIA,uCKNN,YLOQ,WAAA,MjBolGR,UACA,UAFA,WuBvmGA,QAIE,SAAA,SAGF,iBACE,YAAA,OCoBE,wBACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAhCJ,WAAA,KAAA,MACA,aAAA,KAAA,MAAA,YACA,cAAA,EACA,YAAA,KAAA,MAAA,YAqDE,8BACE,YAAA,ED1CN,eACE,SAAA,SACA,IAAA,KACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,MAAA,KACA,UAAA,MACA,QAAA,MAAA,EACA,OAAA,QAAA,EAAA,EtBsGI,UAAA,KsBpGJ,MAAA,QACA,WAAA,KACA,WAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,gBfdE,cAAA,OeuBA,oBACE,MAAA,KACA,KAAA,EAGF,qBACE,MAAA,EACA,KAAA,KXYF,yBWnBA,uBACE,MAAA,KACA,KAAA,EAGF,wBACE,MAAA,EACA,KAAA,MXYF,yBWnBA,uBACE,MAAA,KACA,KAAA,EAGF,wBACE,MAAA,EACA,KAAA,MXYF,yBWnBA,uBACE,MAAA,KACA,KAAA,EAGF,wBACE,MAAA,EACA,KAAA,MXYF,0BWnBA,uBACE,MAAA,KACA,KAAA,EAGF,wBACE,MAAA,EACA,KAAA,MAON,uBAEI,IAAA,KACA,OAAA,KACA,WAAA,EACA,cAAA,QC/BA,gCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAzBJ,WAAA,EACA,aAAA,KAAA,MAAA,YACA,cAAA,KAAA,MACA,YAAA,KAAA,MAAA,YA8CE,sCACE,YAAA,EDUN,0BAEI,IAAA,EACA,MAAA,KACA,KAAA,KACA,WAAA,EACA,YAAA,QC7CA,mCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAlBJ,WAAA,KAAA,MAAA,YACA,aAAA,EACA,cAAA,KAAA,MAAA,YACA,YAAA,KAAA,MAuCE,yCACE,YAAA,EA7BF,mCDmDE,eAAA,EAKN,yBAEI,IAAA,EACA,MAAA,KACA,KAAA,KACA,WAAA,EACA,aAAA,QC9DA,kCACE,QAAA,aACA,YAAA,OACA,eAAA,OACA,QAAA,GAJF,kCAgBI,QAAA,KAGF,mCACE,QAAA,aACA,aAAA,OACA,eAAA,OACA,QAAA,GA9BN,WAAA,KAAA,MAAA,YACA,aAAA,KAAA,MACA,cAAA,KAAA,MAAA,YAiCE,wCACE,YAAA,EAVA,mCDiDA,eAAA,EAON,oCAAA,kCAAA,mCAAA,iCAKI,MAAA,KACA,OAAA,KAKJ,kBE9GE,OAAA,EACA,OAAA,MAAA,EACA,SAAA,OACA,WAAA,IAAA,MAAA,QFkHF,eACE,QAAA,MACA,MAAA,KACA,QAAA,OAAA,OACA,MAAA,KACA,YAAA,IACA,MAAA,QACA,WAAA,QAEA,YAAA,OACA,iBAAA,YACA,OAAA,EpBrHA,qBAAA,qBoBoIE,MAAA,QACA,gBAAA,KJ/IA,iBAAA,QIoHJ,sBAAA,sBAiCI,MAAA,KACA,gBAAA,KJtJA,iBAAA,QIoHJ,wBAAA,wBAwCI,MAAA,QACA,eAAA,KACA,iBAAA,YAQJ,oBACE,QAAA,MAIF,iBACE,QAAA,MACA,QAAA,MAAA,OACA,cAAA,EtBrDI,UAAA,QsBuDJ,MAAA,QACA,YAAA,OAIF,oBACE,QAAA,MACA,QAAA,OAAA,OACA,MAAA,QG3LF,W1B61GA,oB0B31GE,SAAA,SACA,QAAA,mBAAA,QAAA,YACA,eAAA,O1Bi2GF,yB0Br2GA,gBAOI,SAAA,SACA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,K1Bo2GJ,+BGn2GE,sBuBII,QAAA,E1Bs2GN,gCADA,gCADA,+B0Bj3GA,uBAAA,uBAAA,sBAkBM,QAAA,EAMN,aACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,cAAA,MAAA,gBAAA,WAHF,0BAMI,MAAA,K1Bu2GJ,wC0Bn2GA,kCAII,YAAA,K1Bo2GJ,4C0Bx2GA,uDlBHI,wBAAA,EACA,2BAAA,ERg3GJ,6C0B92GA,kClBWI,uBAAA,EACA,0BAAA,EkBmBJ,uBACE,cAAA,SACA,aAAA,SAFF,8B1B21GA,yCADA,sC0Bn1GI,YAAA,EAGF,yCACE,aAAA,EAIJ,0CAAA,+BACE,cAAA,QACA,aAAA,QAGF,0CAAA,+BACE,cAAA,OACA,aAAA,OAoBF,oBACE,mBAAA,OAAA,eAAA,OACA,eAAA,MAAA,YAAA,WACA,cAAA,OAAA,gBAAA,OAHF,yB1B60GA,+B0Bt0GI,MAAA,K1B20GJ,iD0Bl1GA,2CAYI,WAAA,K1B20GJ,qD0Bv1GA,gElBrEI,2BAAA,EACA,0BAAA,ERi6GJ,sD0B71GA,2ClBnFI,uBAAA,EACA,wBAAA,EkB0HJ,uB1B2zGA,kC0BxzGI,cAAA,E1B6zGJ,4C0Bh0GA,yC1Bk0GA,uDADA,oD0B1zGM,SAAA,SACA,KAAA,cACA,eAAA,KCzJN,aACE,SAAA,SACA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,eAAA,QAAA,YAAA,QACA,MAAA,K3Bi+GF,0BADA,4B2Br+GA,2B3Bo+GA,qC2Bz9GI,SAAA,SACA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,GACA,UAAA,EACA,cAAA,E3B2+GJ,uCADA,yCADA,wCADA,yCADA,2CADA,0CAJA,wCADA,0C2Bh/GA,yC3Bo/GA,kDADA,oDADA,mD2B99GM,YAAA,K3B4+GN,sEADA,kC2B//GA,iCA4BI,QAAA,EA5BJ,mDAiCI,QAAA,E3Bw+GJ,6C2BzgHA,4CnB4BI,wBAAA,EACA,2BAAA,ERk/GJ,8C2B/gHA,6CnB0CI,uBAAA,EACA,0BAAA,EmB3CJ,0BA6CI,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OA9CJ,8D3B4hHA,qEQhgHI,wBAAA,EACA,2BAAA,EmB7BJ,+DnB0CI,uBAAA,EACA,0BAAA,ER4/GJ,oB2B1+GA,qBAEE,QAAA,YAAA,QAAA,K3B8+GF,yB2Bh/GA,0BAQI,SAAA,SACA,QAAA,E3B6+GJ,+B2Bt/GA,gCAYM,QAAA,E3Bk/GN,8BACA,2CAEA,2CADA,wD2BhgHA,+B3B2/GA,4CAEA,4CADA,yD2Bx+GI,YAAA,KAIJ,qBAAuB,aAAA,KACvB,oBAAsB,YAAA,KAQtB,kBACE,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,QAAA,QAAA,OACA,cAAA,E1BuBI,UAAA,K0BrBJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,OACA,YAAA,OACA,iBAAA,QACA,OAAA,IAAA,MAAA,QnB9FE,cAAA,ORilHJ,uC2B//GA,oCAkBI,WAAA,E3Bk/GJ,+B2Bx+GA,4CAEE,OAAA,yB3B2+GF,+B2Bx+GA,8B3B4+GA,yCAFA,sDACA,0CAFA,uD2Bn+GE,QAAA,MAAA,K1BZI,UAAA,Q0BcJ,YAAA,InB3HE,cAAA,MRumHJ,+B2Bx+GA,4CAEE,OAAA,0B3B2+GF,+B2Bx+GA,8B3B4+GA,yCAFA,sDACA,0CAFA,uD2Bn+GE,QAAA,OAAA,M1B7BI,UAAA,Q0B+BJ,YAAA,InB5IE,cAAA,MmBgJJ,+B3Bw+GA,+B2Bt+GE,cAAA,Q3B8+GF,wFACA,+EAHA,uDACA,oE2Bl+GA,uC3Bg+GA,oDQ7mHI,wBAAA,EACA,2BAAA,EmBqJJ,sC3Bi+GA,mDAGA,qEACA,kFAHA,yDACA,sEQ3mHI,uBAAA,EACA,0BAAA,EoBxCJ,gBACE,SAAA,SACA,QAAA,EACA,QAAA,MACA,WAAA,OACA,aAAA,OAGF,uBACE,QAAA,mBAAA,QAAA,YACA,aAAA,KAGF,sBACE,SAAA,SACA,KAAA,EACA,QAAA,GACA,MAAA,KACA,OAAA,QACA,QAAA,EANF,4DASI,MAAA,KACA,aAAA,QT1BA,iBAAA,QSgBJ,0DAoBM,WAAA,EAAA,EAAA,EAAA,MAAA,oBApBN,wEAyBI,aAAA,QAzBJ,0EA6BI,MAAA,KACA,iBAAA,QACA,aAAA,QA/BJ,qDAAA,sDAuCM,MAAA,QAvCN,6DAAA,8DA0CQ,iBAAA,QAUR,sBACE,SAAA,SACA,cAAA,EAEA,eAAA,IAJF,8BASI,SAAA,SACA,IAAA,OACA,KAAA,QACA,QAAA,MACA,MAAA,KACA,OAAA,KACA,eAAA,KACA,QAAA,GACA,iBAAA,KACA,OAAA,QAAA,MAAA,IAlBJ,6BAwBI,SAAA,SACA,IAAA,OACA,KAAA,QACA,QAAA,MACA,MAAA,KACA,OAAA,KACA,QAAA,GACA,WAAA,UAAA,GAAA,CAAA,IAAA,IASJ,+CpBhGI,cAAA,OoBgGJ,4EAOM,iBAAA,iNAPN,mFAaM,aAAA,QTzHF,iBAAA,QS4GJ,kFAkBM,iBAAA,8JAlBN,sFT5GI,iBAAA,mBS4GJ,4FT5GI,iBAAA,mBSgJJ,4CAGI,cAAA,IAHJ,yEAQM,iBAAA,6JARN,mFThJI,iBAAA,mBSwKJ,eACE,aAAA,QADF,6CAKM,KAAA,SACA,MAAA,QACA,eAAA,IAEA,cAAA,MATN,4CAaM,IAAA,mBACA,KAAA,qBACA,MAAA,iBACA,OAAA,iBACA,iBAAA,QAEA,cAAA,MXjLA,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,WAAA,CAAA,kBAAA,KAAA,YAAA,WAAA,UAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAAA,WAAA,UAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,WAAA,CAAA,kBAAA,KAAA,YAIA,uCW0JN,4CXzJQ,WAAA,MWyJR,0EA0BM,iBAAA,KACA,kBAAA,mBAAA,UAAA,mBA3BN,oFTxKI,iBAAA,mBSqNJ,eACE,QAAA,aACA,MAAA,KACA,OAAA,2BACA,QAAA,QAAA,QAAA,QAAA,O3BhGI,UAAA,K2BmGJ,YAAA,IACA,YAAA,IACA,MAAA,QACA,eAAA,OACA,WAAA,KAAA,+KAAA,UAAA,MAAA,OAAA,MAAA,CAAA,IAAA,KACA,OAAA,IAAA,MAAA,QpBrNE,cAAA,OoBwNF,mBAAA,KAAA,gBAAA,KAAA,WAAA,KAfF,qBAkBI,aAAA,QACA,QAAA,EAKE,WAAA,EAAA,EAAA,EAAA,MAAA,oBAxBN,gCAiCM,MAAA,QACA,iBAAA,KAlCN,yBAAA,qCAwCI,OAAA,KACA,cAAA,OACA,iBAAA,KA1CJ,wBA8CI,MAAA,QACA,iBAAA,QA/CJ,2BAoDI,QAAA,KApDJ,8BAyDI,MAAA,YACA,YAAA,EAAA,EAAA,EAAA,QAIJ,kBACE,OAAA,0BACA,YAAA,OACA,eAAA,OACA,aAAA,M3B9JI,UAAA,Q2BkKN,kBACE,OAAA,yBACA,YAAA,MACA,eAAA,MACA,aAAA,K3BtKI,UAAA,Q2B+KN,aACE,SAAA,SACA,QAAA,aACA,MAAA,KACA,OAAA,2BACA,cAAA,EAGF,mBACE,SAAA,SACA,QAAA,EACA,MAAA,KACA,OAAA,2BACA,OAAA,EACA,QAAA,EANF,4CASI,aAAA,QACA,WAAA,EAAA,EAAA,EAAA,MAAA,oB5BulHJ,+C4BjmHA,gDAgBI,iBAAA,QAhBJ,sDAqBM,QAAA,SArBN,0DA0BI,QAAA,kBAIJ,mBACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,KAAA,EACA,QAAA,EACA,OAAA,2BACA,QAAA,QAAA,OAEA,YAAA,IACA,YAAA,IACA,MAAA,QACA,iBAAA,KACA,OAAA,IAAA,MAAA,QpB/UE,cAAA,OoBkUJ,0BAkBI,SAAA,SACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,QAAA,EACA,QAAA,MACA,OAAA,qBACA,QAAA,QAAA,OACA,YAAA,IACA,MAAA,QACA,QAAA,ST1WA,iBAAA,QS4WA,YAAA,QpBhWA,cAAA,EAAA,OAAA,OAAA,EoB2WJ,cACE,MAAA,KACA,OAAA,OACA,QAAA,EACA,iBAAA,YACA,mBAAA,KAAA,gBAAA,KAAA,WAAA,KALF,oBAQI,QAAA,EARJ,0CAY8B,WAAA,EAAA,EAAA,EAAA,IAAA,IAAA,CAAA,EAAA,EAAA,EAAA,MAAA,oBAZ9B,sCAa8B,WAAA,EAAA,EAAA,EAAA,IAAA,IAAA,CAAA,EAAA,EAAA,EAAA,MAAA,oBAb9B,+BAc8B,WAAA,EAAA,EAAA,EAAA,IAAA,IAAA,CAAA,EAAA,EAAA,EAAA,MAAA,oBAd9B,gCAkBI,OAAA,EAlBJ,oCAsBI,MAAA,KACA,OAAA,KACA,WAAA,QT/YA,iBAAA,QSiZA,OAAA,EpBrYA,cAAA,KSFE,mBAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAAA,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YW2YF,mBAAA,KAAA,WAAA,KXvYE,uCWyWN,oCXxWQ,mBAAA,KAAA,WAAA,MWwWR,2CTvXI,iBAAA,QSuXJ,6CAsCI,MAAA,KACA,OAAA,MACA,MAAA,YACA,OAAA,QACA,iBAAA,QACA,aAAA,YpBtZA,cAAA,KoB2WJ,gCAiDI,MAAA,KACA,OAAA,KTzaA,iBAAA,QS2aA,OAAA,EpB/ZA,cAAA,KSFE,gBAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAAA,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YWqaF,gBAAA,KAAA,WAAA,KXjaE,uCWyWN,gCXxWQ,gBAAA,KAAA,WAAA,MWwWR,uCTvXI,iBAAA,QSuXJ,gCAgEI,MAAA,KACA,OAAA,MACA,MAAA,YACA,OAAA,QACA,iBAAA,QACA,aAAA,YpBhbA,cAAA,KoB2WJ,yBA2EI,MAAA,KACA,OAAA,KACA,WAAA,EACA,aAAA,MACA,YAAA,MTtcA,iBAAA,QSwcA,OAAA,EpB5bA,cAAA,KSFE,eAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAAA,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YWkcF,WAAA,KX9bE,uCWyWN,yBXxWQ,eAAA,KAAA,WAAA,MWwWR,gCTvXI,iBAAA,QSuXJ,yBA6FI,MAAA,KACA,OAAA,MACA,MAAA,YACA,OAAA,QACA,iBAAA,YACA,aAAA,YACA,aAAA,MAnGJ,8BAwGI,iBAAA,QpBndA,cAAA,KoB2WJ,8BA6GI,aAAA,KACA,iBAAA,QpBzdA,cAAA,KoB2WJ,6CAoHM,iBAAA,QApHN,sDAwHM,OAAA,QAxHN,yCA4HM,iBAAA,QA5HN,yCAgIM,OAAA,QAhIN,kCAoIM,iBAAA,QAKN,8B5BkmHA,mBACA,eiBzlIM,WAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCWkfN,8B5BymHE,mBACA,eiB3lIM,WAAA,MYhBR,KACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,aAAA,EACA,cAAA,EACA,WAAA,KAGF,UACE,QAAA,MACA,QAAA,MAAA,K1BCA,gBAAA,gB0BGE,gBAAA,KANJ,mBAWI,MAAA,QACA,eAAA,KACA,OAAA,QAQJ,UACE,cAAA,IAAA,MAAA,QADF,oBAII,cAAA,KAJJ,oBAQI,OAAA,IAAA,MAAA,YrBfA,uBAAA,OACA,wBAAA,OLZF,0BAAA,0B0B8BI,aAAA,QAAA,QAAA,QAZN,6BAgBM,MAAA,QACA,iBAAA,YACA,aAAA,Y7BmnIN,mC6BroIA,2BAwBI,MAAA,QACA,iBAAA,KACA,aAAA,QAAA,QAAA,KA1BJ,yBA+BI,WAAA,KrBtCA,uBAAA,EACA,wBAAA,EqBgDJ,qBrB1DI,cAAA,OqB0DJ,4B7B4mIA,2B6BrmII,MAAA,KACA,iBAAA,Q7B0mIJ,oB6BjmIA,oBAGI,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,WAAA,O7BomIJ,yB6BhmIA,yBAGI,wBAAA,EAAA,WAAA,EACA,kBAAA,EAAA,UAAA,EACA,WAAA,OASJ,uBAEI,QAAA,KAFJ,qBAKI,QAAA,MCvGJ,QACE,SAAA,SACA,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,QAAA,gBAAA,cACA,QAAA,MAAA,KANF,mB9BktIA,yBAAwE,sBAAvB,sBAAvB,sBAAqE,sB8BvsI3F,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,QAAA,gBAAA,cAoBJ,cACE,QAAA,aACA,YAAA,SACA,eAAA,SACA,aAAA,K7BwEI,UAAA,Q6BtEJ,YAAA,QACA,YAAA,O3B1CA,oBAAA,oB2B6CE,gBAAA,KASJ,YACE,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OACA,aAAA,EACA,cAAA,EACA,WAAA,KALF,sBAQI,cAAA,EACA,aAAA,EATJ,2BAaI,SAAA,OACA,MAAA,KASJ,aACE,QAAA,aACA,YAAA,MACA,eAAA,MAYF,iBACE,wBAAA,KAAA,WAAA,KACA,kBAAA,EAAA,UAAA,EAGA,eAAA,OAAA,YAAA,OAIF,gBACE,QAAA,OAAA,O7BSI,UAAA,Q6BPJ,YAAA,EACA,iBAAA,YACA,OAAA,IAAA,MAAA,YtBxGE,cAAA,OLFF,sBAAA,sB2B8GE,gBAAA,KAMJ,qBACE,QAAA,aACA,MAAA,MACA,OAAA,MACA,eAAA,OACA,QAAA,GACA,WAAA,UAAA,OAAA,OACA,gBAAA,KAAA,KlBlEE,4BkB4EC,6B9BmqIH,mCAA4G,gCAAnC,gCAAnC,gCAAyG,gC8BhqIvI,cAAA,EACA,aAAA,GlB7FN,yBkByFA,kBAoBI,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WArBH,8BAwBK,mBAAA,IAAA,eAAA,IAxBL,6CA2BO,SAAA,SA3BP,wCA+BO,cAAA,MACA,aAAA,MAhCP,6B9B4rIH,mCAA4G,gCAAnC,gCAAnC,gCAAyG,gC8BtpIvI,cAAA,OAAA,UAAA,OAtCL,mCAqDK,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KAxDL,kCA4DK,QAAA,MlBxIN,4BkB4EC,6B9B6sIH,mCAA4G,gCAAnC,gCAAnC,gCAAyG,gC8B1sIvI,cAAA,EACA,aAAA,GlB7FN,yBkByFA,kBAoBI,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WArBH,8BAwBK,mBAAA,IAAA,eAAA,IAxBL,6CA2BO,SAAA,SA3BP,wCA+BO,cAAA,MACA,aAAA,MAhCP,6B9BsuIH,mCAA4G,gCAAnC,gCAAnC,gCAAyG,gC8BhsIvI,cAAA,OAAA,UAAA,OAtCL,mCAqDK,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KAxDL,kCA4DK,QAAA,MlBxIN,4BkB4EC,6B9BuvIH,mCAA4G,gCAAnC,gCAAnC,gCAAyG,gC8BpvIvI,cAAA,EACA,aAAA,GlB7FN,yBkByFA,kBAoBI,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WArBH,8BAwBK,mBAAA,IAAA,eAAA,IAxBL,6CA2BO,SAAA,SA3BP,wCA+BO,cAAA,MACA,aAAA,MAhCP,6B9BgxIH,mCAA4G,gCAAnC,gCAAnC,gCAAyG,gC8B1uIvI,cAAA,OAAA,UAAA,OAtCL,mCAqDK,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KAxDL,kCA4DK,QAAA,MlBxIN,6BkB4EC,6B9BiyIH,mCAA4G,gCAAnC,gCAAnC,gCAAyG,gC8B9xIvI,cAAA,EACA,aAAA,GlB7FN,0BkByFA,kBAoBI,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WArBH,8BAwBK,mBAAA,IAAA,eAAA,IAxBL,6CA2BO,SAAA,SA3BP,wCA+BO,cAAA,MACA,aAAA,MAhCP,6B9B0zIH,mCAA4G,gCAAnC,gCAAnC,gCAAyG,gC8BpxIvI,cAAA,OAAA,UAAA,OAtCL,mCAqDK,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KAxDL,kCA4DK,QAAA,MAjEV,eAyBQ,cAAA,IAAA,OAAA,UAAA,IAAA,OACA,cAAA,MAAA,gBAAA,WA1BR,0B9Bs1IA,gCAAmG,6BAAhC,6BAAhC,6BAAgG,6B8B90IzH,cAAA,EACA,aAAA,EATV,2BA6BU,mBAAA,IAAA,eAAA,IA7BV,0CAgCY,SAAA,SAhCZ,qCAoCY,cAAA,MACA,aAAA,MArCZ,0B9B02IA,gCAAmG,6BAAhC,6BAAhC,6BAAgG,6B8B/zIzH,cAAA,OAAA,UAAA,OA3CV,gCA0DU,QAAA,sBAAA,QAAA,eAGA,wBAAA,KAAA,WAAA,KA7DV,+BAiEU,QAAA,KAaV,4BAEI,MAAA,e3BhNF,kCAAA,kC2BmNI,MAAA,eALN,oCAWM,MAAA,e3BzNJ,0CAAA,0C2B4NM,MAAA,eAdR,6CAkBQ,MAAA,e9B+yIR,4CAEA,2CADA,yC8Bl0IA,0CA0BM,MAAA,eA1BN,8BA+BI,MAAA,eACA,aAAA,eAhCJ,mCAoCI,iBAAA,kQApCJ,2BAwCI,MAAA,eAxCJ,6BA0CM,MAAA,e3BxPJ,mCAAA,mC2B2PM,MAAA,eAOR,2BAEI,MAAA,K3BpQF,iCAAA,iC2BuQI,MAAA,KALN,mCAWM,MAAA,qB3B7QJ,yCAAA,yC2BgRM,MAAA,sBAdR,4CAkBQ,MAAA,sB9B2yIR,2CAEA,0CADA,wC8B9zIA,yCA0BM,MAAA,KA1BN,6BA+BI,MAAA,qBACA,aAAA,qBAhCJ,kCAoCI,iBAAA,wQApCJ,0BAwCI,MAAA,qBAxCJ,4BA0CM,MAAA,K3B5SJ,kCAAA,kC2B+SM,MAAA,KC3TR,MACE,SAAA,SACA,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OACA,UAAA,EAEA,UAAA,WACA,iBAAA,KACA,gBAAA,WACA,OAAA,IAAA,MAAA,iBvBKE,cAAA,OuBdJ,SAaI,aAAA,EACA,YAAA,EAdJ,kBAkBI,WAAA,QACA,cAAA,QAnBJ,8BAsBM,iBAAA,EvBCF,uBAAA,mBACA,wBAAA,mBuBxBJ,6BA2BM,oBAAA,EvBUF,2BAAA,mBACA,0BAAA,mBuBtCJ,+B/B2oJA,+B+BvmJI,WAAA,EAIJ,WAGE,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KAGA,WAAA,IACA,QAAA,QAIF,YACE,cAAA,OAGF,eACE,WAAA,SACA,cAAA,EAGF,sBACE,cAAA,E5BrDA,iB4B0DE,gBAAA,KAFJ,sBAMI,YAAA,QAQJ,aACE,QAAA,OAAA,QACA,cAAA,EAEA,iBAAA,gBACA,cAAA,IAAA,MAAA,iBALF,yBvBhEI,cAAA,mBAAA,mBAAA,EAAA,EuB4EJ,aACE,QAAA,OAAA,QAEA,iBAAA,gBACA,WAAA,IAAA,MAAA,iBAJF,wBvB5EI,cAAA,EAAA,EAAA,mBAAA,mBuB4FJ,kBACE,aAAA,SACA,cAAA,QACA,YAAA,SACA,cAAA,EAGF,mBACE,aAAA,SACA,YAAA,SAIF,kBACE,SAAA,SACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,QvB/GE,cAAA,mBuBmHJ,U/BulJA,iBADA,c+BnlJE,kBAAA,EAAA,YAAA,EACA,MAAA,KAGF,U/BulJA,cQxsJI,uBAAA,mBACA,wBAAA,mBuBqHJ,U/BwlJA,iBQhsJI,2BAAA,mBACA,0BAAA,mBuB+GJ,iBAEI,cAAA,KnB/FA,yBmB6FJ,WAMI,QAAA,YAAA,QAAA,KACA,cAAA,IAAA,KAAA,UAAA,IAAA,KACA,aAAA,MACA,YAAA,MATJ,iBAaM,SAAA,EAAA,EAAA,GAAA,KAAA,EAAA,EAAA,GACA,aAAA,KACA,cAAA,EACA,YAAA,MAUN,kBAII,cAAA,KnB3HA,yBmBuHJ,YAQI,QAAA,YAAA,QAAA,KACA,cAAA,IAAA,KAAA,UAAA,IAAA,KATJ,kBAcM,SAAA,EAAA,EAAA,GAAA,KAAA,EAAA,EAAA,GACA,cAAA,EAfN,wBAkBQ,YAAA,EACA,YAAA,EAnBR,mCvBjJI,wBAAA,EACA,2BAAA,ER0vJF,gD+B1mJF,iDA8BY,wBAAA,E/BglJV,gD+B9mJF,oDAmCY,2BAAA,EAnCZ,oCvBnII,uBAAA,EACA,0BAAA,ERwvJF,iD+BtnJF,kDA6CY,uBAAA,E/B6kJV,iD+B1nJF,qDAkDY,0BAAA,GAaZ,oBAEI,cAAA,OnBxLA,yBmBsLJ,cAMI,qBAAA,EAAA,kBAAA,EAAA,aAAA,EACA,mBAAA,QAAA,gBAAA,QAAA,WAAA,QACA,QAAA,EACA,OAAA,EATJ,oBAYM,QAAA,aACA,MAAA,MAUN,WACE,gBAAA,KADF,iBAII,SAAA,OAJJ,oCAOM,cAAA,EvBvOF,2BAAA,EACA,0BAAA,EuB+NJ,qCvB9OI,uBAAA,EACA,wBAAA,EuB6OJ,8BvBvPI,cAAA,EuBwQE,cAAA,KC1RN,YACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,QAAA,OAAA,KACA,cAAA,KAEA,WAAA,KACA,iBAAA,QxBWE,cAAA,OwBPJ,iBACE,QAAA,YAAA,QAAA,KADF,kCAKI,aAAA,MALJ,0CAQM,QAAA,aACA,cAAA,MACA,MAAA,QACA,QAAA,IAXN,gDAsBI,gBAAA,UAtBJ,gDA0BI,gBAAA,KA1BJ,wBA8BI,MAAA,QCzCJ,YACE,QAAA,YAAA,QAAA,K5BGA,aAAA,EACA,WAAA,KGaE,cAAA,OyBZJ,WACE,SAAA,SACA,QAAA,MACA,QAAA,MAAA,OACA,YAAA,KACA,YAAA,KACA,MAAA,QAEA,iBAAA,KACA,OAAA,IAAA,MAAA,QATF,iBAYI,QAAA,EACA,MAAA,QACA,gBAAA,KACA,iBAAA,QACA,aAAA,QAhBJ,iBAoBI,QAAA,EACA,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBAIJ,kCAGM,YAAA,EzBaF,uBAAA,OACA,0BAAA,OyBjBJ,iCzBEI,wBAAA,OACA,2BAAA,OyBHJ,6BAcI,QAAA,EACA,MAAA,KACA,iBAAA,QACA,aAAA,QAjBJ,+BAqBI,MAAA,QACA,eAAA,KAEA,OAAA,KACA,iBAAA,KACA,aAAA,QCvDF,0BACE,QAAA,OAAA,OjC2HE,UAAA,QiCzHF,YAAA,IAKE,iD1BqCF,uBAAA,MACA,0BAAA,M0BjCE,gD1BkBF,wBAAA,MACA,2BAAA,M0BhCF,0BACE,QAAA,OAAA,MjC2HE,UAAA,QiCzHF,YAAA,IAKE,iD1BqCF,uBAAA,MACA,0BAAA,M0BjCE,gD1BkBF,wBAAA,MACA,2BAAA,M2B9BJ,OACE,QAAA,aACA,QAAA,MAAA,KlCiEE,UAAA,IkC/DF,YAAA,IACA,YAAA,EACA,WAAA,OACA,YAAA,OACA,eAAA,S3BKE,cAAA,OSFE,WAAA,MAAA,KAAA,WAAA,CAAA,iBAAA,KAAA,WAAA,CAAA,aAAA,KAAA,WAAA,CAAA,WAAA,KAAA,YAIA,uCkBfN,OlBgBQ,WAAA,MdLN,cAAA,cgCGI,gBAAA,KAdN,aAoBI,QAAA,KAKJ,YACE,SAAA,SACA,IAAA,KAOF,YACE,cAAA,KACA,aAAA,K3BvBE,cAAA,M2BgCF,eCjDA,MAAA,KACA,iBAAA,QjCcA,sBAAA,sBiCVI,MAAA,KACA,iBAAA,QAHI,sBAAA,sBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,mBDqCJ,iBCjDA,MAAA,KACA,iBAAA,QjCcA,wBAAA,wBiCVI,MAAA,KACA,iBAAA,QAHI,wBAAA,wBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,qBDqCJ,eCjDA,MAAA,KACA,iBAAA,QjCcA,sBAAA,sBiCVI,MAAA,KACA,iBAAA,QAHI,sBAAA,sBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,mBDqCJ,YCjDA,MAAA,KACA,iBAAA,QjCcA,mBAAA,mBiCVI,MAAA,KACA,iBAAA,QAHI,mBAAA,mBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,oBDqCJ,eCjDA,MAAA,QACA,iBAAA,QjCcA,sBAAA,sBiCVI,MAAA,QACA,iBAAA,QAHI,sBAAA,sBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,mBDqCJ,cCjDA,MAAA,KACA,iBAAA,QjCcA,qBAAA,qBiCVI,MAAA,KACA,iBAAA,QAHI,qBAAA,qBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,mBDqCJ,aCjDA,MAAA,QACA,iBAAA,QjCcA,oBAAA,oBiCVI,MAAA,QACA,iBAAA,QAHI,oBAAA,oBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,qBDqCJ,YCjDA,MAAA,KACA,iBAAA,QjCcA,mBAAA,mBiCVI,MAAA,KACA,iBAAA,QAHI,mBAAA,mBAQJ,QAAA,EACA,WAAA,EAAA,EAAA,EAAA,MAAA,kBCbN,WACE,QAAA,KAAA,KACA,cAAA,KAEA,iBAAA,Q7BcE,cAAA,MI0CA,yByB5DJ,WAQI,QAAA,KAAA,MAIJ,iBACE,cAAA,EACA,aAAA,E7BIE,cAAA,E8BdJ,OACE,SAAA,SACA,QAAA,OAAA,QACA,cAAA,KACA,OAAA,IAAA,MAAA,Y9BUE,cAAA,O8BLJ,eAEE,MAAA,QAIF,YACE,YAAA,IAQF,mBACE,cAAA,KADF,0BAKI,SAAA,SACA,IAAA,EACA,MAAA,EACA,QAAA,OAAA,QACA,MAAA,QAUF,eC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,kBACE,iBAAA,QAGF,2BACE,MAAA,QDqCF,iBC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,oBACE,iBAAA,QAGF,6BACE,MAAA,QDqCF,eC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,kBACE,iBAAA,QAGF,2BACE,MAAA,QDqCF,YC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,eACE,iBAAA,QAGF,wBACE,MAAA,QDqCF,eC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,kBACE,iBAAA,QAGF,2BACE,MAAA,QDqCF,cC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,iBACE,iBAAA,QAGF,0BACE,MAAA,QDqCF,aC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,gBACE,iBAAA,QAGF,yBACE,MAAA,QDqCF,YC9CA,MAAA,QpBKE,iBAAA,QoBHF,aAAA,QAEA,eACE,iBAAA,QAGF,wBACE,MAAA,QCRF,wCACE,KAAO,oBAAA,KAAA,EACP,GAAK,oBAAA,EAAA,GAFP,gCACE,KAAO,oBAAA,KAAA,EACP,GAAK,oBAAA,EAAA,GAIT,UACE,QAAA,YAAA,QAAA,KACA,OAAA,KACA,SAAA,OACA,YAAA,EvCmHI,UAAA,OuCjHJ,iBAAA,QhCIE,cAAA,OgCCJ,cACE,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OACA,cAAA,OAAA,gBAAA,OACA,SAAA,OACA,MAAA,KACA,WAAA,OACA,YAAA,OACA,iBAAA,QvBXI,WAAA,MAAA,IAAA,KAIA,uCuBDN,cvBEQ,WAAA,MuBUR,sBrBYE,iBAAA,iKqBVA,gBAAA,KAAA,KAIA,uBACE,kBAAA,qBAAA,GAAA,OAAA,SAAA,UAAA,qBAAA,GAAA,OAAA,SAGE,uCAJJ,uBAKM,kBAAA,KAAA,UAAA,MC1CR,OACE,QAAA,YAAA,QAAA,KACA,eAAA,MAAA,YAAA,WAGF,YACE,SAAA,EAAA,KAAA,ECFF,YACE,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OAGA,aAAA,EACA,cAAA,ElCQE,cAAA,OkCEJ,wBACE,MAAA,KACA,MAAA,QACA,WAAA,QvCPA,8BAAA,8BuCWE,QAAA,EACA,MAAA,QACA,gBAAA,KACA,iBAAA,QAVJ,+BAcI,MAAA,QACA,iBAAA,QASJ,iBACE,SAAA,SACA,QAAA,MACA,QAAA,OAAA,QAGA,iBAAA,KACA,OAAA,IAAA,MAAA,iBAPF,6BlCjBI,uBAAA,QACA,wBAAA,QkCgBJ,4BlCHI,2BAAA,QACA,0BAAA,QkCEJ,0BAAA,0BAmBI,MAAA,QACA,eAAA,KACA,iBAAA,KArBJ,wBA0BI,QAAA,EACA,MAAA,KACA,iBAAA,QACA,aAAA,QA7BJ,kCAiCI,iBAAA,EAjCJ,yCAoCM,WAAA,KACA,iBAAA,IAcF,uBACE,mBAAA,IAAA,eAAA,IADF,oDlCtBA,0BAAA,OAZA,wBAAA,EkCkCA,mDlClCA,wBAAA,OAYA,0BAAA,EkCsBA,+CAeM,WAAA,EAfN,yDAmBM,iBAAA,IACA,kBAAA,EApBN,gEAuBQ,YAAA,KACA,kBAAA,I9B3DR,yB8BmCA,0BACE,mBAAA,IAAA,eAAA,IADF,uDlCtBA,0BAAA,OAZA,wBAAA,EkCkCA,sDlClCA,wBAAA,OAYA,0BAAA,EkCsBA,kDAeM,WAAA,EAfN,4DAmBM,iBAAA,IACA,kBAAA,EApBN,mEAuBQ,YAAA,KACA,kBAAA,K9B3DR,yB8BmCA,0BACE,mBAAA,IAAA,eAAA,IADF,uDlCtBA,0BAAA,OAZA,wBAAA,EkCkCA,sDlClCA,wBAAA,OAYA,0BAAA,EkCsBA,kDAeM,WAAA,EAfN,4DAmBM,iBAAA,IACA,kBAAA,EApBN,mEAuBQ,YAAA,KACA,kBAAA,K9B3DR,yB8BmCA,0BACE,mBAAA,IAAA,eAAA,IADF,uDlCtBA,0BAAA,OAZA,wBAAA,EkCkCA,sDlClCA,wBAAA,OAYA,0BAAA,EkCsBA,kDAeM,WAAA,EAfN,4DAmBM,iBAAA,IACA,kBAAA,EApBN,mEAuBQ,YAAA,KACA,kBAAA,K9B3DR,0B8BmCA,0BACE,mBAAA,IAAA,eAAA,IADF,uDlCtBA,0BAAA,OAZA,wBAAA,EkCkCA,sDlClCA,wBAAA,OAYA,0BAAA,EkCsBA,kDAeM,WAAA,EAfN,4DAmBM,iBAAA,IACA,kBAAA,EApBN,mEAuBQ,YAAA,KACA,kBAAA,KAcZ,kBlCnHI,cAAA,EkCmHJ,mCAII,aAAA,EAAA,EAAA,IAJJ,8CAOM,oBAAA,ECzIJ,yBACE,MAAA,QACA,iBAAA,QxCWF,sDAAA,sDwCPM,MAAA,QACA,iBAAA,QAPN,uDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,2BACE,MAAA,QACA,iBAAA,QxCWF,wDAAA,wDwCPM,MAAA,QACA,iBAAA,QAPN,yDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,yBACE,MAAA,QACA,iBAAA,QxCWF,sDAAA,sDwCPM,MAAA,QACA,iBAAA,QAPN,uDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,sBACE,MAAA,QACA,iBAAA,QxCWF,mDAAA,mDwCPM,MAAA,QACA,iBAAA,QAPN,oDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,yBACE,MAAA,QACA,iBAAA,QxCWF,sDAAA,sDwCPM,MAAA,QACA,iBAAA,QAPN,uDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,wBACE,MAAA,QACA,iBAAA,QxCWF,qDAAA,qDwCPM,MAAA,QACA,iBAAA,QAPN,sDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,uBACE,MAAA,QACA,iBAAA,QxCWF,oDAAA,oDwCPM,MAAA,QACA,iBAAA,QAPN,qDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QAbN,sBACE,MAAA,QACA,iBAAA,QxCWF,mDAAA,mDwCPM,MAAA,QACA,iBAAA,QAPN,oDAWM,MAAA,KACA,iBAAA,QACA,aAAA,QChBR,OACE,MAAA,M3C8HI,UAAA,O2C5HJ,YAAA,IACA,YAAA,EACA,MAAA,KACA,YAAA,EAAA,IAAA,EAAA,KACA,QAAA,GzCKA,ayCDE,MAAA,KACA,gBAAA,KzCIF,2CAAA,2CyCCI,QAAA,IAWN,aACE,QAAA,EACA,iBAAA,YACA,OAAA,EAMF,iBACE,eAAA,KCtCF,OAGE,wBAAA,MAAA,WAAA,MACA,UAAA,M5C2HI,UAAA,Q4CxHJ,iBAAA,sBACA,gBAAA,YACA,OAAA,IAAA,MAAA,eACA,WAAA,EAAA,OAAA,OAAA,eACA,QAAA,ErCOE,cAAA,OqClBJ,wBAeI,cAAA,OAfJ,eAmBI,QAAA,EAnBJ,YAuBI,QAAA,MACA,QAAA,EAxBJ,YA4BI,QAAA,KAIJ,cACE,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,QAAA,OAAA,OACA,MAAA,QACA,iBAAA,sBACA,gBAAA,YACA,cAAA,IAAA,MAAA,gBrCZE,uBAAA,mBACA,wBAAA,mBqCeJ,YACE,QAAA,OCtCF,YAEE,SAAA,OAFF,mBAKI,WAAA,OACA,WAAA,KAKJ,OACE,SAAA,MACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,KACA,MAAA,KACA,OAAA,KACA,SAAA,OAGA,QAAA,EAOF,cACE,SAAA,SACA,MAAA,KACA,OAAA,MAEA,eAAA,KAGA,0B7B3BI,WAAA,kBAAA,IAAA,SAAA,WAAA,UAAA,IAAA,SAAA,WAAA,UAAA,IAAA,QAAA,CAAA,kBAAA,IAAA,S6B6BF,kBAAA,mBAAA,UAAA,mB7BzBE,uC6BuBJ,0B7BtBM,WAAA,M6B0BN,0BACE,kBAAA,KAAA,UAAA,KAIF,kCACE,kBAAA,YAAA,UAAA,YAIJ,yBACE,QAAA,YAAA,QAAA,KACA,WAAA,kBAFF,wCAKI,WAAA,mBACA,SAAA,O9CixLJ,uC8CvxLA,uCAWI,kBAAA,EAAA,YAAA,EAXJ,qCAeI,WAAA,KAIJ,uBACE,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,WAAA,kBAHF,+BAOI,QAAA,MACA,OAAA,mBACA,OAAA,oBAAA,OAAA,iBAAA,OAAA,YACA,QAAA,GAVJ,+CAeI,mBAAA,OAAA,eAAA,OACA,cAAA,OAAA,gBAAA,OACA,OAAA,KAjBJ,8DAoBM,WAAA,KApBN,uDAwBM,QAAA,KAMN,eACE,SAAA,SACA,QAAA,YAAA,QAAA,KACA,mBAAA,OAAA,eAAA,OACA,MAAA,KAGA,eAAA,KACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,etClGE,cAAA,MsCsGF,QAAA,EAIF,gBACE,SAAA,MACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,MAAA,MACA,OAAA,MACA,iBAAA,KAPF,qBAUW,QAAA,EAVX,qBAWW,QAAA,GAKX,cACE,QAAA,YAAA,QAAA,KACA,eAAA,MAAA,YAAA,WACA,cAAA,QAAA,gBAAA,cACA,QAAA,KAAA,KACA,cAAA,IAAA,MAAA,QtCtHE,uBAAA,kBACA,wBAAA,kBsCgHJ,qBASI,QAAA,KAAA,KAEA,OAAA,MAAA,MAAA,MAAA,KAKJ,aACE,cAAA,EACA,YAAA,IAKF,YACE,SAAA,SAGA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,QAAA,KAIF,cACE,QAAA,YAAA,QAAA,KACA,cAAA,KAAA,UAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,IAAA,gBAAA,SACA,QAAA,OACA,WAAA,IAAA,MAAA,QtCzIE,2BAAA,kBACA,0BAAA,kBsCkIJ,gBAaI,OAAA,OAKJ,yBACE,SAAA,SACA,IAAA,QACA,MAAA,KACA,OAAA,KACA,SAAA,OlCvIE,yBkCzBJ,cAuKI,UAAA,MACA,OAAA,QAAA,KAlJJ,yBAsJI,WAAA,oBAtJJ,wCAyJM,WAAA,qBAtIN,uBA2II,WAAA,oBA3IJ,+BA8IM,OAAA,qBACA,OAAA,oBAAA,OAAA,iBAAA,OAAA,YAQJ,UAAY,UAAA,OlCvKV,yBkC2KF,U9CwwLA,U8CtwLE,UAAA,OlC7KA,0BkCkLF,UAAY,UAAA,QC7Od,SACE,SAAA,SACA,QAAA,KACA,QAAA,MACA,OAAA,ECJA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBAEA,WAAA,OACA,YAAA,IACA,YAAA,IACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,YAAA,OACA,WAAA,K/CgHI,UAAA,Q8CpHJ,UAAA,WACA,QAAA,EAXF,cAaW,QAAA,GAbX,gBAgBI,SAAA,SACA,QAAA,MACA,MAAA,MACA,OAAA,MAnBJ,wBAsBM,SAAA,SACA,QAAA,GACA,aAAA,YACA,aAAA,MAKN,mCAAA,gBACE,QAAA,MAAA,EADF,0CAAA,uBAII,OAAA,EAJJ,kDAAA,+BAOM,IAAA,EACA,aAAA,MAAA,MAAA,EACA,iBAAA,KAKN,qCAAA,kBACE,QAAA,EAAA,MADF,4CAAA,yBAII,KAAA,EACA,MAAA,MACA,OAAA,MANJ,oDAAA,iCASM,MAAA,EACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,KAKN,sCAAA,mBACE,QAAA,MAAA,EADF,6CAAA,0BAII,IAAA,EAJJ,qDAAA,kCAOM,OAAA,EACA,aAAA,EAAA,MAAA,MACA,oBAAA,KAKN,oCAAA,iBACE,QAAA,EAAA,MADF,2CAAA,wBAII,MAAA,EACA,MAAA,MACA,OAAA,MANJ,mDAAA,gCASM,KAAA,EACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,KAqBN,eACE,UAAA,MACA,QAAA,OAAA,MACA,MAAA,KACA,WAAA,OACA,iBAAA,KvC9FE,cAAA,OyClBJ,SACE,SAAA,SACA,IAAA,EACA,KAAA,EACA,QAAA,KACA,QAAA,MACA,UAAA,MDLA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,WAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,iBAAA,CAAA,mBAEA,WAAA,OACA,YAAA,IACA,YAAA,IACA,WAAA,KACA,WAAA,MACA,gBAAA,KACA,YAAA,KACA,eAAA,KACA,eAAA,OACA,WAAA,OACA,aAAA,OACA,YAAA,OACA,WAAA,K/CgHI,UAAA,QgDnHJ,UAAA,WACA,iBAAA,KACA,gBAAA,YACA,OAAA,IAAA,MAAA,ezCGE,cAAA,MyClBJ,gBAoBI,SAAA,SACA,QAAA,MACA,MAAA,KACA,OAAA,MACA,OAAA,EAAA,MAxBJ,uBAAA,wBA4BM,SAAA,SACA,QAAA,MACA,QAAA,GACA,aAAA,YACA,aAAA,MAKN,mCAAA,gBACE,cAAA,MADF,0CAAA,uBAII,OAAA,mBAJJ,kDAAA,+BAOM,OAAA,EACA,aAAA,MAAA,MAAA,EACA,iBAAA,gBATN,iDAAA,8BAaM,OAAA,IACA,aAAA,MAAA,MAAA,EACA,iBAAA,KAKN,qCAAA,kBACE,YAAA,MADF,4CAAA,yBAII,KAAA,mBACA,MAAA,MACA,OAAA,KACA,OAAA,MAAA,EAPJ,oDAAA,iCAUM,KAAA,EACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,gBAZN,mDAAA,gCAgBM,KAAA,IACA,aAAA,MAAA,MAAA,MAAA,EACA,mBAAA,KAKN,sCAAA,mBACE,WAAA,MADF,6CAAA,0BAII,IAAA,mBAJJ,qDAAA,kCAOM,IAAA,EACA,aAAA,EAAA,MAAA,MAAA,MACA,oBAAA,gBATN,oDAAA,iCAaM,IAAA,IACA,aAAA,EAAA,MAAA,MAAA,MACA,oBAAA,KAfN,8DAAA,2CAqBI,SAAA,SACA,IAAA,EACA,KAAA,IACA,QAAA,MACA,MAAA,KACA,YAAA,OACA,QAAA,GACA,cAAA,IAAA,MAAA,QAIJ,oCAAA,iBACE,aAAA,MADF,2CAAA,wBAII,MAAA,mBACA,MAAA,MACA,OAAA,KACA,OAAA,MAAA,EAPJ,mDAAA,gCAUM,MAAA,EACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,gBAZN,kDAAA,+BAgBM,MAAA,IACA,aAAA,MAAA,EAAA,MAAA,MACA,kBAAA,KAsBN,gBACE,QAAA,MAAA,OACA,cAAA,EhD3BI,UAAA,KgD8BJ,iBAAA,QACA,cAAA,IAAA,MAAA,QzCnIE,uBAAA,kBACA,wBAAA,kByC4HJ,sBAUI,QAAA,KAIJ,cACE,QAAA,MAAA,OACA,MAAA,QC3JF,UACE,SAAA,SAGF,wBACE,iBAAA,MAAA,aAAA,MAGF,gBACE,SAAA,SACA,MAAA,KACA,SAAA,OCvBA,uBACE,QAAA,MACA,MAAA,KACA,QAAA,GDwBJ,eACE,SAAA,SACA,QAAA,KACA,MAAA,KACA,MAAA,KACA,aAAA,MACA,4BAAA,OAAA,oBAAA,OjClBI,WAAA,kBAAA,IAAA,YAAA,WAAA,UAAA,IAAA,YAAA,WAAA,UAAA,IAAA,WAAA,CAAA,kBAAA,IAAA,YAIA,uCiCQN,ejCPQ,WAAA,MjB8xMR,oBACA,oBkD9wMA,sBAGE,QAAA,MlDgxMF,4BkD7wMA,6CAEE,kBAAA,iBAAA,UAAA,iBlDixMF,2BkD9wMA,8CAEE,kBAAA,kBAAA,UAAA,kBAQF,8BAEI,QAAA,EACA,oBAAA,QACA,kBAAA,KAAA,UAAA,KlD6wMJ,sDACA,uDkDlxMA,qCAUI,QAAA,EACA,QAAA,EAXJ,0ClDwxMA,2CkDxwMI,QAAA,EACA,QAAA,EjC5DE,WAAA,QAAA,GAAA,IAIA,uCiCuCN,0ClDgyME,2CiBt0MM,WAAA,MjB40MR,uBkD3wMA,uBAEE,SAAA,SACA,IAAA,EACA,OAAA,EACA,QAAA,EAEA,QAAA,YAAA,QAAA,KACA,eAAA,OAAA,YAAA,OACA,cAAA,OAAA,gBAAA,OACA,MAAA,IACA,MAAA,KACA,WAAA,OACA,QAAA,GjCnFI,WAAA,QAAA,KAAA,KAIA,uCjBi2MJ,uBkD/xMF,uBjCjEQ,WAAA,MjBu2MR,6BADA,6BG32ME,6BAAA,6B+CwFE,MAAA,KACA,gBAAA,KACA,QAAA,EACA,QAAA,GAGJ,uBACE,KAAA,EAKF,uBACE,MAAA,ElDuxMF,4BkDhxMA,4BAEE,QAAA,aACA,MAAA,KACA,OAAA,KACA,WAAA,UAAA,GAAA,CAAA,KAAA,KAEF,4BACE,iBAAA,qMAEF,4BACE,iBAAA,sMASF,qBACE,SAAA,SACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,GACA,QAAA,YAAA,QAAA,KACA,cAAA,OAAA,gBAAA,OACA,aAAA,EAEA,aAAA,IACA,YAAA,IACA,WAAA,KAZF,wBAeI,WAAA,YACA,SAAA,EAAA,EAAA,KAAA,KAAA,EAAA,EAAA,KACA,MAAA,KACA,OAAA,IACA,aAAA,IACA,YAAA,IACA,YAAA,OACA,OAAA,QACA,iBAAA,KACA,gBAAA,YAEA,WAAA,KAAA,MAAA,YACA,cAAA,KAAA,MAAA,YACA,QAAA,GjC5JE,WAAA,QAAA,IAAA,KAIA,uCiC4HN,wBjC3HQ,WAAA,MiC2HR,6BAiCI,QAAA,EASJ,kBACE,SAAA,SACA,MAAA,IACA,OAAA,KACA,KAAA,IACA,QAAA,GACA,YAAA,KACA,eAAA,KACA,MAAA,KACA,WAAA,OE/LF,kCACE,GAAK,kBAAA,eAAA,UAAA,gBADP,0BACE,GAAK,kBAAA,eAAA,UAAA,gBAGP,gBACE,QAAA,aACA,MAAA,KACA,OAAA,KACA,eAAA,YACA,OAAA,MAAA,MAAA,aACA,mBAAA,YAEA,cAAA,IACA,kBAAA,eAAA,KAAA,OAAA,SAAA,UAAA,eAAA,KAAA,OAAA,SAGF,mBACE,MAAA,KACA,OAAA,KACA,aAAA,KAOF,gCACE,GACE,kBAAA,SAAA,UAAA,SAEF,IACE,QAAA,EACA,kBAAA,KAAA,UAAA,MANJ,wBACE,GACE,kBAAA,SAAA,UAAA,SAEF,IACE,QAAA,EACA,kBAAA,KAAA,UAAA,MAIJ,cACE,QAAA,aACA,MAAA,KACA,OAAA,KACA,eAAA,YACA,iBAAA,aAEA,cAAA,IACA,QAAA,EACA,kBAAA,aAAA,KAAA,OAAA,SAAA,UAAA,aAAA,KAAA,OAAA,SAGF,iBACE,MAAA,KACA,OAAA,KCpDF,gBAAqB,eAAA,mBACrB,WAAqB,eAAA,cACrB,cAAqB,eAAA,iBACrB,cAAqB,eAAA,iBACrB,mBAAqB,eAAA,sBACrB,gBAAqB,eAAA,mBCFnB,YACE,iBAAA,kBnDUF,mBAAA,mBH0iNF,wBADA,wBsD9iNM,iBAAA,kBANJ,cACE,iBAAA,kBnDUF,qBAAA,qBHojNF,0BADA,0BsDxjNM,iBAAA,kBANJ,YACE,iBAAA,kBnDUF,mBAAA,mBH8jNF,wBADA,wBsDlkNM,iBAAA,kBANJ,SACE,iBAAA,kBnDUF,gBAAA,gBHwkNF,qBADA,qBsD5kNM,iBAAA,kBANJ,YACE,iBAAA,kBnDUF,mBAAA,mBHklNF,wBADA,wBsDtlNM,iBAAA,kBANJ,WACE,iBAAA,kBnDUF,kBAAA,kBH4lNF,uBADA,uBsDhmNM,iBAAA,kBANJ,UACE,iBAAA,kBnDUF,iBAAA,iBHsmNF,sBADA,sBsD1mNM,iBAAA,kBANJ,SACE,iBAAA,kBnDUF,gBAAA,gBHgnNF,qBADA,qBsDpnNM,iBAAA,kBCCN,UACE,iBAAA,eAGF,gBACE,iBAAA,sBCXF,QAAkB,OAAA,IAAA,MAAA,kBAClB,YAAkB,WAAA,IAAA,MAAA,kBAClB,cAAkB,aAAA,IAAA,MAAA,kBAClB,eAAkB,cAAA,IAAA,MAAA,kBAClB,aAAkB,YAAA,IAAA,MAAA,kBAElB,UAAmB,OAAA,YACnB,cAAmB,WAAA,YACnB,gBAAmB,aAAA,YACnB,iBAAmB,cAAA,YACnB,eAAmB,YAAA,YAGjB,gBACE,aAAA,kBADF,kBACE,aAAA,kBADF,gBACE,aAAA,kBADF,aACE,aAAA,kBADF,gBACE,aAAA,kBADF,eACE,aAAA,kBADF,cACE,aAAA,kBADF,aACE,aAAA,kBAIJ,cACE,aAAA,eAOF,YACE,cAAA,gBAGF,SACE,cAAA,iBAGF,aACE,uBAAA,iBACA,wBAAA,iBAGF,eACE,wBAAA,iBACA,2BAAA,iBAGF,gBACE,2BAAA,iBACA,0BAAA,iBAGF,cACE,uBAAA,iBACA,0BAAA,iBAGF,YACE,cAAA,gBAGF,gBACE,cAAA,cAGF,cACE,cAAA,gBAGF,WACE,cAAA,YLxEA,iBACE,QAAA,MACA,MAAA,KACA,QAAA,GMOE,QAAwB,QAAA,eAAxB,UAAwB,QAAA,iBAAxB,gBAAwB,QAAA,uBAAxB,SAAwB,QAAA,gBAAxB,SAAwB,QAAA,gBAAxB,aAAwB,QAAA,oBAAxB,cAAwB,QAAA,qBAAxB,QAAwB,QAAA,sBAAA,QAAA,eAAxB,eAAwB,QAAA,6BAAA,QAAA,sB7CiD1B,yB6CjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uB7CiD1B,yB6CjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uB7CiD1B,yB6CjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uB7CiD1B,0B6CjDE,WAAwB,QAAA,eAAxB,aAAwB,QAAA,iBAAxB,mBAAwB,QAAA,uBAAxB,YAAwB,QAAA,gBAAxB,YAAwB,QAAA,gBAAxB,gBAAwB,QAAA,oBAAxB,iBAAwB,QAAA,qBAAxB,WAAwB,QAAA,sBAAA,QAAA,eAAxB,kBAAwB,QAAA,6BAAA,QAAA,uBAU9B,aAEI,cAAqB,QAAA,eAArB,gBAAqB,QAAA,iBAArB,sBAAqB,QAAA,uBAArB,eAAqB,QAAA,gBAArB,eAAqB,QAAA,gBAArB,mBAAqB,QAAA,oBAArB,oBAAqB,QAAA,qBAArB,cAAqB,QAAA,sBAAA,QAAA,eAArB,qBAAqB,QAAA,6BAAA,QAAA,uBCrBzB,kBACE,SAAA,SACA,QAAA,MACA,MAAA,KACA,QAAA,EACA,SAAA,OALF,0BAQI,QAAA,MACA,QAAA,GATJ,yC1D69NA,wBADA,yBAEA,yBACA,wB0D98NI,SAAA,SACA,IAAA,EACA,OAAA,EACA,KAAA,EACA,MAAA,KACA,OAAA,KACA,OAAA,EAQF,gCAEI,YAAA,WAFJ,gCAEI,YAAA,OAFJ,+BAEI,YAAA,IAFJ,+BAEI,YAAA,KCzBF,UAAgC,mBAAA,cAAA,eAAA,cAChC,aAAgC,mBAAA,iBAAA,eAAA,iBAChC,kBAAgC,mBAAA,sBAAA,eAAA,sBAChC,qBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,WAA8B,cAAA,eAAA,UAAA,eAC9B,aAA8B,cAAA,iBAAA,UAAA,iBAC9B,mBAA8B,cAAA,uBAAA,UAAA,uBAC9B,WAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,aAA8B,kBAAA,YAAA,UAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAC9B,eAA8B,kBAAA,YAAA,YAAA,YAE9B,uBAAoC,cAAA,gBAAA,gBAAA,qBACpC,qBAAoC,cAAA,cAAA,gBAAA,mBACpC,wBAAoC,cAAA,iBAAA,gBAAA,iBACpC,yBAAoC,cAAA,kBAAA,gBAAA,wBACpC,wBAAoC,cAAA,qBAAA,gBAAA,uBAEpC,mBAAiC,eAAA,gBAAA,YAAA,qBACjC,iBAAiC,eAAA,cAAA,YAAA,mBACjC,oBAAiC,eAAA,iBAAA,YAAA,iBACjC,sBAAiC,eAAA,mBAAA,YAAA,mBACjC,qBAAiC,eAAA,kBAAA,YAAA,kBAEjC,qBAAkC,mBAAA,gBAAA,cAAA,qBAClC,mBAAkC,mBAAA,cAAA,cAAA,mBAClC,sBAAkC,mBAAA,iBAAA,cAAA,iBAClC,uBAAkC,mBAAA,kBAAA,cAAA,wBAClC,sBAAkC,mBAAA,qBAAA,cAAA,uBAClC,uBAAkC,mBAAA,kBAAA,cAAA,kBAElC,iBAAgC,oBAAA,eAAA,WAAA,eAChC,kBAAgC,oBAAA,gBAAA,WAAA,qBAChC,gBAAgC,oBAAA,cAAA,WAAA,mBAChC,mBAAgC,oBAAA,iBAAA,WAAA,iBAChC,qBAAgC,oBAAA,mBAAA,WAAA,mBAChC,oBAAgC,oBAAA,kBAAA,WAAA,kB/CYhC,yB+ClDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mB/CYhC,yB+ClDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mB/CYhC,yB+ClDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mB/CYhC,0B+ClDA,aAAgC,mBAAA,cAAA,eAAA,cAChC,gBAAgC,mBAAA,iBAAA,eAAA,iBAChC,qBAAgC,mBAAA,sBAAA,eAAA,sBAChC,wBAAgC,mBAAA,yBAAA,eAAA,yBAEhC,cAA8B,cAAA,eAAA,UAAA,eAC9B,gBAA8B,cAAA,iBAAA,UAAA,iBAC9B,sBAA8B,cAAA,uBAAA,UAAA,uBAC9B,cAA8B,SAAA,EAAA,EAAA,eAAA,KAAA,EAAA,EAAA,eAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,gBAA8B,kBAAA,YAAA,UAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAC9B,kBAA8B,kBAAA,YAAA,YAAA,YAE9B,0BAAoC,cAAA,gBAAA,gBAAA,qBACpC,wBAAoC,cAAA,cAAA,gBAAA,mBACpC,2BAAoC,cAAA,iBAAA,gBAAA,iBACpC,4BAAoC,cAAA,kBAAA,gBAAA,wBACpC,2BAAoC,cAAA,qBAAA,gBAAA,uBAEpC,sBAAiC,eAAA,gBAAA,YAAA,qBACjC,oBAAiC,eAAA,cAAA,YAAA,mBACjC,uBAAiC,eAAA,iBAAA,YAAA,iBACjC,yBAAiC,eAAA,mBAAA,YAAA,mBACjC,wBAAiC,eAAA,kBAAA,YAAA,kBAEjC,wBAAkC,mBAAA,gBAAA,cAAA,qBAClC,sBAAkC,mBAAA,cAAA,cAAA,mBAClC,yBAAkC,mBAAA,iBAAA,cAAA,iBAClC,0BAAkC,mBAAA,kBAAA,cAAA,wBAClC,yBAAkC,mBAAA,qBAAA,cAAA,uBAClC,0BAAkC,mBAAA,kBAAA,cAAA,kBAElC,oBAAgC,oBAAA,eAAA,WAAA,eAChC,qBAAgC,oBAAA,gBAAA,WAAA,qBAChC,mBAAgC,oBAAA,cAAA,WAAA,mBAChC,sBAAgC,oBAAA,iBAAA,WAAA,iBAChC,wBAAgC,oBAAA,mBAAA,WAAA,mBAChC,uBAAgC,oBAAA,kBAAA,WAAA,mBC1ChC,YAAwB,MAAA,eACxB,aAAwB,MAAA,gBACxB,YAAwB,MAAA,ehDoDxB,yBgDtDA,eAAwB,MAAA,eACxB,gBAAwB,MAAA,gBACxB,eAAwB,MAAA,gBhDoDxB,yBgDtDA,eAAwB,MAAA,eACxB,gBAAwB,MAAA,gBACxB,eAAwB,MAAA,gBhDoDxB,yBgDtDA,eAAwB,MAAA,eACxB,gBAAwB,MAAA,gBACxB,eAAwB,MAAA,gBhDoDxB,0BgDtDA,eAAwB,MAAA,eACxB,gBAAwB,MAAA,gBACxB,eAAwB,MAAA,gBCL1B,iBAAyB,oBAAA,cAAA,iBAAA,cAAA,gBAAA,cAAA,YAAA,cAAzB,kBAAyB,oBAAA,eAAA,iBAAA,eAAA,gBAAA,eAAA,YAAA,eAAzB,kBAAyB,oBAAA,eAAA,iBAAA,eAAA,gBAAA,eAAA,YAAA,eCAzB,eAAsB,SAAA,eAAtB,iBAAsB,SAAA,iBCCtB,iBAAyB,SAAA,iBAAzB,mBAAyB,SAAA,mBAAzB,mBAAyB,SAAA,mBAAzB,gBAAyB,SAAA,gBAAzB,iBAAyB,SAAA,yBAAA,SAAA,iBAK3B,WACE,SAAA,MACA,IAAA,EACA,MAAA,EACA,KAAA,EACA,QAAA,KAGF,cACE,SAAA,MACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,KAI4B,2DAD9B,YAEI,SAAA,eAAA,SAAA,OACA,IAAA,EACA,QAAA,MCzBJ,SCEE,SAAA,SACA,MAAA,IACA,OAAA,IACA,QAAA,EACA,OAAA,KACA,SAAA,OACA,KAAA,cACA,YAAA,OACA,OAAA,EAUA,0BAAA,yBAEE,SAAA,OACA,MAAA,KACA,OAAA,KACA,SAAA,QACA,KAAA,KACA,YAAA,OC7BJ,WAAa,WAAA,EAAA,QAAA,OAAA,2BACb,QAAU,WAAA,EAAA,MAAA,KAAA,0BACV,WAAa,WAAA,EAAA,KAAA,KAAA,2BACb,aAAe,WAAA,eCCX,MAAuB,MAAA,cAAvB,MAAuB,MAAA,cAAvB,MAAuB,MAAA,cAAvB,OAAuB,MAAA,eAAvB,QAAuB,MAAA,eAAvB,MAAuB,OAAA,cAAvB,MAAuB,OAAA,cAAvB,MAAuB,OAAA,cAAvB,OAAuB,OAAA,eAAvB,QAAuB,OAAA,eAI3B,QAAU,UAAA,eACV,QAAU,WAAA,eAIV,YAAc,UAAA,gBACd,YAAc,WAAA,gBAEd,QAAU,MAAA,gBACV,QAAU,OAAA,gBCTF,KAAgC,OAAA,YAChC,MpEu7PR,MoEr7PU,WAAA,YAEF,MpEw7PR,MoEt7PU,aAAA,YAEF,MpEy7PR,MoEv7PU,cAAA,YAEF,MpE07PR,MoEx7PU,YAAA,YAfF,KAAgC,OAAA,iBAChC,MpE+8PR,MoE78PU,WAAA,iBAEF,MpEg9PR,MoE98PU,aAAA,iBAEF,MpEi9PR,MoE/8PU,cAAA,iBAEF,MpEk9PR,MoEh9PU,YAAA,iBAfF,KAAgC,OAAA,gBAChC,MpEu+PR,MoEr+PU,WAAA,gBAEF,MpEw+PR,MoEt+PU,aAAA,gBAEF,MpEy+PR,MoEv+PU,cAAA,gBAEF,MpE0+PR,MoEx+PU,YAAA,gBAfF,KAAgC,OAAA,eAChC,MpE+/PR,MoE7/PU,WAAA,eAEF,MpEggQR,MoE9/PU,aAAA,eAEF,MpEigQR,MoE//PU,cAAA,eAEF,MpEkgQR,MoEhgQU,YAAA,eAfF,KAAgC,OAAA,iBAChC,MpEuhQR,MoErhQU,WAAA,iBAEF,MpEwhQR,MoEthQU,aAAA,iBAEF,MpEyhQR,MoEvhQU,cAAA,iBAEF,MpE0hQR,MoExhQU,YAAA,iBAfF,KAAgC,OAAA,eAChC,MpE+iQR,MoE7iQU,WAAA,eAEF,MpEgjQR,MoE9iQU,aAAA,eAEF,MpEijQR,MoE/iQU,cAAA,eAEF,MpEkjQR,MoEhjQU,YAAA,eAfF,KAAgC,QAAA,YAChC,MpEukQR,MoErkQU,YAAA,YAEF,MpEwkQR,MoEtkQU,cAAA,YAEF,MpEykQR,MoEvkQU,eAAA,YAEF,MpE0kQR,MoExkQU,aAAA,YAfF,KAAgC,QAAA,iBAChC,MpE+lQR,MoE7lQU,YAAA,iBAEF,MpEgmQR,MoE9lQU,cAAA,iBAEF,MpEimQR,MoE/lQU,eAAA,iBAEF,MpEkmQR,MoEhmQU,aAAA,iBAfF,KAAgC,QAAA,gBAChC,MpEunQR,MoErnQU,YAAA,gBAEF,MpEwnQR,MoEtnQU,cAAA,gBAEF,MpEynQR,MoEvnQU,eAAA,gBAEF,MpE0nQR,MoExnQU,aAAA,gBAfF,KAAgC,QAAA,eAChC,MpE+oQR,MoE7oQU,YAAA,eAEF,MpEgpQR,MoE9oQU,cAAA,eAEF,MpEipQR,MoE/oQU,eAAA,eAEF,MpEkpQR,MoEhpQU,aAAA,eAfF,KAAgC,QAAA,iBAChC,MpEuqQR,MoErqQU,YAAA,iBAEF,MpEwqQR,MoEtqQU,cAAA,iBAEF,MpEyqQR,MoEvqQU,eAAA,iBAEF,MpE0qQR,MoExqQU,aAAA,iBAfF,KAAgC,QAAA,eAChC,MpE+rQR,MoE7rQU,YAAA,eAEF,MpEgsQR,MoE9rQU,cAAA,eAEF,MpEisQR,MoE/rQU,eAAA,eAEF,MpEksQR,MoEhsQU,aAAA,eAQF,MAAwB,OAAA,kBACxB,OpEgsQR,OoE9rQU,WAAA,kBAEF,OpEisQR,OoE/rQU,aAAA,kBAEF,OpEksQR,OoEhsQU,cAAA,kBAEF,OpEmsQR,OoEjsQU,YAAA,kBAfF,MAAwB,OAAA,iBACxB,OpEwtQR,OoEttQU,WAAA,iBAEF,OpEytQR,OoEvtQU,aAAA,iBAEF,OpE0tQR,OoExtQU,cAAA,iBAEF,OpE2tQR,OoEztQU,YAAA,iBAfF,MAAwB,OAAA,gBACxB,OpEgvQR,OoE9uQU,WAAA,gBAEF,OpEivQR,OoE/uQU,aAAA,gBAEF,OpEkvQR,OoEhvQU,cAAA,gBAEF,OpEmvQR,OoEjvQU,YAAA,gBAfF,MAAwB,OAAA,kBACxB,OpEwwQR,OoEtwQU,WAAA,kBAEF,OpEywQR,OoEvwQU,aAAA,kBAEF,OpE0wQR,OoExwQU,cAAA,kBAEF,OpE2wQR,OoEzwQU,YAAA,kBAfF,MAAwB,OAAA,gBACxB,OpEgyQR,OoE9xQU,WAAA,gBAEF,OpEiyQR,OoE/xQU,aAAA,gBAEF,OpEkyQR,OoEhyQU,cAAA,gBAEF,OpEmyQR,OoEjyQU,YAAA,gBAMN,QAAmB,OAAA,eACnB,SpEmyQJ,SoEjyQM,WAAA,eAEF,SpEoyQJ,SoElyQM,aAAA,eAEF,SpEqyQJ,SoEnyQM,cAAA,eAEF,SpEsyQJ,SoEpyQM,YAAA,exDTF,yBwDlDI,QAAgC,OAAA,YAChC,SpEu2QN,SoEr2QQ,WAAA,YAEF,SpEu2QN,SoEr2QQ,aAAA,YAEF,SpEu2QN,SoEr2QQ,cAAA,YAEF,SpEu2QN,SoEr2QQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SpE03QN,SoEx3QQ,WAAA,iBAEF,SpE03QN,SoEx3QQ,aAAA,iBAEF,SpE03QN,SoEx3QQ,cAAA,iBAEF,SpE03QN,SoEx3QQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SpE64QN,SoE34QQ,WAAA,gBAEF,SpE64QN,SoE34QQ,aAAA,gBAEF,SpE64QN,SoE34QQ,cAAA,gBAEF,SpE64QN,SoE34QQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SpEg6QN,SoE95QQ,WAAA,eAEF,SpEg6QN,SoE95QQ,aAAA,eAEF,SpEg6QN,SoE95QQ,cAAA,eAEF,SpEg6QN,SoE95QQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SpEm7QN,SoEj7QQ,WAAA,iBAEF,SpEm7QN,SoEj7QQ,aAAA,iBAEF,SpEm7QN,SoEj7QQ,cAAA,iBAEF,SpEm7QN,SoEj7QQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SpEs8QN,SoEp8QQ,WAAA,eAEF,SpEs8QN,SoEp8QQ,aAAA,eAEF,SpEs8QN,SoEp8QQ,cAAA,eAEF,SpEs8QN,SoEp8QQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SpEy9QN,SoEv9QQ,YAAA,YAEF,SpEy9QN,SoEv9QQ,cAAA,YAEF,SpEy9QN,SoEv9QQ,eAAA,YAEF,SpEy9QN,SoEv9QQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SpE4+QN,SoE1+QQ,YAAA,iBAEF,SpE4+QN,SoE1+QQ,cAAA,iBAEF,SpE4+QN,SoE1+QQ,eAAA,iBAEF,SpE4+QN,SoE1+QQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SpE+/QN,SoE7/QQ,YAAA,gBAEF,SpE+/QN,SoE7/QQ,cAAA,gBAEF,SpE+/QN,SoE7/QQ,eAAA,gBAEF,SpE+/QN,SoE7/QQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SpEkhRN,SoEhhRQ,YAAA,eAEF,SpEkhRN,SoEhhRQ,cAAA,eAEF,SpEkhRN,SoEhhRQ,eAAA,eAEF,SpEkhRN,SoEhhRQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SpEqiRN,SoEniRQ,YAAA,iBAEF,SpEqiRN,SoEniRQ,cAAA,iBAEF,SpEqiRN,SoEniRQ,eAAA,iBAEF,SpEqiRN,SoEniRQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SpEwjRN,SoEtjRQ,YAAA,eAEF,SpEwjRN,SoEtjRQ,cAAA,eAEF,SpEwjRN,SoEtjRQ,eAAA,eAEF,SpEwjRN,SoEtjRQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UpEojRN,UoEljRQ,WAAA,kBAEF,UpEojRN,UoEljRQ,aAAA,kBAEF,UpEojRN,UoEljRQ,cAAA,kBAEF,UpEojRN,UoEljRQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UpEukRN,UoErkRQ,WAAA,iBAEF,UpEukRN,UoErkRQ,aAAA,iBAEF,UpEukRN,UoErkRQ,cAAA,iBAEF,UpEukRN,UoErkRQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UpE0lRN,UoExlRQ,WAAA,gBAEF,UpE0lRN,UoExlRQ,aAAA,gBAEF,UpE0lRN,UoExlRQ,cAAA,gBAEF,UpE0lRN,UoExlRQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UpE6mRN,UoE3mRQ,WAAA,kBAEF,UpE6mRN,UoE3mRQ,aAAA,kBAEF,UpE6mRN,UoE3mRQ,cAAA,kBAEF,UpE6mRN,UoE3mRQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UpEgoRN,UoE9nRQ,WAAA,gBAEF,UpEgoRN,UoE9nRQ,aAAA,gBAEF,UpEgoRN,UoE9nRQ,cAAA,gBAEF,UpEgoRN,UoE9nRQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YpE8nRF,YoE5nRI,WAAA,eAEF,YpE8nRF,YoE5nRI,aAAA,eAEF,YpE8nRF,YoE5nRI,cAAA,eAEF,YpE8nRF,YoE5nRI,YAAA,gBxDTF,yBwDlDI,QAAgC,OAAA,YAChC,SpEgsRN,SoE9rRQ,WAAA,YAEF,SpEgsRN,SoE9rRQ,aAAA,YAEF,SpEgsRN,SoE9rRQ,cAAA,YAEF,SpEgsRN,SoE9rRQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SpEmtRN,SoEjtRQ,WAAA,iBAEF,SpEmtRN,SoEjtRQ,aAAA,iBAEF,SpEmtRN,SoEjtRQ,cAAA,iBAEF,SpEmtRN,SoEjtRQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SpEsuRN,SoEpuRQ,WAAA,gBAEF,SpEsuRN,SoEpuRQ,aAAA,gBAEF,SpEsuRN,SoEpuRQ,cAAA,gBAEF,SpEsuRN,SoEpuRQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SpEyvRN,SoEvvRQ,WAAA,eAEF,SpEyvRN,SoEvvRQ,aAAA,eAEF,SpEyvRN,SoEvvRQ,cAAA,eAEF,SpEyvRN,SoEvvRQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SpE4wRN,SoE1wRQ,WAAA,iBAEF,SpE4wRN,SoE1wRQ,aAAA,iBAEF,SpE4wRN,SoE1wRQ,cAAA,iBAEF,SpE4wRN,SoE1wRQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SpE+xRN,SoE7xRQ,WAAA,eAEF,SpE+xRN,SoE7xRQ,aAAA,eAEF,SpE+xRN,SoE7xRQ,cAAA,eAEF,SpE+xRN,SoE7xRQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SpEkzRN,SoEhzRQ,YAAA,YAEF,SpEkzRN,SoEhzRQ,cAAA,YAEF,SpEkzRN,SoEhzRQ,eAAA,YAEF,SpEkzRN,SoEhzRQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SpEq0RN,SoEn0RQ,YAAA,iBAEF,SpEq0RN,SoEn0RQ,cAAA,iBAEF,SpEq0RN,SoEn0RQ,eAAA,iBAEF,SpEq0RN,SoEn0RQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SpEw1RN,SoEt1RQ,YAAA,gBAEF,SpEw1RN,SoEt1RQ,cAAA,gBAEF,SpEw1RN,SoEt1RQ,eAAA,gBAEF,SpEw1RN,SoEt1RQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SpE22RN,SoEz2RQ,YAAA,eAEF,SpE22RN,SoEz2RQ,cAAA,eAEF,SpE22RN,SoEz2RQ,eAAA,eAEF,SpE22RN,SoEz2RQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SpE83RN,SoE53RQ,YAAA,iBAEF,SpE83RN,SoE53RQ,cAAA,iBAEF,SpE83RN,SoE53RQ,eAAA,iBAEF,SpE83RN,SoE53RQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SpEi5RN,SoE/4RQ,YAAA,eAEF,SpEi5RN,SoE/4RQ,cAAA,eAEF,SpEi5RN,SoE/4RQ,eAAA,eAEF,SpEi5RN,SoE/4RQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UpE64RN,UoE34RQ,WAAA,kBAEF,UpE64RN,UoE34RQ,aAAA,kBAEF,UpE64RN,UoE34RQ,cAAA,kBAEF,UpE64RN,UoE34RQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UpEg6RN,UoE95RQ,WAAA,iBAEF,UpEg6RN,UoE95RQ,aAAA,iBAEF,UpEg6RN,UoE95RQ,cAAA,iBAEF,UpEg6RN,UoE95RQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UpEm7RN,UoEj7RQ,WAAA,gBAEF,UpEm7RN,UoEj7RQ,aAAA,gBAEF,UpEm7RN,UoEj7RQ,cAAA,gBAEF,UpEm7RN,UoEj7RQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UpEs8RN,UoEp8RQ,WAAA,kBAEF,UpEs8RN,UoEp8RQ,aAAA,kBAEF,UpEs8RN,UoEp8RQ,cAAA,kBAEF,UpEs8RN,UoEp8RQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UpEy9RN,UoEv9RQ,WAAA,gBAEF,UpEy9RN,UoEv9RQ,aAAA,gBAEF,UpEy9RN,UoEv9RQ,cAAA,gBAEF,UpEy9RN,UoEv9RQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YpEu9RF,YoEr9RI,WAAA,eAEF,YpEu9RF,YoEr9RI,aAAA,eAEF,YpEu9RF,YoEr9RI,cAAA,eAEF,YpEu9RF,YoEr9RI,YAAA,gBxDTF,yBwDlDI,QAAgC,OAAA,YAChC,SpEyhSN,SoEvhSQ,WAAA,YAEF,SpEyhSN,SoEvhSQ,aAAA,YAEF,SpEyhSN,SoEvhSQ,cAAA,YAEF,SpEyhSN,SoEvhSQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SpE4iSN,SoE1iSQ,WAAA,iBAEF,SpE4iSN,SoE1iSQ,aAAA,iBAEF,SpE4iSN,SoE1iSQ,cAAA,iBAEF,SpE4iSN,SoE1iSQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SpE+jSN,SoE7jSQ,WAAA,gBAEF,SpE+jSN,SoE7jSQ,aAAA,gBAEF,SpE+jSN,SoE7jSQ,cAAA,gBAEF,SpE+jSN,SoE7jSQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SpEklSN,SoEhlSQ,WAAA,eAEF,SpEklSN,SoEhlSQ,aAAA,eAEF,SpEklSN,SoEhlSQ,cAAA,eAEF,SpEklSN,SoEhlSQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SpEqmSN,SoEnmSQ,WAAA,iBAEF,SpEqmSN,SoEnmSQ,aAAA,iBAEF,SpEqmSN,SoEnmSQ,cAAA,iBAEF,SpEqmSN,SoEnmSQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SpEwnSN,SoEtnSQ,WAAA,eAEF,SpEwnSN,SoEtnSQ,aAAA,eAEF,SpEwnSN,SoEtnSQ,cAAA,eAEF,SpEwnSN,SoEtnSQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SpE2oSN,SoEzoSQ,YAAA,YAEF,SpE2oSN,SoEzoSQ,cAAA,YAEF,SpE2oSN,SoEzoSQ,eAAA,YAEF,SpE2oSN,SoEzoSQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SpE8pSN,SoE5pSQ,YAAA,iBAEF,SpE8pSN,SoE5pSQ,cAAA,iBAEF,SpE8pSN,SoE5pSQ,eAAA,iBAEF,SpE8pSN,SoE5pSQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SpEirSN,SoE/qSQ,YAAA,gBAEF,SpEirSN,SoE/qSQ,cAAA,gBAEF,SpEirSN,SoE/qSQ,eAAA,gBAEF,SpEirSN,SoE/qSQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SpEosSN,SoElsSQ,YAAA,eAEF,SpEosSN,SoElsSQ,cAAA,eAEF,SpEosSN,SoElsSQ,eAAA,eAEF,SpEosSN,SoElsSQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SpEutSN,SoErtSQ,YAAA,iBAEF,SpEutSN,SoErtSQ,cAAA,iBAEF,SpEutSN,SoErtSQ,eAAA,iBAEF,SpEutSN,SoErtSQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SpE0uSN,SoExuSQ,YAAA,eAEF,SpE0uSN,SoExuSQ,cAAA,eAEF,SpE0uSN,SoExuSQ,eAAA,eAEF,SpE0uSN,SoExuSQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UpEsuSN,UoEpuSQ,WAAA,kBAEF,UpEsuSN,UoEpuSQ,aAAA,kBAEF,UpEsuSN,UoEpuSQ,cAAA,kBAEF,UpEsuSN,UoEpuSQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UpEyvSN,UoEvvSQ,WAAA,iBAEF,UpEyvSN,UoEvvSQ,aAAA,iBAEF,UpEyvSN,UoEvvSQ,cAAA,iBAEF,UpEyvSN,UoEvvSQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UpE4wSN,UoE1wSQ,WAAA,gBAEF,UpE4wSN,UoE1wSQ,aAAA,gBAEF,UpE4wSN,UoE1wSQ,cAAA,gBAEF,UpE4wSN,UoE1wSQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UpE+xSN,UoE7xSQ,WAAA,kBAEF,UpE+xSN,UoE7xSQ,aAAA,kBAEF,UpE+xSN,UoE7xSQ,cAAA,kBAEF,UpE+xSN,UoE7xSQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UpEkzSN,UoEhzSQ,WAAA,gBAEF,UpEkzSN,UoEhzSQ,aAAA,gBAEF,UpEkzSN,UoEhzSQ,cAAA,gBAEF,UpEkzSN,UoEhzSQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YpEgzSF,YoE9ySI,WAAA,eAEF,YpEgzSF,YoE9ySI,aAAA,eAEF,YpEgzSF,YoE9ySI,cAAA,eAEF,YpEgzSF,YoE9ySI,YAAA,gBxDTF,0BwDlDI,QAAgC,OAAA,YAChC,SpEk3SN,SoEh3SQ,WAAA,YAEF,SpEk3SN,SoEh3SQ,aAAA,YAEF,SpEk3SN,SoEh3SQ,cAAA,YAEF,SpEk3SN,SoEh3SQ,YAAA,YAfF,QAAgC,OAAA,iBAChC,SpEq4SN,SoEn4SQ,WAAA,iBAEF,SpEq4SN,SoEn4SQ,aAAA,iBAEF,SpEq4SN,SoEn4SQ,cAAA,iBAEF,SpEq4SN,SoEn4SQ,YAAA,iBAfF,QAAgC,OAAA,gBAChC,SpEw5SN,SoEt5SQ,WAAA,gBAEF,SpEw5SN,SoEt5SQ,aAAA,gBAEF,SpEw5SN,SoEt5SQ,cAAA,gBAEF,SpEw5SN,SoEt5SQ,YAAA,gBAfF,QAAgC,OAAA,eAChC,SpE26SN,SoEz6SQ,WAAA,eAEF,SpE26SN,SoEz6SQ,aAAA,eAEF,SpE26SN,SoEz6SQ,cAAA,eAEF,SpE26SN,SoEz6SQ,YAAA,eAfF,QAAgC,OAAA,iBAChC,SpE87SN,SoE57SQ,WAAA,iBAEF,SpE87SN,SoE57SQ,aAAA,iBAEF,SpE87SN,SoE57SQ,cAAA,iBAEF,SpE87SN,SoE57SQ,YAAA,iBAfF,QAAgC,OAAA,eAChC,SpEi9SN,SoE/8SQ,WAAA,eAEF,SpEi9SN,SoE/8SQ,aAAA,eAEF,SpEi9SN,SoE/8SQ,cAAA,eAEF,SpEi9SN,SoE/8SQ,YAAA,eAfF,QAAgC,QAAA,YAChC,SpEo+SN,SoEl+SQ,YAAA,YAEF,SpEo+SN,SoEl+SQ,cAAA,YAEF,SpEo+SN,SoEl+SQ,eAAA,YAEF,SpEo+SN,SoEl+SQ,aAAA,YAfF,QAAgC,QAAA,iBAChC,SpEu/SN,SoEr/SQ,YAAA,iBAEF,SpEu/SN,SoEr/SQ,cAAA,iBAEF,SpEu/SN,SoEr/SQ,eAAA,iBAEF,SpEu/SN,SoEr/SQ,aAAA,iBAfF,QAAgC,QAAA,gBAChC,SpE0gTN,SoExgTQ,YAAA,gBAEF,SpE0gTN,SoExgTQ,cAAA,gBAEF,SpE0gTN,SoExgTQ,eAAA,gBAEF,SpE0gTN,SoExgTQ,aAAA,gBAfF,QAAgC,QAAA,eAChC,SpE6hTN,SoE3hTQ,YAAA,eAEF,SpE6hTN,SoE3hTQ,cAAA,eAEF,SpE6hTN,SoE3hTQ,eAAA,eAEF,SpE6hTN,SoE3hTQ,aAAA,eAfF,QAAgC,QAAA,iBAChC,SpEgjTN,SoE9iTQ,YAAA,iBAEF,SpEgjTN,SoE9iTQ,cAAA,iBAEF,SpEgjTN,SoE9iTQ,eAAA,iBAEF,SpEgjTN,SoE9iTQ,aAAA,iBAfF,QAAgC,QAAA,eAChC,SpEmkTN,SoEjkTQ,YAAA,eAEF,SpEmkTN,SoEjkTQ,cAAA,eAEF,SpEmkTN,SoEjkTQ,eAAA,eAEF,SpEmkTN,SoEjkTQ,aAAA,eAQF,SAAwB,OAAA,kBACxB,UpE+jTN,UoE7jTQ,WAAA,kBAEF,UpE+jTN,UoE7jTQ,aAAA,kBAEF,UpE+jTN,UoE7jTQ,cAAA,kBAEF,UpE+jTN,UoE7jTQ,YAAA,kBAfF,SAAwB,OAAA,iBACxB,UpEklTN,UoEhlTQ,WAAA,iBAEF,UpEklTN,UoEhlTQ,aAAA,iBAEF,UpEklTN,UoEhlTQ,cAAA,iBAEF,UpEklTN,UoEhlTQ,YAAA,iBAfF,SAAwB,OAAA,gBACxB,UpEqmTN,UoEnmTQ,WAAA,gBAEF,UpEqmTN,UoEnmTQ,aAAA,gBAEF,UpEqmTN,UoEnmTQ,cAAA,gBAEF,UpEqmTN,UoEnmTQ,YAAA,gBAfF,SAAwB,OAAA,kBACxB,UpEwnTN,UoEtnTQ,WAAA,kBAEF,UpEwnTN,UoEtnTQ,aAAA,kBAEF,UpEwnTN,UoEtnTQ,cAAA,kBAEF,UpEwnTN,UoEtnTQ,YAAA,kBAfF,SAAwB,OAAA,gBACxB,UpE2oTN,UoEzoTQ,WAAA,gBAEF,UpE2oTN,UoEzoTQ,aAAA,gBAEF,UpE2oTN,UoEzoTQ,cAAA,gBAEF,UpE2oTN,UoEzoTQ,YAAA,gBAMN,WAAmB,OAAA,eACnB,YpEyoTF,YoEvoTI,WAAA,eAEF,YpEyoTF,YoEvoTI,aAAA,eAEF,YpEyoTF,YoEvoTI,cAAA,eAEF,YpEyoTF,YoEvoTI,YAAA,gBCjEN,uBAEI,SAAA,SACA,IAAA,EACA,MAAA,EACA,OAAA,EACA,KAAA,EACA,QAAA,EAEA,eAAA,KACA,QAAA,GAEA,iBAAA,cCVJ,gBAAkB,YAAA,cAAA,CAAA,KAAA,CAAA,MAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,aAAA,CAAA,oBAIlB,cAAiB,WAAA,kBACjB,WAAiB,YAAA,iBACjB,aAAiB,YAAA,iBACjB,eCTE,SAAA,OACA,cAAA,SACA,YAAA,ODeE,WAAwB,WAAA,eACxB,YAAwB,WAAA,gBACxB,aAAwB,WAAA,iB1DqCxB,yB0DvCA,cAAwB,WAAA,eACxB,eAAwB,WAAA,gBACxB,gBAAwB,WAAA,kB1DqCxB,yB0DvCA,cAAwB,WAAA,eACxB,eAAwB,WAAA,gBACxB,gBAAwB,WAAA,kB1DqCxB,yB0DvCA,cAAwB,WAAA,eACxB,eAAwB,WAAA,gBACxB,gBAAwB,WAAA,kB1DqCxB,0B0DvCA,cAAwB,WAAA,eACxB,eAAwB,WAAA,gBACxB,gBAAwB,WAAA,kBAM5B,gBAAmB,eAAA,oBACnB,gBAAmB,eAAA,oBACnB,iBAAmB,eAAA,qBAInB,mBAAuB,YAAA,cACvB,qBAAuB,YAAA,kBACvB,oBAAuB,YAAA,cACvB,kBAAuB,YAAA,cACvB,oBAAuB,YAAA,iBACvB,aAAuB,WAAA,iBAIvB,YAAc,MAAA,eEvCZ,cACE,MAAA,kBrEUF,qBAAA,qBqELM,MAAA,kBANN,gBACE,MAAA,kBrEUF,uBAAA,uBqELM,MAAA,kBANN,cACE,MAAA,kBrEUF,qBAAA,qBqELM,MAAA,kBANN,WACE,MAAA,kBrEUF,kBAAA,kBqELM,MAAA,kBANN,cACE,MAAA,kBrEUF,qBAAA,qBqELM,MAAA,kBANN,aACE,MAAA,kBrEUF,oBAAA,oBqELM,MAAA,kBANN,YACE,MAAA,kBrEUF,mBAAA,mBqELM,MAAA,kBANN,WACE,MAAA,kBrEUF,kBAAA,kBqELM,MAAA,kBFuCR,WAAa,MAAA,kBACb,YAAc,MAAA,kBAEd,eAAiB,MAAA,yBACjB,eAAiB,MAAA,+BAIjB,WGvDE,KAAA,CAAA,CAAA,EAAA,EACA,MAAA,YACA,YAAA,KACA,iBAAA,YACA,OAAA,EHuDF,sBAAwB,gBAAA,eAExB,YACE,WAAA,qBACA,cAAA,qBAKF,YAAc,MAAA,kBIjEd,SACE,WAAA,kBAGF,WACE,WAAA,iBCAA,a5EOF,ECq7TE,QADA,S2Er7TI,YAAA,eAEA,WAAA,eAGF,YAEI,gBAAA,UASJ,mBACE,QAAA,KAAA,YAAA,I5E8LN,I4E/KM,YAAA,mB3Eo6TJ,W2El6TE,IAEE,OAAA,IAAA,MAAA,QACA,kBAAA,MAQF,MACE,QAAA,mB3E85TJ,I2E35TE,GAEE,kBAAA,M3E65TJ,GACA,G2E35TE,EAGE,QAAA,EACA,OAAA,EAGF,G3Ey5TF,G2Ev5TI,iBAAA,MAQF,MACE,KAAA,G5E5CN,K4E+CM,UAAA,gBAEF,WACE,UAAA,gB7C9EN,Q6CmFM,QAAA,KxC/FN,OwCkGM,OAAA,IAAA,MAAA,K7DnGN,O6DuGM,gBAAA,mBADF,U3Em5TF,U2E94TM,iBAAA,e3Ek5TN,mBcr9TF,mB6D0EQ,OAAA,IAAA,MAAA,kB7DWR,Y6DNM,MAAA,Q3E+4TJ,wBAFA,eengUA,efogUA,qB2Ex4TM,aAAA,Q7DlBR,sB6DuBM,MAAA,QACA,aAAA","sourcesContent":["/*!\n * Bootstrap v4.5.2 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n","// Do not forget to update getting-started/theming.md!\n:root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-` + +

+ +
+ + + +
+

Use manual mode for items that use shared assets (meshes, etc).

+
+ +
+ + + + +
+ + + + +

+
+
+
+
+ +

You haven't created any .

+ More creations +
+
+ +
+

Note

+

this is for uploading special assets only (hats, faces, etc)

+ +

for regular asset creation (shirts, pants, etc) just use the Develop page

+

Important

+

make sure the asset URLs in your asset are represented as %ASSETURL%

+

so for instance, http:///asset/?id=1818 would be %ASSETURL%1818

+
+ + + +
+
+
+
+ +
+
+ $name +

Created $created

+
+
+

Total Sales: $sales-total

+

Last 7 days: $sales-week

+
+ +
+
+
+
+ + + + + + + +buildFooter(); ?> diff --git a/directory_admin/error-log.php b/directory_admin/error-log.php new file mode 100644 index 0000000..74bfe3b --- /dev/null +++ b/directory_admin/error-log.php @@ -0,0 +1,50 @@ + $pages) $page = $pages; +if(!is_numeric($page) || $page < 1) $page = 1; +$offset = ($page - 1)*15; + +$pageBuilder = new PageBuilder(["title" => "Staff Logs"]); +$pageBuilder->buildHeader(); +?> +

Error Log

+ + + + + + + + + + + $Error) { ?> + + + + + + + + +
IDErrorTimeParameters
+ 1) { ?> + + +buildFooter(); ?> diff --git a/directory_admin/give-asset.php b/directory_admin/give-asset.php new file mode 100644 index 0000000..78ec491 --- /dev/null +++ b/directory_admin/give-asset.php @@ -0,0 +1,95 @@ + ["Type" => "Integer"], + "UserID" => ["Type" => "Integer"] +]; + +function SetError($text) +{ + global $Alert; + $Alert = ["text" => $text, "color" => "danger"]; +} + +if($_SERVER["REQUEST_METHOD"] == "POST") +{ + $AssetID = $_POST["AssetID"] ?? ""; + $Condition = $_POST["Condition"] ?? ""; + $ConditionData = $_POST["ConditionData"] ?? ""; + + if (empty($AssetID)) SetError("Asset ID cannot be empty"); + else if (!isset($Conditions[$Condition])) SetError("Condition is not valid"); + else if (empty($ConditionData)) SetError("Condition data must be set"); + + else if ($Conditions[$Condition]["Type"] == "Integer" && !is_numeric($ConditionData)) SetError("Condition data must be a number"); + else if (!Catalog::GetAssetInfo($AssetID)) SetError("The asset you're trying to give does not exist"); + + if ($Alert === false) + { + $ItemName = Catalog::GetAssetInfo($AssetID)->name; + $ConditionString = ""; + $UserIDs = []; + $TagID = generateUUID(); + + if ($Condition == "UserID") + { + $ConditionString = "had the user ID $ConditionData"; + + $UserIDs = Database::singleton()->run( + "SELECT id FROM users WHERE id = :ConditionData + AND NOT (SELECT COUNT(*) FROM ownedAssets WHERE userId = users.id AND assetId = :AssetID)", + [":AssetID" => $AssetID, ":ConditionData" => $ConditionData] + )->fetchAll(\PDO::FETCH_COLUMN); + } + else if ($Condition == "AssetID") + { + $ConditionString = "purchased an asset with ID $ConditionData"; + + $UserIDs = Database::singleton()->run( + "SELECT id FROM users WHERE id IN (SELECT userId FROM ownedAssets WHERE assetId = :ConditionData) + AND NOT (SELECT COUNT(*) FROM ownedAssets WHERE userId = users.id AND assetId = :AssetID)", + [":AssetID" => $AssetID, ":ConditionData" => $ConditionData] + )->fetchAll(\PDO::FETCH_COLUMN); + } + + foreach ($UserIDs as $UserID) + { + Database::singleton()->run( + "INSERT INTO ownedAssets (assetId, userId, TagID, timestamp) VALUES (:AssetID, :UserID, :TagID, UNIX_TIMESTAMP())", + [":UserID" => $UserID, ":AssetID" => $AssetID, ":TagID" => $TagID] + ); + } + + Database::singleton()->run( + "UPDATE assets SET Sales = Sales + :UserCount WHERE id = :AssetID", + [":AssetID" => $AssetID, ":UserCount" => count($UserIDs)] + ); + + $Alert = ["text" => sprintf("\"%s\" has been given to %d user(s) (Tag ID %s)", $ItemName, count($UserIDs), $TagID), "color" => "primary"]; + + Users::LogStaffAction(sprintf( + "[ Give Asset ] %s gave \"%s\" (ID %s) to %d user(s) who %s (Tag ID %s)", + SESSION["user"]["username"], $ItemName, $AssetID, count($UserIDs), $ConditionString, $TagID + )); + } +} + +$pageBuilder = new PageBuilder(["title" => "Give Asset"]); +$pageBuilder->buildHeader(); +?> +

Give Asset

+
px-2 py-1" role="alert">
+

Be careful about how you use this. Actions done here can be reverted, but may take a while to roll back.

+
+ Give to anyone who has +
+buildFooter(); ?> diff --git a/directory_admin/give-currency.php b/directory_admin/give-currency.php new file mode 100644 index 0000000..42bb13b --- /dev/null +++ b/directory_admin/give-currency.php @@ -0,0 +1,63 @@ + "Give ".SITE_CONFIG["site"]["currency"]]); +$pageBuilder->buildHeader(); +?> +

Give

+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+

Some notes

+
    +
  • dont mess up the economy with this (please)
  • +
  • to take away just make it a negative number
  • +
  • maximum amount of you can give/take at a time is 500
  • +
  • you cant give someones amount negative (why would you)
  • +
  • this is logged btw lol
  • +
+
+
+ + +buildFooter(); ?> diff --git a/directory_admin/invites.php b/directory_admin/invites.php new file mode 100644 index 0000000..2674ba2 --- /dev/null +++ b/directory_admin/invites.php @@ -0,0 +1,98 @@ +run( + "SELECT TimeCreated FROM InviteTickets WHERE CreatedBy = :UserID AND TimeCreated + 5 > UNIX_TIMESTAMP()", + [":UserID" => SESSION["user"]["id"]] + ); + + if($LastTicket->rowCount()) + { + $Alert = ["Text" => "Please wait ".GetReadableTime($LastTicket->fetchColumn(), ["RelativeTime" => "5 seconds"])." before creating a new invite ticket", "Color" => "danger"]; + } + else + { + $Ticket = sprintf("PolygonTicket(%s)", bin2hex(random_bytes(16))); + + Database::singleton()->run( + "INSERT INTO InviteTickets (Ticket, TimeCreated, CreatedBy) VALUES (:Ticket, UNIX_TIMESTAMP(), :UserID)", + [":Ticket" => $Ticket, ":UserID" => SESSION["user"]["id"]] + ); + + Users::LogStaffAction(sprintf( + "[ Create Invite Ticket ] %s created an invite ticket with the key \"%s\"", + SESSION["user"]["username"], $Ticket + )); + + $Alert = ["Text" => sprintf("Your key has been created! %s", $Ticket), "Color" => "success"]; + } +} + +$Page = $_GET["Page"] ?? 1; +$TicketCount = Database::singleton()->run("SELECT COUNT(*) FROM InviteTickets")->fetchColumn(); + +$Pagination = Pagination($Page, $TicketCount, 20); + +$Tickets = Database::singleton()->run( + "SELECT InviteTickets.*, Users1.username AS CreatedByName, Users2.username AS UsedByName FROM InviteTickets + INNER JOIN users AS Users1 ON Users1.id = InviteTickets.CreatedBy + LEFT JOIN users AS Users2 ON Users2.id = InviteTickets.UsedBy + ORDER BY TimeCreated DESC LIMIT 20 OFFSET :Offset", + [":Offset" => $Pagination->Offset] +); + +$pageBuilder = new PageBuilder(["title" => "Invite Tickets"]); +$pageBuilder->buildHeader(); +?> +
px-2 py-1" role="alert">
+
+
+

Invite Tickets

+
+
+
+ +
+
+
+ + + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + + UsedBy == NULL) { ?> + + + + + + + + +
TicketCreatorUsed ByCreated
Ticket)?>CreatedByName?>No OneUsedByName?>TimeCreated)?>
+Pages > 1) { ?> + + +buildFooter(); ?> diff --git a/directory_admin/manage-gameservers.php b/directory_admin/manage-gameservers.php new file mode 100644 index 0000000..2b649e6 --- /dev/null +++ b/directory_admin/manage-gameservers.php @@ -0,0 +1,159 @@ + "warning", "Loading" => "warning", "Ready" => "success", "Closed" => "primary", "Crashed" => "danger"]; + +$View = $_GET["View"] ?? "Gameservers"; +$Page = $_GET["Page"] ?? 1; + +if ($View == "Gameservers") +{ + $GameserverCount = Database::singleton()->run("SELECT COUNT(*) FROM GameServers")->fetchColumn(); + + $Pagination = Pagination($Page, $GameserverCount, 15); + + $Gameservers = Database::singleton()->run( + "SELECT * FROM GameServers ORDER BY ServerID DESC LIMIT 15 OFFSET :Offset", + [":Offset" => $Pagination->Offset] + ); +} +else if ($View == "GameJobs") +{ + $GameJobCount = Database::singleton()->run("SELECT COUNT(*) FROM GameJobs")->fetchColumn(); + + $Pagination = Pagination($Page, $GameJobCount, 15); + + $GameJobs = Database::singleton()->run( + "SELECT GameJobs.*, GameServers.Name FROM GameJobs + INNER JOIN GameServers ON GameServers.ServerID = GameJobs.ServerID + ORDER BY TimeCreated DESC LIMIT 15 OFFSET :Offset", + [":Offset" => $Pagination->Offset] + ); +} +else if ($View == "GameSessions") +{ + $GameSessionCount = Database::singleton()->run("SELECT COUNT(*) FROM GameJobSessions")->fetchColumn(); + + $Pagination = Pagination($Page, $GameSessionCount, 15); + + $GameSessions = Database::singleton()->run( + "SELECT * FROM GameJobSessions ORDER BY TimeCreated DESC LIMIT 15 OFFSET :Offset", + [":Offset" => $Pagination->Offset] + ); +} + +$pageBuilder = new PageBuilder(["title" => "Manage Games"]); +$pageBuilder->buildHeader(); +?> +

Manage Games

+ + + + + + + + + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + + + + + + + + + + +
IDNameStatusActive JobsCPU UsageAvailable MemoryService PortUpdated
ServerID?>Name?>Online && ($Gameserver->LastUpdated + 65) > time() ? "Online" : "Offline"?>ActiveJobs?>/MaximumJobs?>CpuUsage?>%AvailableMemory?> MBServicePort?>LastUpdated)?>
+ + + + + + + + + + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + + + + + + + + + + + + +
Job IDGameserverStatusVersionPlace IDPlayersAddressPortCreatedUpdated
JobID?>Name?>Status?>Version?>PlaceID?>PlayerCount?>MachineAddress?>ServerPort?>TimeCreated)?>LastUpdated)?>
+ + + + + + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + + + + + + + + +
TicketJob IDUser IDStatusVerifiedCreated
Ticket?>JobID?>UserID?>Active ? "Online" : "Offline"?>Verified ? "Yes" : "No"?>TimeCreated)?>
+Pages > 1) { ?> + + +buildFooter(); ?> diff --git a/directory_admin/moderate-assets.php b/directory_admin/moderate-assets.php new file mode 100644 index 0000000..0083277 --- /dev/null +++ b/directory_admin/moderate-assets.php @@ -0,0 +1,47 @@ + "Moderate Assets"]); +$pageBuilder->addResource("polygonScripts", "/js/polygon/admin/asset-moderation.js"); +$pageBuilder->buildHeader(); +?> +

Asset Moderation

+

To view the template of an asset, click on the thumbnail.

+
+
+
+
+

There are no assets to moderate. Go back

+
+ +
+
+
+ $item_name +
+

$item_name

+

Creator: $creator_name

+

Type: $type

+

Created: $created

+

Price: $price

+ + Request re-render +
+
+
+
+
+buildFooter(); ?> diff --git a/directory_admin/moderate-user.php b/directory_admin/moderate-user.php new file mode 100644 index 0000000..cef557d --- /dev/null +++ b/directory_admin/moderate-user.php @@ -0,0 +1,226 @@ +run("SELECT * FROM bans ORDER BY id DESC"); + +$pageBuilder = new PageBuilder(["title" => "Moderate User"]); +$pageBuilder->addResource("stylesheets", "https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.6.1/css/bootstrap4-toggle.min.css"); +$pageBuilder->addResource("stylesheets", "/css/bootstrap-datepicker.min.css"); +$pageBuilder->addResource("scripts", "https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.6.1/js/bootstrap4-toggle.min.js"); +$pageBuilder->addResource("scripts", "/js/bootstrap-datepicker.min.js"); +$pageBuilder->buildHeader(); +?> + +

User Moderation

+ + + + + +buildFooter(); ?> diff --git a/directory_admin/render-queue.php b/directory_admin/render-queue.php new file mode 100644 index 0000000..1dde603 --- /dev/null +++ b/directory_admin/render-queue.php @@ -0,0 +1,55 @@ +run("SELECT COUNT(*) FROM renderqueue")->fetchColumn(); + +$pages = ceil($count/20); +if($page > $pages) $page = $pages; +if(!is_numeric($page) || $page < 1) $page = 1; +$offset = ($page - 1)*20; + +$query = Database::singleton()->run("SELECT * FROM renderqueue ORDER BY timestampRequested DESC LIMIT 20 OFFSET $offset"); + +$pageBuilder = new PageBuilder(["title" => "Render queue"]); +$pageBuilder->buildHeader(); +?> +

Render queue

+ + + + + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + + + + + + + + +
Job IDTypeData IDRequestedTime takenStatus
jobID?>renderType?>assetID?>timestampRequested)?>renderStatus == 2 ? ($render->timestampCompleted - $render->timestampAcknowledged) . " seconds" : "N/A"?>renderStatus)?>">renderStatus)?>
+ 1) { ?> + + +buildFooter(); ?> diff --git a/directory_admin/site-banners.php b/directory_admin/site-banners.php new file mode 100644 index 0000000..8cf5d6a --- /dev/null +++ b/directory_admin/site-banners.php @@ -0,0 +1,136 @@ + 128) $error = "The banner text must be less than 128 characters"; + elseif(!in_array($textcolor, ["light", "dark"])) $error = "That doesn't appear to be a valid text color"; + elseif(empty($backcolor)) $error = "You haven't set a background color"; + elseif(!ctype_xdigit(ltrim($backcolor, "#"))) $error = "That doesn't appear to be a valid background color"; + elseif(Database::singleton()->run("SELECT COUNT(*) FROM announcements WHERE activated")->fetchColumn() > 5) $error = "There's too many banners currently active!"; + else + { + Database::singleton()->run( + "INSERT INTO announcements (createdBy, text, bgcolor, textcolor) VALUES (:uid, :text, :bgc, :tc)", + [":uid" => SESSION["user"]["id"], ":text" => $text, ":bgc" => $backcolor, ":tc" => $textcolor] + ); + + Users::LogStaffAction("[ Banners ] Created site banner with text: ".$text); + } + } + else//if($mode == "delete") + { + $panel = "manage"; + $id = $_POST['delete'] ?? false; + Database::singleton()->run("UPDATE announcements SET activated = 0 WHERE id = :id", [":id" => $id]); + } + + Polygon::GetAnnouncements(); +} + +$pageBuilder = new PageBuilder(["title" => "Site banners"]); +$pageBuilder->addResource("stylesheets", "/css/bootstrap-colorpicker.min.css"); +$pageBuilder->addResource("scripts", "/js/bootstrap-colorpicker.min.js"); +$pageBuilder->addResource("scripts", "https://cdnjs.cloudflare.com/ajax/libs/markdown-it/11.0.1/markdown-it.min.js"); +$pageBuilder->buildHeader(); +?> + +

Site banners

+
+ + +
+ + + +buildFooter(); ?> diff --git a/directory_admin/staff-audit.php b/directory_admin/staff-audit.php new file mode 100644 index 0000000..8f00b1f --- /dev/null +++ b/directory_admin/staff-audit.php @@ -0,0 +1,111 @@ + "%", + "UserModeration" => "[ User Moderation ]%", + "AssetModeration" => "[ Asset Moderation ]%", + "AssetCreation" => "[ Asset creation ]%", + "Forums" => "[ Forums ]%", + "Currency" => "[ Currency ]%", + "Banners" => "[ Banners ]%", + "Render" => "[ Render ]%", +]; + +$Filter = $_GET["Filter"] ?? "All"; +$FilterSQL = $FilterCategories[$Filter] ?? "%"; + +$Query = $_GET["Query"] ?? ""; +$QuerySQL = empty($Query) ? "%" : "%{$Query}%"; + +$page = $_GET['Page'] ?? 1; + +$count = Database::singleton()->run( + "SELECT COUNT(*) FROM stafflogs WHERE action LIKE :filterBy AND action LIKE :query", + [":filterBy" => $FilterSQL, ":query" => $QuerySQL] +)->fetchColumn(); + +$pages = ceil($count/15); +if($page > $pages) $page = $pages; +if(!is_numeric($page) || $page < 1) $page = 1; +$offset = ($page - 1)*15; + +$logs = Database::singleton()->run( + "SELECT * FROM stafflogs WHERE action LIKE :filterBy AND action LIKE :query ORDER BY id DESC LIMIT 15 OFFSET $offset", + [":filterBy" => $FilterSQL, ":query" => $QuerySQL] +); + +function buildURL($page) +{ + global $Filter; + global $Query; + + $url = "?"; + $url .= "Filter=$Filter&"; + if(!empty($Query)) $url .= "Query=$Query&"; + $url .= "Page=$page"; + return $url; +} + +$pageBuilder = new PageBuilder(["title" => "Staff Logs"]); +$pageBuilder->buildHeader(); +?> + +
+
+

Audit Log

+
+
+
+
+ +
+ +
+ +
+
+
+
+ + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + + + + + +
TimeDone byAction
time)?>adminId)?>action)?>
+ 1) { ?> + + +buildFooter(); ?> diff --git a/directory_admin/transactions.php b/directory_admin/transactions.php new file mode 100644 index 0000000..a730597 --- /dev/null +++ b/directory_admin/transactions.php @@ -0,0 +1,81 @@ + "Transactions"]); + +$Data = (object) +[ + "Type" => "", + "Name" => "" +]; + +if (isset($_GET["UserID"])) +{ + $UserInfo = Users::GetInfoFromID($_GET["UserID"]); + if (!$UserInfo) $pageBuilder->errorCode(404); + + $Data->Type = "User"; + $Data->Name = $UserInfo->username; + + $pageBuilder->addAppAttribute("data-user-id", $UserInfo->id); +} +else if (isset($_GET["AssetID"])) +{ + $AssetInfo = Catalog::GetAssetInfo($_GET["AssetID"]); + if (!$AssetInfo) $pageBuilder->errorCode(404); + + $Data->Type = "Asset"; + $Data->Name = $AssetInfo->name; + + $pageBuilder->addAppAttribute("data-asset-id", $AssetInfo->id); +} +else +{ + $pageBuilder->errorCode(404); +} + + +$pageBuilder->addResource("polygonScripts", "/js/polygon/admin/transactions.js"); +$pageBuilder->buildHeader(); +?> +
+ Type == "User") { ?> +

Name?>'s Transactions

+

Transactions with a red background color indicates a flagged transaction - no money was transferred

+
+ + +
+ +

Transactions for "Name)?>"

+

Transactions with a red background color indicates a flagged transaction - no money was transferred

+ +
+ + + + + + + + + + +
DateMemberDescriptionAmount
+
+ +
+buildFooter(); ?> diff --git a/directory_forum/addpost.php b/directory_forum/addpost.php new file mode 100644 index 0000000..989263c --- /dev/null +++ b/directory_forum/addpost.php @@ -0,0 +1,169 @@ +deleted){ PageBuilder::instance()->errorCode(404); } + + $subforumId = $threadInfo->subforumid; +} +elseif(isset($_GET['ForumID'])) +{ + $threadInfo = false; + $subforumId = $_GET['ForumID']; +} +else +{ + PageBuilder::instance()->errorCode(404); +} + +$subforumInfo = Forum::GetSubforumInfo($subforumId); +if(!$subforumInfo){ PageBuilder::instance()->errorCode(404); } +if(!$threadInfo && $subforumInfo->minadminlevel && SESSION["user"]["adminlevel"] < $subforumInfo->minadminlevel){ PageBuilder::instance()->errorCode(404); } + +$errors = ["subject"=>false, "body"=>false, "general"=>false]; +$subject = $body = false; + +if($_SERVER['REQUEST_METHOD'] == "POST") +{ + $subject = $_POST["subject"] ?? ""; + $body = $_POST["body"] ?? ""; + $userid = SESSION["user"]["id"]; + + if(!$threadInfo) + { + if(!strlen($subject)) $errors["subject"] = "Subject cannot be empty"; + else if(strlen($subject) > 64) $errors["subject"] = "Subject must be shorter than 64 characters"; + else if(Polygon::IsExplicitlyFiltered($subject)) $errors["subject"] = "Subject contains inappropriate text"; + } + + if(!strlen($body)) $errors["body"] = "Body cannot be empty"; + else if(strlen($body) > 10000) $errors["body"] = "Body must be shorter than 10,000 characters"; + else if(Polygon::IsExplicitlyFiltered($body)) $errors["subject"] = "Body contains inappropriate text"; + + $floodcheck = Database::singleton()->run( + "SELECT (SELECT COUNT(*) FROM forum_threads WHERE author = :uid AND postTime+30 > UNIX_TIMESTAMP()) + + (SELECT COUNT(*) FROM forum_replies WHERE author = :uid AND postTime+30 > UNIX_TIMESTAMP()) AS floodcheck", + [":uid" => SESSION["user"]["id"]] + )->fetchColumn(); + + if($floodcheck) $errors["general"] = "Please wait 30 seconds before sending another forum post"; + + if(!$errors["subject"] && !$errors["body"] && !$errors["general"]) + { + if($threadInfo) + { + Database::singleton()->run( + "UPDATE forum_threads SET bumpIndex = UNIX_TIMESTAMP() WHERE id = :threadId; + UPDATE users SET ForumReplies = ForumReplies + 1 WHERE id = :author; + INSERT INTO forum_replies (body, threadId, author, postTime) VALUES (:body, :threadId, :author, UNIX_TIMESTAMP());", + [":body" => $body, ":threadId" => $threadInfo->id, ":author" => SESSION["user"]["id"]] + ); + + redirect("/forum/showpost?PostID=".$threadInfo->id."#reply".Database::singleton()->lastInsertId()); + } + else + { + Database::singleton()->run( + "UPDATE users SET ForumThreads = ForumThreads + 1 WHERE id = :author; + INSERT INTO forum_threads (subject, body, subforumid, author, postTime, bumpIndex) + VALUES (:subject, :body, :subId, :author, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());", + [":subject" => $subject, ":body" => $body, ":subId" => $subforumId, ":author" => SESSION["user"]["id"]] + ); + + redirect("/forum/showpost?PostID=".Database::singleton()->lastInsertId()); + } + } +} + +$pageBuilder = new PageBuilder(["title" => "New ".($threadInfo?"Reply":"Post")]); +$pageBuilder->addResource("stylesheets", "/css/simplemde.min.css"); +$pageBuilder->addResource("scripts", "/js/simplemde.min.js"); +$pageBuilder->buildHeader(); +?> + + + +

New

+
+
+
+ +
+ +
+ +
+
+ +
+ +
+ " id="subject" name="subject" placeholder="64 characters max" value="" required> +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ + + +
+
+
+
+
+ Markdown +
+
+ Markdown is supported, allowing you to format your forum post.
Learn more about how to use markdown here. +
+
+
+
+ + + + + +buildFooter(); ?> diff --git a/directory_forum/showpost.php b/directory_forum/showpost.php new file mode 100644 index 0000000..4224a15 --- /dev/null +++ b/directory_forum/showpost.php @@ -0,0 +1,141 @@ +deleted && (!SESSION || !SESSION["user"]["adminlevel"])) PageBuilder::instance()->errorCode(404); + +$authorInfo = Users::GetInfoFromID($threadInfo->author); + +//markdown +$markdown = new Parsedown(); +$markdown->setMarkupEscaped(true); +$markdown->setBreaksEnabled(true); +$markdown->setSafeMode(true); +$markdown->setUrlsLinked(true); + +//reply pagination +$page = $_GET['page'] ?? 1; + +$repliesCount = Database::singleton()->run( + "SELECT COUNT(*) FROM forum_replies WHERE threadId = :id AND NOT deleted", + [":id" => $threadInfo->id] +)->fetchColumn(); + +$pagination = Pagination($page, $repliesCount, 10); + +$subforumInfo = Forum::GetSubforumInfo($threadInfo->subforumid); + +$replies = Database::singleton()->run( + "SELECT * FROM forum_replies WHERE threadId = :id AND NOT deleted + ORDER BY id ASC LIMIT 10 OFFSET :offset", + [":id" => $threadInfo->id, ":offset" => $pagination->Offset] +); + +Pagination::$page = $page; +Pagination::$pages = $pagination->Pages; +Pagination::$url = '/forum/showpost?PostID='.$threadInfo->id.'&page='; +Pagination::initialize(); + +$pageBuilder = new PageBuilder(["title" => Polygon::FilterText($threadInfo->subject, true, false)." - ".Polygon::ReplaceVars($subforumInfo->name)]); +$pageBuilder->addMetaTag("og:description", Polygon::FilterText($threadInfo->body, true, false)); +$pageBuilder->buildHeader(); +?> + + +
+
+ New Reply + deleted?'[ This is a deleted thread ]':''?> +
+ Pages > 1) { ?> +
+ +
+ +
+ +
+
+ subject)?> +
+
+
+
+
+

username?>adminlevel == 2) { ?>

+ <?=$authorInfo->username?> +

Joined: jointime)?>

+

Total posts: ForumThreads + $authorInfo->ForumReplies?>

+
+
+ Posted on postTime);?> + + + Thread ID id?> ›› + Edit + Delete + + +
+ text($threadInfo->body, $authorInfo->adminlevel == 2), false)?> +
+
+
+fetch(\PDO::FETCH_OBJ)) { $authorInfo = Users::GetInfoFromID($reply->author); ?> +
+
+
+

username?> adminlevel == 2) { ?>

+ <?=$authorInfo->username?> +

Joined: jointime)?>

+

Total posts: ForumThreads + $authorInfo->ForumReplies?>

+
+
+ Posted on postTime);?> deleted){ ?>This is a deleted reply + + + Reply ID id?> ›› + Edit + Delete + + +
+ text($reply->body, $authorInfo->adminlevel == 2), false)?> +
+
+
+ +
+
+ New Reply + deleted?'[ This is a deleted thread ]':''?> +
+
+ +
+
+
+ + +buildFooter(); ?> diff --git a/directory_login/2fa.php b/directory_login/2fa.php new file mode 100644 index 0000000..1285642 --- /dev/null +++ b/directory_login/2fa.php @@ -0,0 +1,76 @@ +checkCode(SESSION["user"]["twofaSecret"], $code, 1)) + { + $error = "Incorrect 2FA code"; + } + else if (!is_numeric($code) && (!isset($recoveryCodes[$code]) || !$recoveryCodes[$code])) + { + $error = "Invalid recovery code"; + } + else + { + // invalidate recovery code + if (!is_numeric($code)) + { + $recoveryCodes[$code] = false; + $recoveryCodes = json_encode($recoveryCodes); + + Database::singleton()->run( + "UPDATE users SET twofaRecoveryCodes = :recoveryCodes WHERE id = :uid", + [":recoveryCodes" => $recoveryCodes, ":uid" => SESSION["user"]["id"]] + ); + } + + Database::singleton()->run("UPDATE sessions SET twofaVerified = 1 WHERE sessionKey = :key", [":key" => SESSION["sessionKey"]]); + + if (isset($_GET['ReturnUrl'])) + redirect($_GET['ReturnUrl']); + + redirect("/"); + } +} + +$pageBuilder = new PageBuilder(["ShowNavbar" => !$studio]); +$pageBuilder->buildHeader(); +?> +

Two-Factor Authentication

+
+
+

Get the code from your 2FA app, or use a one-time recovery code

+
+
+ +
+ + > +
+
+ +
+
+
+
+
+ +buildFooter(); ?> diff --git a/directory_places/create.php b/directory_places/create.php new file mode 100644 index 0000000..3da25bc --- /dev/null +++ b/directory_places/create.php @@ -0,0 +1,371 @@ +run("SELECT COUNT(*) FROM assets WHERE creator = :UserID AND type = 9", [":UserID" => SESSION["user"]["id"]])->fetchColumn(); +if ($PlaceCount >= SESSION["user"]["PlaceSlots"]) +{ + PageBuilder::instance()->errorCode(200, [ + "title" => "Maximum place slots reached", + "text" => "You have reached the maximum number of place slots. Update any spare existing place slots you may have." + ]); +} + +$TemplatePlaces = Database::singleton()->run("SELECT * FROM assets WHERE TemplateOrder IS NOT NULL ORDER BY TemplateOrder")->fetchAll(); + +$PlayerCounts = range(1, 20); +$Versions = [2010, 2011, 2012]; +$Error = false; + +function IsTemplatePlace($PlaceID) +{ + global $TemplatePlaces; + + foreach ($TemplatePlaces as $TemplatePlace) + { + if ($TemplatePlace["id"] == $PlaceID) return true; + } + + return false; +} + +if ($_SERVER["REQUEST_METHOD"] == "POST") +{ + $Name = $_POST["Name"] ?? ""; + $Description = $_POST["Description"] ?? ""; + $PlayerCount = $_POST["PlayerCount"] ?? 10; + $Access = $_POST["Access"] ?? "Everyone"; + $Version = $_POST["Version"] ?? 2010; + $PlaceTemplate = $_POST["PlaceTemplateSelection"] ?? "none"; + $ChatType = $_POST["ChatType"] ?? "Classic"; + + $Copylocked = ($_POST["Copylocked"] ?? "") == "on"; + $CommentsAllowed = ($_POST["CommentsAllowed"] ?? "") == "on"; + + $PlaceUpload = $_FILES["PlaceUpload"] ?? false; + + Catalog::ParseGearAttributes(); + + if (!strlen($Name)) + { + $Error = "Place Name is required"; + } + else if (strlen($Name) > 50) + { + $Error = "Place Name cannot be longer than 50 characters"; + } + else if (Polygon::IsExplicitlyFiltered($Name)) + { + $Error = "Place Name contains inappropriate text"; + } + else if (strlen($Description) > 1000) + { + $Error = "Place Description cannot be longer than 1000 characters"; + } + else if (Polygon::IsExplicitlyFiltered($Description)) + { + $Error = "Place Description contains inappropriate text"; + } + else if ($PlaceTemplate == "none") + { + $Error = "No Place Template has been selected"; + } + else if ($PlaceTemplate != "custom" && !IsTemplatePlace($PlaceTemplate)) + { + $Error = "Invalid Place Template selected"; + } + else if (!in_array((int)$PlayerCount, $PlayerCounts)) + { + $Error = "Maximum Visitor Count must be within 1 - 20 Players"; + } + else if (!in_array((int)$Version, $Versions)) + { + $Error = "Invalid Place Version selected"; + } + else if (!in_array($Access, ["Everyone", "Friends"])) + { + $Error = "Invalid access level selected"; + } + else if ($Access != "Friends" && $Version != "2011") + { + $Error = "2010 and 2012 places must be set to friends-only"; + } + else if (!in_array($ChatType, ["Classic", "Bubble", "Both"])) + { + $Error = "Invalid Chat Type selected"; + } + else if ($PlaceTemplate == "custom" && ($PlaceUpload === false || $PlaceUpload["size"] == 0)) + { + $Error = "No Place File has been selected for upload"; + } + else if ($PlaceTemplate == "custom" && $PlaceUpload["size"] > 32000000) + { + $Error = "Place File cannot be larger than 32 megabytes"; + } + else + { + if ($PlaceTemplate == "custom") + { + $PlaceXML = file_get_contents($PlaceUpload["tmp_name"]); + $PlaceXML = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset/?id=", "%ASSETURL%", $PlaceXML); + $PlaceXML = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset?id=", "%ASSETURL%", $PlaceXML); + $PlaceXML = preg_replace("/rbxasset:\/\/..\/[^<]*/", "", $PlaceXML); + + + libxml_use_internal_errors(true); + $SimpleXML = simplexml_load_string($PlaceXML); + + if ($SimpleXML === false) + { + // temporary hack + foreach (libxml_get_errors() as $XMLError) + { + // ignore "invalid xmlChar value" error + // this can trigger false positives as some scripts may use binary xml characters + if ($XMLError->code != 9) + { + $Error = "Place File is invalid, are you sure it is an older format place file?"; + break; + } + } + } + } + } + + if (!$Error) + { + $PlaceID = Catalog::CreateAsset([ + "type" => 9, + "creator" => SESSION["user"]["id"], + "name" => $Name, + "description" => $Description, + "comments" => (int)$CommentsAllowed, + "PublicDomain" => (int)!$Copylocked, + "ServerRunning" => 0, + "MaxPlayers" => $PlayerCount, + "Access" => $Access, + "ActivePlayers" => 0, + "Visits" => 0, + "Version" => $Version, + "ChatType" => $ChatType, + "gear_attributes" => json_encode(Catalog::$GearAttributes), + "approved" => 1 + ]); + + $PlaceLocation = Polygon::GetSharedResource("assets/{$PlaceID}"); + + if ($PlaceTemplate == "custom") + { + file_put_contents($PlaceLocation, $PlaceXML); + Gzip::Compress($PlaceLocation); + } + else + { + copy(Polygon::GetSharedResource("assets/{$PlaceTemplate}"), $PlaceLocation); + } + + Polygon::RequestRender("Place", $PlaceID); + + redirect("/" . encode_asset_name($Name) . "-place?id={$PlaceID}"); + } +} + +$pageBuilder = new PageBuilder(["title" => "Create Place"]); +$pageBuilder->buildHeader(); +?> +
+

Create Place

+ + +
+
+

Place Templates

+
+ +
+
"> + " alt="" src=""> +
+

">

+
+
+
+ +
+
+ Create from Place File"> +
+

Create from Place File

+
+
+
+
+
+
+

Basic Settings

+
+ + 's Place= 1 ? " Number: " . ($PlaceCount+1) : ""?>" maxlength="50" class="form-control form-control-sm"> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+

Gear Permissions

+ +
+ $GearEnabled) { ?> +
+
+ + +
+
+ +
+
+

Other Permissions

+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + +
+ + +buildFooter(); ?> diff --git a/directory_places/update.php b/directory_places/update.php new file mode 100644 index 0000000..6230403 --- /dev/null +++ b/directory_places/update.php @@ -0,0 +1,393 @@ +run("SELECT * FROM assets WHERE TemplateOrder IS NOT NULL ORDER BY TemplateOrder")->fetchAll(); + +$PlayerCounts = range(1, 20); +$Versions = [2010, 2011, 2012]; +$Error = false; + +$PlaceInfo = Catalog::GetAssetInfo($PlaceID); +if (!$PlaceInfo || $PlaceInfo->type != 9) PageBuilder::instance()->errorCode(404); +if (!$IsAdmin && $PlaceInfo->creator != SESSION["user"]["id"]) redirect(encode_asset_name($PlaceInfo->name) . "-place?id={$PlaceID}"); + +Catalog::$GearAttributes = json_decode($PlaceInfo->gear_attributes, true); + +function IsTemplatePlace($PlaceID) +{ + global $TemplatePlaces; + + foreach ($TemplatePlaces as $TemplatePlace) + { + if ($TemplatePlace["id"] == $PlaceID) return true; + } + + return false; +} + +if ($_SERVER["REQUEST_METHOD"] == "POST") +{ + $Name = $_POST["Name"] ?? ""; + $Description = $_POST["Description"] ?? ""; + $PlayerCount = $_POST["PlayerCount"] ?? 10; + $Access = $_POST["Access"] ?? "Everyone"; + $Version = $_POST["Version"] ?? 2010; + $PlaceTemplate = $_POST["PlaceTemplateSelection"] ?? "none"; + $ChatType = $_POST["ChatType"] ?? "Classic"; + + $Copylocked = ($_POST["Copylocked"] ?? "") == "on"; + $CommentsAllowed = ($_POST["CommentsAllowed"] ?? "") == "on"; + + $PlaceUpload = $_FILES["PlaceUpload"] ?? false; + + Catalog::ParseGearAttributes(); + + if (!strlen($Name)) + { + $Error = "Place Name is required"; + } + else if (strlen($Name) > 50) + { + $Error = "Place Name cannot be longer than 50 characters"; + } + else if (Polygon::IsExplicitlyFiltered($Name)) + { + $Error = "Place Name contains inappropriate text"; + } + else if (strlen($Description) > 1000) + { + $Error = "Place Description cannot be longer than 1000 characters"; + } + else if (Polygon::IsExplicitlyFiltered($Description)) + { + $Error = "Place Description contains inappropriate text"; + } + else if ($PlaceTemplate != "none" && $PlaceTemplate != "custom" && !IsTemplatePlace($PlaceTemplate)) + { + $Error = "Invalid Place Template selected"; + } + else if (!in_array((int)$PlayerCount, $PlayerCounts)) + { + $Error = "Maximum Visitor Count must be within 1 - 20 Players"; + } + else if (!in_array((int)$Version, $Versions)) + { + $Error = "Invalid Place Version selected"; + } + else if (!in_array($Access, ["Everyone", "Friends"])) + { + $Error = "Invalid access level selected"; + } + else if ($Access != "Friends" && $Version != "2011") + { + $Error = "2010 and 2012 places must be set to friends-only"; + } + else if (!in_array($ChatType, ["Classic", "Bubble", "Both"])) + { + $Error = "Invalid Chat Type selected"; + } + else if ($PlaceTemplate == "custom" && ($PlaceUpload === false || $PlaceUpload["size"] == 0)) + { + $Error = "No Place File has been selected for upload"; + } + else if ($PlaceTemplate == "custom" && $PlaceUpload["size"] > 32000000) + { + $Error = "Place File cannot be larger than 32 megabytes"; + } + else + { + if ($PlaceTemplate == "custom") + { + $PlaceXML = file_get_contents($PlaceUpload["tmp_name"]); + $PlaceXML = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset/?id=", "%ASSETURL%", $PlaceXML); + $PlaceXML = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset?id=", "%ASSETURL%", $PlaceXML); + $PlaceXML = preg_replace("/rbxasset:\/\/..\/[^<]*/", "", $PlaceXML); + + libxml_use_internal_errors(true); + $SimpleXML = simplexml_load_string($PlaceXML); + + if ($SimpleXML === false) + { + // temporary hack + ini_set('memory_limit', '256M'); + foreach (libxml_get_errors() as $XMLError) + { + // ignore "invalid xmlChar value" error + // this can trigger false positives as some scripts may use binary xml characters + if ($XMLError->code != 9) + { + $Error = "Place File is invalid, are you sure it is an older format place file?"; + break; + } + } + ini_set('memory_limit', '128M'); + } + } + } + + if (!$Error) + { + Database::singleton()->run( + "UPDATE assets + SET name = :Name, + description = :Description, + comments = :CommentsAllowed, + publicDomain = :PublicDomain, + MaxPlayers = :MaxPlayers, + Access = :Access, + Version = :Version, + ChatType = :ChatType, + gear_attributes = :Gears, + updated = UNIX_TIMESTAMP() + WHERE id = :PlaceID", + [ + ":PlaceID" => $PlaceID, + ":Name" => $Name, + ":Description" => $Description, + ":CommentsAllowed" => (int)$CommentsAllowed, + ":PublicDomain" => (int)!$Copylocked, + ":MaxPlayers" => $PlayerCount, + ":Access" => $Access, + ":Version" => $Version, + ":ChatType" => $ChatType, + ":Gears" => json_encode(Catalog::$GearAttributes) + ] + ); + + if ($PlaceTemplate != "none") + { + $PlaceLocation = Polygon::GetSharedResource("assets/{$PlaceID}"); + unlink($PlaceLocation); + + if ($PlaceTemplate == "custom") + { + file_put_contents($PlaceLocation, $PlaceXML); + Gzip::Compress($PlaceLocation); + } + else + { + copy(Polygon::GetSharedResource("assets/{$PlaceTemplate}"), $PlaceLocation); + } + + Polygon::RequestRender("Place", $PlaceID); + } + + if (SESSION["user"]["id"] != $PlaceInfo->creator && $PlaceInfo->creator != 1) + { + Users::LogStaffAction("[ Asset Modification ] Updated \"{$Name}\" [Place ID {$PlaceID}]"); + } + + redirect("/" . encode_asset_name($Name) . "-place?id={$PlaceID}"); + } +} + +$pageBuilder = new PageBuilder(["title" => "Configure Place"]); +$pageBuilder->buildHeader(); +?> +
+

Configure Place

+ +
+
+ +
+
+
+
+

Basic Settings

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+

Gear Permissions

+ +
+ $GearEnabled) { ?> +
+
+ > + +
+
+ +
+
+

Other Permissions

+
+ + +
+
+ publicDomain ? "" : " checked=\"checked\""?>> + +
+
+ comments ? " checked=\"checked\"" : ""?>> + +
+
+
+

Place Templates

+
+ +
+
"> + " alt="" src=""> +
+

">

+
+
+
+ +
+
+ Create from Place File"> +
+

Create from Place File

+
+
+
+
+
+
+ + + + +
+
+
+ + +buildFooter(); ?> diff --git a/discord.php b/discord.php new file mode 100644 index 0000000..52ba8ae --- /dev/null +++ b/discord.php @@ -0,0 +1 @@ +buildHeader(); +?> + +buildFooter(); ?> \ No newline at end of file diff --git a/error.php b/error.php new file mode 100644 index 0000000..72aae18 --- /dev/null +++ b/error.php @@ -0,0 +1,80 @@ +errorCode($_GET['code']); +} + +$info = false; + +if (isset($_GET["id"]) && $_GET["verbose"] ?? "" == "true") +{ + require $_SERVER['DOCUMENT_ROOT'] . "/api/private/classes/pizzaboxer/ProjectPolygon/ErrorHandler.php"; + $info = ErrorHandler::getLog($_GET["id"])["Message"] ?? false; +} + +?> + + + + Project Polygon + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..29109e6 Binary files /dev/null and b/favicon.ico differ diff --git a/forum.php b/forum.php new file mode 100644 index 0000000..d1d28af --- /dev/null +++ b/forum.php @@ -0,0 +1,190 @@ +errorCode(404); + + $page = isset($_GET['page']) && is_numeric($_GET['page']) ? intval($_GET['page']) : 1; + $searchquery = isset($_GET['searchq']) ? "%".$_GET['searchq']."%" : "%"; + + $threadcount = Database::singleton()->run( + "SELECT COUNT(*) FROM forum_threads WHERE subforumid = :id AND NOT deleted AND (subject LIKE :query OR body LIKE :query)", + [":id" => $subforumInfo->id, ":query" => $searchquery] + ); + + $pages = ceil($threadcount->fetchColumn()/20); + $offset = intval(($page - 1)*20); + + $threads = Database::singleton()->run( + "SELECT * FROM forum_threads WHERE subforumid = :id AND NOT deleted AND (subject LIKE :query OR body LIKE :query) + ORDER BY pinned, bumpIndex DESC LIMIT 20 OFFSET $offset", + [":id" => $subforumInfo->id, ":query" => $searchquery] + ); + + Pagination::$page = $page; + Pagination::$pages = $pages; + Pagination::$url = '/forum?ID='.$subforumInfo->id.'&page='; + Pagination::initialize(); + + $isSubforum = true; +} +else +{ + $forums = Database::singleton()->run("SELECT * FROM forum_forums"); + $isSubforum = false; +} + +if($isSubforum) +{ + $pageBuilder = new PageBuilder(["title" => Polygon::ReplaceVars($subforumInfo->name)." - ".SITE_CONFIG["site"]["name_secondary"]." Forum"]); + $pageBuilder->addMetaTag("og:description", $subforumInfo->description); +} +else +{ + $pageBuilder = new PageBuilder(["title" => SITE_CONFIG["site"]["name_secondary"]." Forum"]); + $pageBuilder->addMetaTag("og:description", "Discourse with the community here!"); +} + +$pageBuilder->buildHeader(); +?> + + + +
+
+ = $subforumInfo->minadminlevel){ ?> Create Post +
+
+
+ + +
+ +
+
+
+
+
+ + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + + + + + + + + +
SubjectAuthorRepliesLast Post
+ +
+ subject)?> +
+
+
author)?>id)?>bumpIndex)?>
id.'"> Create Post'?>
+
+
+
+ = $subforumInfo->minadminlevel){ ?> Create Post +
+
+ +
+
+ +fetch(\PDO::FETCH_OBJ)){ ?> +
+ + + + + + + + + run( + "SELECT * FROM forum_subforums WHERE forumid = :id ORDER BY displayposition ASC", + [":id" => $forum->id] + ); + + while ($subforum = $subforums->fetch(\PDO::FETCH_OBJ)) + { + $lastactive = Database::singleton()->run( + "SELECT bumpIndex FROM forum_threads WHERE subforumid = :id AND NOT deleted ORDER BY bumpIndex DESC LIMIT 1", + [":id" => $subforum->id] + )->fetchColumn(); + ?> + + + + + + + + +
name)?>ThreadsPostsLast Post
+ +
+
name)?>
+ description)?> +
+
+
id)?>id, true)?>
+
+ + + + +buildFooter(); ?> diff --git a/friends.php b/friends.php new file mode 100644 index 0000000..871b7de --- /dev/null +++ b/friends.php @@ -0,0 +1,117 @@ +errorCode(404); + +$pageBuilder = new PageBuilder(["title" => $selfFriends ? "My Friends" : $username."'s Friends"]); +$pageBuilder->addAppAttribute("data-user-id", $userid); +$pageBuilder->addResource("polygonScripts", "/js/polygon/friends.js"); +$pageBuilder->buildHeader(); +?> + +

Friends

+ +
+
+
+

+
+ + +
+
+
+ +
+ +
+

+
+ +
+
+
+ " data-src="$Avatar" title="$Username" alt="$Username"> + $Username +
+ Accept + Decline +
+
+
+
+
+ +
+buildFooter(); ?> diff --git a/game/fixassets.php b/game/fixassets.php new file mode 100644 index 0000000..0015734 --- /dev/null +++ b/game/fixassets.php @@ -0,0 +1,47 @@ + +local Properties = {"Texture","TextureId","SoundId","MeshId","SkyboxUp","SkyboxLf","SkyboxBk","SkyboxRt","SkyboxFt","SkyboxDn","PantsTemplate","ShirtTemplate","Graphic","Image","LinkedSource","AnimationId"} + +local AssetURLs = {"http://www%.roblox%.com/asset","http://%roblox%.com/asset"} + +function GetDescendants(c) + local d = {} + function FindChildren(e) + for f,g in pairs(e:GetChildren()) do + table.insert(d,g) + FindChildren(g) + end + end + FindChildren(c) + return d +end + +local h = 0 + +print("Replacing Asset URLs, please wait...") + +for i,g in pairs(GetDescendants(game)) do + for f,j in pairs(Properties) do + pcall(function() + if g[j] and not g:FindFirstChild(j) then + assetText = string.lower(g[j]) + for f,k in pairs(AssetURLs) do + g[j], matches=string.gsub(assetText, k, "https://assetdelivery%.roblox%.com/v1/asset") + if matches > 0 then + h = h + 1 + print("Replaced "..j.." asset link for "..g.Name) + break + end + end + end + end) + end +end + +print("Done! Replaced " .. h .. " URLs") + "Games"]); +$pageBuilder->addResource("scripts", "/js/protocolcheck.js"); +$pageBuilder->addResource("polygonScripts", "/js/polygon/games.js"); +$pageBuilder->buildHeader(); +?> +
+
+
+

Games

+
+
+ Filter By: + +
+
+ Version: + +
+
+   +
+ > +
+ +
+
+
+
+
+ +

+
+
+ +
+
+ +
+
+
+

by $CreatorName

+

played $Visits times

+
+
+
+
+
+
+buildFooter(); ?> diff --git a/groups.php b/groups.php new file mode 100644 index 0000000..c9fbe57 --- /dev/null +++ b/groups.php @@ -0,0 +1,296 @@ +errorCode(404); +} +else +{ + Users::RequireLogin(); + + if(!$MyGroups->rowCount()) $HasGroups = false; + else $GroupInfo = Groups::GetLastGroupUserJoined(SESSION["user"]["id"]); +} + +$pageBuilder = new PageBuilder(["title" => $HasGroups ? Polygon::FilterText($GroupInfo->name).", a Group by ".$GroupInfo->ownername : "My Groups"]); + +if($HasGroups) +{ + $GroupsCount = SESSION ? $MyGroups->rowCount() : 0; + $Emblem = Thumbnails::GetAssetFromID($GroupInfo->emblem); + $Status = Groups::GetGroupStatus($GroupInfo->id); + $Ranks = Groups::GetGroupRanks($GroupInfo->id); + $MyRank = Groups::GetUserRank(SESSION["user"]["id"] ?? 0, $GroupInfo->id); + + if(!$MyRank) throw new Exception("Groups::GetUserRank() returned false, the group roles might have updated"); + + $pageBuilder->addAppAttribute("data-group-id", $GroupInfo->id); + $pageBuilder->addMetaTag("og:description", Polygon::FilterText($GroupInfo->description)); + $pageBuilder->addMetaTag("og:image", $Emblem); + $pageBuilder->addResource("polygonScripts", "/js/polygon/groups.js"); +} + +$pageBuilder->buildHeader(); +?> +
+
+
+ +
+
+ +
+
+
+ +
+
+ + + +
+
+
+
+
+
+ +
+
+

Owned By: owner)?>

+

Members: MemberCount?>

+ Level == 0) { ?> + = 20) { ?> + + + + + +

My Rank: Name)?>

+ +
+
+
+
+

name)?>

+

description) ? nl2br(Polygon::FilterText($GroupInfo->description)) : "No description available."?>

+ Permissions->CanViewGroupStatus) { ?> + +
+ text)?> +
+

username?> - timestamp, ["Threshold" => "1 day ago"])?>

+ Permissions->CanPostGroupStatus) { ?> +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+ +
+
+ +

+
+
+ + +
+
+
+ +

+
+
+ + +
+
+
+ +

+
+
+ + +
+
+

Groups have the ability to create and sell official shirts, pants, and t-shirts! All revenue goes to group funds.

+
+
+ Permissions->CanViewGroupWall) { ?> +
+
Wall
+
+ Permissions->CanPostOnGroupWall) { ?> +
+
+
+ + +
+
+ +
+
+
+ +
+
+ +

+
+
+ +
+
+
+
+ "data-src="$avatar" class="img-fluid"> +
+
+

$content

+ $time by $usernamePermissions->CanDeleteGroupWallPosts) { ?> | Delete +
+
+
+
+
+
+
+ +
+
+
+
+ Level != 0) { ?> +
+
+
Controls
+ Permissions->CanManageGroupAdmin) { ?>Group Admin + owner != SESSION["user"]["id"]) { ?> + Permissions->CanViewAuditLog) { ?>Audit Log +
+
+ +
+
+ +

You are not currently in any groups. Search for some above, or create one!

+ +buildFooter(); \ No newline at end of file diff --git a/home.php b/home.php new file mode 100644 index 0000000..e0128e2 --- /dev/null +++ b/home.php @@ -0,0 +1,137 @@ + "Home"]); +$pageBuilder->addAppAttribute("data-user-id", SESSION["user"]["id"]); +$pageBuilder->addResource("polygonScripts", "/js/polygon/friends.js"); +$pageBuilder->addResource("polygonScripts", "/js/polygon/home.js"); + +$pageBuilder->addResource("polygonScripts", "/js/3D/ThumbnailView.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/ThreeDeeThumbnails.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/three.min.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/MTLLoader.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/OBJMTLLoader.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/tween.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/PolygonOrbitControls.js"); + +$pageBuilder->buildHeader(); +?> +

Hello, !

+
+
+
+
"> + "> + <?=SESSION[" class="img-fluid" src=""> + + +
+
+
+
+ Edit +

My Friends

+
+
+

+
+
+
+
+ " data-src="$avatar" title="$username" alt="$username" class="ml-2 img-fluid"> +
+
+

$username

+ "$status" +
+
+
+
+
+
+
+ +
+
+

My Feed

+
+
+
+ " aria-label="What's on your mind?"> +
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ " data-src="$img" title="$userName" alt="$userName" class="img-fluid"> +
+
+ $header +

$message

+
+
+
+
+
+
+
+

Recently Played Games

+
+
+

You haven't played any games recently. Play Now

+
+
+
+
+ " data-src="$Thumbnail" title="$Name" alt="$Name" class="img-fluid"> +
+
+

$Name

+ $OnlinePlayers players online +
+
+
+
+
+
+buildFooter(); ?> diff --git a/img/2013/BuildPage/btn-gear_sprite_27px.png b/img/2013/BuildPage/btn-gear_sprite_27px.png new file mode 100644 index 0000000..3082a39 Binary files /dev/null and b/img/2013/BuildPage/btn-gear_sprite_27px.png differ diff --git a/img/2013/Buttons/Arrows/btn-silver-left-27.png b/img/2013/Buttons/Arrows/btn-silver-left-27.png new file mode 100644 index 0000000..ab3d656 Binary files /dev/null and b/img/2013/Buttons/Arrows/btn-silver-left-27.png differ diff --git a/img/2013/Buttons/Arrows/btn-silver-right-27.png b/img/2013/Buttons/Arrows/btn-silver-right-27.png new file mode 100644 index 0000000..ec3aab1 Binary files /dev/null and b/img/2013/Buttons/Arrows/btn-silver-right-27.png differ diff --git a/img/2013/Buttons/StyleGuide/bg-btn-blue-arrow-md.png b/img/2013/Buttons/StyleGuide/bg-btn-blue-arrow-md.png new file mode 100644 index 0000000..48a984c Binary files /dev/null and b/img/2013/Buttons/StyleGuide/bg-btn-blue-arrow-md.png differ diff --git a/img/2013/Buttons/StyleGuide/bg-btn-blue.png b/img/2013/Buttons/StyleGuide/bg-btn-blue.png new file mode 100644 index 0000000..e82b97e Binary files /dev/null and b/img/2013/Buttons/StyleGuide/bg-btn-blue.png differ diff --git a/img/2013/Buttons/StyleGuide/bg-btn-gray-arrow-md.png b/img/2013/Buttons/StyleGuide/bg-btn-gray-arrow-md.png new file mode 100644 index 0000000..e55322e Binary files /dev/null and b/img/2013/Buttons/StyleGuide/bg-btn-gray-arrow-md.png differ diff --git a/img/2013/Buttons/StyleGuide/bg-btn-gray.png b/img/2013/Buttons/StyleGuide/bg-btn-gray.png new file mode 100644 index 0000000..ec014f9 Binary files /dev/null and b/img/2013/Buttons/StyleGuide/bg-btn-gray.png differ diff --git a/img/2013/Buttons/StyleGuide/bg-btn-green.png b/img/2013/Buttons/StyleGuide/bg-btn-green.png new file mode 100644 index 0000000..b19e6fc Binary files /dev/null and b/img/2013/Buttons/StyleGuide/bg-btn-green.png differ diff --git a/img/2013/Buttons/StyleGuide/bg-lg-green-play.png b/img/2013/Buttons/StyleGuide/bg-lg-green-play.png new file mode 100644 index 0000000..10e91c9 Binary files /dev/null and b/img/2013/Buttons/StyleGuide/bg-lg-green-play.png differ diff --git a/img/2013/Buttons/bg-drop_down_btn.png b/img/2013/Buttons/bg-drop_down_btn.png new file mode 100644 index 0000000..a7ae6db Binary files /dev/null and b/img/2013/Buttons/bg-drop_down_btn.png differ diff --git a/img/2013/Buttons/bg-form_btn_lg-tile.png b/img/2013/Buttons/bg-form_btn_lg-tile.png new file mode 100644 index 0000000..71195d8 Binary files /dev/null and b/img/2013/Buttons/bg-form_btn_lg-tile.png differ diff --git a/img/2013/Buttons/questionmark-12x12.png b/img/2013/Buttons/questionmark-12x12.png new file mode 100644 index 0000000..be9db8f Binary files /dev/null and b/img/2013/Buttons/questionmark-12x12.png differ diff --git a/img/2013/GamesPage/arrow_left.png b/img/2013/GamesPage/arrow_left.png new file mode 100644 index 0000000..bc7ef2b Binary files /dev/null and b/img/2013/GamesPage/arrow_left.png differ diff --git a/img/2013/GamesPage/arrow_right.png b/img/2013/GamesPage/arrow_right.png new file mode 100644 index 0000000..99349c9 Binary files /dev/null and b/img/2013/GamesPage/arrow_right.png differ diff --git a/img/2013/Icons/Navigation2014/Nav2014-icon-sprite-sheet.png b/img/2013/Icons/Navigation2014/Nav2014-icon-sprite-sheet.png new file mode 100644 index 0000000..ec6d1a9 Binary files /dev/null and b/img/2013/Icons/Navigation2014/Nav2014-icon-sprite-sheet.png differ diff --git a/img/2013/StyleGuide/btn-control-large-tile.png b/img/2013/StyleGuide/btn-control-large-tile.png new file mode 100644 index 0000000..c9bedf6 Binary files /dev/null and b/img/2013/StyleGuide/btn-control-large-tile.png differ diff --git a/img/2013/StyleGuide/btn-control-medium-tile.png b/img/2013/StyleGuide/btn-control-medium-tile.png new file mode 100644 index 0000000..487aaeb Binary files /dev/null and b/img/2013/StyleGuide/btn-control-medium-tile.png differ diff --git a/img/2013/StyleGuide/btn-control-small-tile.png b/img/2013/StyleGuide/btn-control-small-tile.png new file mode 100644 index 0000000..2eb9aa9 Binary files /dev/null and b/img/2013/StyleGuide/btn-control-small-tile.png differ diff --git a/img/2013/roblox_logo.png b/img/2013/roblox_logo.png new file mode 100644 index 0000000..ee0cedc Binary files /dev/null and b/img/2013/roblox_logo.png differ diff --git a/img/PolygonChristmas.png b/img/PolygonChristmas.png new file mode 100644 index 0000000..b313459 Binary files /dev/null and b/img/PolygonChristmas.png differ diff --git a/img/PolygonStudio.png b/img/PolygonStudio.png new file mode 100644 index 0000000..f29274a Binary files /dev/null and b/img/PolygonStudio.png differ diff --git a/img/ProjectPolygon.ico b/img/ProjectPolygon.ico new file mode 100644 index 0000000..29109e6 Binary files /dev/null and b/img/ProjectPolygon.ico differ diff --git a/img/ProjectPolygon.png b/img/ProjectPolygon.png new file mode 100644 index 0000000..2346bd2 Binary files /dev/null and b/img/ProjectPolygon.png differ diff --git a/img/TinyBcIcon.ico b/img/TinyBcIcon.ico new file mode 100644 index 0000000..1eb7e5d Binary files /dev/null and b/img/TinyBcIcon.ico differ diff --git a/img/badges/1.png b/img/badges/1.png new file mode 100644 index 0000000..9f676ee Binary files /dev/null and b/img/badges/1.png differ diff --git a/img/badges/2.png b/img/badges/2.png new file mode 100644 index 0000000..ad8a738 Binary files /dev/null and b/img/badges/2.png differ diff --git a/img/badges/CatalogManager.png b/img/badges/CatalogManager.png new file mode 100644 index 0000000..d7cae12 Binary files /dev/null and b/img/badges/CatalogManager.png differ diff --git a/img/badges/Friends.png b/img/badges/Friends.png new file mode 100644 index 0000000..e44a891 Binary files /dev/null and b/img/badges/Friends.png differ diff --git a/img/badges/Moderator.png b/img/badges/Moderator.png new file mode 100644 index 0000000..1e6ad60 Binary files /dev/null and b/img/badges/Moderator.png differ diff --git a/img/badges/Veteran.png b/img/badges/Veteran.png new file mode 100644 index 0000000..93da954 Binary files /dev/null and b/img/badges/Veteran.png differ diff --git a/img/error.png b/img/error.png new file mode 100644 index 0000000..d6392a6 Binary files /dev/null and b/img/error.png differ diff --git a/img/feed-starter.png b/img/feed-starter.png new file mode 100644 index 0000000..de53aee Binary files /dev/null and b/img/feed-starter.png differ diff --git a/img/feed/cart.png b/img/feed/cart.png new file mode 100644 index 0000000..41d0540 Binary files /dev/null and b/img/feed/cart.png differ diff --git a/img/feed/friends.png b/img/feed/friends.png new file mode 100644 index 0000000..de53aee Binary files /dev/null and b/img/feed/friends.png differ diff --git a/img/korb.jpg b/img/korb.jpg new file mode 100644 index 0000000..c5b0646 Binary files /dev/null and b/img/korb.jpg differ diff --git a/img/landing/cr.png b/img/landing/cr.png new file mode 100644 index 0000000..f03070e Binary files /dev/null and b/img/landing/cr.png differ diff --git a/img/landing/gh.png b/img/landing/gh.png new file mode 100644 index 0000000..db35bbc Binary files /dev/null and b/img/landing/gh.png differ diff --git a/img/landing/polygonville-edit.png b/img/landing/polygonville-edit.png new file mode 100644 index 0000000..9932512 Binary files /dev/null and b/img/landing/polygonville-edit.png differ diff --git a/img/landing/polygonville-edit2.jpg b/img/landing/polygonville-edit2.jpg new file mode 100644 index 0000000..e5232df Binary files /dev/null and b/img/landing/polygonville-edit2.jpg differ diff --git a/img/landing/polygonville-edit2.png b/img/landing/polygonville-edit2.png new file mode 100644 index 0000000..593a93c Binary files /dev/null and b/img/landing/polygonville-edit2.png differ diff --git a/img/landing/polygonville-winter-edit.jpg b/img/landing/polygonville-winter-edit.jpg new file mode 100644 index 0000000..cd85703 Binary files /dev/null and b/img/landing/polygonville-winter-edit.jpg differ diff --git a/img/landing/polygonville-winter-edit.png b/img/landing/polygonville-winter-edit.png new file mode 100644 index 0000000..538bdf7 Binary files /dev/null and b/img/landing/polygonville-winter-edit.png differ diff --git a/img/landing/polygonville-winter.png b/img/landing/polygonville-winter.png new file mode 100644 index 0000000..05ddbe6 Binary files /dev/null and b/img/landing/polygonville-winter.png differ diff --git a/img/landing/polygonville.png b/img/landing/polygonville.png new file mode 100644 index 0000000..ec453ce Binary files /dev/null and b/img/landing/polygonville.png differ diff --git a/img/landing/polygonwinter-edit.jpg b/img/landing/polygonwinter-edit.jpg new file mode 100644 index 0000000..d8d5cc3 Binary files /dev/null and b/img/landing/polygonwinter-edit.jpg differ diff --git a/img/landing/polygonwinter.jpg b/img/landing/polygonwinter.jpg new file mode 100644 index 0000000..bc3d715 Binary files /dev/null and b/img/landing/polygonwinter.jpg differ diff --git a/img/landing/skypanorama.png b/img/landing/skypanorama.png new file mode 100644 index 0000000..a258df9 Binary files /dev/null and b/img/landing/skypanorama.png differ diff --git a/img/spinners/ajax_loader_blue_300.gif b/img/spinners/ajax_loader_blue_300.gif new file mode 100644 index 0000000..27718a9 Binary files /dev/null and b/img/spinners/ajax_loader_blue_300.gif differ diff --git a/img/spinners/loading_bluesquares.gif b/img/spinners/loading_bluesquares.gif new file mode 100644 index 0000000..2dc37ba Binary files /dev/null and b/img/spinners/loading_bluesquares.gif differ diff --git a/img/spinners/spinner100x100.gif b/img/spinners/spinner100x100.gif new file mode 100644 index 0000000..cc70a7a Binary files /dev/null and b/img/spinners/spinner100x100.gif differ diff --git a/img/spinners/spinner16x16.gif b/img/spinners/spinner16x16.gif new file mode 100644 index 0000000..1560b64 Binary files /dev/null and b/img/spinners/spinner16x16.gif differ diff --git a/img/spinners/waiting-loader.gif b/img/spinners/waiting-loader.gif new file mode 100644 index 0000000..98a62c4 Binary files /dev/null and b/img/spinners/waiting-loader.gif differ diff --git a/img/spinners/waiting-spinner.gif b/img/spinners/waiting-spinner.gif new file mode 100644 index 0000000..57fa1b1 Binary files /dev/null and b/img/spinners/waiting-spinner.gif differ diff --git a/img/tshirt-template.png b/img/tshirt-template.png new file mode 100644 index 0000000..81177c2 Binary files /dev/null and b/img/tshirt-template.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..1460c84 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +"); + + setTimeout(function () { + try { + myWindow.location.href; + myWindow.setTimeout("window.close()", 1000); + successCb(); + } catch (e) { + myWindow.close(); + failCb(); + } + }, 1000); +} + +function openUriWithMsLaunchUri(uri, failCb, successCb) { + navigator.msLaunchUri(uri, + successCb, + failCb + ); +} + +function checkBrowser() { + var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; + var ua = navigator.userAgent.toLowerCase(); + return { + isOpera : isOpera, + isFirefox : typeof InstallTrigger !== 'undefined', + isSafari : (~ua.indexOf('safari') && !~ua.indexOf('chrome')) || Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0, + isIOS : /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, + isChrome : !!window.chrome && !isOpera, + isIE : /*@cc_on!@*/false || !!document.documentMode // At least IE6 + } +} + +function getInternetExplorerVersion() { + var rv = -1; + if (navigator.appName === "Microsoft Internet Explorer") { + var ua = navigator.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) + rv = parseFloat(RegExp.$1); + } + else if (navigator.appName === "Netscape") { + var ua = navigator.userAgent; + var re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})"); + if (re.exec(ua) != null) { + rv = parseFloat(RegExp.$1); + } + } + return rv; +} + +module.exports = function(uri, failCb, successCb, unsupportedCb) { + function failCallback() { + failCb && failCb(); + } + + function successCallback() { + successCb && successCb(); + } + + if (navigator.msLaunchUri) { //for IE and Edge in Win 8 and Win 10 + openUriWithMsLaunchUri(uri, failCb, successCb); + } else { + var browser = checkBrowser(); + + if (browser.isFirefox) { + openUriUsingFirefox(uri, failCallback, successCallback); + } else if (browser.isChrome || browser.isIOS || browser.isOpera) { + openUriWithTimeoutHack(uri, failCallback, successCallback); + } else if (browser.isIE) { + openUriUsingIEInOlderWindows(uri, failCallback, successCallback); + } else if (browser.isSafari) { + openUriWithHiddenFrame(uri, failCallback, successCallback); + } else { + unsupportedCb(); + //not supported, implement please + } + } +} + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/js/protocolcheck.old.js b/js/protocolcheck.old.js new file mode 100644 index 0000000..cf4c483 --- /dev/null +++ b/js/protocolcheck.old.js @@ -0,0 +1,2 @@ +//many thanks to https://github.com/vireshshah/custom-protocol-check +!function(e,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("customProtocolCheck",[],n):"object"==typeof exports?exports.customProtocolCheck=n():e.customProtocolCheck=n()}(window,(function(){return function(e){var n={};function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)t.d(o,r,function(n){return e[n]}.bind(null,r));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n,t){e.exports=t(1)},function(e,n){var t,o={getUserAgent:function(){return window.navigator.userAgent},userAgentContains:function(e){return e=e.toLowerCase(),this.getUserAgent().toLowerCase().indexOf(e)>-1},isOSX:function(){return this.userAgentContains("Macintosh")},isFirefox:function(){return this.userAgentContains("firefox")},isInternetExplorer:function(){return this.userAgentContains("trident")},isIE:function(){var e=this.getUserAgent().toLowerCase();return e.indexOf("msie")>0||e.indexOf("trident/")>0},isEdge:function(){return this.getUserAgent().toLowerCase().indexOf("edge")>0},isChrome:function(){var e=window.chrome,n=window.navigator,t=n.vendor,o=void 0!==window.opr,r=n.userAgent.indexOf("Edge")>-1,i=n.userAgent.match("CriOS");return null!=e&&"Google Inc."===t&&!1===o&&!1===r||i},isOpera:function(){return this.userAgentContains(" OPR/")}},r=function(e,n,t){return e.addEventListener?(e.addEventListener(n,t),{remove:function(){e.removeEventListener(n,t)}}):(e.attachEvent(n,t),{remove:function(){e.detachEvent(n,t)}})},i=function(e,n){var t=document.createElement("iframe");return t.src=n,t.id="hiddenIframe",t.style.display="none",e.appendChild(t),t},u=function(e,n,o){var u=setTimeout((function(){n(),a.remove()}),t),c=document.querySelector("#hiddenIframe");c||(c=i(document.body,"about:blank")),onBlur=function(){clearTimeout(u),a.remove(),o()};var a=r(window,"blur",onBlur);c.contentWindow.location.href=e},c=function(e,n,o){for(var i=setTimeout((function(){n(),c.remove()}),t),u=window;u.parent&&u!=u.parent;)u=u.parent;onBlur=function(){clearTimeout(i),c.remove(),o()};var c=r(u,"blur",onBlur);window.location=e},a=function(e,n,t){var o=document.querySelector("#hiddenIframe");o||(o=i(document.body,"about:blank"));try{o.contentWindow.location.href=e,t()}catch(e){"NS_ERROR_UNKNOWN_PROTOCOL"==e.name&&n()}},f=function(e,n,t){navigator.msLaunchUri(e,t,n)},s=function(){var e,n=window.navigator.userAgent,t=n.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i)||[];return/trident/i.test(t[1])?(e=/\brv[ :]+(\d+)/g.exec(n)||[],parseFloat(e[1])||""):"Chrome"===t[1]&&null!=(e=n.match(/\b(OPR|Edge)\/(\d+)/))?parseFloat(e[2]):(t=t[2]?[t[1],t[2]]:[window.navigator.appName,window.navigator.appVersion,"-?"],null!=(e=n.match(/version\/(\d+)/i))&&t.splice(1,1,e[1]),parseFloat(t[1]))};e.exports=function(e,n,i){var d=arguments.length>3&&void 0!==arguments[3]?arguments[3]:2e3,l=arguments.length>4?arguments[4]:void 0,m=function(){n&&n()},v=function(){i&&i()},p=function(){l&&l()},g=function(){o.isFirefox()?s()>=64?u(e,m,v):a(e,m,v):o.isChrome()?c(e,m,v):o.isOSX()?u(e,m,v):p()};if(d&&(t=d),o.isEdge()||o.isIE())f(e,n,i);else if(document.hasFocus())g();else var h=r(window,"focus",(function(){h.remove(),g()}))}}])})); \ No newline at end of file diff --git a/js/simplemde.min.js b/js/simplemde.min.js new file mode 100644 index 0000000..9bc1566 --- /dev/null +++ b/js/simplemde.min.js @@ -0,0 +1,15 @@ +/** + * simplemde v1.11.2 + * Copyright Next Step Webs, Inc. + * @link https://github.com/NextStepWebs/simplemde-markdown-editor + * @license MIT + */ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.SimpleMDE=e()}}(function(){var e;return function t(e,n,r){function i(a,l){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!l&&s)return s(a,!0);if(o)return o(a,!0);var c=new Error("Cannot find module '"+a+"'");throw c.code="MODULE_NOT_FOUND",c}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,r)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;at;++t)s[t]=e[t],c[e.charCodeAt(t)]=t;c["-".charCodeAt(0)]=62,c["_".charCodeAt(0)]=63}function i(e){var t,n,r,i,o,a,l=e.length;if(l%4>0)throw new Error("Invalid string. Length must be a multiple of 4");o="="===e[l-2]?2:"="===e[l-1]?1:0,a=new u(3*l/4-o),r=o>0?l-4:l;var s=0;for(t=0,n=0;r>t;t+=4,n+=3)i=c[e.charCodeAt(t)]<<18|c[e.charCodeAt(t+1)]<<12|c[e.charCodeAt(t+2)]<<6|c[e.charCodeAt(t+3)],a[s++]=i>>16&255,a[s++]=i>>8&255,a[s++]=255&i;return 2===o?(i=c[e.charCodeAt(t)]<<2|c[e.charCodeAt(t+1)]>>4,a[s++]=255&i):1===o&&(i=c[e.charCodeAt(t)]<<10|c[e.charCodeAt(t+1)]<<4|c[e.charCodeAt(t+2)]>>2,a[s++]=i>>8&255,a[s++]=255&i),a}function o(e){return s[e>>18&63]+s[e>>12&63]+s[e>>6&63]+s[63&e]}function a(e,t,n){for(var r,i=[],a=t;n>a;a+=3)r=(e[a]<<16)+(e[a+1]<<8)+e[a+2],i.push(o(r));return i.join("")}function l(e){for(var t,n=e.length,r=n%3,i="",o=[],l=16383,c=0,u=n-r;u>c;c+=l)o.push(a(e,c,c+l>u?u:c+l));return 1===r?(t=e[n-1],i+=s[t>>2],i+=s[t<<4&63],i+="=="):2===r&&(t=(e[n-2]<<8)+e[n-1],i+=s[t>>10],i+=s[t>>4&63],i+=s[t<<2&63],i+="="),o.push(i),o.join("")}n.toByteArray=i,n.fromByteArray=l;var s=[],c=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array;r()},{}],2:[function(e,t,n){},{}],3:[function(e,t,n){(function(t){"use strict";function r(){try{var e=new Uint8Array(1);return e.foo=function(){return 42},42===e.foo()&&"function"==typeof e.subarray&&0===e.subarray(1,1).byteLength}catch(t){return!1}}function i(){return a.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function o(e,t){if(i()=t?o(e,t):void 0!==n?"string"==typeof r?o(e,t).fill(n,r):o(e,t).fill(n):o(e,t)}function u(e,t){if(s(t),e=o(e,0>t?0:0|m(t)),!a.TYPED_ARRAY_SUPPORT)for(var n=0;t>n;n++)e[n]=0;return e}function f(e,t,n){if("string"==typeof n&&""!==n||(n="utf8"),!a.isEncoding(n))throw new TypeError('"encoding" must be a valid string encoding');var r=0|v(t,n);return e=o(e,r),e.write(t,n),e}function h(e,t){var n=0|m(t.length);e=o(e,n);for(var r=0;n>r;r+=1)e[r]=255&t[r];return e}function d(e,t,n,r){if(t.byteLength,0>n||t.byteLength=i())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i().toString(16)+" bytes");return 0|e}function g(e){return+e!=e&&(e=0),a.alloc(+e)}function v(e,t){if(a.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"binary":case"raw":case"raws":return n;case"utf8":case"utf-8":case void 0:return q(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return $(e).length;default:if(r)return q(e).length;t=(""+t).toLowerCase(),r=!0}}function y(e,t,n){var r=!1;if((void 0===t||0>t)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),0>=n)return"";if(n>>>=0,t>>>=0,t>=n)return"";for(e||(e="utf8");;)switch(e){case"hex":return I(this,t,n);case"utf8":case"utf-8":return N(this,t,n);case"ascii":return E(this,t,n);case"binary":return O(this,t,n);case"base64":return M(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return P(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function x(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function b(e,t,n,r){function i(e,t){return 1===o?e[t]:e.readUInt16BE(t*o)}var o=1,a=e.length,l=t.length;if(void 0!==r&&(r=String(r).toLowerCase(),"ucs2"===r||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;o=2,a/=2,l/=2,n/=2}for(var s=-1,c=0;a>n+c;c++)if(i(e,n+c)===i(t,-1===s?0:c-s)){if(-1===s&&(s=c),c-s+1===l)return(n+s)*o}else-1!==s&&(c-=c-s),s=-1;return-1}function w(e,t,n,r){n=Number(n)||0;var i=e.length-n;r?(r=Number(r),r>i&&(r=i)):r=i;var o=t.length;if(o%2!==0)throw new Error("Invalid hex string");r>o/2&&(r=o/2);for(var a=0;r>a;a++){var l=parseInt(t.substr(2*a,2),16);if(isNaN(l))return a;e[n+a]=l}return a}function k(e,t,n,r){return V(q(t,e.length-n),e,n,r)}function S(e,t,n,r){return V(G(t),e,n,r)}function C(e,t,n,r){return S(e,t,n,r)}function L(e,t,n,r){return V($(t),e,n,r)}function T(e,t,n,r){return V(Y(t,e.length-n),e,n,r)}function M(e,t,n){return 0===t&&n===e.length?X.fromByteArray(e):X.fromByteArray(e.slice(t,n))}function N(e,t,n){n=Math.min(e.length,n);for(var r=[],i=t;n>i;){var o=e[i],a=null,l=o>239?4:o>223?3:o>191?2:1;if(n>=i+l){var s,c,u,f;switch(l){case 1:128>o&&(a=o);break;case 2:s=e[i+1],128===(192&s)&&(f=(31&o)<<6|63&s,f>127&&(a=f));break;case 3:s=e[i+1],c=e[i+2],128===(192&s)&&128===(192&c)&&(f=(15&o)<<12|(63&s)<<6|63&c,f>2047&&(55296>f||f>57343)&&(a=f));break;case 4:s=e[i+1],c=e[i+2],u=e[i+3],128===(192&s)&&128===(192&c)&&128===(192&u)&&(f=(15&o)<<18|(63&s)<<12|(63&c)<<6|63&u,f>65535&&1114112>f&&(a=f))}}null===a?(a=65533,l=1):a>65535&&(a-=65536,r.push(a>>>10&1023|55296),a=56320|1023&a),r.push(a),i+=l}return A(r)}function A(e){var t=e.length;if(Q>=t)return String.fromCharCode.apply(String,e);for(var n="",r=0;t>r;)n+=String.fromCharCode.apply(String,e.slice(r,r+=Q));return n}function E(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;n>i;i++)r+=String.fromCharCode(127&e[i]);return r}function O(e,t,n){var r="";n=Math.min(e.length,n);for(var i=t;n>i;i++)r+=String.fromCharCode(e[i]);return r}function I(e,t,n){var r=e.length;(!t||0>t)&&(t=0),(!n||0>n||n>r)&&(n=r);for(var i="",o=t;n>o;o++)i+=U(e[o]);return i}function P(e,t,n){for(var r=e.slice(t,n),i="",o=0;oe)throw new RangeError("offset is not uint");if(e+t>n)throw new RangeError("Trying to access beyond buffer length")}function D(e,t,n,r,i,o){if(!a.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>i||o>t)throw new RangeError('"value" argument is out of bounds');if(n+r>e.length)throw new RangeError("Index out of range")}function H(e,t,n,r){0>t&&(t=65535+t+1);for(var i=0,o=Math.min(e.length-n,2);o>i;i++)e[n+i]=(t&255<<8*(r?i:1-i))>>>8*(r?i:1-i)}function W(e,t,n,r){0>t&&(t=4294967295+t+1);for(var i=0,o=Math.min(e.length-n,4);o>i;i++)e[n+i]=t>>>8*(r?i:3-i)&255}function B(e,t,n,r,i,o){if(n+r>e.length)throw new RangeError("Index out of range");if(0>n)throw new RangeError("Index out of range")}function _(e,t,n,r,i){return i||B(e,t,n,4,3.4028234663852886e38,-3.4028234663852886e38),Z.write(e,t,n,r,23,4),n+4}function F(e,t,n,r,i){return i||B(e,t,n,8,1.7976931348623157e308,-1.7976931348623157e308),Z.write(e,t,n,r,52,8),n+8}function z(e){if(e=j(e).replace(ee,""),e.length<2)return"";for(;e.length%4!==0;)e+="=";return e}function j(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}function U(e){return 16>e?"0"+e.toString(16):e.toString(16)}function q(e,t){t=t||1/0;for(var n,r=e.length,i=null,o=[],a=0;r>a;a++){if(n=e.charCodeAt(a),n>55295&&57344>n){if(!i){if(n>56319){(t-=3)>-1&&o.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&o.push(239,191,189);continue}i=n;continue}if(56320>n){(t-=3)>-1&&o.push(239,191,189),i=n;continue}n=(i-55296<<10|n-56320)+65536}else i&&(t-=3)>-1&&o.push(239,191,189);if(i=null,128>n){if((t-=1)<0)break;o.push(n)}else if(2048>n){if((t-=2)<0)break;o.push(n>>6|192,63&n|128)}else if(65536>n){if((t-=3)<0)break;o.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(1114112>n))throw new Error("Invalid code point");if((t-=4)<0)break;o.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return o}function G(e){for(var t=[],n=0;n>8,i=n%256,o.push(i),o.push(r);return o}function $(e){return X.toByteArray(z(e))}function V(e,t,n,r){for(var i=0;r>i&&!(i+n>=t.length||i>=e.length);i++)t[i+n]=e[i];return i}function K(e){return e!==e}var X=e("base64-js"),Z=e("ieee754"),J=e("isarray");n.Buffer=a,n.SlowBuffer=g,n.INSPECT_MAX_BYTES=50,a.TYPED_ARRAY_SUPPORT=void 0!==t.TYPED_ARRAY_SUPPORT?t.TYPED_ARRAY_SUPPORT:r(),n.kMaxLength=i(),a.poolSize=8192,a._augment=function(e){return e.__proto__=a.prototype,e},a.from=function(e,t,n){return l(null,e,t,n)},a.TYPED_ARRAY_SUPPORT&&(a.prototype.__proto__=Uint8Array.prototype,a.__proto__=Uint8Array,"undefined"!=typeof Symbol&&Symbol.species&&a[Symbol.species]===a&&Object.defineProperty(a,Symbol.species,{value:null,configurable:!0})),a.alloc=function(e,t,n){return c(null,e,t,n)},a.allocUnsafe=function(e){return u(null,e)},a.allocUnsafeSlow=function(e){return u(null,e)},a.isBuffer=function(e){return!(null==e||!e._isBuffer)},a.compare=function(e,t){if(!a.isBuffer(e)||!a.isBuffer(t))throw new TypeError("Arguments must be Buffers");if(e===t)return 0;for(var n=e.length,r=t.length,i=0,o=Math.min(n,r);o>i;++i)if(e[i]!==t[i]){n=e[i],r=t[i];break}return r>n?-1:n>r?1:0},a.isEncoding=function(e){switch(String(e).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"raw":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},a.concat=function(e,t){if(!J(e))throw new TypeError('"list" argument must be an Array of Buffers');if(0===e.length)return a.alloc(0);var n;if(void 0===t)for(t=0,n=0;nt;t+=2)x(this,t,t+1);return this},a.prototype.swap32=function(){var e=this.length;if(e%4!==0)throw new RangeError("Buffer size must be a multiple of 32-bits");for(var t=0;e>t;t+=4)x(this,t,t+3),x(this,t+1,t+2);return this},a.prototype.toString=function(){var e=0|this.length;return 0===e?"":0===arguments.length?N(this,0,e):y.apply(this,arguments)},a.prototype.equals=function(e){if(!a.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e?!0:0===a.compare(this,e)},a.prototype.inspect=function(){var e="",t=n.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,t).match(/.{2}/g).join(" "),this.length>t&&(e+=" ... ")),""},a.prototype.compare=function(e,t,n,r,i){if(!a.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===i&&(i=this.length),0>t||n>e.length||0>r||i>this.length)throw new RangeError("out of range index");if(r>=i&&t>=n)return 0;if(r>=i)return-1;if(t>=n)return 1;if(t>>>=0,n>>>=0,r>>>=0,i>>>=0,this===e)return 0;for(var o=i-r,l=n-t,s=Math.min(o,l),c=this.slice(r,i),u=e.slice(t,n),f=0;s>f;++f)if(c[f]!==u[f]){o=c[f],l=u[f];break}return l>o?-1:o>l?1:0},a.prototype.indexOf=function(e,t,n){if("string"==typeof t?(n=t,t=0):t>2147483647?t=2147483647:-2147483648>t&&(t=-2147483648),t>>=0,0===this.length)return-1;if(t>=this.length)return-1;if(0>t&&(t=Math.max(this.length+t,0)),"string"==typeof e&&(e=a.from(e,n)),a.isBuffer(e))return 0===e.length?-1:b(this,e,t,n);if("number"==typeof e)return a.TYPED_ARRAY_SUPPORT&&"function"===Uint8Array.prototype.indexOf?Uint8Array.prototype.indexOf.call(this,e,t):b(this,[e],t,n);throw new TypeError("val must be string, number or Buffer")},a.prototype.includes=function(e,t,n){return-1!==this.indexOf(e,t,n)},a.prototype.write=function(e,t,n,r){if(void 0===t)r="utf8",n=this.length,t=0;else if(void 0===n&&"string"==typeof t)r=t,n=this.length,t=0;else{if(!isFinite(t))throw new Error("Buffer.write(string, encoding, offset[, length]) is no longer supported");t=0|t,isFinite(n)?(n=0|n,void 0===r&&(r="utf8")):(r=n,n=void 0)}var i=this.length-t;if((void 0===n||n>i)&&(n=i),e.length>0&&(0>n||0>t)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return w(this,e,t,n);case"utf8":case"utf-8":return k(this,e,t,n);case"ascii":return S(this,e,t,n);case"binary":return C(this,e,t,n);case"base64":return L(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,e,t,n);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},a.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var Q=4096;a.prototype.slice=function(e,t){var n=this.length;e=~~e,t=void 0===t?n:~~t,0>e?(e+=n,0>e&&(e=0)):e>n&&(e=n),0>t?(t+=n,0>t&&(t=0)):t>n&&(t=n),e>t&&(t=e);var r;if(a.TYPED_ARRAY_SUPPORT)r=this.subarray(e,t),r.__proto__=a.prototype;else{var i=t-e;r=new a(i,void 0);for(var o=0;i>o;o++)r[o]=this[o+e]}return r},a.prototype.readUIntLE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e],i=1,o=0;++o0&&(i*=256);)r+=this[e+--t]*i;return r},a.prototype.readUInt8=function(e,t){return t||R(e,1,this.length),this[e]},a.prototype.readUInt16LE=function(e,t){return t||R(e,2,this.length),this[e]|this[e+1]<<8},a.prototype.readUInt16BE=function(e,t){return t||R(e,2,this.length),this[e]<<8|this[e+1]},a.prototype.readUInt32LE=function(e,t){return t||R(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},a.prototype.readUInt32BE=function(e,t){return t||R(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},a.prototype.readIntLE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=this[e],i=1,o=0;++o=i&&(r-=Math.pow(2,8*t)),r},a.prototype.readIntBE=function(e,t,n){e=0|e,t=0|t,n||R(e,t,this.length);for(var r=t,i=1,o=this[e+--r];r>0&&(i*=256);)o+=this[e+--r]*i;return i*=128,o>=i&&(o-=Math.pow(2,8*t)),o},a.prototype.readInt8=function(e,t){return t||R(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},a.prototype.readInt16LE=function(e,t){t||R(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},a.prototype.readInt16BE=function(e,t){t||R(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},a.prototype.readInt32LE=function(e,t){return t||R(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},a.prototype.readInt32BE=function(e,t){return t||R(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},a.prototype.readFloatLE=function(e,t){return t||R(e,4,this.length),Z.read(this,e,!0,23,4)},a.prototype.readFloatBE=function(e,t){return t||R(e,4,this.length),Z.read(this,e,!1,23,4)},a.prototype.readDoubleLE=function(e,t){return t||R(e,8,this.length),Z.read(this,e,!0,52,8)},a.prototype.readDoubleBE=function(e,t){return t||R(e,8,this.length),Z.read(this,e,!1,52,8)},a.prototype.writeUIntLE=function(e,t,n,r){if(e=+e,t=0|t,n=0|n,!r){var i=Math.pow(2,8*n)-1;D(this,e,t,n,i,0)}var o=1,a=0;for(this[t]=255&e;++a=0&&(a*=256);)this[t+o]=e/a&255;return t+n},a.prototype.writeUInt8=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,1,255,0),a.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},a.prototype.writeUInt16LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,65535,0),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):H(this,e,t,!0),t+2},a.prototype.writeUInt16BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,65535,0),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):H(this,e,t,!1),t+2},a.prototype.writeUInt32LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,4294967295,0),a.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):W(this,e,t,!0),t+4},a.prototype.writeUInt32BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,4294967295,0),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):W(this,e,t,!1),t+4},a.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t=0|t,!r){var i=Math.pow(2,8*n-1);D(this,e,t,n,i-1,-i)}var o=0,a=1,l=0;for(this[t]=255&e;++oe&&0===l&&0!==this[t+o-1]&&(l=1),this[t+o]=(e/a>>0)-l&255;return t+n},a.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t=0|t,!r){var i=Math.pow(2,8*n-1);D(this,e,t,n,i-1,-i)}var o=n-1,a=1,l=0;for(this[t+o]=255&e;--o>=0&&(a*=256);)0>e&&0===l&&0!==this[t+o+1]&&(l=1),this[t+o]=(e/a>>0)-l&255;return t+n},a.prototype.writeInt8=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,1,127,-128),a.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),0>e&&(e=255+e+1),this[t]=255&e,t+1},a.prototype.writeInt16LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,32767,-32768),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):H(this,e,t,!0),t+2},a.prototype.writeInt16BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,2,32767,-32768),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):H(this,e,t,!1),t+2},a.prototype.writeInt32LE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,2147483647,-2147483648),a.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):W(this,e,t,!0),t+4},a.prototype.writeInt32BE=function(e,t,n){return e=+e,t=0|t,n||D(this,e,t,4,2147483647,-2147483648),0>e&&(e=4294967295+e+1),a.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):W(this,e,t,!1),t+4},a.prototype.writeFloatLE=function(e,t,n){return _(this,e,t,!0,n)},a.prototype.writeFloatBE=function(e,t,n){return _(this,e,t,!1,n)},a.prototype.writeDoubleLE=function(e,t,n){return F(this,e,t,!0,n)},a.prototype.writeDoubleBE=function(e,t,n){return F(this,e,t,!1,n)},a.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&n>r&&(r=n),r===n)return 0;if(0===e.length||0===this.length)return 0;if(0>t)throw new RangeError("targetStart out of bounds");if(0>n||n>=this.length)throw new RangeError("sourceStart out of bounds");if(0>r)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-tn&&r>t)for(i=o-1;i>=0;i--)e[i+t]=this[i+n];else if(1e3>o||!a.TYPED_ARRAY_SUPPORT)for(i=0;o>i;i++)e[i+t]=this[i+n];else Uint8Array.prototype.set.call(e,this.subarray(n,n+o),t);return o},a.prototype.fill=function(e,t,n,r){if("string"==typeof e){if("string"==typeof t?(r=t,t=0,n=this.length):"string"==typeof n&&(r=n,n=this.length),1===e.length){var i=e.charCodeAt(0);256>i&&(e=i)}if(void 0!==r&&"string"!=typeof r)throw new TypeError("encoding must be a string");if("string"==typeof r&&!a.isEncoding(r))throw new TypeError("Unknown encoding: "+r)}else"number"==typeof e&&(e=255&e);if(0>t||this.length=n)return this;t>>>=0,n=void 0===n?this.length:n>>>0,e||(e=0);var o;if("number"==typeof e)for(o=t;n>o;o++)this[o]=e;else{var l=a.isBuffer(e)?e:q(new a(e,r).toString()),s=l.length;for(o=0;n-t>o;o++)this[o+t]=l[o%s]}return this};var ee=/[^+\/0-9A-Za-z-_]/g}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"base64-js":1,ieee754:15,isarray:16}],4:[function(e,t,n){"use strict";function r(e){return e=e||{},"function"!=typeof e.codeMirrorInstance||"function"!=typeof e.codeMirrorInstance.defineMode?void console.log("CodeMirror Spell Checker: You must provide an instance of CodeMirror via the option `codeMirrorInstance`"):(String.prototype.includes||(String.prototype.includes=function(){return-1!==String.prototype.indexOf.apply(this,arguments)}),void e.codeMirrorInstance.defineMode("spell-checker",function(t){if(!r.aff_loading){r.aff_loading=!0;var n=new XMLHttpRequest;n.open("GET","https://cdn.jsdelivr.net/codemirror.spell-checker/latest/en_US.aff",!0),n.onload=function(){4===n.readyState&&200===n.status&&(r.aff_data=n.responseText,r.num_loaded++,2==r.num_loaded&&(r.typo=new i("en_US",r.aff_data,r.dic_data,{platform:"any"})))},n.send(null)}if(!r.dic_loading){r.dic_loading=!0;var o=new XMLHttpRequest;o.open("GET","https://cdn.jsdelivr.net/codemirror.spell-checker/latest/en_US.dic",!0),o.onload=function(){4===o.readyState&&200===o.status&&(r.dic_data=o.responseText,r.num_loaded++,2==r.num_loaded&&(r.typo=new i("en_US",r.aff_data,r.dic_data,{platform:"any"})))},o.send(null)}var a='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~ ',l={token:function(e){var t=e.peek(),n="";if(a.includes(t))return e.next(),null;for(;null!=(t=e.peek())&&!a.includes(t);)n+=t,e.next();return r.typo&&!r.typo.check(n)?"spell-error":null}},s=e.codeMirrorInstance.getMode(t,t.backdrop||"text/plain");return e.codeMirrorInstance.overlayMode(s,l,!0)}))}var i=e("typo-js");r.num_loaded=0,r.aff_loading=!1,r.dic_loading=!1,r.aff_data="",r.dic_data="",r.typo,t.exports=r},{"typo-js":18}],5:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";function t(e){var t=e.getWrapperElement();e.state.fullScreenRestore={scrollTop:window.pageYOffset,scrollLeft:window.pageXOffset,width:t.style.width,height:t.style.height},t.style.width="",t.style.height="auto",t.className+=" CodeMirror-fullscreen",document.documentElement.style.overflow="hidden",e.refresh()}function n(e){var t=e.getWrapperElement();t.className=t.className.replace(/\s*CodeMirror-fullscreen\b/,""),document.documentElement.style.overflow="";var n=e.state.fullScreenRestore;t.style.width=n.width,t.style.height=n.height,window.scrollTo(n.scrollLeft,n.scrollTop),e.refresh()}e.defineOption("fullScreen",!1,function(r,i,o){o==e.Init&&(o=!1),!o!=!i&&(i?t(r):n(r))})})},{"../../lib/codemirror":10}],6:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){function t(e){e.state.placeholder&&(e.state.placeholder.parentNode.removeChild(e.state.placeholder),e.state.placeholder=null)}function n(e){t(e);var n=e.state.placeholder=document.createElement("pre");n.style.cssText="height: 0; overflow: visible",n.className="CodeMirror-placeholder";var r=e.getOption("placeholder");"string"==typeof r&&(r=document.createTextNode(r)),n.appendChild(r),e.display.lineSpace.insertBefore(n,e.display.lineSpace.firstChild)}function r(e){o(e)&&n(e)}function i(e){var r=e.getWrapperElement(),i=o(e);r.className=r.className.replace(" CodeMirror-empty","")+(i?" CodeMirror-empty":""),i?n(e):t(e)}function o(e){return 1===e.lineCount()&&""===e.getLine(0)}e.defineOption("placeholder","",function(n,o,a){var l=a&&a!=e.Init;if(o&&!l)n.on("blur",r),n.on("change",i),n.on("swapDoc",i),i(n);else if(!o&&l){n.off("blur",r),n.off("change",i),n.off("swapDoc",i),t(n);var s=n.getWrapperElement();s.className=s.className.replace(" CodeMirror-empty","")}o&&!n.hasFocus()&&r(n)})})},{"../../lib/codemirror":10}],7:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";var t=/^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))(\s*)/,n=/^(\s*)(>[> ]*|[*+-]|(\d+)[.)])(\s*)$/,r=/[*+-]\s/;e.commands.newlineAndIndentContinueMarkdownList=function(i){if(i.getOption("disableInput"))return e.Pass;for(var o=i.listSelections(),a=[],l=0;l")>=0?d[2]:parseInt(d[3],10)+1+d[4];a[l]="\n"+p+g+m}}i.replaceSelections(a)}})},{"../../lib/codemirror":10}],8:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror")):"function"==typeof e&&e.amd?e(["../../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.overlayMode=function(t,n,r){return{startState:function(){return{base:e.startState(t),overlay:e.startState(n),basePos:0,baseCur:null,overlayPos:0,overlayCur:null,streamSeen:null}},copyState:function(r){return{base:e.copyState(t,r.base),overlay:e.copyState(n,r.overlay),basePos:r.basePos,baseCur:null,overlayPos:r.overlayPos,overlayCur:null}},token:function(e,i){return(e!=i.streamSeen||Math.min(i.basePos,i.overlayPos)=n.line,d=h?n:s(f,0),p=e.markText(u,d,{className:o});if(null==r?i.push(p):i.splice(r++,0,p),h)break;a=f}}function i(e){for(var t=e.state.markedSelection,n=0;n1)return o(e);var t=e.getCursor("start"),n=e.getCursor("end"),a=e.state.markedSelection;if(!a.length)return r(e,t,n);var s=a[0].find(),u=a[a.length-1].find();if(!s||!u||n.line-t.line=0||c(n,s.from)<=0)return o(e);for(;c(t,s.from)>0;)a.shift().clear(),s=a[0].find();for(c(t,s.from)<0&&(s.to.line-t.line0&&(n.line-u.from.linebo&&setTimeout(function(){s.display.input.reset(!0)},20),jt(this),Ki(),bt(this),this.curOp.forceUpdate=!0,Xr(this,i),r.autofocus&&!Ao||s.hasFocus()?setTimeout(Bi(vn,this),20):yn(this);for(var u in ta)ta.hasOwnProperty(u)&&ta[u](this,r[u],na);k(this),r.finishInit&&r.finishInit(this);for(var f=0;fbo&&(r.gutters.style.zIndex=-1,r.scroller.style.paddingRight=0),wo||go&&Ao||(r.scroller.draggable=!0),e&&(e.appendChild?e.appendChild(r.wrapper):e(r.wrapper)),r.viewFrom=r.viewTo=t.first,r.reportedViewFrom=r.reportedViewTo=t.first,r.view=[],r.renderedView=null,r.externalMeasured=null,r.viewOffset=0,r.lastWrapHeight=r.lastWrapWidth=0,r.updateLineNumbers=null,r.nativeBarWidth=r.barHeight=r.barWidth=0,r.scrollbarsClipped=!1,r.lineNumWidth=r.lineNumInnerWidth=r.lineNumChars=null,r.alignWidgets=!1,r.cachedCharWidth=r.cachedTextHeight=r.cachedPaddingH=null, +r.maxLine=null,r.maxLineLength=0,r.maxLineChanged=!1,r.wheelDX=r.wheelDY=r.wheelStartX=r.wheelStartY=null,r.shift=!1,r.selForContextMenu=null,r.activeTouch=null,n.init(r)}function n(t){t.doc.mode=e.getMode(t.options,t.doc.modeOption),r(t)}function r(e){e.doc.iter(function(e){e.stateAfter&&(e.stateAfter=null),e.styles&&(e.styles=null)}),e.doc.frontier=e.doc.first,_e(e,100),e.state.modeGen++,e.curOp&&Dt(e)}function i(e){e.options.lineWrapping?(Ja(e.display.wrapper,"CodeMirror-wrap"),e.display.sizer.style.minWidth="",e.display.sizerWidth=null):(Za(e.display.wrapper,"CodeMirror-wrap"),h(e)),a(e),Dt(e),lt(e),setTimeout(function(){y(e)},100)}function o(e){var t=yt(e.display),n=e.options.lineWrapping,r=n&&Math.max(5,e.display.scroller.clientWidth/xt(e.display)-3);return function(i){if(kr(e.doc,i))return 0;var o=0;if(i.widgets)for(var a=0;at.maxLineLength&&(t.maxLineLength=n,t.maxLine=e)})}function d(e){var t=Pi(e.gutters,"CodeMirror-linenumbers");-1==t&&e.lineNumbers?e.gutters=e.gutters.concat(["CodeMirror-linenumbers"]):t>-1&&!e.lineNumbers&&(e.gutters=e.gutters.slice(0),e.gutters.splice(t,1))}function p(e){var t=e.display,n=t.gutters.offsetWidth,r=Math.round(e.doc.height+qe(e.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:e.options.fixedGutter?n:0,docHeight:r,scrollHeight:r+Ye(e)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:n}}function m(e,t,n){this.cm=n;var r=this.vert=ji("div",[ji("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),i=this.horiz=ji("div",[ji("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");e(r),e(i),Ea(r,"scroll",function(){r.clientHeight&&t(r.scrollTop,"vertical")}),Ea(i,"scroll",function(){i.clientWidth&&t(i.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,xo&&8>bo&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")}function g(){}function v(t){t.display.scrollbars&&(t.display.scrollbars.clear(),t.display.scrollbars.addClass&&Za(t.display.wrapper,t.display.scrollbars.addClass)),t.display.scrollbars=new e.scrollbarModel[t.options.scrollbarStyle](function(e){t.display.wrapper.insertBefore(e,t.display.scrollbarFiller),Ea(e,"mousedown",function(){t.state.focused&&setTimeout(function(){t.display.input.focus()},0)}),e.setAttribute("cm-not-content","true")},function(e,n){"horizontal"==n?on(t,e):rn(t,e)},t),t.display.scrollbars.addClass&&Ja(t.display.wrapper,t.display.scrollbars.addClass)}function y(e,t){t||(t=p(e));var n=e.display.barWidth,r=e.display.barHeight;x(e,t);for(var i=0;4>i&&n!=e.display.barWidth||r!=e.display.barHeight;i++)n!=e.display.barWidth&&e.options.lineWrapping&&O(e),x(e,p(e)),n=e.display.barWidth,r=e.display.barHeight}function x(e,t){var n=e.display,r=n.scrollbars.update(t);n.sizer.style.paddingRight=(n.barWidth=r.right)+"px",n.sizer.style.paddingBottom=(n.barHeight=r.bottom)+"px",n.heightForcer.style.borderBottom=r.bottom+"px solid transparent",r.right&&r.bottom?(n.scrollbarFiller.style.display="block",n.scrollbarFiller.style.height=r.bottom+"px",n.scrollbarFiller.style.width=r.right+"px"):n.scrollbarFiller.style.display="",r.bottom&&e.options.coverGutterNextToScrollbar&&e.options.fixedGutter?(n.gutterFiller.style.display="block",n.gutterFiller.style.height=r.bottom+"px",n.gutterFiller.style.width=t.gutterWidth+"px"):n.gutterFiller.style.display=""}function b(e,t,n){var r=n&&null!=n.top?Math.max(0,n.top):e.scroller.scrollTop;r=Math.floor(r-Ue(e));var i=n&&null!=n.bottom?n.bottom:r+e.wrapper.clientHeight,o=ni(t,r),a=ni(t,i);if(n&&n.ensure){var l=n.ensure.from.line,s=n.ensure.to.line;o>l?(o=l,a=ni(t,ri(Zr(t,l))+e.wrapper.clientHeight)):Math.min(s,t.lastLine())>=a&&(o=ni(t,ri(Zr(t,s))-e.wrapper.clientHeight),a=s)}return{from:o,to:Math.max(a,o+1)}}function w(e){var t=e.display,n=t.view;if(t.alignWidgets||t.gutters.firstChild&&e.options.fixedGutter){for(var r=C(t)-t.scroller.scrollLeft+e.doc.scrollLeft,i=t.gutters.offsetWidth,o=r+"px",a=0;a=n.viewFrom&&t.visible.to<=n.viewTo&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo)&&n.renderedView==n.view&&0==zt(e))return!1;k(e)&&(Wt(e),t.dims=P(e));var i=r.first+r.size,o=Math.max(t.visible.from-e.options.viewportMargin,r.first),a=Math.min(i,t.visible.to+e.options.viewportMargin);n.viewFroma&&n.viewTo-a<20&&(a=Math.min(i,n.viewTo)),Wo&&(o=br(e.doc,o),a=wr(e.doc,a));var l=o!=n.viewFrom||a!=n.viewTo||n.lastWrapHeight!=t.wrapperHeight||n.lastWrapWidth!=t.wrapperWidth;Ft(e,o,a),n.viewOffset=ri(Zr(e.doc,n.viewFrom)),e.display.mover.style.top=n.viewOffset+"px";var s=zt(e);if(!l&&0==s&&!t.force&&n.renderedView==n.view&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo))return!1;var c=Gi();return s>4&&(n.lineDiv.style.display="none"),R(e,n.updateLineNumbers,t.dims),s>4&&(n.lineDiv.style.display=""),n.renderedView=n.view,c&&Gi()!=c&&c.offsetHeight&&c.focus(),Ui(n.cursorDiv),Ui(n.selectionDiv),n.gutters.style.height=n.sizer.style.minHeight=0,l&&(n.lastWrapHeight=t.wrapperHeight,n.lastWrapWidth=t.wrapperWidth,_e(e,400)),n.updateLineNumbers=null,!0}function N(e,t){for(var n=t.viewport,r=!0;(r&&e.options.lineWrapping&&t.oldDisplayWidth!=$e(e)||(n&&null!=n.top&&(n={top:Math.min(e.doc.height+qe(e.display)-Ve(e),n.top)}),t.visible=b(e.display,e.doc,n),!(t.visible.from>=e.display.viewFrom&&t.visible.to<=e.display.viewTo)))&&M(e,t);r=!1){O(e);var i=p(e);Re(e),y(e,i),E(e,i)}t.signal(e,"update",e),e.display.viewFrom==e.display.reportedViewFrom&&e.display.viewTo==e.display.reportedViewTo||(t.signal(e,"viewportChange",e,e.display.viewFrom,e.display.viewTo),e.display.reportedViewFrom=e.display.viewFrom,e.display.reportedViewTo=e.display.viewTo)}function A(e,t){var n=new L(e,t);if(M(e,n)){O(e),N(e,n);var r=p(e);Re(e),y(e,r),E(e,r),n.finish()}}function E(e,t){e.display.sizer.style.minHeight=t.docHeight+"px",e.display.heightForcer.style.top=t.docHeight+"px",e.display.gutters.style.height=t.docHeight+e.display.barHeight+Ye(e)+"px"}function O(e){for(var t=e.display,n=t.lineDiv.offsetTop,r=0;rbo){var a=o.node.offsetTop+o.node.offsetHeight;i=a-n,n=a}else{var l=o.node.getBoundingClientRect();i=l.bottom-l.top}var s=o.line.height-i;if(2>i&&(i=yt(t)),(s>.001||-.001>s)&&(ei(o.line,i),I(o.line),o.rest))for(var c=0;c=t&&f.lineNumber;f.changes&&(Pi(f.changes,"gutter")>-1&&(h=!1),D(e,f,c,n)),h&&(Ui(f.lineNumber),f.lineNumber.appendChild(document.createTextNode(S(e.options,c)))),l=f.node.nextSibling}else{var d=U(e,f,c,n);a.insertBefore(d,l)}c+=f.size}for(;l;)l=r(l)}function D(e,t,n,r){for(var i=0;ibo&&(e.node.style.zIndex=2)),e.node}function W(e){var t=e.bgClass?e.bgClass+" "+(e.line.bgClass||""):e.line.bgClass;if(t&&(t+=" CodeMirror-linebackground"),e.background)t?e.background.className=t:(e.background.parentNode.removeChild(e.background),e.background=null);else if(t){var n=H(e);e.background=n.insertBefore(ji("div",null,t),n.firstChild)}}function B(e,t){var n=e.display.externalMeasured;return n&&n.line==t.line?(e.display.externalMeasured=null,t.measure=n.measure,n.built):Br(e,t)}function _(e,t){var n=t.text.className,r=B(e,t);t.text==t.node&&(t.node=r.pre),t.text.parentNode.replaceChild(r.pre,t.text),t.text=r.pre,r.bgClass!=t.bgClass||r.textClass!=t.textClass?(t.bgClass=r.bgClass,t.textClass=r.textClass,F(t)):n&&(t.text.className=n)}function F(e){W(e),e.line.wrapClass?H(e).className=e.line.wrapClass:e.node!=e.text&&(e.node.className="");var t=e.textClass?e.textClass+" "+(e.line.textClass||""):e.line.textClass;e.text.className=t||""}function z(e,t,n,r){if(t.gutter&&(t.node.removeChild(t.gutter),t.gutter=null),t.gutterBackground&&(t.node.removeChild(t.gutterBackground),t.gutterBackground=null),t.line.gutterClass){var i=H(t);t.gutterBackground=ji("div",null,"CodeMirror-gutter-background "+t.line.gutterClass,"left: "+(e.options.fixedGutter?r.fixedPos:-r.gutterTotalWidth)+"px; width: "+r.gutterTotalWidth+"px"),i.insertBefore(t.gutterBackground,t.text)}var o=t.line.gutterMarkers;if(e.options.lineNumbers||o){var i=H(t),a=t.gutter=ji("div",null,"CodeMirror-gutter-wrapper","left: "+(e.options.fixedGutter?r.fixedPos:-r.gutterTotalWidth)+"px");if(e.display.input.setUneditable(a),i.insertBefore(a,t.text),t.line.gutterClass&&(a.className+=" "+t.line.gutterClass),!e.options.lineNumbers||o&&o["CodeMirror-linenumbers"]||(t.lineNumber=a.appendChild(ji("div",S(e.options,n),"CodeMirror-linenumber CodeMirror-gutter-elt","left: "+r.gutterLeft["CodeMirror-linenumbers"]+"px; width: "+e.display.lineNumInnerWidth+"px"))),o)for(var l=0;l1)if(Fo&&Fo.text.join("\n")==t){if(r.ranges.length%Fo.text.length==0){s=[];for(var c=0;c=0;c--){var u=r.ranges[c],f=u.from(),h=u.to();u.empty()&&(n&&n>0?f=Bo(f.line,f.ch-n):e.state.overwrite&&!a?h=Bo(h.line,Math.min(Zr(o,h.line).text.length,h.ch+Ii(l).length)):Fo&&Fo.lineWise&&Fo.text.join("\n")==t&&(f=h=Bo(f.line,0)));var d=e.curOp.updateInput,p={from:f,to:h,text:s?s[c%s.length]:l,origin:i||(a?"paste":e.state.cutIncoming?"cut":"+input")};Tn(e.doc,p),Ci(e,"inputRead",e,p)}t&&!a&&Q(e,t),Bn(e),e.curOp.updateInput=d,e.curOp.typing=!0,e.state.pasteIncoming=e.state.cutIncoming=!1}function J(e,t){var n=e.clipboardData&&e.clipboardData.getData("text/plain");return n?(e.preventDefault(),t.isReadOnly()||t.options.disableInput||At(t,function(){Z(t,n,0,null,"paste")}),!0):void 0}function Q(e,t){if(e.options.electricChars&&e.options.smartIndent)for(var n=e.doc.sel,r=n.ranges.length-1;r>=0;r--){var i=n.ranges[r];if(!(i.head.ch>100||r&&n.ranges[r-1].head.line==i.head.line)){var o=e.getModeAt(i.head),a=!1;if(o.electricChars){for(var l=0;l-1){a=Fn(e,i.head.line,"smart");break}}else o.electricInput&&o.electricInput.test(Zr(e.doc,i.head.line).text.slice(0,i.head.ch))&&(a=Fn(e,i.head.line,"smart"));a&&Ci(e,"electricInput",e,i.head.line)}}}function ee(e){for(var t=[],n=[],r=0;ri?c.map:u[i],a=0;ai?e.line:e.rest[i]),f=o[a]+r;return(0>r||l!=t)&&(f=o[a+(r?1:0)]),Bo(s,f)}}}var i=e.text.firstChild,o=!1;if(!t||!Va(i,t))return ae(Bo(ti(e.line),0),!0);if(t==i&&(o=!0,t=i.childNodes[n],n=0,!t)){var a=e.rest?Ii(e.rest):e.line;return ae(Bo(ti(a),a.text.length),o)}var l=3==t.nodeType?t:null,s=t;for(l||1!=t.childNodes.length||3!=t.firstChild.nodeType||(l=t.firstChild,n&&(n=l.nodeValue.length));s.parentNode!=i;)s=s.parentNode;var c=e.measure,u=c.maps,f=r(l,s,n);if(f)return ae(f,o);for(var h=s.nextSibling,d=l?l.nodeValue.length-n:0;h;h=h.nextSibling){if(f=r(h,h.firstChild,0))return ae(Bo(f.line,f.ch-d),o);d+=h.textContent.length}for(var p=s.previousSibling,d=n;p;p=p.previousSibling){if(f=r(p,p.firstChild,-1))return ae(Bo(f.line,f.ch+d),o);d+=h.textContent.length}}function ce(e,t,n,r,i){function o(e){return function(t){return t.id==e}}function a(t){if(1==t.nodeType){var n=t.getAttribute("cm-text");if(null!=n)return""==n&&(n=t.textContent.replace(/\u200b/g,"")),void(l+=n);var u,f=t.getAttribute("cm-marker");if(f){var h=e.findMarks(Bo(r,0),Bo(i+1,0),o(+f));return void(h.length&&(u=h[0].find())&&(l+=Jr(e.doc,u.from,u.to).join(c)))}if("false"==t.getAttribute("contenteditable"))return;for(var d=0;d=0){var a=K(o.from(),i.from()),l=V(o.to(),i.to()),s=o.empty()?i.from()==i.head:o.from()==o.head;t>=r&&--t,e.splice(--r,2,new fe(s?l:a,s?a:l))}}return new ue(e,t)}function de(e,t){return new ue([new fe(e,t||e)],0)}function pe(e,t){return Math.max(e.first,Math.min(t,e.first+e.size-1))}function me(e,t){if(t.linen?Bo(n,Zr(e,n).text.length):ge(t,Zr(e,t.line).text.length)}function ge(e,t){var n=e.ch;return null==n||n>t?Bo(e.line,t):0>n?Bo(e.line,0):e}function ve(e,t){return t>=e.first&&t=t.ch:l.to>t.ch))){if(i&&(Pa(s,"beforeCursorEnter"),s.explicitlyCleared)){if(o.markedSpans){--a;continue}break}if(!s.atomic)continue;if(n){var c,u=s.find(0>r?1:-1);if((0>r?s.inclusiveRight:s.inclusiveLeft)&&(u=Pe(e,u,-r,u&&u.line==t.line?o:null)),u&&u.line==t.line&&(c=_o(u,n))&&(0>r?0>c:c>0))return Oe(e,u,t,r,i)}var f=s.find(0>r?-1:1);return(0>r?s.inclusiveLeft:s.inclusiveRight)&&(f=Pe(e,f,r,f.line==t.line?o:null)),f?Oe(e,f,t,r,i):null}}return t}function Ie(e,t,n,r,i){var o=r||1,a=Oe(e,t,n,o,i)||!i&&Oe(e,t,n,o,!0)||Oe(e,t,n,-o,i)||!i&&Oe(e,t,n,-o,!0);return a?a:(e.cantEdit=!0,Bo(e.first,0))}function Pe(e,t,n,r){return 0>n&&0==t.ch?t.line>e.first?me(e,Bo(t.line-1)):null:n>0&&t.ch==(r||Zr(e,t.line)).text.length?t.line=e.display.viewTo||l.to().linet&&(t=0),t=Math.round(t),r=Math.round(r),l.appendChild(ji("div",null,"CodeMirror-selected","position: absolute; left: "+e+"px; top: "+t+"px; width: "+(null==n?u-e:n)+"px; height: "+(r-t)+"px"))}function i(t,n,i){function o(n,r){return ht(e,Bo(t,n),"div",f,r)}var l,s,f=Zr(a,t),h=f.text.length;return eo(ii(f),n||0,null==i?h:i,function(e,t,a){var f,d,p,m=o(e,"left");if(e==t)f=m,d=p=m.left;else{if(f=o(t-1,"right"),"rtl"==a){var g=m;m=f,f=g}d=m.left,p=f.right}null==n&&0==e&&(d=c),f.top-m.top>3&&(r(d,m.top,null,m.bottom),d=c,m.bottoms.bottom||f.bottom==s.bottom&&f.right>s.right)&&(s=f),c+1>d&&(d=c),r(d,f.top,p-d,f.bottom)}),{start:l,end:s}}var o=e.display,a=e.doc,l=document.createDocumentFragment(),s=Ge(e.display),c=s.left,u=Math.max(o.sizerWidth,$e(e)-o.sizer.offsetLeft)-s.right,f=t.from(),h=t.to();if(f.line==h.line)i(f.line,f.ch,h.ch);else{var d=Zr(a,f.line),p=Zr(a,h.line),m=yr(d)==yr(p),g=i(f.line,f.ch,m?d.text.length+1:null).end,v=i(h.line,m?0:null,h.ch).start;m&&(g.top0?t.blinker=setInterval(function(){t.cursorDiv.style.visibility=(n=!n)?"":"hidden"},e.options.cursorBlinkRate):e.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function _e(e,t){e.doc.mode.startState&&e.doc.frontier=e.display.viewTo)){var n=+new Date+e.options.workTime,r=sa(t.mode,je(e,t.frontier)),i=[];t.iter(t.frontier,Math.min(t.first+t.size,e.display.viewTo+500),function(o){if(t.frontier>=e.display.viewFrom){var a=o.styles,l=o.text.length>e.options.maxHighlightLength,s=Rr(e,o,l?sa(t.mode,r):r,!0);o.styles=s.styles;var c=o.styleClasses,u=s.classes;u?o.styleClasses=u:c&&(o.styleClasses=null);for(var f=!a||a.length!=o.styles.length||c!=u&&(!c||!u||c.bgClass!=u.bgClass||c.textClass!=u.textClass),h=0;!f&&hn?(_e(e,e.options.workDelay),!0):void 0}),i.length&&At(e,function(){for(var t=0;ta;--l){if(l<=o.first)return o.first;var s=Zr(o,l-1);if(s.stateAfter&&(!n||l<=o.frontier))return l;var c=Fa(s.text,null,e.options.tabSize);(null==i||r>c)&&(i=l-1,r=c)}return i}function je(e,t,n){var r=e.doc,i=e.display;if(!r.mode.startState)return!0;var o=ze(e,t,n),a=o>r.first&&Zr(r,o-1).stateAfter;return a=a?sa(r.mode,a):ca(r.mode),r.iter(o,t,function(n){Hr(e,n.text,a);var l=o==t-1||o%5==0||o>=i.viewFrom&&o2&&o.push((s.bottom+c.top)/2-n.top)}}o.push(n.bottom-n.top)}}function Xe(e,t,n){if(e.line==t)return{map:e.measure.map,cache:e.measure.cache};for(var r=0;rn)return{map:e.measure.maps[r],cache:e.measure.caches[r],before:!0}}function Ze(e,t){t=yr(t);var n=ti(t),r=e.display.externalMeasured=new Pt(e.doc,t,n);r.lineN=n;var i=r.built=Br(e,r);return r.text=i.pre,qi(e.display.lineMeasure,i.pre),r}function Je(e,t,n,r){return tt(e,et(e,t),n,r)}function Qe(e,t){if(t>=e.display.viewFrom&&t=n.lineN&&tt?(i=0,o=1,a="left"):c>t?(i=t-s,o=i+1):(l==e.length-3||t==c&&e[l+3]>t)&&(o=c-s,i=o-1,t>=c&&(a="right")),null!=i){if(r=e[l+2],s==c&&n==(r.insertLeft?"left":"right")&&(a=n),"left"==n&&0==i)for(;l&&e[l-2]==e[l-3]&&e[l-1].insertLeft;)r=e[(l-=3)+2],a="left";if("right"==n&&i==c-s)for(;lu;u++){for(;l&&zi(t.line.text.charAt(o.coverStart+l));)--l;for(;o.coverStart+sbo&&0==l&&s==o.coverEnd-o.coverStart)i=a.parentNode.getBoundingClientRect();else if(xo&&e.options.lineWrapping){var f=qa(a,l,s).getClientRects();i=f.length?f["right"==r?f.length-1:0]:qo}else i=qa(a,l,s).getBoundingClientRect()||qo;if(i.left||i.right||0==l)break;s=l,l-=1,c="right"}xo&&11>bo&&(i=it(e.display.measure,i))}else{l>0&&(c=r="right");var f;i=e.options.lineWrapping&&(f=a.getClientRects()).length>1?f["right"==r?f.length-1:0]:a.getBoundingClientRect()}if(xo&&9>bo&&!l&&(!i||!i.left&&!i.right)){var h=a.parentNode.getClientRects()[0];i=h?{left:h.left,right:h.left+xt(e.display),top:h.top,bottom:h.bottom}:qo}for(var d=i.top-t.rect.top,p=i.bottom-t.rect.top,m=(d+p)/2,g=t.view.measure.heights,u=0;un.from?a(e-1):a(e,r)}r=r||Zr(e.doc,t.line),i||(i=et(e,r));var s=ii(r),c=t.ch;if(!s)return a(c);var u=co(s,c),f=l(c,u);return null!=al&&(f.other=l(c,al)),f}function pt(e,t){var n=0,t=me(e.doc,t);e.options.lineWrapping||(n=xt(e.display)*t.ch);var r=Zr(e.doc,t.line),i=ri(r)+Ue(e.display);return{left:n,right:n,top:i,bottom:i+r.height}}function mt(e,t,n,r){var i=Bo(e,t);return i.xRel=r,n&&(i.outside=!0),i}function gt(e,t,n){var r=e.doc;if(n+=e.display.viewOffset,0>n)return mt(r.first,0,!0,-1);var i=ni(r,n),o=r.first+r.size-1;if(i>o)return mt(r.first+r.size-1,Zr(r,o).text.length,!0,1);0>t&&(t=0);for(var a=Zr(r,i);;){var l=vt(e,a,i,t,n),s=gr(a),c=s&&s.find(0,!0);if(!s||!(l.ch>c.from.ch||l.ch==c.from.ch&&l.xRel>0))return l;i=ti(a=c.to.line)}}function vt(e,t,n,r,i){function o(r){var i=dt(e,Bo(n,r),"line",t,c);return l=!0,a>i.bottom?i.left-s:ag)return mt(n,d,v,1);for(;;){if(u?d==h||d==fo(t,h,1):1>=d-h){for(var y=p>r||g-r>=r-p?h:d,x=r-(y==h?p:g);zi(t.text.charAt(y));)++y;var b=mt(n,y,y==h?m:v,-1>x?-1:x>1?1:0);return b}var w=Math.ceil(f/2),k=h+w;if(u){k=h;for(var S=0;w>S;++S)k=fo(t,k,1)}var C=o(k);C>r?(d=k,g=C,(v=l)&&(g+=1e3),f=w):(h=k,p=C,m=l,f-=w)}}function yt(e){if(null!=e.cachedTextHeight)return e.cachedTextHeight;if(null==zo){zo=ji("pre");for(var t=0;49>t;++t)zo.appendChild(document.createTextNode("x")),zo.appendChild(ji("br"));zo.appendChild(document.createTextNode("x"))}qi(e.measure,zo);var n=zo.offsetHeight/50;return n>3&&(e.cachedTextHeight=n),Ui(e.measure),n||1}function xt(e){if(null!=e.cachedCharWidth)return e.cachedCharWidth;var t=ji("span","xxxxxxxxxx"),n=ji("pre",[t]);qi(e.measure,n);var r=t.getBoundingClientRect(),i=(r.right-r.left)/10;return i>2&&(e.cachedCharWidth=i),i||10}function bt(e){e.curOp={cm:e,viewChanged:!1,startHeight:e.doc.height,forceUpdate:!1,updateInput:null,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Yo},Go?Go.ops.push(e.curOp):e.curOp.ownsGroup=Go={ops:[e.curOp],delayedCallbacks:[]}}function wt(e){var t=e.delayedCallbacks,n=0;do{for(;n=n.viewTo)||n.maxLineChanged&&t.options.lineWrapping,e.update=e.mustUpdate&&new L(t,e.mustUpdate&&{top:e.scrollTop,ensure:e.scrollToPos},e.forceUpdate)}function Lt(e){e.updatedDisplay=e.mustUpdate&&M(e.cm,e.update)}function Tt(e){var t=e.cm,n=t.display;e.updatedDisplay&&O(t),e.barMeasure=p(t),n.maxLineChanged&&!t.options.lineWrapping&&(e.adjustWidthTo=Je(t,n.maxLine,n.maxLine.text.length).left+3,t.display.sizerWidth=e.adjustWidthTo,e.barMeasure.scrollWidth=Math.max(n.scroller.clientWidth,n.sizer.offsetLeft+e.adjustWidthTo+Ye(t)+t.display.barWidth),e.maxScrollLeft=Math.max(0,n.sizer.offsetLeft+e.adjustWidthTo-$e(t))),(e.updatedDisplay||e.selectionChanged)&&(e.preparedSelection=n.input.prepareSelection(e.focus))}function Mt(e){var t=e.cm;null!=e.adjustWidthTo&&(t.display.sizer.style.minWidth=e.adjustWidthTo+"px",e.maxScrollLefto;o=r){var a=new Pt(e.doc,Zr(e.doc,o),o);r=o+a.size,i.push(a)}return i}function Dt(e,t,n,r){null==t&&(t=e.doc.first),null==n&&(n=e.doc.first+e.doc.size),r||(r=0);var i=e.display;if(r&&nt)&&(i.updateLineNumbers=t),e.curOp.viewChanged=!0,t>=i.viewTo)Wo&&br(e.doc,t)i.viewFrom?Wt(e):(i.viewFrom+=r,i.viewTo+=r);else if(t<=i.viewFrom&&n>=i.viewTo)Wt(e);else if(t<=i.viewFrom){var o=_t(e,n,n+r,1);o?(i.view=i.view.slice(o.index),i.viewFrom=o.lineN,i.viewTo+=r):Wt(e)}else if(n>=i.viewTo){var o=_t(e,t,t,-1);o?(i.view=i.view.slice(0,o.index),i.viewTo=o.lineN):Wt(e)}else{var a=_t(e,t,t,-1),l=_t(e,n,n+r,1);a&&l?(i.view=i.view.slice(0,a.index).concat(Rt(e,a.lineN,l.lineN)).concat(i.view.slice(l.index)),i.viewTo+=r):Wt(e)}var s=i.externalMeasured;s&&(n=i.lineN&&t=r.viewTo)){var o=r.view[Bt(e,t)];if(null!=o.node){var a=o.changes||(o.changes=[]);-1==Pi(a,n)&&a.push(n)}}}function Wt(e){e.display.viewFrom=e.display.viewTo=e.doc.first,e.display.view=[],e.display.viewOffset=0}function Bt(e,t){if(t>=e.display.viewTo)return null;if(t-=e.display.viewFrom,0>t)return null;for(var n=e.display.view,r=0;rt)return r}function _t(e,t,n,r){var i,o=Bt(e,t),a=e.display.view;if(!Wo||n==e.doc.first+e.doc.size)return{index:o,lineN:n};for(var l=0,s=e.display.viewFrom;o>l;l++)s+=a[l].size;if(s!=t){if(r>0){if(o==a.length-1)return null;i=s+a[o].size-t,o++}else i=s-t;t+=i,n+=i}for(;br(e.doc,n)!=n;){if(o==(0>r?0:a.length-1))return null;n+=r*a[o-(0>r?1:0)].size,o+=r}return{index:o,lineN:n}}function Ft(e,t,n){var r=e.display,i=r.view;0==i.length||t>=r.viewTo||n<=r.viewFrom?(r.view=Rt(e,t,n),r.viewFrom=t):(r.viewFrom>t?r.view=Rt(e,t,r.viewFrom).concat(r.view):r.viewFromn&&(r.view=r.view.slice(0,Bt(e,n)))),r.viewTo=n}function zt(e){for(var t=e.display.view,n=0,r=0;r400}var i=e.display;Ea(i.scroller,"mousedown",Et(e,$t)),xo&&11>bo?Ea(i.scroller,"dblclick",Et(e,function(t){if(!Ti(e,t)){var n=Yt(e,t);if(n&&!Jt(e,t)&&!Gt(e.display,t)){Ma(t);var r=e.findWordAt(n);be(e.doc,r.anchor,r.head)}}})):Ea(i.scroller,"dblclick",function(t){Ti(e,t)||Ma(t)}),Do||Ea(i.scroller,"contextmenu",function(t){xn(e,t)});var o,a={end:0};Ea(i.scroller,"touchstart",function(t){if(!Ti(e,t)&&!n(t)){clearTimeout(o);var r=+new Date;i.activeTouch={start:r,moved:!1,prev:r-a.end<=300?a:null},1==t.touches.length&&(i.activeTouch.left=t.touches[0].pageX,i.activeTouch.top=t.touches[0].pageY)}}),Ea(i.scroller,"touchmove",function(){i.activeTouch&&(i.activeTouch.moved=!0)}),Ea(i.scroller,"touchend",function(n){var o=i.activeTouch;if(o&&!Gt(i,n)&&null!=o.left&&!o.moved&&new Date-o.start<300){var a,l=e.coordsChar(i.activeTouch,"page");a=!o.prev||r(o,o.prev)?new fe(l,l):!o.prev.prev||r(o,o.prev.prev)?e.findWordAt(l):new fe(Bo(l.line,0),me(e.doc,Bo(l.line+1,0))),e.setSelection(a.anchor,a.head),e.focus(),Ma(n)}t()}),Ea(i.scroller,"touchcancel",t),Ea(i.scroller,"scroll",function(){i.scroller.clientHeight&&(rn(e,i.scroller.scrollTop),on(e,i.scroller.scrollLeft,!0),Pa(e,"scroll",e))}),Ea(i.scroller,"mousewheel",function(t){an(e,t)}),Ea(i.scroller,"DOMMouseScroll",function(t){an(e,t)}),Ea(i.wrapper,"scroll",function(){i.wrapper.scrollTop=i.wrapper.scrollLeft=0}),i.dragFunctions={enter:function(t){Ti(e,t)||Aa(t)},over:function(t){Ti(e,t)||(tn(e,t),Aa(t))},start:function(t){en(e,t)},drop:Et(e,Qt),leave:function(t){Ti(e,t)||nn(e)}};var l=i.input.getField();Ea(l,"keyup",function(t){pn.call(e,t)}),Ea(l,"keydown",Et(e,hn)),Ea(l,"keypress",Et(e,mn)),Ea(l,"focus",Bi(vn,e)),Ea(l,"blur",Bi(yn,e))}function Ut(t,n,r){var i=r&&r!=e.Init;if(!n!=!i){var o=t.display.dragFunctions,a=n?Ea:Ia;a(t.display.scroller,"dragstart",o.start),a(t.display.scroller,"dragenter",o.enter),a(t.display.scroller,"dragover",o.over),a(t.display.scroller,"dragleave",o.leave),a(t.display.scroller,"drop",o.drop)}}function qt(e){var t=e.display;t.lastWrapHeight==t.wrapper.clientHeight&&t.lastWrapWidth==t.wrapper.clientWidth||(t.cachedCharWidth=t.cachedTextHeight=t.cachedPaddingH=null,t.scrollbarsClipped=!1,e.setSize())}function Gt(e,t){for(var n=wi(t);n!=e.wrapper;n=n.parentNode)if(!n||1==n.nodeType&&"true"==n.getAttribute("cm-ignore-events")||n.parentNode==e.sizer&&n!=e.mover)return!0}function Yt(e,t,n,r){var i=e.display;if(!n&&"true"==wi(t).getAttribute("cm-not-content"))return null;var o,a,l=i.lineSpace.getBoundingClientRect();try{o=t.clientX-l.left,a=t.clientY-l.top}catch(t){return null}var s,c=gt(e,o,a);if(r&&1==c.xRel&&(s=Zr(e.doc,c.line).text).length==c.ch){var u=Fa(s,s.length,e.options.tabSize)-s.length;c=Bo(c.line,Math.max(0,Math.round((o-Ge(e.display).left)/xt(e.display))-u))}return c}function $t(e){var t=this,n=t.display;if(!(Ti(t,e)||n.activeTouch&&n.input.supportsTouch())){if(n.shift=e.shiftKey,Gt(n,e))return void(wo||(n.scroller.draggable=!1,setTimeout(function(){n.scroller.draggable=!0},100)));if(!Jt(t,e)){var r=Yt(t,e);switch(window.focus(),ki(e)){case 1:t.state.selectingText?t.state.selectingText(e):r?Vt(t,e,r):wi(e)==n.scroller&&Ma(e);break;case 2:wo&&(t.state.lastMiddleDown=+new Date),r&&be(t.doc,r),setTimeout(function(){n.input.focus()},20),Ma(e);break;case 3:Do?xn(t,e):gn(t)}}}}function Vt(e,t,n){xo?setTimeout(Bi(X,e),0):e.curOp.focus=Gi();var r,i=+new Date;Uo&&Uo.time>i-400&&0==_o(Uo.pos,n)?r="triple":jo&&jo.time>i-400&&0==_o(jo.pos,n)?(r="double",Uo={time:i,pos:n}):(r="single",jo={time:i,pos:n});var o,a=e.doc.sel,l=Eo?t.metaKey:t.ctrlKey;e.options.dragDrop&&el&&!e.isReadOnly()&&"single"==r&&(o=a.contains(n))>-1&&(_o((o=a.ranges[o]).from(),n)<0||n.xRel>0)&&(_o(o.to(),n)>0||n.xRel<0)?Kt(e,t,n,l):Xt(e,t,n,r,l)}function Kt(e,t,n,r){var i=e.display,o=+new Date,a=Et(e,function(l){wo&&(i.scroller.draggable=!1),e.state.draggingText=!1,Ia(document,"mouseup",a),Ia(i.scroller,"drop",a),Math.abs(t.clientX-l.clientX)+Math.abs(t.clientY-l.clientY)<10&&(Ma(l),!r&&+new Date-200=p;p++){var v=Zr(c,p).text,y=za(v,s,o);s==d?i.push(new fe(Bo(p,y),Bo(p,y))):v.length>y&&i.push(new fe(Bo(p,y),Bo(p,za(v,d,o))))}i.length||i.push(new fe(n,n)),Te(c,he(h.ranges.slice(0,f).concat(i),f),{origin:"*mouse",scroll:!1}),e.scrollIntoView(t)}else{var x=u,b=x.anchor,w=t;if("single"!=r){if("double"==r)var k=e.findWordAt(t);else var k=new fe(Bo(t.line,0),me(c,Bo(t.line+1,0)));_o(k.anchor,b)>0?(w=k.head,b=K(x.from(),k.anchor)):(w=k.anchor,b=V(x.to(),k.head))}var i=h.ranges.slice(0);i[f]=new fe(me(c,b),w),Te(c,he(i,f),Ba)}}function a(t){var n=++y,i=Yt(e,t,!0,"rect"==r);if(i)if(0!=_o(i,g)){e.curOp.focus=Gi(),o(i);var l=b(s,c);(i.line>=l.to||i.linev.bottom?20:0;u&&setTimeout(Et(e,function(){y==n&&(s.scroller.scrollTop+=u,a(t))}),50)}}function l(t){e.state.selectingText=!1,y=1/0,Ma(t),s.input.focus(),Ia(document,"mousemove",x),Ia(document,"mouseup",w),c.history.lastSelOrigin=null}var s=e.display,c=e.doc;Ma(t);var u,f,h=c.sel,d=h.ranges;if(i&&!t.shiftKey?(f=c.sel.contains(n),u=f>-1?d[f]:new fe(n,n)):(u=c.sel.primary(),f=c.sel.primIndex),Oo?t.shiftKey&&t.metaKey:t.altKey)r="rect",i||(u=new fe(n,n)),n=Yt(e,t,!0,!0),f=-1;else if("double"==r){var p=e.findWordAt(n);u=e.display.shift||c.extend?xe(c,u,p.anchor,p.head):p}else if("triple"==r){var m=new fe(Bo(n.line,0),me(c,Bo(n.line+1,0)));u=e.display.shift||c.extend?xe(c,u,m.anchor,m.head):m}else u=xe(c,u,n);i?-1==f?(f=d.length,Te(c,he(d.concat([u]),f),{scroll:!1,origin:"*mouse"})):d.length>1&&d[f].empty()&&"single"==r&&!t.shiftKey?(Te(c,he(d.slice(0,f).concat(d.slice(f+1)),0),{scroll:!1,origin:"*mouse"}),h=c.sel):ke(c,f,u,Ba):(f=0,Te(c,new ue([u],0),Ba),h=c.sel);var g=n,v=s.wrapper.getBoundingClientRect(),y=0,x=Et(e,function(e){ki(e)?a(e):l(e)}),w=Et(e,l);e.state.selectingText=w,Ea(document,"mousemove",x),Ea(document,"mouseup",w)}function Zt(e,t,n,r){try{var i=t.clientX,o=t.clientY}catch(t){return!1}if(i>=Math.floor(e.display.gutters.getBoundingClientRect().right))return!1;r&&Ma(t);var a=e.display,l=a.lineDiv.getBoundingClientRect();if(o>l.bottom||!Ni(e,n))return bi(t);o-=l.top-a.viewOffset;for(var s=0;s=i){var u=ni(e.doc,o),f=e.options.gutters[s];return Pa(e,n,e,u,f,t),bi(t)}}}function Jt(e,t){return Zt(e,t,"gutterClick",!0)}function Qt(e){var t=this;if(nn(t),!Ti(t,e)&&!Gt(t.display,e)){Ma(e),xo&&($o=+new Date);var n=Yt(t,e,!0),r=e.dataTransfer.files;if(n&&!t.isReadOnly())if(r&&r.length&&window.FileReader&&window.File)for(var i=r.length,o=Array(i),a=0,l=function(e,r){if(!t.options.allowDropFileTypes||-1!=Pi(t.options.allowDropFileTypes,e.type)){var l=new FileReader;l.onload=Et(t,function(){var e=l.result;if(/[\x00-\x08\x0e-\x1f]{2}/.test(e)&&(e=""),o[r]=e,++a==i){n=me(t.doc,n);var s={from:n,to:n,text:t.doc.splitLines(o.join(t.doc.lineSeparator())),origin:"paste"};Tn(t.doc,s),Le(t.doc,de(n,Qo(s)))}}),l.readAsText(e)}},s=0;i>s;++s)l(r[s],s);else{if(t.state.draggingText&&t.doc.sel.contains(n)>-1)return t.state.draggingText(e),void setTimeout(function(){t.display.input.focus()},20);try{var o=e.dataTransfer.getData("Text");if(o){if(t.state.draggingText&&!(Eo?e.altKey:e.ctrlKey))var c=t.listSelections();if(Me(t.doc,de(n,n)),c)for(var s=0;sa.clientWidth,s=a.scrollHeight>a.clientHeight;if(r&&l||i&&s){if(i&&Eo&&wo)e:for(var c=t.target,u=o.view;c!=a;c=c.parentNode)for(var f=0;fh?d=Math.max(0,d+h-50):p=Math.min(e.doc.height,p+h+50),A(e,{top:d,bottom:p})}20>Vo&&(null==o.wheelStartX?(o.wheelStartX=a.scrollLeft,o.wheelStartY=a.scrollTop,o.wheelDX=r,o.wheelDY=i,setTimeout(function(){if(null!=o.wheelStartX){var e=a.scrollLeft-o.wheelStartX,t=a.scrollTop-o.wheelStartY,n=t&&o.wheelDY&&t/o.wheelDY||e&&o.wheelDX&&e/o.wheelDX;o.wheelStartX=o.wheelStartY=null,n&&(Ko=(Ko*Vo+n)/(Vo+1),++Vo)}},200)):(o.wheelDX+=r,o.wheelDY+=i))}}function ln(e,t,n){if("string"==typeof t&&(t=ua[t],!t))return!1;e.display.input.ensurePolled();var r=e.display.shift,i=!1;try{e.isReadOnly()&&(e.state.suppressEdits=!0),n&&(e.display.shift=!1),i=t(e)!=Ha}finally{e.display.shift=r,e.state.suppressEdits=!1}return i}function sn(e,t,n){for(var r=0;rbo&&27==e.keyCode&&(e.returnValue=!1);var n=e.keyCode;t.display.shift=16==n||e.shiftKey;var r=un(t,e);Co&&(Jo=r?n:null,!r&&88==n&&!rl&&(Eo?e.metaKey:e.ctrlKey)&&t.replaceSelection("",null,"cut")),18!=n||/\bCodeMirror-crosshair\b/.test(t.display.lineDiv.className)||dn(t)}}function dn(e){function t(e){18!=e.keyCode&&e.altKey||(Za(n,"CodeMirror-crosshair"),Ia(document,"keyup",t),Ia(document,"mouseover",t))}var n=e.display.lineDiv;Ja(n,"CodeMirror-crosshair"),Ea(document,"keyup",t),Ea(document,"mouseover",t)}function pn(e){16==e.keyCode&&(this.doc.sel.shift=!1),Ti(this,e)}function mn(e){var t=this;if(!(Gt(t.display,e)||Ti(t,e)||e.ctrlKey&&!e.altKey||Eo&&e.metaKey)){var n=e.keyCode,r=e.charCode;if(Co&&n==Jo)return Jo=null,void Ma(e);if(!Co||e.which&&!(e.which<10)||!un(t,e)){var i=String.fromCharCode(null==r?n:r);fn(t,e,i)||t.display.input.onKeyPress(e)}}}function gn(e){e.state.delayingBlurEvent=!0,setTimeout(function(){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1,yn(e))},100)}function vn(e){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1),"nocursor"!=e.options.readOnly&&(e.state.focused||(Pa(e,"focus",e),e.state.focused=!0,Ja(e.display.wrapper,"CodeMirror-focused"),e.curOp||e.display.selForContextMenu==e.doc.sel||(e.display.input.reset(),wo&&setTimeout(function(){e.display.input.reset(!0)},20)),e.display.input.receivedFocus()),Be(e))}function yn(e){e.state.delayingBlurEvent||(e.state.focused&&(Pa(e,"blur",e),e.state.focused=!1,Za(e.display.wrapper,"CodeMirror-focused")),clearInterval(e.display.blinker),setTimeout(function(){e.state.focused||(e.display.shift=!1)},150))}function xn(e,t){Gt(e.display,t)||bn(e,t)||Ti(e,t,"contextmenu")||e.display.input.onContextMenu(t)}function bn(e,t){return Ni(e,"gutterContextMenu")?Zt(e,t,"gutterContextMenu",!1):!1}function wn(e,t){if(_o(e,t.from)<0)return e;if(_o(e,t.to)<=0)return Qo(t);var n=e.line+t.text.length-(t.to.line-t.from.line)-1,r=e.ch;return e.line==t.to.line&&(r+=Qo(t).ch-t.to.ch),Bo(n,r)}function kn(e,t){for(var n=[],r=0;r=0;--i)Mn(e,{from:r[i].from,to:r[i].to,text:i?[""]:t.text});else Mn(e,t)}}function Mn(e,t){if(1!=t.text.length||""!=t.text[0]||0!=_o(t.from,t.to)){var n=kn(e,t);ci(e,t,n,e.cm?e.cm.curOp.id:NaN),En(e,t,n,or(e,t));var r=[];Kr(e,function(e,n){n||-1!=Pi(r,e.history)||(xi(e.history,t),r.push(e.history)),En(e,t,null,or(e,t))})}}function Nn(e,t,n){if(!e.cm||!e.cm.state.suppressEdits){for(var r,i=e.history,o=e.sel,a="undo"==t?i.done:i.undone,l="undo"==t?i.undone:i.done,s=0;s=0;--s){var f=r.changes[s];if(f.origin=t,u&&!Ln(e,f,!1))return void(a.length=0);c.push(ai(e,f));var h=s?kn(e,f):Ii(a);En(e,f,h,lr(e,f)),!s&&e.cm&&e.cm.scrollIntoView({from:f.from,to:Qo(f)});var d=[];Kr(e,function(e,t){t||-1!=Pi(d,e.history)||(xi(e.history,f),d.push(e.history)),En(e,f,null,lr(e,f))})}}}}function An(e,t){if(0!=t&&(e.first+=t,e.sel=new ue(Ri(e.sel.ranges,function(e){return new fe(Bo(e.anchor.line+t,e.anchor.ch),Bo(e.head.line+t,e.head.ch))}),e.sel.primIndex),e.cm)){Dt(e.cm,e.first,e.first-t,t);for(var n=e.cm.display,r=n.viewFrom;re.lastLine())){if(t.from.lineo&&(t={from:t.from,to:Bo(o,Zr(e,o).text.length),text:[t.text[0]],origin:t.origin}),t.removed=Jr(e,t.from,t.to),n||(n=kn(e,t)),e.cm?On(e.cm,t,r):Yr(e,t,r),Me(e,n,Wa)}}function On(e,t,n){var r=e.doc,i=e.display,a=t.from,l=t.to,s=!1,c=a.line;e.options.lineWrapping||(c=ti(yr(Zr(r,a.line))),r.iter(c,l.line+1,function(e){return e==i.maxLine?(s=!0,!0):void 0})),r.sel.contains(t.from,t.to)>-1&&Mi(e),Yr(r,t,n,o(e)),e.options.lineWrapping||(r.iter(c,a.line+t.text.length,function(e){var t=f(e);t>i.maxLineLength&&(i.maxLine=e,i.maxLineLength=t,i.maxLineChanged=!0,s=!1)}),s&&(e.curOp.updateMaxLine=!0)),r.frontier=Math.min(r.frontier,a.line),_e(e,400);var u=t.text.length-(l.line-a.line)-1;t.full?Dt(e):a.line!=l.line||1!=t.text.length||Gr(e.doc,t)?Dt(e,a.line,l.line+1,u):Ht(e,a.line,"text");var h=Ni(e,"changes"),d=Ni(e,"change");if(d||h){var p={from:a,to:l,text:t.text,removed:t.removed,origin:t.origin};d&&Ci(e,"change",e,p),h&&(e.curOp.changeObjs||(e.curOp.changeObjs=[])).push(p)}e.display.selForContextMenu=null}function In(e,t,n,r,i){if(r||(r=n),_o(r,n)<0){var o=r;r=n,n=o}"string"==typeof t&&(t=e.splitLines(t)),Tn(e,{from:n,to:r,text:t,origin:i})}function Pn(e,t){if(!Ti(e,"scrollCursorIntoView")){var n=e.display,r=n.sizer.getBoundingClientRect(),i=null;if(t.top+r.top<0?i=!0:t.bottom+r.top>(window.innerHeight||document.documentElement.clientHeight)&&(i=!1),null!=i&&!Mo){var o=ji("div","​",null,"position: absolute; top: "+(t.top-n.viewOffset-Ue(e.display))+"px; height: "+(t.bottom-t.top+Ye(e)+n.barHeight)+"px; left: "+t.left+"px; width: 2px;");e.display.lineSpace.appendChild(o),o.scrollIntoView(i),e.display.lineSpace.removeChild(o)}}}function Rn(e,t,n,r){null==r&&(r=0);for(var i=0;5>i;i++){var o=!1,a=dt(e,t),l=n&&n!=t?dt(e,n):a,s=Hn(e,Math.min(a.left,l.left),Math.min(a.top,l.top)-r,Math.max(a.left,l.left),Math.max(a.bottom,l.bottom)+r),c=e.doc.scrollTop,u=e.doc.scrollLeft;if(null!=s.scrollTop&&(rn(e,s.scrollTop),Math.abs(e.doc.scrollTop-c)>1&&(o=!0)),null!=s.scrollLeft&&(on(e,s.scrollLeft),Math.abs(e.doc.scrollLeft-u)>1&&(o=!0)),!o)break}return a}function Dn(e,t,n,r,i){var o=Hn(e,t,n,r,i);null!=o.scrollTop&&rn(e,o.scrollTop),null!=o.scrollLeft&&on(e,o.scrollLeft)}function Hn(e,t,n,r,i){var o=e.display,a=yt(e.display);0>n&&(n=0);var l=e.curOp&&null!=e.curOp.scrollTop?e.curOp.scrollTop:o.scroller.scrollTop,s=Ve(e),c={};i-n>s&&(i=n+s);var u=e.doc.height+qe(o),f=a>n,h=i>u-a;if(l>n)c.scrollTop=f?0:n;else if(i>l+s){var d=Math.min(n,(h?u:i)-s);d!=l&&(c.scrollTop=d)}var p=e.curOp&&null!=e.curOp.scrollLeft?e.curOp.scrollLeft:o.scroller.scrollLeft,m=$e(e)-(e.options.fixedGutter?o.gutters.offsetWidth:0),g=r-t>m;return g&&(r=t+m),10>t?c.scrollLeft=0:p>t?c.scrollLeft=Math.max(0,t-(g?0:10)):r>m+p-3&&(c.scrollLeft=r+(g?0:10)-m),c}function Wn(e,t,n){null==t&&null==n||_n(e),null!=t&&(e.curOp.scrollLeft=(null==e.curOp.scrollLeft?e.doc.scrollLeft:e.curOp.scrollLeft)+t),null!=n&&(e.curOp.scrollTop=(null==e.curOp.scrollTop?e.doc.scrollTop:e.curOp.scrollTop)+n)}function Bn(e){_n(e);var t=e.getCursor(),n=t,r=t;e.options.lineWrapping||(n=t.ch?Bo(t.line,t.ch-1):t,r=Bo(t.line,t.ch+1)),e.curOp.scrollToPos={from:n,to:r,margin:e.options.cursorScrollMargin,isCursor:!0}}function _n(e){var t=e.curOp.scrollToPos;if(t){e.curOp.scrollToPos=null;var n=pt(e,t.from),r=pt(e,t.to),i=Hn(e,Math.min(n.left,r.left),Math.min(n.top,r.top)-t.margin,Math.max(n.right,r.right),Math.max(n.bottom,r.bottom)+t.margin);e.scrollTo(i.scrollLeft,i.scrollTop)}}function Fn(e,t,n,r){var i,o=e.doc;null==n&&(n="add"),"smart"==n&&(o.mode.indent?i=je(e,t):n="prev");var a=e.options.tabSize,l=Zr(o,t),s=Fa(l.text,null,a);l.stateAfter&&(l.stateAfter=null);var c,u=l.text.match(/^\s*/)[0];if(r||/\S/.test(l.text)){if("smart"==n&&(c=o.mode.indent(i,l.text.slice(u.length),l.text),c==Ha||c>150)){if(!r)return;n="prev"}}else c=0,n="not";"prev"==n?c=t>o.first?Fa(Zr(o,t-1).text,null,a):0:"add"==n?c=s+e.options.indentUnit:"subtract"==n?c=s-e.options.indentUnit:"number"==typeof n&&(c=s+n),c=Math.max(0,c);var f="",h=0;if(e.options.indentWithTabs)for(var d=Math.floor(c/a);d;--d)h+=a,f+=" ";if(c>h&&(f+=Oi(c-h)),f!=u)return In(o,f,Bo(t,0),Bo(t,u.length),"+input"),l.stateAfter=null,!0;for(var d=0;d=0;t--)In(e.doc,"",r[t].from,r[t].to,"+delete");Bn(e)})}function Un(e,t,n,r,i){function o(){var t=l+n;return t=e.first+e.size?!1:(l=t,u=Zr(e,t))}function a(e){var t=(i?fo:ho)(u,s,n,!0);if(null==t){if(e||!o())return!1;s=i?(0>n?io:ro)(u):0>n?u.text.length:0}else s=t;return!0}var l=t.line,s=t.ch,c=n,u=Zr(e,l);if("char"==r)a();else if("column"==r)a(!0);else if("word"==r||"group"==r)for(var f=null,h="group"==r,d=e.cm&&e.cm.getHelper(t,"wordChars"),p=!0;!(0>n)||a(!p);p=!1){var m=u.text.charAt(s)||"\n",g=_i(m,d)?"w":h&&"\n"==m?"n":!h||/\s/.test(m)?null:"p";if(!h||p||g||(g="s"),f&&f!=g){0>n&&(n=1,a());break}if(g&&(f=g),n>0&&!a(!p))break}var v=Ie(e,Bo(l,s),t,c,!0);return _o(t,v)||(v.hitSide=!0),v}function qn(e,t,n,r){var i,o=e.doc,a=t.left;if("page"==r){var l=Math.min(e.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight);i=t.top+n*(l-(0>n?1.5:.5)*yt(e.display))}else"line"==r&&(i=n>0?t.bottom+3:t.top-3);for(;;){var s=gt(e,a,i);if(!s.outside)break;if(0>n?0>=i:i>=o.height){s.hitSide=!0;break}i+=5*n}return s}function Gn(t,n,r,i){e.defaults[t]=n,r&&(ta[t]=i?function(e,t,n){n!=na&&r(e,t,n)}:r)}function Yn(e){for(var t,n,r,i,o=e.split(/-(?!$)/),e=o[o.length-1],a=0;a0||0==a&&o.clearWhenEmpty!==!1)return o;if(o.replacedWith&&(o.collapsed=!0,o.widgetNode=ji("span",[o.replacedWith],"CodeMirror-widget"),r.handleMouseEvents||o.widgetNode.setAttribute("cm-ignore-events","true"),r.insertLeft&&(o.widgetNode.insertLeft=!0)),o.collapsed){if(vr(e,t.line,t,n,o)||t.line!=n.line&&vr(e,n.line,t,n,o))throw new Error("Inserting collapsed marker partially overlapping an existing one");Wo=!0}o.addToHistory&&ci(e,{from:t,to:n,origin:"markText"},e.sel,NaN);var l,s=t.line,c=e.cm;if(e.iter(s,n.line+1,function(e){c&&o.collapsed&&!c.options.lineWrapping&&yr(e)==c.display.maxLine&&(l=!0),o.collapsed&&s!=t.line&&ei(e,0),nr(e,new Qn(o,s==t.line?t.ch:null,s==n.line?n.ch:null)),++s}),o.collapsed&&e.iter(t.line,n.line+1,function(t){kr(e,t)&&ei(t,0)}),o.clearOnEnter&&Ea(o,"beforeCursorEnter",function(){o.clear()}),o.readOnly&&(Ho=!0,(e.history.done.length||e.history.undone.length)&&e.clearHistory()),o.collapsed&&(o.id=++ga,o.atomic=!0),c){if(l&&(c.curOp.updateMaxLine=!0),o.collapsed)Dt(c,t.line,n.line+1);else if(o.className||o.title||o.startStyle||o.endStyle||o.css)for(var u=t.line;u<=n.line;u++)Ht(c,u,"text");o.atomic&&Ae(c.doc),Ci(c,"markerAdded",c,o)}return o}function Kn(e,t,n,r,i){r=Wi(r),r.shared=!1;var o=[Vn(e,t,n,r,i)],a=o[0],l=r.widgetNode;return Kr(e,function(e){l&&(r.widgetNode=l.cloneNode(!0)),o.push(Vn(e,me(e,t),me(e,n),r,i));for(var s=0;s=t:o.to>t);(r||(r=[])).push(new Qn(a,o.from,s?null:o.to))}}return r}function ir(e,t,n){if(e)for(var r,i=0;i=t:o.to>t);if(l||o.from==t&&"bookmark"==a.type&&(!n||o.marker.insertLeft)){var s=null==o.from||(a.inclusiveLeft?o.from<=t:o.from0&&l)for(var f=0;ff;++f)p.push(m);p.push(s)}return p}function ar(e){for(var t=0;t0)){var u=[s,1],f=_o(c.from,l.from),h=_o(c.to,l.to);(0>f||!a.inclusiveLeft&&!f)&&u.push({from:c.from,to:l.from}),(h>0||!a.inclusiveRight&&!h)&&u.push({from:l.to,to:c.to}),i.splice.apply(i,u),s+=u.length-1}}return i}function cr(e){var t=e.markedSpans;if(t){for(var n=0;n=0&&0>=f||0>=u&&f>=0)&&(0>=u&&(s.marker.inclusiveRight&&i.inclusiveLeft?_o(c.to,n)>=0:_o(c.to,n)>0)||u>=0&&(s.marker.inclusiveRight&&i.inclusiveLeft?_o(c.from,r)<=0:_o(c.from,r)<0)))return!0}}}function yr(e){for(var t;t=mr(e);)e=t.find(-1,!0).line;return e}function xr(e){for(var t,n;t=gr(e);)e=t.find(1,!0).line,(n||(n=[])).push(e);return n}function br(e,t){var n=Zr(e,t),r=yr(n);return n==r?t:ti(r)}function wr(e,t){if(t>e.lastLine())return t;var n,r=Zr(e,t);if(!kr(e,r))return t;for(;n=gr(r);)r=n.find(1,!0).line;return ti(r)+1}function kr(e,t){var n=Wo&&t.markedSpans;if(n)for(var r,i=0;io;o++){i&&(i[0]=e.innerMode(t,r).mode);var a=t.token(n,r);if(n.pos>n.start)return a}throw new Error("Mode "+t.name+" failed to advance stream.")}function Ir(e,t,n,r){function i(e){return{start:f.start,end:f.pos,string:f.current(),type:o||null,state:e?sa(a.mode,u):u}}var o,a=e.doc,l=a.mode;t=me(a,t);var s,c=Zr(a,t.line),u=je(e,t.line,n),f=new ma(c.text,e.options.tabSize);for(r&&(s=[]);(r||f.pose.options.maxHighlightLength?(l=!1,a&&Hr(e,t,r,f.pos),f.pos=t.length,s=null):s=Ar(Or(n,f,r,h),o),h){var d=h[0].name;d&&(s="m-"+(s?d+" "+s:d))}if(!l||u!=s){for(;cc;){var r=i[s];r>e&&i.splice(s,1,e,i[s+1],r),s+=2,c=Math.min(e,r)}if(t)if(l.opaque)i.splice(n,s-n,e,"cm-overlay "+t),s=n+2;else for(;s>n;n+=2){var o=i[n+1];i[n+1]=(o?o+" ":"")+"cm-overlay "+t}},o)}return{styles:i,classes:o.bgClass||o.textClass?o:null}}function Dr(e,t,n){if(!t.styles||t.styles[0]!=e.state.modeGen){var r=je(e,ti(t)),i=Rr(e,t,t.text.length>e.options.maxHighlightLength?sa(e.doc.mode,r):r);t.stateAfter=r,t.styles=i.styles,i.classes?t.styleClasses=i.classes:t.styleClasses&&(t.styleClasses=null),n===e.doc.frontier&&e.doc.frontier++}return t.styles}function Hr(e,t,n,r){var i=e.doc.mode,o=new ma(t,e.options.tabSize);for(o.start=o.pos=r||0,""==t&&Er(i,n);!o.eol();)Or(i,o,n),o.start=o.pos}function Wr(e,t){if(!e||/^\s*$/.test(e))return null;var n=t.addModeClass?ka:wa;return n[e]||(n[e]=e.replace(/\S+/g,"cm-$&"))}function Br(e,t){var n=ji("span",null,null,wo?"padding-right: .1px":null),r={pre:ji("pre",[n],"CodeMirror-line"),content:n,col:0,pos:0,cm:e,splitSpaces:(xo||wo)&&e.getOption("lineWrapping")};t.measure={};for(var i=0;i<=(t.rest?t.rest.length:0);i++){var o,a=i?t.rest[i-1]:t.line;r.pos=0,r.addToken=Fr,Ji(e.display.measure)&&(o=ii(a))&&(r.addToken=jr(r.addToken,o)),r.map=[];var l=t!=e.display.externalMeasured&&ti(a);qr(a,r,Dr(e,a,l)),a.styleClasses&&(a.styleClasses.bgClass&&(r.bgClass=$i(a.styleClasses.bgClass,r.bgClass||"")),a.styleClasses.textClass&&(r.textClass=$i(a.styleClasses.textClass,r.textClass||""))),0==r.map.length&&r.map.push(0,0,r.content.appendChild(Zi(e.display.measure))),0==i?(t.measure.map=r.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(r.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(wo){var s=r.content.lastChild;(/\bcm-tab\b/.test(s.className)||s.querySelector&&s.querySelector(".cm-tab"))&&(r.content.className="cm-tab-wrap-hack")}return Pa(e,"renderLine",e,t.line,r.pre),r.pre.className&&(r.textClass=$i(r.pre.className,r.textClass||"")),r}function _r(e){var t=ji("span","•","cm-invalidchar");return t.title="\\u"+e.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function Fr(e,t,n,r,i,o,a){if(t){var l=e.splitSpaces?t.replace(/ {3,}/g,zr):t,s=e.cm.state.specialChars,c=!1;if(s.test(t))for(var u=document.createDocumentFragment(),f=0;;){s.lastIndex=f;var h=s.exec(t),d=h?h.index-f:t.length-f;if(d){var p=document.createTextNode(l.slice(f,f+d));xo&&9>bo?u.appendChild(ji("span",[p])):u.appendChild(p),e.map.push(e.pos,e.pos+d,p),e.col+=d,e.pos+=d}if(!h)break;if(f+=d+1," "==h[0]){var m=e.cm.options.tabSize,g=m-e.col%m,p=u.appendChild(ji("span",Oi(g),"cm-tab"));p.setAttribute("role","presentation"),p.setAttribute("cm-text"," "),e.col+=g}else if("\r"==h[0]||"\n"==h[0]){var p=u.appendChild(ji("span","\r"==h[0]?"␍":"␤","cm-invalidchar"));p.setAttribute("cm-text",h[0]),e.col+=1}else{var p=e.cm.options.specialCharPlaceholder(h[0]);p.setAttribute("cm-text",h[0]),xo&&9>bo?u.appendChild(ji("span",[p])):u.appendChild(p),e.col+=1}e.map.push(e.pos,e.pos+1,p),e.pos++}else{e.col+=t.length;var u=document.createTextNode(l);e.map.push(e.pos,e.pos+t.length,u),xo&&9>bo&&(c=!0),e.pos+=t.length}if(n||r||i||c||a){var v=n||"";r&&(v+=r),i&&(v+=i);var y=ji("span",[u],v,a);return o&&(y.title=o),e.content.appendChild(y)}e.content.appendChild(u)}}function zr(e){for(var t=" ",n=0;nc&&h.from<=c)break}if(h.to>=u)return e(n,r,i,o,a,l,s);e(n,r.slice(0,h.to-c),i,o,null,l,s),o=null,r=r.slice(h.to-c),c=h.to}}}function Ur(e,t,n,r){var i=!r&&n.widgetNode;i&&e.map.push(e.pos,e.pos+t,i),!r&&e.cm.display.input.needsContentAttribute&&(i||(i=e.content.appendChild(document.createElement("span"))),i.setAttribute("cm-marker",n.id)),i&&(e.cm.display.input.setUneditable(i),e.content.appendChild(i)),e.pos+=t}function qr(e,t,n){var r=e.markedSpans,i=e.text,o=0;if(r)for(var a,l,s,c,u,f,h,d=i.length,p=0,m=1,g="",v=0;;){if(v==p){s=c=u=f=l="",h=null,v=1/0;for(var y,x=[],b=0;bp||k.collapsed&&w.to==p&&w.from==p)?(null!=w.to&&w.to!=p&&v>w.to&&(v=w.to,c=""),k.className&&(s+=" "+k.className),k.css&&(l=(l?l+";":"")+k.css),k.startStyle&&w.from==p&&(u+=" "+k.startStyle),k.endStyle&&w.to==v&&(y||(y=[])).push(k.endStyle,w.to),k.title&&!f&&(f=k.title),k.collapsed&&(!h||dr(h.marker,k)<0)&&(h=w)):w.from>p&&v>w.from&&(v=w.from)}if(y)for(var b=0;b=d)break;for(var S=Math.min(d,v);;){if(g){var C=p+g.length;if(!h){var L=C>S?g.slice(0,S-p):g;t.addToken(t,L,a?a+s:s,u,p+L.length==v?c:"",f,l)}if(C>=S){g=g.slice(S-p),p=S;break}p=C,u=""}g=i.slice(o,o=n[m++]),a=Wr(n[m++],t.cm.options)}}else for(var m=1;mn;++n)o.push(new ba(c[n],i(n),r));return o}var l=t.from,s=t.to,c=t.text,u=Zr(e,l.line),f=Zr(e,s.line),h=Ii(c),d=i(c.length-1),p=s.line-l.line;if(t.full)e.insert(0,a(0,c.length)),e.remove(c.length,e.size-c.length);else if(Gr(e,t)){var m=a(0,c.length-1);o(f,f.text,d),p&&e.remove(l.line,p),m.length&&e.insert(l.line,m)}else if(u==f)if(1==c.length)o(u,u.text.slice(0,l.ch)+h+u.text.slice(s.ch),d);else{var m=a(1,c.length-1);m.push(new ba(h+u.text.slice(s.ch),d,r)),o(u,u.text.slice(0,l.ch)+c[0],i(0)),e.insert(l.line+1,m)}else if(1==c.length)o(u,u.text.slice(0,l.ch)+c[0]+f.text.slice(s.ch),i(0)),e.remove(l.line+1,p);else{o(u,u.text.slice(0,l.ch)+c[0],i(0)),o(f,h+f.text.slice(s.ch),d);var m=a(1,c.length-1);p>1&&e.remove(l.line+1,p-1),e.insert(l.line+1,m)}Ci(e,"change",e,t)}function $r(e){this.lines=e,this.parent=null;for(var t=0,n=0;tt||t>=e.size)throw new Error("There is no line "+(t+e.first)+" in the document.");for(var n=e;!n.lines;)for(var r=0;;++r){var i=n.children[r],o=i.chunkSize();if(o>t){n=i;break}t-=o}return n.lines[t]}function Jr(e,t,n){var r=[],i=t.line;return e.iter(t.line,n.line+1,function(e){var o=e.text;i==n.line&&(o=o.slice(0,n.ch)),i==t.line&&(o=o.slice(t.ch)),r.push(o),++i}),r}function Qr(e,t,n){var r=[];return e.iter(t,n,function(e){r.push(e.text)}),r}function ei(e,t){var n=t-e.height;if(n)for(var r=e;r;r=r.parent)r.height+=n}function ti(e){if(null==e.parent)return null;for(var t=e.parent,n=Pi(t.lines,e),r=t.parent;r;t=r,r=r.parent)for(var i=0;r.children[i]!=t;++i)n+=r.children[i].chunkSize();return n+t.first}function ni(e,t){var n=e.first;e:do{for(var r=0;rt){e=i;continue e}t-=o,n+=i.chunkSize()}return n}while(!e.lines);for(var r=0;rt)break;t-=l}return n+r}function ri(e){e=yr(e);for(var t=0,n=e.parent,r=0;r1&&!e.done[e.done.length-2].ranges?(e.done.pop(),Ii(e.done)):void 0}function ci(e,t,n,r){var i=e.history;i.undone.length=0;var o,a=+new Date;if((i.lastOp==r||i.lastOrigin==t.origin&&t.origin&&("+"==t.origin.charAt(0)&&e.cm&&i.lastModTime>a-e.cm.options.historyEventDelay||"*"==t.origin.charAt(0)))&&(o=si(i,i.lastOp==r))){var l=Ii(o.changes);0==_o(t.from,t.to)&&0==_o(t.from,l.to)?l.to=Qo(t):o.changes.push(ai(e,t))}else{var s=Ii(i.done);for(s&&s.ranges||hi(e.sel,i.done),o={changes:[ai(e,t)],generation:i.generation},i.done.push(o);i.done.length>i.undoDepth;)i.done.shift(),i.done[0].ranges||i.done.shift()}i.done.push(n),i.generation=++i.maxGeneration,i.lastModTime=i.lastSelTime=a,i.lastOp=i.lastSelOp=r,i.lastOrigin=i.lastSelOrigin=t.origin,l||Pa(e,"historyAdded")}function ui(e,t,n,r){var i=t.charAt(0);return"*"==i||"+"==i&&n.ranges.length==r.ranges.length&&n.somethingSelected()==r.somethingSelected()&&new Date-e.history.lastSelTime<=(e.cm?e.cm.options.historyEventDelay:500)}function fi(e,t,n,r){var i=e.history,o=r&&r.origin;n==i.lastSelOp||o&&i.lastSelOrigin==o&&(i.lastModTime==i.lastSelTime&&i.lastOrigin==o||ui(e,o,Ii(i.done),t))?i.done[i.done.length-1]=t:hi(t,i.done),i.lastSelTime=+new Date,i.lastSelOrigin=o,i.lastSelOp=n,r&&r.clearRedo!==!1&&li(i.undone)}function hi(e,t){var n=Ii(t);n&&n.ranges&&n.equals(e)||t.push(e)}function di(e,t,n,r){var i=t["spans_"+e.id],o=0;e.iter(Math.max(e.first,n),Math.min(e.first+e.size,r),function(n){n.markedSpans&&((i||(i=t["spans_"+e.id]={}))[o]=n.markedSpans),++o})}function pi(e){if(!e)return null;for(var t,n=0;n-1&&(Ii(l)[f]=u[f],delete u[f])}}}return i}function vi(e,t,n,r){n0?r.slice():Oa:r||Oa}function Ci(e,t){function n(e){return function(){e.apply(null,o)}}var r=Si(e,t,!1);if(r.length){var i,o=Array.prototype.slice.call(arguments,2);Go?i=Go.delayedCallbacks:Ra?i=Ra:(i=Ra=[],setTimeout(Li,0));for(var a=0;a0}function Ai(e){e.prototype.on=function(e,t){Ea(this,e,t)},e.prototype.off=function(e,t){Ia(this,e,t)}}function Ei(){this.id=null}function Oi(e){for(;ja.length<=e;)ja.push(Ii(ja)+" ");return ja[e]}function Ii(e){return e[e.length-1]}function Pi(e,t){for(var n=0;n-1&&Ya(e)?!0:t.test(e):Ya(e)}function Fi(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t])return!1;return!0}function zi(e){return e.charCodeAt(0)>=768&&$a.test(e)}function ji(e,t,n,r){var i=document.createElement(e);if(n&&(i.className=n),r&&(i.style.cssText=r),"string"==typeof t)i.appendChild(document.createTextNode(t));else if(t)for(var o=0;o0;--t)e.removeChild(e.firstChild);return e}function qi(e,t){return Ui(e).appendChild(t)}function Gi(){for(var e=document.activeElement;e&&e.root&&e.root.activeElement;)e=e.root.activeElement;return e}function Yi(e){return new RegExp("(^|\\s)"+e+"(?:$|\\s)\\s*")}function $i(e,t){for(var n=e.split(" "),r=0;r2&&!(xo&&8>bo))}var n=Ka?ji("span","​"):ji("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");return n.setAttribute("cm-text",""),n}function Ji(e){if(null!=Xa)return Xa;var t=qi(e,document.createTextNode("AخA")),n=qa(t,0,1).getBoundingClientRect();if(!n||n.left==n.right)return!1;var r=qa(t,1,2).getBoundingClientRect();return Xa=r.right-n.right<3}function Qi(e){if(null!=il)return il;var t=qi(e,ji("span","x")),n=t.getBoundingClientRect(),r=qa(t,0,1).getBoundingClientRect();return il=Math.abs(n.left-r.left)>1}function eo(e,t,n,r){if(!e)return r(t,n,"ltr");for(var i=!1,o=0;ot||t==n&&a.to==t)&&(r(Math.max(a.from,t),Math.min(a.to,n),1==a.level?"rtl":"ltr"),i=!0)}i||r(t,n,"ltr")}function to(e){return e.level%2?e.to:e.from}function no(e){return e.level%2?e.from:e.to}function ro(e){var t=ii(e);return t?to(t[0]):0}function io(e){var t=ii(e);return t?no(Ii(t)):e.text.length}function oo(e,t){var n=Zr(e.doc,t),r=yr(n);r!=n&&(t=ti(r));var i=ii(r),o=i?i[0].level%2?io(r):ro(r):0;return Bo(t,o)}function ao(e,t){for(var n,r=Zr(e.doc,t);n=gr(r);)r=n.find(1,!0).line,t=null;var i=ii(r),o=i?i[0].level%2?ro(r):io(r):r.text.length;return Bo(null==t?ti(r):t,o)}function lo(e,t){var n=oo(e,t.line),r=Zr(e.doc,n.line),i=ii(r);if(!i||0==i[0].level){var o=Math.max(0,r.text.search(/\S/)),a=t.line==n.line&&t.ch<=o&&t.ch;return Bo(n.line,a?0:o)}return n}function so(e,t,n){var r=e[0].level;return t==r?!0:n==r?!1:n>t}function co(e,t){al=null;for(var n,r=0;rt)return r;if(i.from==t||i.to==t){if(null!=n)return so(e,i.level,e[n].level)?(i.from!=i.to&&(al=n),r):(i.from!=i.to&&(al=r),n);n=r}}return n}function uo(e,t,n,r){if(!r)return t+n;do t+=n;while(t>0&&zi(e.text.charAt(t)));return t}function fo(e,t,n,r){var i=ii(e);if(!i)return ho(e,t,n,r);for(var o=co(i,t),a=i[o],l=uo(e,t,a.level%2?-n:n,r);;){if(l>a.from&&l0==a.level%2?a.to:a.from);if(a=i[o+=n],!a)return null;l=n>0==a.level%2?uo(e,a.to,-1,r):uo(e,a.from,1,r)}}function ho(e,t,n,r){var i=t+n;if(r)for(;i>0&&zi(e.text.charAt(i));)i+=n;return 0>i||i>e.text.length?null:i}var po=navigator.userAgent,mo=navigator.platform,go=/gecko\/\d/i.test(po),vo=/MSIE \d/.test(po),yo=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(po),xo=vo||yo,bo=xo&&(vo?document.documentMode||6:yo[1]),wo=/WebKit\//.test(po),ko=wo&&/Qt\/\d+\.\d+/.test(po),So=/Chrome\//.test(po),Co=/Opera\//.test(po),Lo=/Apple Computer/.test(navigator.vendor),To=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(po),Mo=/PhantomJS/.test(po),No=/AppleWebKit/.test(po)&&/Mobile\/\w+/.test(po),Ao=No||/Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(po),Eo=No||/Mac/.test(mo),Oo=/\bCrOS\b/.test(po),Io=/win/i.test(mo),Po=Co&&po.match(/Version\/(\d*\.\d*)/);Po&&(Po=Number(Po[1])),Po&&Po>=15&&(Co=!1,wo=!0);var Ro=Eo&&(ko||Co&&(null==Po||12.11>Po)),Do=go||xo&&bo>=9,Ho=!1,Wo=!1;m.prototype=Wi({update:function(e){var t=e.scrollWidth>e.clientWidth+1,n=e.scrollHeight>e.clientHeight+1,r=e.nativeBarWidth;if(n){this.vert.style.display="block",this.vert.style.bottom=t?r+"px":"0";var i=e.viewHeight-(t?r:0);this.vert.firstChild.style.height=Math.max(0,e.scrollHeight-e.clientHeight+i)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=n?r+"px":"0",this.horiz.style.left=e.barLeft+"px";var o=e.viewWidth-e.barLeft-(n?r:0);this.horiz.firstChild.style.width=e.scrollWidth-e.clientWidth+o+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&e.clientHeight>0&&(0==r&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:n?r:0,bottom:t?r:0}},setScrollLeft:function(e){this.horiz.scrollLeft!=e&&(this.horiz.scrollLeft=e),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz)},setScrollTop:function(e){this.vert.scrollTop!=e&&(this.vert.scrollTop=e),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert)},zeroWidthHack:function(){var e=Eo&&!To?"12px":"18px";this.horiz.style.height=this.vert.style.width=e,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new Ei,this.disableVert=new Ei},enableZeroWidthBar:function(e,t){function n(){var r=e.getBoundingClientRect(),i=document.elementFromPoint(r.left+1,r.bottom-1);i!=e?e.style.pointerEvents="none":t.set(1e3,n)}e.style.pointerEvents="auto",t.set(1e3,n)},clear:function(){var e=this.horiz.parentNode;e.removeChild(this.horiz),e.removeChild(this.vert)}},m.prototype),g.prototype=Wi({update:function(){return{bottom:0,right:0}},setScrollLeft:function(){},setScrollTop:function(){},clear:function(){}},g.prototype),e.scrollbarModel={"native":m,"null":g},L.prototype.signal=function(e,t){Ni(e,t)&&this.events.push(arguments)},L.prototype.finish=function(){for(var e=0;e=9&&n.hasSelection&&(n.hasSelection=null),n.poll()}),Ea(o,"paste",function(e){Ti(r,e)||J(e,r)||(r.state.pasteIncoming=!0,n.fastPoll())}),Ea(o,"cut",t),Ea(o,"copy",t),Ea(e.scroller,"paste",function(t){Gt(e,t)||Ti(r,t)||(r.state.pasteIncoming=!0,n.focus())}),Ea(e.lineSpace,"selectstart",function(t){Gt(e,t)||Ma(t)}),Ea(o,"compositionstart",function(){var e=r.getCursor("from");n.composing&&n.composing.range.clear(),n.composing={start:e,range:r.markText(e,r.getCursor("to"),{className:"CodeMirror-composing"})}}),Ea(o,"compositionend",function(){n.composing&&(n.poll(),n.composing.range.clear(),n.composing=null)})},prepareSelection:function(){var e=this.cm,t=e.display,n=e.doc,r=De(e);if(e.options.moveInputWithCursor){var i=dt(e,n.sel.primary().head,"div"),o=t.wrapper.getBoundingClientRect(),a=t.lineDiv.getBoundingClientRect();r.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,i.top+a.top-o.top)),r.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,i.left+a.left-o.left))}return r},showSelection:function(e){var t=this.cm,n=t.display;qi(n.cursorDiv,e.cursors),qi(n.selectionDiv,e.selection),null!=e.teTop&&(this.wrapper.style.top=e.teTop+"px",this.wrapper.style.left=e.teLeft+"px")},reset:function(e){if(!this.contextMenuPending){var t,n,r=this.cm,i=r.doc;if(r.somethingSelected()){this.prevInput="";var o=i.sel.primary();t=rl&&(o.to().line-o.from().line>100||(n=r.getSelection()).length>1e3);var a=t?"-":n||r.getSelection();this.textarea.value=a,r.state.focused&&Ua(this.textarea),xo&&bo>=9&&(this.hasSelection=a)}else e||(this.prevInput=this.textarea.value="",xo&&bo>=9&&(this.hasSelection=null));this.inaccurateSelection=t}},getField:function(){return this.textarea},supportsTouch:function(){return!1},focus:function(){if("nocursor"!=this.cm.options.readOnly&&(!Ao||Gi()!=this.textarea))try{this.textarea.focus()}catch(e){}},blur:function(){this.textarea.blur()},resetPosition:function(){this.wrapper.style.top=this.wrapper.style.left=0; +},receivedFocus:function(){this.slowPoll()},slowPoll:function(){var e=this;e.pollingFast||e.polling.set(this.cm.options.pollInterval,function(){e.poll(),e.cm.state.focused&&e.slowPoll()})},fastPoll:function(){function e(){var r=n.poll();r||t?(n.pollingFast=!1,n.slowPoll()):(t=!0,n.polling.set(60,e))}var t=!1,n=this;n.pollingFast=!0,n.polling.set(20,e)},poll:function(){var e=this.cm,t=this.textarea,n=this.prevInput;if(this.contextMenuPending||!e.state.focused||nl(t)&&!n&&!this.composing||e.isReadOnly()||e.options.disableInput||e.state.keySeq)return!1;var r=t.value;if(r==n&&!e.somethingSelected())return!1;if(xo&&bo>=9&&this.hasSelection===r||Eo&&/[\uf700-\uf7ff]/.test(r))return e.display.input.reset(),!1;if(e.doc.sel==e.display.selForContextMenu){var i=r.charCodeAt(0);if(8203!=i||n||(n="​"),8666==i)return this.reset(),this.cm.execCommand("undo")}for(var o=0,a=Math.min(n.length,r.length);a>o&&n.charCodeAt(o)==r.charCodeAt(o);)++o;var l=this;return At(e,function(){Z(e,r.slice(o),n.length-o,null,l.composing?"*compose":null),r.length>1e3||r.indexOf("\n")>-1?t.value=l.prevInput="":l.prevInput=r,l.composing&&(l.composing.range.clear(),l.composing.range=e.markText(l.composing.start,e.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},ensurePolled:function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},onKeyPress:function(){xo&&bo>=9&&(this.hasSelection=null),this.fastPoll()},onContextMenu:function(e){function t(){if(null!=a.selectionStart){var e=i.somethingSelected(),t="​"+(e?a.value:"");a.value="⇚",a.value=t,r.prevInput=e?"":"​",a.selectionStart=1,a.selectionEnd=t.length,o.selForContextMenu=i.doc.sel}}function n(){if(r.contextMenuPending=!1,r.wrapper.style.cssText=f,a.style.cssText=u,xo&&9>bo&&o.scrollbars.setScrollTop(o.scroller.scrollTop=s),null!=a.selectionStart){(!xo||xo&&9>bo)&&t();var e=0,n=function(){o.selForContextMenu==i.doc.sel&&0==a.selectionStart&&a.selectionEnd>0&&"​"==r.prevInput?Et(i,ua.selectAll)(i):e++<10?o.detectingSelectAll=setTimeout(n,500):o.input.reset()};o.detectingSelectAll=setTimeout(n,200)}}var r=this,i=r.cm,o=i.display,a=r.textarea,l=Yt(i,e),s=o.scroller.scrollTop;if(l&&!Co){var c=i.options.resetSelectionOnContextMenu;c&&-1==i.doc.sel.contains(l)&&Et(i,Te)(i.doc,de(l),Wa);var u=a.style.cssText,f=r.wrapper.style.cssText;r.wrapper.style.cssText="position: absolute";var h=r.wrapper.getBoundingClientRect();if(a.style.cssText="position: absolute; width: 30px; height: 30px; top: "+(e.clientY-h.top-5)+"px; left: "+(e.clientX-h.left-5)+"px; z-index: 1000; background: "+(xo?"rgba(255, 255, 255, .05)":"transparent")+"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",wo)var d=window.scrollY;if(o.input.focus(),wo&&window.scrollTo(null,d),o.input.reset(),i.somethingSelected()||(a.value=r.prevInput=" "),r.contextMenuPending=!0,o.selForContextMenu=i.doc.sel,clearTimeout(o.detectingSelectAll),xo&&bo>=9&&t(),Do){Aa(e);var p=function(){Ia(window,"mouseup",p),setTimeout(n,20)};Ea(window,"mouseup",p)}else setTimeout(n,50)}},readOnlyChanged:function(e){e||this.reset()},setUneditable:Di,needsContentAttribute:!1},ne.prototype),ie.prototype=Wi({init:function(e){function t(e){if(!Ti(r,e)){if(r.somethingSelected())Fo={lineWise:!1,text:r.getSelections()},"cut"==e.type&&r.replaceSelection("",null,"cut");else{if(!r.options.lineWiseCopyCut)return;var t=ee(r);Fo={lineWise:!0,text:t.text},"cut"==e.type&&r.operation(function(){r.setSelections(t.ranges,0,Wa),r.replaceSelection("",null,"cut")})}if(e.clipboardData&&!No)e.preventDefault(),e.clipboardData.clearData(),e.clipboardData.setData("text/plain",Fo.text.join("\n"));else{var n=re(),i=n.firstChild;r.display.lineSpace.insertBefore(n,r.display.lineSpace.firstChild),i.value=Fo.text.join("\n");var o=document.activeElement;Ua(i),setTimeout(function(){r.display.lineSpace.removeChild(n),o.focus()},50)}}}var n=this,r=n.cm,i=n.div=e.lineDiv;te(i),Ea(i,"paste",function(e){Ti(r,e)||J(e,r)}),Ea(i,"compositionstart",function(e){var t=e.data;if(n.composing={sel:r.doc.sel,data:t,startData:t},t){var i=r.doc.sel.primary(),o=r.getLine(i.head.line),a=o.indexOf(t,Math.max(0,i.head.ch-t.length));a>-1&&a<=i.head.ch&&(n.composing.sel=de(Bo(i.head.line,a),Bo(i.head.line,a+t.length)))}}),Ea(i,"compositionupdate",function(e){n.composing.data=e.data}),Ea(i,"compositionend",function(e){var t=n.composing;t&&(e.data==t.startData||/\u200b/.test(e.data)||(t.data=e.data),setTimeout(function(){t.handled||n.applyComposition(t),n.composing==t&&(n.composing=null)},50))}),Ea(i,"touchstart",function(){n.forceCompositionEnd()}),Ea(i,"input",function(){n.composing||!r.isReadOnly()&&n.pollContent()||At(n.cm,function(){Dt(r)})}),Ea(i,"copy",t),Ea(i,"cut",t)},prepareSelection:function(){var e=De(this.cm,!1);return e.focus=this.cm.state.focused,e},showSelection:function(e,t){e&&this.cm.display.view.length&&((e.focus||t)&&this.showPrimarySelection(),this.showMultipleSelections(e))},showPrimarySelection:function(){var e=window.getSelection(),t=this.cm.doc.sel.primary(),n=le(this.cm,e.anchorNode,e.anchorOffset),r=le(this.cm,e.focusNode,e.focusOffset);if(!n||n.bad||!r||r.bad||0!=_o(K(n,r),t.from())||0!=_o(V(n,r),t.to())){var i=oe(this.cm,t.from()),o=oe(this.cm,t.to());if(i||o){var a=this.cm.display.view,l=e.rangeCount&&e.getRangeAt(0);if(i){if(!o){var s=a[a.length-1].measure,c=s.maps?s.maps[s.maps.length-1]:s.map;o={node:c[c.length-1],offset:c[c.length-2]-c[c.length-3]}}}else i={node:a[0].measure.map[2],offset:0};try{var u=qa(i.node,i.offset,o.offset,o.node)}catch(f){}u&&(!go&&this.cm.state.focused?(e.collapse(i.node,i.offset),u.collapsed||e.addRange(u)):(e.removeAllRanges(),e.addRange(u)),l&&null==e.anchorNode?e.addRange(l):go&&this.startGracePeriod()),this.rememberSelection()}}},startGracePeriod:function(){var e=this;clearTimeout(this.gracePeriod),this.gracePeriod=setTimeout(function(){e.gracePeriod=!1,e.selectionChanged()&&e.cm.operation(function(){e.cm.curOp.selectionChanged=!0})},20)},showMultipleSelections:function(e){qi(this.cm.display.cursorDiv,e.cursors),qi(this.cm.display.selectionDiv,e.selection)},rememberSelection:function(){var e=window.getSelection();this.lastAnchorNode=e.anchorNode,this.lastAnchorOffset=e.anchorOffset,this.lastFocusNode=e.focusNode,this.lastFocusOffset=e.focusOffset},selectionInEditor:function(){var e=window.getSelection();if(!e.rangeCount)return!1;var t=e.getRangeAt(0).commonAncestorContainer;return Va(this.div,t)},focus:function(){"nocursor"!=this.cm.options.readOnly&&this.div.focus()},blur:function(){this.div.blur()},getField:function(){return this.div},supportsTouch:function(){return!0},receivedFocus:function(){function e(){t.cm.state.focused&&(t.pollSelection(),t.polling.set(t.cm.options.pollInterval,e))}var t=this;this.selectionInEditor()?this.pollSelection():At(this.cm,function(){t.cm.curOp.selectionChanged=!0}),this.polling.set(this.cm.options.pollInterval,e)},selectionChanged:function(){var e=window.getSelection();return e.anchorNode!=this.lastAnchorNode||e.anchorOffset!=this.lastAnchorOffset||e.focusNode!=this.lastFocusNode||e.focusOffset!=this.lastFocusOffset},pollSelection:function(){if(!this.composing&&!this.gracePeriod&&this.selectionChanged()){var e=window.getSelection(),t=this.cm;this.rememberSelection();var n=le(t,e.anchorNode,e.anchorOffset),r=le(t,e.focusNode,e.focusOffset);n&&r&&At(t,function(){Te(t.doc,de(n,r),Wa),(n.bad||r.bad)&&(t.curOp.selectionChanged=!0)})}},pollContent:function(){var e=this.cm,t=e.display,n=e.doc.sel.primary(),r=n.from(),i=n.to();if(r.linet.viewTo-1)return!1;var o;if(r.line==t.viewFrom||0==(o=Bt(e,r.line)))var a=ti(t.view[0].line),l=t.view[0].node;else var a=ti(t.view[o].line),l=t.view[o-1].node.nextSibling;var s=Bt(e,i.line);if(s==t.view.length-1)var c=t.viewTo-1,u=t.lineDiv.lastChild;else var c=ti(t.view[s+1].line)-1,u=t.view[s+1].node.previousSibling;for(var f=e.doc.splitLines(ce(e,l,u,a,c)),h=Jr(e.doc,Bo(a,0),Bo(c,Zr(e.doc,c).text.length));f.length>1&&h.length>1;)if(Ii(f)==Ii(h))f.pop(),h.pop(),c--;else{if(f[0]!=h[0])break;f.shift(),h.shift(),a++}for(var d=0,p=0,m=f[0],g=h[0],v=Math.min(m.length,g.length);v>d&&m.charCodeAt(d)==g.charCodeAt(d);)++d;for(var y=Ii(f),x=Ii(h),b=Math.min(y.length-(1==f.length?d:0),x.length-(1==h.length?d:0));b>p&&y.charCodeAt(y.length-p-1)==x.charCodeAt(x.length-p-1);)++p;f[f.length-1]=y.slice(0,y.length-p),f[0]=f[0].slice(d);var w=Bo(a,d),k=Bo(c,h.length?Ii(h).length-p:0);return f.length>1||f[0]||_o(w,k)?(In(e.doc,f,w,k,"+input"),!0):void 0},ensurePolled:function(){this.forceCompositionEnd()},reset:function(){this.forceCompositionEnd()},forceCompositionEnd:function(){this.composing&&!this.composing.handled&&(this.applyComposition(this.composing),this.composing.handled=!0,this.div.blur(),this.div.focus())},applyComposition:function(e){this.cm.isReadOnly()?Et(this.cm,Dt)(this.cm):e.data&&e.data!=e.startData&&Et(this.cm,Z)(this.cm,e.data,0,e.sel)},setUneditable:function(e){e.contentEditable="false"},onKeyPress:function(e){e.preventDefault(),this.cm.isReadOnly()||Et(this.cm,Z)(this.cm,String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),0)},readOnlyChanged:function(e){this.div.contentEditable=String("nocursor"!=e)},onContextMenu:Di,resetPosition:Di,needsContentAttribute:!0},ie.prototype),e.inputStyles={textarea:ne,contenteditable:ie},ue.prototype={primary:function(){return this.ranges[this.primIndex]},equals:function(e){if(e==this)return!0;if(e.primIndex!=this.primIndex||e.ranges.length!=this.ranges.length)return!1;for(var t=0;t=0&&_o(e,r.to())<=0)return n}return-1}},fe.prototype={from:function(){return K(this.anchor,this.head)},to:function(){return V(this.anchor,this.head)},empty:function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch}};var zo,jo,Uo,qo={left:0,right:0,top:0,bottom:0},Go=null,Yo=0,$o=0,Vo=0,Ko=null;xo?Ko=-.53:go?Ko=15:So?Ko=-.7:Lo&&(Ko=-1/3);var Xo=function(e){var t=e.wheelDeltaX,n=e.wheelDeltaY;return null==t&&e.detail&&e.axis==e.HORIZONTAL_AXIS&&(t=e.detail),null==n&&e.detail&&e.axis==e.VERTICAL_AXIS?n=e.detail:null==n&&(n=e.wheelDelta),{x:t,y:n}};e.wheelEventPixels=function(e){var t=Xo(e);return t.x*=Ko,t.y*=Ko,t};var Zo=new Ei,Jo=null,Qo=e.changeEnd=function(e){return e.text?Bo(e.from.line+e.text.length-1,Ii(e.text).length+(1==e.text.length?e.from.ch:0)):e.to};e.prototype={constructor:e,focus:function(){window.focus(),this.display.input.focus()},setOption:function(e,t){var n=this.options,r=n[e];n[e]==t&&"mode"!=e||(n[e]=t,ta.hasOwnProperty(e)&&Et(this,ta[e])(this,t,r))},getOption:function(e){return this.options[e]},getDoc:function(){return this.doc},addKeyMap:function(e,t){this.state.keyMaps[t?"push":"unshift"]($n(e))},removeKeyMap:function(e){for(var t=this.state.keyMaps,n=0;nn&&(Fn(this,i.head.line,e,!0),n=i.head.line,r==this.doc.sel.primIndex&&Bn(this));else{var o=i.from(),a=i.to(),l=Math.max(n,o.line);n=Math.min(this.lastLine(),a.line-(a.ch?0:1))+1;for(var s=l;n>s;++s)Fn(this,s,e);var c=this.doc.sel.ranges;0==o.ch&&t.length==c.length&&c[r].from().ch>0&&ke(this.doc,r,new fe(o,c[r].to()),Wa)}}}),getTokenAt:function(e,t){return Ir(this,e,t)},getLineTokens:function(e,t){return Ir(this,Bo(e),t,!0)},getTokenTypeAt:function(e){e=me(this.doc,e);var t,n=Dr(this,Zr(this.doc,e.line)),r=0,i=(n.length-1)/2,o=e.ch;if(0==o)t=n[2];else for(;;){var a=r+i>>1;if((a?n[2*a-1]:0)>=o)i=a;else{if(!(n[2*a+1]l?t:0==l?null:t.slice(0,l-1)},getModeAt:function(t){var n=this.doc.mode;return n.innerMode?e.innerMode(n,this.getTokenAt(t).state).mode:n},getHelper:function(e,t){return this.getHelpers(e,t)[0]},getHelpers:function(e,t){var n=[];if(!la.hasOwnProperty(t))return n;var r=la[t],i=this.getModeAt(e);if("string"==typeof i[t])r[i[t]]&&n.push(r[i[t]]);else if(i[t])for(var o=0;oi&&(e=i,r=!0),n=Zr(this.doc,e)}else n=e;return ut(this,n,{top:0,left:0},t||"page").top+(r?this.doc.height-ri(n):0)},defaultTextHeight:function(){return yt(this.display)},defaultCharWidth:function(){return xt(this.display)},setGutterMarker:Ot(function(e,t,n){return zn(this.doc,e,"gutter",function(e){var r=e.gutterMarkers||(e.gutterMarkers={});return r[t]=n,!n&&Fi(r)&&(e.gutterMarkers=null),!0})}),clearGutter:Ot(function(e){var t=this,n=t.doc,r=n.first;n.iter(function(n){n.gutterMarkers&&n.gutterMarkers[e]&&(n.gutterMarkers[e]=null,Ht(t,r,"gutter"),Fi(n.gutterMarkers)&&(n.gutterMarkers=null)),++r})}),lineInfo:function(e){if("number"==typeof e){if(!ve(this.doc,e))return null;var t=e;if(e=Zr(this.doc,e),!e)return null}else{var t=ti(e);if(null==t)return null}return{line:t,handle:e,text:e.text,gutterMarkers:e.gutterMarkers,textClass:e.textClass,bgClass:e.bgClass,wrapClass:e.wrapClass,widgets:e.widgets}},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(e,t,n,r,i){var o=this.display;e=dt(this,me(this.doc,e));var a=e.bottom,l=e.left;if(t.style.position="absolute",t.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(t),o.sizer.appendChild(t),"over"==r)a=e.top;else if("above"==r||"near"==r){var s=Math.max(o.wrapper.clientHeight,this.doc.height),c=Math.max(o.sizer.clientWidth,o.lineSpace.clientWidth);("above"==r||e.bottom+t.offsetHeight>s)&&e.top>t.offsetHeight?a=e.top-t.offsetHeight:e.bottom+t.offsetHeight<=s&&(a=e.bottom),l+t.offsetWidth>c&&(l=c-t.offsetWidth)}t.style.top=a+"px",t.style.left=t.style.right="","right"==i?(l=o.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==i?l=0:"middle"==i&&(l=(o.sizer.clientWidth-t.offsetWidth)/2),t.style.left=l+"px"),n&&Dn(this,l,a,l+t.offsetWidth,a+t.offsetHeight)},triggerOnKeyDown:Ot(hn),triggerOnKeyPress:Ot(mn),triggerOnKeyUp:pn,execCommand:function(e){return ua.hasOwnProperty(e)?ua[e].call(null,this):void 0},triggerElectric:Ot(function(e){Q(this,e)}),findPosH:function(e,t,n,r){var i=1;0>t&&(i=-1,t=-t);for(var o=0,a=me(this.doc,e);t>o&&(a=Un(this.doc,a,i,n,r),!a.hitSide);++o);return a},moveH:Ot(function(e,t){var n=this;n.extendSelectionsBy(function(r){return n.display.shift||n.doc.extend||r.empty()?Un(n.doc,r.head,e,t,n.options.rtlMoveVisually):0>e?r.from():r.to()},_a)}),deleteH:Ot(function(e,t){var n=this.doc.sel,r=this.doc;n.somethingSelected()?r.replaceSelection("",null,"+delete"):jn(this,function(n){var i=Un(r,n.head,e,t,!1);return 0>e?{from:i,to:n.head}:{from:n.head,to:i}})}),findPosV:function(e,t,n,r){var i=1,o=r;0>t&&(i=-1,t=-t);for(var a=0,l=me(this.doc,e);t>a;++a){var s=dt(this,l,"div");if(null==o?o=s.left:s.left=o,l=qn(this,s,i,n),l.hitSide)break}return l},moveV:Ot(function(e,t){var n=this,r=this.doc,i=[],o=!n.display.shift&&!r.extend&&r.sel.somethingSelected();if(r.extendSelectionsBy(function(a){if(o)return 0>e?a.from():a.to();var l=dt(n,a.head,"div");null!=a.goalColumn&&(l.left=a.goalColumn),i.push(l.left);var s=qn(n,l,e,t);return"page"==t&&a==r.sel.primary()&&Wn(n,null,ht(n,s,"div").top-l.top),s},_a),i.length)for(var a=0;a0&&l(n.charAt(r-1));)--r;for(;i.5)&&a(this),Pa(this,"refresh",this)}),swapDoc:Ot(function(e){var t=this.doc;return t.cm=null,Xr(this,e),lt(this),this.display.input.reset(),this.scrollTo(e.scrollLeft,e.scrollTop),this.curOp.forceScroll=!0,Ci(this,"swapDoc",this,t),t}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Ai(e);var ea=e.defaults={},ta=e.optionHandlers={},na=e.Init={toString:function(){return"CodeMirror.Init"}};Gn("value","",function(e,t){e.setValue(t)},!0),Gn("mode",null,function(e,t){e.doc.modeOption=t,n(e)},!0),Gn("indentUnit",2,n,!0),Gn("indentWithTabs",!1),Gn("smartIndent",!0),Gn("tabSize",4,function(e){r(e),lt(e),Dt(e)},!0),Gn("lineSeparator",null,function(e,t){if(e.doc.lineSep=t,t){var n=[],r=e.doc.first;e.doc.iter(function(e){for(var i=0;;){var o=e.text.indexOf(t,i);if(-1==o)break;i=o+t.length,n.push(Bo(r,o))}r++});for(var i=n.length-1;i>=0;i--)In(e.doc,t,n[i],Bo(n[i].line,n[i].ch+t.length))}}),Gn("specialChars",/[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g,function(t,n,r){t.state.specialChars=new RegExp(n.source+(n.test(" ")?"":"| "),"g"),r!=e.Init&&t.refresh()}),Gn("specialCharPlaceholder",_r,function(e){e.refresh()},!0),Gn("electricChars",!0),Gn("inputStyle",Ao?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),Gn("rtlMoveVisually",!Io),Gn("wholeLineUpdateBefore",!0),Gn("theme","default",function(e){l(e),s(e)},!0),Gn("keyMap","default",function(t,n,r){var i=$n(n),o=r!=e.Init&&$n(r);o&&o.detach&&o.detach(t,i),i.attach&&i.attach(t,o||null)}),Gn("extraKeys",null),Gn("lineWrapping",!1,i,!0),Gn("gutters",[],function(e){d(e.options),s(e)},!0),Gn("fixedGutter",!0,function(e,t){e.display.gutters.style.left=t?C(e.display)+"px":"0",e.refresh()},!0),Gn("coverGutterNextToScrollbar",!1,function(e){y(e)},!0),Gn("scrollbarStyle","native",function(e){v(e),y(e),e.display.scrollbars.setScrollTop(e.doc.scrollTop),e.display.scrollbars.setScrollLeft(e.doc.scrollLeft)},!0),Gn("lineNumbers",!1,function(e){d(e.options),s(e)},!0),Gn("firstLineNumber",1,s,!0),Gn("lineNumberFormatter",function(e){return e},s,!0),Gn("showCursorWhenSelecting",!1,Re,!0),Gn("resetSelectionOnContextMenu",!0),Gn("lineWiseCopyCut",!0),Gn("readOnly",!1,function(e,t){"nocursor"==t?(yn(e),e.display.input.blur(),e.display.disabled=!0):e.display.disabled=!1,e.display.input.readOnlyChanged(t)}),Gn("disableInput",!1,function(e,t){t||e.display.input.reset()},!0),Gn("dragDrop",!0,Ut),Gn("allowDropFileTypes",null),Gn("cursorBlinkRate",530),Gn("cursorScrollMargin",0),Gn("cursorHeight",1,Re,!0),Gn("singleCursorHeightPerLine",!0,Re,!0),Gn("workTime",100),Gn("workDelay",100),Gn("flattenSpans",!0,r,!0),Gn("addModeClass",!1,r,!0),Gn("pollInterval",100),Gn("undoDepth",200,function(e,t){e.doc.history.undoDepth=t}),Gn("historyEventDelay",1250),Gn("viewportMargin",10,function(e){e.refresh()},!0),Gn("maxHighlightLength",1e4,r,!0),Gn("moveInputWithCursor",!0,function(e,t){t||e.display.input.resetPosition()}),Gn("tabindex",null,function(e,t){e.display.input.getField().tabIndex=t||""}),Gn("autofocus",null);var ra=e.modes={},ia=e.mimeModes={};e.defineMode=function(t,n){e.defaults.mode||"null"==t||(e.defaults.mode=t),arguments.length>2&&(n.dependencies=Array.prototype.slice.call(arguments,2)),ra[t]=n},e.defineMIME=function(e,t){ia[e]=t},e.resolveMode=function(t){if("string"==typeof t&&ia.hasOwnProperty(t))t=ia[t];else if(t&&"string"==typeof t.name&&ia.hasOwnProperty(t.name)){var n=ia[t.name];"string"==typeof n&&(n={name:n}),t=Hi(n,t),t.name=n.name}else if("string"==typeof t&&/^[\w\-]+\/[\w\-]+\+xml$/.test(t))return e.resolveMode("application/xml");return"string"==typeof t?{name:t}:t||{name:"null"}},e.getMode=function(t,n){var n=e.resolveMode(n),r=ra[n.name];if(!r)return e.getMode(t,"text/plain");var i=r(t,n);if(oa.hasOwnProperty(n.name)){var o=oa[n.name];for(var a in o)o.hasOwnProperty(a)&&(i.hasOwnProperty(a)&&(i["_"+a]=i[a]),i[a]=o[a])}if(i.name=n.name,n.helperType&&(i.helperType=n.helperType),n.modeProps)for(var a in n.modeProps)i[a]=n.modeProps[a];return i},e.defineMode("null",function(){return{token:function(e){e.skipToEnd()}}}),e.defineMIME("text/plain","null");var oa=e.modeExtensions={};e.extendMode=function(e,t){var n=oa.hasOwnProperty(e)?oa[e]:oa[e]={};Wi(t,n)},e.defineExtension=function(t,n){e.prototype[t]=n},e.defineDocExtension=function(e,t){Ca.prototype[e]=t},e.defineOption=Gn;var aa=[];e.defineInitHook=function(e){aa.push(e)};var la=e.helpers={};e.registerHelper=function(t,n,r){la.hasOwnProperty(t)||(la[t]=e[t]={_global:[]}),la[t][n]=r},e.registerGlobalHelper=function(t,n,r,i){e.registerHelper(t,n,i),la[t]._global.push({pred:r,val:i})};var sa=e.copyState=function(e,t){if(t===!0)return t;if(e.copyState)return e.copyState(t);var n={};for(var r in t){var i=t[r];i instanceof Array&&(i=i.concat([])),n[r]=i}return n},ca=e.startState=function(e,t,n){return e.startState?e.startState(t,n):!0};e.innerMode=function(e,t){for(;e.innerMode;){var n=e.innerMode(t);if(!n||n.mode==e)break;t=n.state,e=n.mode}return n||{mode:e,state:t}};var ua=e.commands={selectAll:function(e){e.setSelection(Bo(e.firstLine(),0),Bo(e.lastLine()),Wa)},singleSelection:function(e){e.setSelection(e.getCursor("anchor"),e.getCursor("head"),Wa)},killLine:function(e){jn(e,function(t){if(t.empty()){var n=Zr(e.doc,t.head.line).text.length;return t.head.ch==n&&t.head.line0)i=new Bo(i.line,i.ch+1),e.replaceRange(o.charAt(i.ch-1)+o.charAt(i.ch-2),Bo(i.line,i.ch-2),i,"+transpose");else if(i.line>e.doc.first){var a=Zr(e.doc,i.line-1).text;a&&e.replaceRange(o.charAt(0)+e.doc.lineSeparator()+a.charAt(a.length-1),Bo(i.line-1,a.length-1),Bo(i.line,1),"+transpose")}n.push(new fe(i,i))}e.setSelections(n)})},newlineAndIndent:function(e){At(e,function(){for(var t=e.listSelections().length,n=0;t>n;n++){var r=e.listSelections()[n];e.replaceRange(e.doc.lineSeparator(),r.anchor,r.head,"+input"),e.indentLine(r.from().line+1,null,!0)}Bn(e)})},openLine:function(e){e.replaceSelection("\n","start")},toggleOverwrite:function(e){e.toggleOverwrite()}},fa=e.keyMap={};fa.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},fa.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},fa.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Alt-F":"goWordRight","Alt-B":"goWordLeft","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-D":"delWordAfter","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars","Ctrl-O":"openLine"},fa.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]},fa["default"]=Eo?fa.macDefault:fa.pcDefault,e.normalizeKeyMap=function(e){var t={};for(var n in e)if(e.hasOwnProperty(n)){var r=e[n];if(/^(name|fallthrough|(de|at)tach)$/.test(n))continue;if("..."==r){delete e[n];continue}for(var i=Ri(n.split(" "),Yn),o=0;o=this.string.length},sol:function(){return this.pos==this.lineStart},peek:function(){return this.string.charAt(this.pos)||void 0},next:function(){return this.post},eatSpace:function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},skipToEnd:function(){this.pos=this.string.length},skipTo:function(e){var t=this.string.indexOf(e,this.pos);return t>-1?(this.pos=t,!0):void 0},backUp:function(e){this.pos-=e},column:function(){return this.lastColumnPos0?null:(r&&t!==!1&&(this.pos+=r[0].length),r)}var i=function(e){return n?e.toLowerCase():e},o=this.string.substr(this.pos,e.length);return i(o)==i(e)?(t!==!1&&(this.pos+=e.length),!0):void 0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}}};var ga=0,va=e.TextMarker=function(e,t){this.lines=[],this.type=t,this.doc=e,this.id=++ga};Ai(va),va.prototype.clear=function(){if(!this.explicitlyCleared){var e=this.doc.cm,t=e&&!e.curOp;if(t&&bt(e),Ni(this,"clear")){var n=this.find();n&&Ci(this,"clear",n.from,n.to)}for(var r=null,i=null,o=0;oe.display.maxLineLength&&(e.display.maxLine=s,e.display.maxLineLength=c,e.display.maxLineChanged=!0)}null!=r&&e&&this.collapsed&&Dt(e,r,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,e&&Ae(e.doc)),e&&Ci(e,"markerCleared",e,this),t&&kt(e),this.parent&&this.parent.clear()}},va.prototype.find=function(e,t){null==e&&"bookmark"==this.type&&(e=1);for(var n,r,i=0;in;++n){var i=this.lines[n];this.height-=i.height,Nr(i),Ci(i,"delete")}this.lines.splice(e,t)},collapse:function(e){e.push.apply(e,this.lines)},insertInner:function(e,t,n){this.height+=n,this.lines=this.lines.slice(0,e).concat(t).concat(this.lines.slice(e));for(var r=0;re;++e)if(n(this.lines[e]))return!0}},Vr.prototype={chunkSize:function(){return this.size},removeInner:function(e,t){this.size-=t;for(var n=0;ne){var o=Math.min(t,i-e),a=r.height;if(r.removeInner(e,o),this.height-=a-r.height,i==o&&(this.children.splice(n--,1),r.parent=null),0==(t-=o))break;e=0}else e-=i}if(this.size-t<25&&(this.children.length>1||!(this.children[0]instanceof $r))){var l=[];this.collapse(l),this.children=[new $r(l)],this.children[0].parent=this}},collapse:function(e){for(var t=0;t=e){if(i.insertInner(e,t,n),i.lines&&i.lines.length>50){for(var a=i.lines.length%25+25,l=a;l10);e.parent.maybeSpill()}},iterN:function(e,t,n){for(var r=0;re){var a=Math.min(t,o-e);if(i.iterN(e,a,n))return!0;if(0==(t-=a))break;e=0}else e-=o}}};var Sa=0,Ca=e.Doc=function(e,t,n,r){if(!(this instanceof Ca))return new Ca(e,t,n,r);null==n&&(n=0),Vr.call(this,[new $r([new ba("",null)])]),this.first=n,this.scrollTop=this.scrollLeft=0,this.cantEdit=!1,this.cleanGeneration=1,this.frontier=n;var i=Bo(n,0);this.sel=de(i),this.history=new oi(null),this.id=++Sa,this.modeOption=t,this.lineSep=r,this.extend=!1,"string"==typeof e&&(e=this.splitLines(e)),Yr(this,{from:i,to:i,text:e}),Te(this,de(i),Wa)};Ca.prototype=Hi(Vr.prototype,{constructor:Ca,iter:function(e,t,n){n?this.iterN(e-this.first,t-e,n):this.iterN(this.first,this.first+this.size,e)},insert:function(e,t){for(var n=0,r=0;r=0;o--)Tn(this,r[o]);l?Le(this,l):this.cm&&Bn(this.cm)}),undo:It(function(){Nn(this,"undo")}),redo:It(function(){Nn(this,"redo")}),undoSelection:It(function(){Nn(this,"undo",!0)}),redoSelection:It(function(){Nn(this,"redo",!0)}),setExtending:function(e){this.extend=e},getExtending:function(){return this.extend},historySize:function(){for(var e=this.history,t=0,n=0,r=0;r=e.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(e,t,n){e=me(this,e),t=me(this,t);var r=[],i=e.line;return this.iter(e.line,t.line+1,function(o){var a=o.markedSpans;if(a)for(var l=0;l=s.to||null==s.from&&i!=e.line||null!=s.from&&i==t.line&&s.from>=t.ch||n&&!n(s.marker)||r.push(s.marker.parent||s.marker)}++i}),r},getAllMarks:function(){var e=[];return this.iter(function(t){var n=t.markedSpans;if(n)for(var r=0;re?(t=e,!0):(e-=o,void++n)}),me(this,Bo(n,t))},indexFromPos:function(e){e=me(this,e);var t=e.ch;if(e.linet&&(t=e.from),null!=e.to&&e.tol||l>=t)return a+(t-o);a+=l-o,a+=n-a%n,o=l+1}},za=e.findColumn=function(e,t,n){for(var r=0,i=0;;){var o=e.indexOf(" ",r);-1==o&&(o=e.length);var a=o-r;if(o==e.length||i+a>=t)return r+Math.min(a,t-i);if(i+=o-r,i+=n-i%n,r=o+1,i>=t)return r}},ja=[""],Ua=function(e){e.select()};No?Ua=function(e){e.selectionStart=0,e.selectionEnd=e.value.length}:xo&&(Ua=function(e){try{e.select()}catch(t){}});var qa,Ga=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,Ya=e.isWordChar=function(e){return/\w/.test(e)||e>"€"&&(e.toUpperCase()!=e.toLowerCase()||Ga.test(e))},$a=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;qa=document.createRange?function(e,t,n,r){var i=document.createRange();return i.setEnd(r||e,n),i.setStart(e,t),i}:function(e,t,n){var r=document.body.createTextRange();try{r.moveToElementText(e.parentNode)}catch(i){return r}return r.collapse(!0),r.moveEnd("character",n),r.moveStart("character",t),r};var Va=e.contains=function(e,t){if(3==t.nodeType&&(t=t.parentNode),e.contains)return e.contains(t);do if(11==t.nodeType&&(t=t.host),t==e)return!0;while(t=t.parentNode)};xo&&11>bo&&(Gi=function(){try{return document.activeElement}catch(e){return document.body}});var Ka,Xa,Za=e.rmClass=function(e,t){var n=e.className,r=Yi(t).exec(n);if(r){var i=n.slice(r.index+r[0].length);e.className=n.slice(0,r.index)+(i?r[1]+i:"")}},Ja=e.addClass=function(e,t){var n=e.className;Yi(t).test(n)||(e.className+=(n?" ":"")+t)},Qa=!1,el=function(){if(xo&&9>bo)return!1;var e=ji("div");return"draggable"in e||"dragDrop"in e}(),tl=e.splitLines=3!="\n\nb".split(/\n/).length?function(e){for(var t=0,n=[],r=e.length;r>=t;){var i=e.indexOf("\n",t);-1==i&&(i=e.length);var o=e.slice(t,"\r"==e.charAt(i-1)?i-1:i),a=o.indexOf("\r");-1!=a?(n.push(o.slice(0,a)),t+=a+1):(n.push(o),t=i+1)}return n}:function(e){return e.split(/\r\n?|\n/)},nl=window.getSelection?function(e){try{return e.selectionStart!=e.selectionEnd}catch(t){return!1}}:function(e){try{var t=e.ownerDocument.selection.createRange()}catch(n){}return t&&t.parentElement()==e?0!=t.compareEndPoints("StartToEnd",t):!1},rl=function(){var e=ji("div");return"oncopy"in e?!0:(e.setAttribute("oncopy","return;"),"function"==typeof e.oncopy)}(),il=null,ol=e.keyNames={3:"Enter",8:"Backspace",9:"Tab",13:"Enter",16:"Shift",17:"Ctrl",18:"Alt",19:"Pause",20:"CapsLock",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"PrintScrn",45:"Insert",46:"Delete",59:";",61:"=",91:"Mod",92:"Mod",93:"Mod",106:"*",107:"=",109:"-",110:".",111:"/",127:"Delete",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",63232:"Up",63233:"Down",63234:"Left",63235:"Right",63272:"Delete",63273:"Home",63275:"End",63276:"PageUp",63277:"PageDown",63302:"Insert"};!function(){for(var e=0;10>e;e++)ol[e+48]=ol[e+96]=String(e);for(var e=65;90>=e;e++)ol[e]=String.fromCharCode(e);for(var e=1;12>=e;e++)ol[e+111]=ol[e+63235]="F"+e}();var al,ll=function(){function e(e){return 247>=e?n.charAt(e):e>=1424&&1524>=e?"R":e>=1536&&1773>=e?r.charAt(e-1536):e>=1774&&2220>=e?"r":e>=8192&&8203>=e?"w":8204==e?"b":"L"}function t(e,t,n){this.level=e,this.from=t,this.to=n}var n="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",r="rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm",i=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,o=/[stwN]/,a=/[LRr]/,l=/[Lb1n]/,s=/[1n]/,c="L";return function(n){if(!i.test(n))return!1;for(var r,u=n.length,f=[],h=0;u>h;++h)f.push(r=e(n.charCodeAt(h)));for(var h=0,d=c;u>h;++h){var r=f[h];"m"==r?f[h]=d:d=r}for(var h=0,p=c;u>h;++h){var r=f[h];"1"==r&&"r"==p?f[h]="n":a.test(r)&&(p=r,"r"==r&&(f[h]="R"))}for(var h=1,d=f[0];u-1>h;++h){var r=f[h];"+"==r&&"1"==d&&"1"==f[h+1]?f[h]="1":","!=r||d!=f[h+1]||"1"!=d&&"n"!=d||(f[h]=d),d=r}for(var h=0;u>h;++h){var r=f[h];if(","==r)f[h]="N";else if("%"==r){for(var m=h+1;u>m&&"%"==f[m];++m);for(var g=h&&"!"==f[h-1]||u>m&&"1"==f[m]?"1":"N",v=h;m>v;++v)f[v]=g;h=m-1}}for(var h=0,p=c;u>h;++h){var r=f[h];"L"==p&&"1"==r?f[h]="L":a.test(r)&&(p=r)}for(var h=0;u>h;++h)if(o.test(f[h])){for(var m=h+1;u>m&&o.test(f[m]);++m);for(var y="L"==(h?f[h-1]:c),x="L"==(u>m?f[m]:c),g=y||x?"L":"R",v=h;m>v;++v)f[v]=g;h=m-1}for(var b,w=[],h=0;u>h;)if(l.test(f[h])){var k=h;for(++h;u>h&&l.test(f[h]);++h);w.push(new t(0,k,h))}else{var S=h,C=w.length;for(++h;u>h&&"L"!=f[h];++h);for(var v=S;h>v;)if(s.test(f[v])){v>S&&w.splice(C,0,new t(1,S,v));var L=v;for(++v;h>v&&s.test(f[v]);++v);w.splice(C,0,new t(2,L,v)),S=v}else++v;h>S&&w.splice(C,0,new t(1,S,h))}return 1==w[0].level&&(b=n.match(/^\s+/))&&(w[0].from=b[0].length,w.unshift(new t(0,0,b[0].length))),1==Ii(w).level&&(b=n.match(/\s+$/))&&(Ii(w).to-=b[0].length,w.push(new t(0,u-b[0].length,u))),2==w[0].level&&w.unshift(new t(1,w[0].to,w[0].to)),w[0].level!=Ii(w).level&&w.push(new t(w[0].level,u,u)),w}}();return e.version="5.15.2",e})},{}],11:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../markdown/markdown"),t("../../addon/mode/overlay")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../markdown/markdown","../../addon/mode/overlay"],i):i(CodeMirror)}(function(e){"use strict";var t=/^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i;e.defineMode("gfm",function(n,r){function i(e){return e.code=!1,null}var o=0,a={startState:function(){return{code:!1,codeBlock:!1,ateSpace:!1}},copyState:function(e){return{code:e.code,codeBlock:e.codeBlock,ateSpace:e.ateSpace}},token:function(e,n){if(n.combineTokens=null,n.codeBlock)return e.match(/^```+/)?(n.codeBlock=!1,null):(e.skipToEnd(),null);if(e.sol()&&(n.code=!1),e.sol()&&e.match(/^```+/))return e.skipToEnd(),n.codeBlock=!0,null;if("`"===e.peek()){e.next();var i=e.pos;e.eatWhile("`");var a=1+e.pos-i;return n.code?a===o&&(n.code=!1):(o=a,n.code=!0),null}if(n.code)return e.next(),null;if(e.eatSpace())return n.ateSpace=!0,null;if((e.sol()||n.ateSpace)&&(n.ateSpace=!1,r.gitHubSpice!==!1)){if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/))return n.combineTokens=!0,"link";if(e.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/))return n.combineTokens=!0,"link"}return e.match(t)&&"]("!=e.string.slice(e.start-2,e.start)&&(0==e.start||/\W/.test(e.string.charAt(e.start-1)))?(n.combineTokens=!0,"link"):(e.next(),null)},blankLine:i},l={underscoresBreakWords:!1,taskLists:!0,fencedCodeBlocks:"```",strikethrough:!0};for(var s in r)l[s]=r[s];return l.name="markdown",e.overlayMode(e.getMode(n,l),a)},"markdown"),e.defineMIME("text/x-gfm","gfm")})},{"../../addon/mode/overlay":8,"../../lib/codemirror":10,"../markdown/markdown":12}],12:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../../lib/codemirror"),t("../xml/xml"),t("../meta")):"function"==typeof e&&e.amd?e(["../../lib/codemirror","../xml/xml","../meta"],i):i(CodeMirror)}(function(e){"use strict";e.defineMode("markdown",function(t,n){function r(n){if(e.findModeByName){var r=e.findModeByName(n);r&&(n=r.mime||r.mimes[0])}var i=e.getMode(t,n);return"null"==i.name?null:i}function i(e,t,n){return t.f=t.inline=n,n(e,t)}function o(e,t,n){return t.f=t.block=n,n(e,t)}function a(e){return!e||!/\S/.test(e.string)}function l(e){return e.linkTitle=!1,e.em=!1,e.strong=!1,e.strikethrough=!1,e.quote=0,e.indentedCode=!1,k&&e.f==c&&(e.f=p,e.block=s),e.trailingSpace=0,e.trailingSpaceNewLine=!1,e.prevLine=e.thisLine,e.thisLine=null,null}function s(t,o){var l=t.sol(),s=o.list!==!1,c=o.indentedCode;o.indentedCode=!1,s&&(o.indentationDiff>=0?(o.indentationDiff<4&&(o.indentation-=o.indentationDiff),o.list=null):o.indentation>0?o.list=null:o.list=!1);var f=null;if(o.indentationDiff>=4)return t.skipToEnd(),c||a(o.prevLine)?(o.indentation-=4,o.indentedCode=!0,S.code):null;if(t.eatSpace())return null;if((f=t.match(A))&&f[1].length<=6)return o.header=f[1].length,n.highlightFormatting&&(o.formatting="header"),o.f=o.inline,h(o);if(!(a(o.prevLine)||o.quote||s||c)&&(f=t.match(E)))return o.header="="==f[0].charAt(0)?1:2,n.highlightFormatting&&(o.formatting="header"),o.f=o.inline,h(o);if(t.eat(">"))return o.quote=l?1:o.quote+1,n.highlightFormatting&&(o.formatting="quote"),t.eatSpace(),h(o);if("["===t.peek())return i(t,o,y);if(t.match(L,!0))return o.hr=!0,S.hr;if((a(o.prevLine)||s)&&(t.match(T,!1)||t.match(M,!1))){var d=null;for(t.match(T,!0)?d="ul":(t.match(M,!0),d="ol"),o.indentation=t.column()+t.current().length,o.list=!0;o.listStack&&t.column()")>-1)&&(n.f=p,n.block=s,n.htmlState=null)}return r}function u(e,t){return t.fencedChars&&e.match(t.fencedChars,!1)?(t.localMode=t.localState=null,t.f=t.block=f,null):t.localMode?t.localMode.token(e,t.localState):(e.skipToEnd(),S.code)}function f(e,t){e.match(t.fencedChars),t.block=s,t.f=p,t.fencedChars=null,n.highlightFormatting&&(t.formatting="code-block"),t.code=1;var r=h(t);return t.code=0,r}function h(e){var t=[];if(e.formatting){t.push(S.formatting),"string"==typeof e.formatting&&(e.formatting=[e.formatting]);for(var r=0;r=e.quote?t.push(S.formatting+"-"+e.formatting[r]+"-"+e.quote):t.push("error"))}if(e.taskOpen)return t.push("meta"),t.length?t.join(" "):null;if(e.taskClosed)return t.push("property"),t.length?t.join(" "):null;if(e.linkHref?t.push(S.linkHref,"url"):(e.strong&&t.push(S.strong),e.em&&t.push(S.em),e.strikethrough&&t.push(S.strikethrough),e.linkText&&t.push(S.linkText),e.code&&t.push(S.code)),e.header&&t.push(S.header,S.header+"-"+e.header),e.quote&&(t.push(S.quote),!n.maxBlockquoteDepth||n.maxBlockquoteDepth>=e.quote?t.push(S.quote+"-"+e.quote):t.push(S.quote+"-"+n.maxBlockquoteDepth)),e.list!==!1){var i=(e.listStack.length-1)%3;i?1===i?t.push(S.list2):t.push(S.list3):t.push(S.list1)}return e.trailingSpaceNewLine?t.push("trailing-space-new-line"):e.trailingSpace&&t.push("trailing-space-"+(e.trailingSpace%2?"a":"b")),t.length?t.join(" "):null}function d(e,t){return e.match(O,!0)?h(t):void 0}function p(t,r){var i=r.text(t,r);if("undefined"!=typeof i)return i;if(r.list)return r.list=null,h(r);if(r.taskList){var a="x"!==t.match(N,!0)[1];return a?r.taskOpen=!0:r.taskClosed=!0,n.highlightFormatting&&(r.formatting="task"),r.taskList=!1,h(r)}if(r.taskOpen=!1,r.taskClosed=!1,r.header&&t.match(/^#+$/,!0))return n.highlightFormatting&&(r.formatting="header"), +h(r);var l=t.sol(),s=t.next();if(r.linkTitle){r.linkTitle=!1;var u=s;"("===s&&(u=")"),u=(u+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1");var f="^\\s*(?:[^"+u+"\\\\]+|\\\\\\\\|\\\\.)"+u;if(t.match(new RegExp(f),!0))return S.linkHref}if("`"===s){var d=r.formatting;n.highlightFormatting&&(r.formatting="code"),t.eatWhile("`");var p=t.current().length;if(0==r.code)return r.code=p,h(r);if(p==r.code){var v=h(r);return r.code=0,v}return r.formatting=d,h(r)}if(r.code)return h(r);if("\\"===s&&(t.next(),n.highlightFormatting)){var y=h(r),x=S.formatting+"-escape";return y?y+" "+x:x}if("!"===s&&t.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return t.match(/\[[^\]]*\]/),r.inline=r.f=g,S.image;if("["===s&&t.match(/[^\]]*\](\(.*\)| ?\[.*?\])/,!1))return r.linkText=!0,n.highlightFormatting&&(r.formatting="link"),h(r);if("]"===s&&r.linkText&&t.match(/\(.*?\)| ?\[.*?\]/,!1)){n.highlightFormatting&&(r.formatting="link");var y=h(r);return r.linkText=!1,r.inline=r.f=g,y}if("<"===s&&t.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkInline}if("<"===s&&t.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1)){r.f=r.inline=m,n.highlightFormatting&&(r.formatting="link");var y=h(r);return y?y+=" ":y="",y+S.linkEmail}if("<"===s&&t.match(/^(!--|\w)/,!1)){var b=t.string.indexOf(">",t.pos);if(-1!=b){var k=t.string.substring(t.start,b);/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(k)&&(r.md_inside=!0)}return t.backUp(1),r.htmlState=e.startState(w),o(t,r,c)}if("<"===s&&t.match(/^\/\w*?>/))return r.md_inside=!1,"tag";var C=!1;if(!n.underscoresBreakWords&&"_"===s&&"_"!==t.peek()&&t.match(/(\w)/,!1)){var L=t.pos-2;if(L>=0){var T=t.string.charAt(L);"_"!==T&&T.match(/(\w)/,!1)&&(C=!0)}}if("*"===s||"_"===s&&!C)if(l&&" "===t.peek());else{if(r.strong===s&&t.eat(s)){n.highlightFormatting&&(r.formatting="strong");var v=h(r);return r.strong=!1,v}if(!r.strong&&t.eat(s))return r.strong=s,n.highlightFormatting&&(r.formatting="strong"),h(r);if(r.em===s){n.highlightFormatting&&(r.formatting="em");var v=h(r);return r.em=!1,v}if(!r.em)return r.em=s,n.highlightFormatting&&(r.formatting="em"),h(r)}else if(" "===s&&(t.eat("*")||t.eat("_"))){if(" "===t.peek())return h(r);t.backUp(1)}if(n.strikethrough)if("~"===s&&t.eatWhile(s)){if(r.strikethrough){n.highlightFormatting&&(r.formatting="strikethrough");var v=h(r);return r.strikethrough=!1,v}if(t.match(/^[^\s]/,!1))return r.strikethrough=!0,n.highlightFormatting&&(r.formatting="strikethrough"),h(r)}else if(" "===s&&t.match(/^~~/,!0)){if(" "===t.peek())return h(r);t.backUp(2)}return" "===s&&(t.match(/ +$/,!1)?r.trailingSpace++:r.trailingSpace&&(r.trailingSpaceNewLine=!0)),h(r)}function m(e,t){var r=e.next();if(">"===r){t.f=t.inline=p,n.highlightFormatting&&(t.formatting="link");var i=h(t);return i?i+=" ":i="",i+S.linkInline}return e.match(/^[^>]+/,!0),S.linkInline}function g(e,t){if(e.eatSpace())return null;var r=e.next();return"("===r||"["===r?(t.f=t.inline=v("("===r?")":"]",0),n.highlightFormatting&&(t.formatting="link-string"),t.linkHref=!0,h(t)):"error"}function v(e){return function(t,r){var i=t.next();if(i===e){r.f=r.inline=p,n.highlightFormatting&&(r.formatting="link-string");var o=h(r);return r.linkHref=!1,o}return t.match(P[e]),r.linkHref=!0,h(r)}}function y(e,t){return e.match(/^([^\]\\]|\\.)*\]:/,!1)?(t.f=x,e.next(),n.highlightFormatting&&(t.formatting="link"),t.linkText=!0,h(t)):i(e,t,p)}function x(e,t){if(e.match(/^\]:/,!0)){t.f=t.inline=b,n.highlightFormatting&&(t.formatting="link");var r=h(t);return t.linkText=!1,r}return e.match(/^([^\]\\]|\\.)+/,!0),S.linkText}function b(e,t){return e.eatSpace()?null:(e.match(/^[^\s]+/,!0),void 0===e.peek()?t.linkTitle=!0:e.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,!0),t.f=t.inline=p,S.linkHref+" url")}var w=e.getMode(t,"text/html"),k="null"==w.name;void 0===n.highlightFormatting&&(n.highlightFormatting=!1),void 0===n.maxBlockquoteDepth&&(n.maxBlockquoteDepth=0),void 0===n.underscoresBreakWords&&(n.underscoresBreakWords=!0),void 0===n.taskLists&&(n.taskLists=!1),void 0===n.strikethrough&&(n.strikethrough=!1),void 0===n.tokenTypeOverrides&&(n.tokenTypeOverrides={});var S={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"tag",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough"};for(var C in S)S.hasOwnProperty(C)&&n.tokenTypeOverrides[C]&&(S[C]=n.tokenTypeOverrides[C]);var L=/^([*\-_])(?:\s*\1){2,}\s*$/,T=/^[*\-+]\s+/,M=/^[0-9]+([.)])\s+/,N=/^\[(x| )\](?=\s)/,A=n.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,E=/^ *(?:\={1,}|-{1,})\s*$/,O=/^[^#!\[\]*_\\<>` "'(~]+/,I=new RegExp("^("+(n.fencedCodeBlocks===!0?"~~~+|```+":n.fencedCodeBlocks)+")[ \\t]*([\\w+#-]*)"),P={")":/^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,"]":/^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\\]]|\\.)*\])*?(?=\])/},R={startState:function(){return{f:s,prevLine:null,thisLine:null,block:s,htmlState:null,indentation:0,inline:p,text:d,formatting:!1,linkText:!1,linkHref:!1,linkTitle:!1,code:0,em:!1,strong:!1,header:0,hr:!1,taskList:!1,list:!1,listStack:[],quote:0,trailingSpace:0,trailingSpaceNewLine:!1,strikethrough:!1,fencedChars:null}},copyState:function(t){return{f:t.f,prevLine:t.prevLine,thisLine:t.thisLine,block:t.block,htmlState:t.htmlState&&e.copyState(w,t.htmlState),indentation:t.indentation,localMode:t.localMode,localState:t.localMode?e.copyState(t.localMode,t.localState):null,inline:t.inline,text:t.text,formatting:!1,linkTitle:t.linkTitle,code:t.code,em:t.em,strong:t.strong,strikethrough:t.strikethrough,header:t.header,hr:t.hr,taskList:t.taskList,list:t.list,listStack:t.listStack.slice(0),quote:t.quote,indentedCode:t.indentedCode,trailingSpace:t.trailingSpace,trailingSpaceNewLine:t.trailingSpaceNewLine,md_inside:t.md_inside,fencedChars:t.fencedChars}},token:function(e,t){if(t.formatting=!1,e!=t.thisLine){var n=t.header||t.hr;if(t.header=0,t.hr=!1,e.match(/^\s*$/,!0)||n){if(l(t),!n)return null;t.prevLine=null}t.prevLine=t.thisLine,t.thisLine=e,t.taskList=!1,t.trailingSpace=0,t.trailingSpaceNewLine=!1,t.f=t.block;var r=e.match(/^\s*/,!0)[0].replace(/\t/g," ").length;if(t.indentationDiff=Math.min(r-t.indentation,4),t.indentation=t.indentation+t.indentationDiff,r>0)return null}return t.f(e,t)},innerMode:function(e){return e.block==c?{state:e.htmlState,mode:w}:e.localState?{state:e.localState,mode:e.localMode}:{state:e,mode:R}},blankLine:l,getType:h,fold:"markdown"};return R},"xml"),e.defineMIME("text/x-markdown","markdown")})},{"../../lib/codemirror":10,"../meta":13,"../xml/xml":14}],13:[function(t,n,r){!function(i){"object"==typeof r&&"object"==typeof n?i(t("../lib/codemirror")):"function"==typeof e&&e.amd?e(["../lib/codemirror"],i):i(CodeMirror)}(function(e){"use strict";e.modeInfo=[{name:"APL",mime:"text/apl",mode:"apl",ext:["dyalog","apl"]},{name:"PGP",mimes:["application/pgp","application/pgp-keys","application/pgp-signature"],mode:"asciiarmor",ext:["pgp"]},{name:"ASN.1",mime:"text/x-ttcn-asn",mode:"asn.1",ext:["asn","asn1"]},{name:"Asterisk",mime:"text/x-asterisk",mode:"asterisk",file:/^extensions\.conf$/i},{name:"Brainfuck",mime:"text/x-brainfuck",mode:"brainfuck",ext:["b","bf"]},{name:"C",mime:"text/x-csrc",mode:"clike",ext:["c","h"]},{name:"C++",mime:"text/x-c++src",mode:"clike",ext:["cpp","c++","cc","cxx","hpp","h++","hh","hxx"],alias:["cpp"]},{name:"Cobol",mime:"text/x-cobol",mode:"cobol",ext:["cob","cpy"]},{name:"C#",mime:"text/x-csharp",mode:"clike",ext:["cs"],alias:["csharp"]},{name:"Clojure",mime:"text/x-clojure",mode:"clojure",ext:["clj","cljc","cljx"]},{name:"ClojureScript",mime:"text/x-clojurescript",mode:"clojure",ext:["cljs"]},{name:"Closure Stylesheets (GSS)",mime:"text/x-gss",mode:"css",ext:["gss"]},{name:"CMake",mime:"text/x-cmake",mode:"cmake",ext:["cmake","cmake.in"],file:/^CMakeLists.txt$/},{name:"CoffeeScript",mime:"text/x-coffeescript",mode:"coffeescript",ext:["coffee"],alias:["coffee","coffee-script"]},{name:"Common Lisp",mime:"text/x-common-lisp",mode:"commonlisp",ext:["cl","lisp","el"],alias:["lisp"]},{name:"Cypher",mime:"application/x-cypher-query",mode:"cypher",ext:["cyp","cypher"]},{name:"Cython",mime:"text/x-cython",mode:"python",ext:["pyx","pxd","pxi"]},{name:"Crystal",mime:"text/x-crystal",mode:"crystal",ext:["cr"]},{name:"CSS",mime:"text/css",mode:"css",ext:["css"]},{name:"CQL",mime:"text/x-cassandra",mode:"sql",ext:["cql"]},{name:"D",mime:"text/x-d",mode:"d",ext:["d"]},{name:"Dart",mimes:["application/dart","text/x-dart"],mode:"dart",ext:["dart"]},{name:"diff",mime:"text/x-diff",mode:"diff",ext:["diff","patch"]},{name:"Django",mime:"text/x-django",mode:"django"},{name:"Dockerfile",mime:"text/x-dockerfile",mode:"dockerfile",file:/^Dockerfile$/},{name:"DTD",mime:"application/xml-dtd",mode:"dtd",ext:["dtd"]},{name:"Dylan",mime:"text/x-dylan",mode:"dylan",ext:["dylan","dyl","intr"]},{name:"EBNF",mime:"text/x-ebnf",mode:"ebnf"},{name:"ECL",mime:"text/x-ecl",mode:"ecl",ext:["ecl"]},{name:"edn",mime:"application/edn",mode:"clojure",ext:["edn"]},{name:"Eiffel",mime:"text/x-eiffel",mode:"eiffel",ext:["e"]},{name:"Elm",mime:"text/x-elm",mode:"elm",ext:["elm"]},{name:"Embedded Javascript",mime:"application/x-ejs",mode:"htmlembedded",ext:["ejs"]},{name:"Embedded Ruby",mime:"application/x-erb",mode:"htmlembedded",ext:["erb"]},{name:"Erlang",mime:"text/x-erlang",mode:"erlang",ext:["erl"]},{name:"Factor",mime:"text/x-factor",mode:"factor",ext:["factor"]},{name:"FCL",mime:"text/x-fcl",mode:"fcl"},{name:"Forth",mime:"text/x-forth",mode:"forth",ext:["forth","fth","4th"]},{name:"Fortran",mime:"text/x-fortran",mode:"fortran",ext:["f","for","f77","f90"]},{name:"F#",mime:"text/x-fsharp",mode:"mllike",ext:["fs"],alias:["fsharp"]},{name:"Gas",mime:"text/x-gas",mode:"gas",ext:["s"]},{name:"Gherkin",mime:"text/x-feature",mode:"gherkin",ext:["feature"]},{name:"GitHub Flavored Markdown",mime:"text/x-gfm",mode:"gfm",file:/^(readme|contributing|history).md$/i},{name:"Go",mime:"text/x-go",mode:"go",ext:["go"]},{name:"Groovy",mime:"text/x-groovy",mode:"groovy",ext:["groovy","gradle"]},{name:"HAML",mime:"text/x-haml",mode:"haml",ext:["haml"]},{name:"Haskell",mime:"text/x-haskell",mode:"haskell",ext:["hs"]},{name:"Haskell (Literate)",mime:"text/x-literate-haskell",mode:"haskell-literate",ext:["lhs"]},{name:"Haxe",mime:"text/x-haxe",mode:"haxe",ext:["hx"]},{name:"HXML",mime:"text/x-hxml",mode:"haxe",ext:["hxml"]},{name:"ASP.NET",mime:"application/x-aspx",mode:"htmlembedded",ext:["aspx"],alias:["asp","aspx"]},{name:"HTML",mime:"text/html",mode:"htmlmixed",ext:["html","htm"],alias:["xhtml"]},{name:"HTTP",mime:"message/http",mode:"http"},{name:"IDL",mime:"text/x-idl",mode:"idl",ext:["pro"]},{name:"Jade",mime:"text/x-jade",mode:"jade",ext:["jade"]},{name:"Java",mime:"text/x-java",mode:"clike",ext:["java"]},{name:"Java Server Pages",mime:"application/x-jsp",mode:"htmlembedded",ext:["jsp"],alias:["jsp"]},{name:"JavaScript",mimes:["text/javascript","text/ecmascript","application/javascript","application/x-javascript","application/ecmascript"],mode:"javascript",ext:["js"],alias:["ecmascript","js","node"]},{name:"JSON",mimes:["application/json","application/x-json"],mode:"javascript",ext:["json","map"],alias:["json5"]},{name:"JSON-LD",mime:"application/ld+json",mode:"javascript",ext:["jsonld"],alias:["jsonld"]},{name:"JSX",mime:"text/jsx",mode:"jsx",ext:["jsx"]},{name:"Jinja2",mime:"null",mode:"jinja2"},{name:"Julia",mime:"text/x-julia",mode:"julia",ext:["jl"]},{name:"Kotlin",mime:"text/x-kotlin",mode:"clike",ext:["kt"]},{name:"LESS",mime:"text/x-less",mode:"css",ext:["less"]},{name:"LiveScript",mime:"text/x-livescript",mode:"livescript",ext:["ls"],alias:["ls"]},{name:"Lua",mime:"text/x-lua",mode:"lua",ext:["lua"]},{name:"Markdown",mime:"text/x-markdown",mode:"markdown",ext:["markdown","md","mkd"]},{name:"mIRC",mime:"text/mirc",mode:"mirc"},{name:"MariaDB SQL",mime:"text/x-mariadb",mode:"sql"},{name:"Mathematica",mime:"text/x-mathematica",mode:"mathematica",ext:["m","nb"]},{name:"Modelica",mime:"text/x-modelica",mode:"modelica",ext:["mo"]},{name:"MUMPS",mime:"text/x-mumps",mode:"mumps",ext:["mps"]},{name:"MS SQL",mime:"text/x-mssql",mode:"sql"},{name:"mbox",mime:"application/mbox",mode:"mbox",ext:["mbox"]},{name:"MySQL",mime:"text/x-mysql",mode:"sql"},{name:"Nginx",mime:"text/x-nginx-conf",mode:"nginx",file:/nginx.*\.conf$/i},{name:"NSIS",mime:"text/x-nsis",mode:"nsis",ext:["nsh","nsi"]},{name:"NTriples",mime:"text/n-triples",mode:"ntriples",ext:["nt"]},{name:"Objective C",mime:"text/x-objectivec",mode:"clike",ext:["m","mm"],alias:["objective-c","objc"]},{name:"OCaml",mime:"text/x-ocaml",mode:"mllike",ext:["ml","mli","mll","mly"]},{name:"Octave",mime:"text/x-octave",mode:"octave",ext:["m"]},{name:"Oz",mime:"text/x-oz",mode:"oz",ext:["oz"]},{name:"Pascal",mime:"text/x-pascal",mode:"pascal",ext:["p","pas"]},{name:"PEG.js",mime:"null",mode:"pegjs",ext:["jsonld"]},{name:"Perl",mime:"text/x-perl",mode:"perl",ext:["pl","pm"]},{name:"PHP",mime:"application/x-httpd-php",mode:"php",ext:["php","php3","php4","php5","phtml"]},{name:"Pig",mime:"text/x-pig",mode:"pig",ext:["pig"]},{name:"Plain Text",mime:"text/plain",mode:"null",ext:["txt","text","conf","def","list","log"]},{name:"PLSQL",mime:"text/x-plsql",mode:"sql",ext:["pls"]},{name:"PowerShell",mime:"application/x-powershell",mode:"powershell",ext:["ps1","psd1","psm1"]},{name:"Properties files",mime:"text/x-properties",mode:"properties",ext:["properties","ini","in"],alias:["ini","properties"]},{name:"ProtoBuf",mime:"text/x-protobuf",mode:"protobuf",ext:["proto"]},{name:"Python",mime:"text/x-python",mode:"python",ext:["BUILD","bzl","py","pyw"],file:/^(BUCK|BUILD)$/},{name:"Puppet",mime:"text/x-puppet",mode:"puppet",ext:["pp"]},{name:"Q",mime:"text/x-q",mode:"q",ext:["q"]},{name:"R",mime:"text/x-rsrc",mode:"r",ext:["r"],alias:["rscript"]},{name:"reStructuredText",mime:"text/x-rst",mode:"rst",ext:["rst"],alias:["rst"]},{name:"RPM Changes",mime:"text/x-rpm-changes",mode:"rpm"},{name:"RPM Spec",mime:"text/x-rpm-spec",mode:"rpm",ext:["spec"]},{name:"Ruby",mime:"text/x-ruby",mode:"ruby",ext:["rb"],alias:["jruby","macruby","rake","rb","rbx"]},{name:"Rust",mime:"text/x-rustsrc",mode:"rust",ext:["rs"]},{name:"SAS",mime:"text/x-sas",mode:"sas",ext:["sas"]},{name:"Sass",mime:"text/x-sass",mode:"sass",ext:["sass"]},{name:"Scala",mime:"text/x-scala",mode:"clike",ext:["scala"]},{name:"Scheme",mime:"text/x-scheme",mode:"scheme",ext:["scm","ss"]},{name:"SCSS",mime:"text/x-scss",mode:"css",ext:["scss"]},{name:"Shell",mime:"text/x-sh",mode:"shell",ext:["sh","ksh","bash"],alias:["bash","sh","zsh"],file:/^PKGBUILD$/},{name:"Sieve",mime:"application/sieve",mode:"sieve",ext:["siv","sieve"]},{name:"Slim",mimes:["text/x-slim","application/x-slim"],mode:"slim",ext:["slim"]},{name:"Smalltalk",mime:"text/x-stsrc",mode:"smalltalk",ext:["st"]},{name:"Smarty",mime:"text/x-smarty",mode:"smarty",ext:["tpl"]},{name:"Solr",mime:"text/x-solr",mode:"solr"},{name:"Soy",mime:"text/x-soy",mode:"soy",ext:["soy"],alias:["closure template"]},{name:"SPARQL",mime:"application/sparql-query",mode:"sparql",ext:["rq","sparql"],alias:["sparul"]},{name:"Spreadsheet",mime:"text/x-spreadsheet",mode:"spreadsheet",alias:["excel","formula"]},{name:"SQL",mime:"text/x-sql",mode:"sql",ext:["sql"]},{name:"Squirrel",mime:"text/x-squirrel",mode:"clike",ext:["nut"]},{name:"Swift",mime:"text/x-swift",mode:"swift",ext:["swift"]},{name:"sTeX",mime:"text/x-stex",mode:"stex"},{name:"LaTeX",mime:"text/x-latex",mode:"stex",ext:["text","ltx"],alias:["tex"]},{name:"SystemVerilog",mime:"text/x-systemverilog",mode:"verilog",ext:["v"]},{name:"Tcl",mime:"text/x-tcl",mode:"tcl",ext:["tcl"]},{name:"Textile",mime:"text/x-textile",mode:"textile",ext:["textile"]},{name:"TiddlyWiki ",mime:"text/x-tiddlywiki",mode:"tiddlywiki"},{name:"Tiki wiki",mime:"text/tiki",mode:"tiki"},{name:"TOML",mime:"text/x-toml",mode:"toml",ext:["toml"]},{name:"Tornado",mime:"text/x-tornado",mode:"tornado"},{name:"troff",mime:"text/troff",mode:"troff",ext:["1","2","3","4","5","6","7","8","9"]},{name:"TTCN",mime:"text/x-ttcn",mode:"ttcn",ext:["ttcn","ttcn3","ttcnpp"]},{name:"TTCN_CFG",mime:"text/x-ttcn-cfg",mode:"ttcn-cfg",ext:["cfg"]},{name:"Turtle",mime:"text/turtle",mode:"turtle",ext:["ttl"]},{name:"TypeScript",mime:"application/typescript",mode:"javascript",ext:["ts"],alias:["ts"]},{name:"Twig",mime:"text/x-twig",mode:"twig"},{name:"Web IDL",mime:"text/x-webidl",mode:"webidl",ext:["webidl"]},{name:"VB.NET",mime:"text/x-vb",mode:"vb",ext:["vb"]},{name:"VBScript",mime:"text/vbscript",mode:"vbscript",ext:["vbs"]},{name:"Velocity",mime:"text/velocity",mode:"velocity",ext:["vtl"]},{name:"Verilog",mime:"text/x-verilog",mode:"verilog",ext:["v"]},{name:"VHDL",mime:"text/x-vhdl",mode:"vhdl",ext:["vhd","vhdl"]},{name:"XML",mimes:["application/xml","text/xml"],mode:"xml",ext:["xml","xsl","xsd"],alias:["rss","wsdl","xsd"]},{name:"XQuery",mime:"application/xquery",mode:"xquery",ext:["xy","xquery"]},{name:"Yacas",mime:"text/x-yacas",mode:"yacas",ext:["ys"]},{name:"YAML",mime:"text/x-yaml",mode:"yaml",ext:["yaml","yml"],alias:["yml"]},{name:"Z80",mime:"text/x-z80",mode:"z80",ext:["z80"]},{name:"mscgen",mime:"text/x-mscgen",mode:"mscgen",ext:["mscgen","mscin","msc"]},{name:"xu",mime:"text/x-xu",mode:"mscgen",ext:["xu"]},{name:"msgenny",mime:"text/x-msgenny",mode:"mscgen",ext:["msgenny"]}];for(var t=0;t-1&&t.substring(i+1,t.length);return o?e.findModeByExtension(o):void 0},e.findModeByName=function(t){t=t.toLowerCase();for(var n=0;n")):null:e.match("--")?n(s("comment","-->")):e.match("DOCTYPE",!0,!0)?(e.eatWhile(/[\w\._\-]/),n(c(1))):null:e.eat("?")?(e.eatWhile(/[\w\._\-]/),t.tokenize=s("meta","?>"),"meta"):(T=e.eat("/")?"closeTag":"openTag",t.tokenize=a,"tag bracket");if("&"==r){var i;return i=e.eat("#")?e.eat("x")?e.eatWhile(/[a-fA-F\d]/)&&e.eat(";"):e.eatWhile(/[\d]/)&&e.eat(";"):e.eatWhile(/[\w\.\-:]/)&&e.eat(";"),i?"atom":"error"}return e.eatWhile(/[^&<]/),null}function a(e,t){var n=e.next();if(">"==n||"/"==n&&e.eat(">"))return t.tokenize=o,T=">"==n?"endTag":"selfcloseTag","tag bracket";if("="==n)return T="equals",null;if("<"==n){t.tokenize=o,t.state=d,t.tagName=t.tagStart=null;var r=t.tokenize(e,t);return r?r+" tag error":"tag error"}return/[\'\"]/.test(n)?(t.tokenize=l(n),t.stringStartCol=e.column(),t.tokenize(e,t)):(e.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function l(e){var t=function(t,n){for(;!t.eol();)if(t.next()==e){n.tokenize=a;break}return"string"};return t.isInAttribute=!0,t}function s(e,t){return function(n,r){for(;!n.eol();){if(n.match(t)){r.tokenize=o;break}n.next()}return e}}function c(e){return function(t,n){for(var r;null!=(r=t.next());){if("<"==r)return n.tokenize=c(e+1),n.tokenize(t,n);if(">"==r){if(1==e){n.tokenize=o;break}return n.tokenize=c(e-1),n.tokenize(t,n)}}return"meta"}}function u(e,t,n){this.prev=e.context,this.tagName=t,this.indent=e.indented,this.startOfLine=n,(S.doNotIndent.hasOwnProperty(t)||e.context&&e.context.noIndent)&&(this.noIndent=!0)}function f(e){e.context&&(e.context=e.context.prev)}function h(e,t){for(var n;;){if(!e.context)return;if(n=e.context.tagName,!S.contextGrabbers.hasOwnProperty(n)||!S.contextGrabbers[n].hasOwnProperty(t))return;f(e)}}function d(e,t,n){return"openTag"==e?(n.tagStart=t.column(),p):"closeTag"==e?m:d}function p(e,t,n){return"word"==e?(n.tagName=t.current(),M="tag",y):(M="error",p)}function m(e,t,n){if("word"==e){var r=t.current();return n.context&&n.context.tagName!=r&&S.implicitlyClosed.hasOwnProperty(n.context.tagName)&&f(n),n.context&&n.context.tagName==r||S.matchClosing===!1?(M="tag",g):(M="tag error",v)}return M="error",v}function g(e,t,n){return"endTag"!=e?(M="error",g):(f(n),d)}function v(e,t,n){return M="error",g(e,t,n)}function y(e,t,n){if("word"==e)return M="attribute",x;if("endTag"==e||"selfcloseTag"==e){var r=n.tagName,i=n.tagStart;return n.tagName=n.tagStart=null,"selfcloseTag"==e||S.autoSelfClosers.hasOwnProperty(r)?h(n,r):(h(n,r),n.context=new u(n,r,i==n.indented)),d}return M="error",y}function x(e,t,n){return"equals"==e?b:(S.allowMissing||(M="error"),y(e,t,n))}function b(e,t,n){return"string"==e?w:"word"==e&&S.allowUnquoted?(M="string",y):(M="error",y(e,t,n))}function w(e,t,n){return"string"==e?w:y(e,t,n)}var k=r.indentUnit,S={},C=i.htmlMode?t:n;for(var L in C)S[L]=C[L];for(var L in i)S[L]=i[L];var T,M;return o.isInText=!0,{startState:function(e){var t={tokenize:o,state:d,indented:e||0,tagName:null,tagStart:null,context:null};return null!=e&&(t.baseIndent=e),t},token:function(e,t){if(!t.tagName&&e.sol()&&(t.indented=e.indentation()),e.eatSpace())return null;T=null;var n=t.tokenize(e,t);return(n||T)&&"comment"!=n&&(M=null,t.state=t.state(T||n,e,t),M&&(n="error"==M?n+" error":M)),n},indent:function(t,n,r){var i=t.context;if(t.tokenize.isInAttribute)return t.tagStart==t.indented?t.stringStartCol+1:t.indented+k;if(i&&i.noIndent)return e.Pass;if(t.tokenize!=a&&t.tokenize!=o)return r?r.match(/^(\s*)/)[0].length:0;if(t.tagName)return S.multilineTagIndentPastTag!==!1?t.tagStart+t.tagName.length+2:t.tagStart+k*(S.multilineTagIndentFactor||1);if(S.alignCDATA&&/$/,blockCommentStart:"",configuration:S.htmlMode?"html":"xml",helperType:S.htmlMode?"html":"xml",skipAttribute:function(e){e.state==b&&(e.state=y)}}}),e.defineMIME("text/xml","xml"),e.defineMIME("application/xml","xml"),e.mimeModes.hasOwnProperty("text/html")||e.defineMIME("text/html",{name:"xml",htmlMode:!0})})},{"../../lib/codemirror":10}],15:[function(e,t,n){n.read=function(e,t,n,r,i){var o,a,l=8*i-r-1,s=(1<>1,u=-7,f=n?i-1:0,h=n?-1:1,d=e[t+f];for(f+=h,o=d&(1<<-u)-1,d>>=-u,u+=l;u>0;o=256*o+e[t+f],f+=h,u-=8);for(a=o&(1<<-u)-1,o>>=-u,u+=r;u>0;a=256*a+e[t+f],f+=h,u-=8);if(0===o)o=1-c;else{if(o===s)return a?NaN:(d?-1:1)*(1/0);a+=Math.pow(2,r),o-=c}return(d?-1:1)*a*Math.pow(2,o-r)},n.write=function(e,t,n,r,i,o){var a,l,s,c=8*o-i-1,u=(1<>1,h=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,d=r?0:o-1,p=r?1:-1,m=0>t||0===t&&0>1/t?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(l=isNaN(t)?1:0,a=u):(a=Math.floor(Math.log(t)/Math.LN2),t*(s=Math.pow(2,-a))<1&&(a--,s*=2),t+=a+f>=1?h/s:h*Math.pow(2,1-f),t*s>=2&&(a++,s/=2),a+f>=u?(l=0,a=u):a+f>=1?(l=(t*s-1)*Math.pow(2,i),a+=f):(l=t*Math.pow(2,f-1)*Math.pow(2,i),a=0));i>=8;e[n+d]=255&l,d+=p,l/=256,i-=8);for(a=a<0;e[n+d]=255&a,d+=p,a/=256,c-=8);e[n+d-p]|=128*m}},{}],16:[function(e,t,n){var r={}.toString;t.exports=Array.isArray||function(e){return"[object Array]"==r.call(e)}},{}],17:[function(t,n,r){(function(t){(function(){function t(e){this.tokens=[],this.tokens.links={},this.options=e||h.defaults,this.rules=d.normal,this.options.gfm&&(this.options.tables?this.rules=d.tables:this.rules=d.gfm)}function i(e,t){if(this.options=t||h.defaults,this.links=e,this.rules=p.normal,this.renderer=this.options.renderer||new o,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=p.breaks:this.rules=p.gfm:this.options.pedantic&&(this.rules=p.pedantic)}function o(e){this.options=e||{}}function a(e){this.tokens=[],this.token=null,this.options=e||h.defaults,this.options.renderer=this.options.renderer||new o,this.renderer=this.options.renderer,this.renderer.options=this.options}function l(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function s(e){return e.replace(/&([#\w]+);/g,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function c(e,t){return e=e.source,t=t||"",function n(r,i){return r?(i=i.source||i,i=i.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,i),n):new RegExp(e,t)}}function u(){}function f(e){for(var t,n,r=1;rAn error occured:

"+l(u.message+"",!0)+"
";throw u}}var d={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:u,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:u,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:u,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};d.bullet=/(?:[*+-]|\d+\.)/,d.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,d.item=c(d.item,"gm")(/bull/g,d.bullet)(),d.list=c(d.list)(/bull/g,d.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+d.def.source+")")(),d.blockquote=c(d.blockquote)("def",d.def)(),d._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b",d.html=c(d.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,d._tag)(),d.paragraph=c(d.paragraph)("hr",d.hr)("heading",d.heading)("lheading",d.lheading)("blockquote",d.blockquote)("tag","<"+d._tag)("def",d.def)(),d.normal=f({},d),d.gfm=f({},d.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/}),d.gfm.paragraph=c(d.paragraph)("(?!","(?!"+d.gfm.fences.source.replace("\\1","\\2")+"|"+d.list.source.replace("\\1","\\3")+"|")(),d.tables=f({},d.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),t.rules=d,t.lex=function(e,n){var r=new t(n);return r.lex(e)},t.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},t.prototype.token=function(e,t,n){for(var r,i,o,a,l,s,c,u,f,e=e.replace(/^ +$/gm,"");e;)if((o=this.rules.newline.exec(e))&&(e=e.substring(o[0].length),o[0].length>1&&this.tokens.push({type:"space"})),o=this.rules.code.exec(e))e=e.substring(o[0].length),o=o[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?o:o.replace(/\n+$/,"")});else if(o=this.rules.fences.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"code",lang:o[2],text:o[3]||""});else if(o=this.rules.heading.exec(e))e=e.substring(o[0].length),this.tokens.push({type:"heading",depth:o[1].length,text:o[2]});else if(t&&(o=this.rules.nptable.exec(e))){for(e=e.substring(o[0].length),s={type:"table",header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/\n$/,"").split("\n")},u=0;u ?/gm,""),this.token(o,t,!0),this.tokens.push({type:"blockquote_end"});else if(o=this.rules.list.exec(e)){for(e=e.substring(o[0].length),a=o[2],this.tokens.push({type:"list_start",ordered:a.length>1}),o=o[0].match(this.rules.item),r=!1,f=o.length,u=0;f>u;u++)s=o[u],c=s.length,s=s.replace(/^ *([*+-]|\d+\.) +/,""),~s.indexOf("\n ")&&(c-=s.length,s=this.options.pedantic?s.replace(/^ {1,4}/gm,""):s.replace(new RegExp("^ {1,"+c+"}","gm"),"")),this.options.smartLists&&u!==f-1&&(l=d.bullet.exec(o[u+1])[0],a===l||a.length>1&&l.length>1||(e=o.slice(u+1).join("\n")+e,u=f-1)),i=r||/\n\n(?!\s*$)/.test(s),u!==f-1&&(r="\n"===s.charAt(s.length-1),i||(i=r)),this.tokens.push({type:i?"loose_item_start":"list_item_start"}),this.token(s,!1,n),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(o=this.rules.html.exec(e))e=e.substring(o[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===o[1]||"script"===o[1]||"style"===o[1]),text:o[0]});else if(!n&&t&&(o=this.rules.def.exec(e)))e=e.substring(o[0].length),this.tokens.links[o[1].toLowerCase()]={href:o[2],title:o[3]};else if(t&&(o=this.rules.table.exec(e))){for(e=e.substring(o[0].length),s={type:"table", +header:o[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:o[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:o[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/,p.link=c(p.link)("inside",p._inside)("href",p._href)(),p.reflink=c(p.reflink)("inside",p._inside)(),p.normal=f({},p),p.pedantic=f({},p.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),p.gfm=f({},p.normal,{escape:c(p.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:c(p.text)("]|","~]|")("|","|https?://|")()}),p.breaks=f({},p.gfm,{br:c(p.br)("{2,}","*")(),text:c(p.gfm.text)("{2,}","*")()}),i.rules=p,i.output=function(e,t,n){var r=new i(t,n);return r.output(e)},i.prototype.output=function(e){for(var t,n,r,i,o="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),o+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=l(i[1]),r=n),o+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),o+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):l(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,o+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){o+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,o+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),o+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),o+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),o+=this.renderer.codespan(l(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),o+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),o+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),o+=this.renderer.text(l(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=l(i[1]),r=n,o+=this.renderer.link(r,null,n);return o},i.prototype.outputLink=function(e,t){var n=l(t.href),r=t.title?l(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,l(e[1]))},i.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},i.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,i=0;r>i;i++)t=e.charCodeAt(i),Math.random()>.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},o.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'
'+(n?e:l(e,!0))+"\n
\n":"
"+(n?e:l(e,!0))+"\n
"},o.prototype.blockquote=function(e){return"
\n"+e+"
\n"},o.prototype.html=function(e){return e},o.prototype.heading=function(e,t,n){return"'+e+"\n"},o.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"},o.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+"\n"},o.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},o.prototype.paragraph=function(e){return"

    "+e+"

    \n"},o.prototype.table=function(e,t){return"\n\n"+e+"\n\n"+t+"\n
    \n"},o.prototype.tablerow=function(e){return"\n"+e+"\n"},o.prototype.tablecell=function(e,t){var n=t.header?"th":"td",r=t.align?"<"+n+' style="text-align:'+t.align+'">':"<"+n+">";return r+e+"\n"},o.prototype.strong=function(e){return""+e+""},o.prototype.em=function(e){return""+e+""},o.prototype.codespan=function(e){return""+e+""},o.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},o.prototype.del=function(e){return""+e+""},o.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(s(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(i){return""}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:"))return""}var o='
    "},o.prototype.image=function(e,t,n){var r=''+n+'":">"},o.prototype.text=function(e){return e},a.parse=function(e,t,n){var r=new a(t,n);return r.parse(e)},a.prototype.parse=function(e){this.inline=new i(e.links,this.options,this.renderer),this.tokens=e.reverse();for(var t="";this.next();)t+=this.tok();return t},a.prototype.next=function(){return this.token=this.tokens.pop()},a.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},a.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},a.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,i,o="",a="";for(n="",e=0;ea;a++)for(var s=this.compoundRules[a],c=0,u=s.length;u>c;c++)this.compoundRuleCodes[s[c]]=[];"ONLYINCOMPOUND"in this.flags&&(this.compoundRuleCodes[this.flags.ONLYINCOMPOUND]=[]),this.dictionaryTable=this._parseDIC(n);for(var a in this.compoundRuleCodes)0==this.compoundRuleCodes[a].length&&delete this.compoundRuleCodes[a];for(var a=0,l=this.compoundRules.length;l>a;a++){for(var f=this.compoundRules[a],h="",c=0,u=f.length;u>c;c++){var d=f[c];h+=d in this.compoundRuleCodes?"("+this.compoundRuleCodes[d].join("|")+")":d}this.compoundRules[a]=new RegExp(h,"i")}}return this};i.prototype={load:function(e){for(var t in e)this[t]=e[t];return this},_readFile:function(t,r){if(r||(r="utf8"),"undefined"!=typeof XMLHttpRequest){var i=new XMLHttpRequest;return i.open("GET",t,!1),i.overrideMimeType&&i.overrideMimeType("text/plain; charset="+r),i.send(null),i.responseText}if("undefined"!=typeof e){var o=e("fs");try{if(o.existsSync(t)){var a=o.statSync(t),l=o.openSync(t,"r"),s=new n(a.size);return o.readSync(l,s,0,s.length,null),s.toString(r,0,s.length)}console.log("Path "+t+" does not exist.")}catch(c){return console.log(c),""}}},_parseAFF:function(e){var t={};e=this._removeAffixComments(e);for(var n=e.split("\n"),r=0,i=n.length;i>r;r++){var o=n[r],a=o.split(/\s+/),l=a[0];if("PFX"==l||"SFX"==l){for(var s=a[1],c=a[2],u=parseInt(a[3],10),f=[],h=r+1,d=r+1+u;d>h;h++){var o=n[h],p=o.split(/\s+/),m=p[2],g=p[3].split("/"),v=g[0];"0"===v&&(v="");var y=this.parseRuleCodes(g[1]),x=p[4],b={};b.add=v,y.length>0&&(b.continuationClasses=y),"."!==x&&("SFX"===l?b.match=new RegExp(x+"$"):b.match=new RegExp("^"+x)),"0"!=m&&("SFX"===l?b.remove=new RegExp(m+"$"):b.remove=m),f.push(b)}t[s]={type:l,combineable:"Y"==c,entries:f},r+=u}else if("COMPOUNDRULE"===l){for(var u=parseInt(a[1],10),h=r+1,d=r+1+u;d>h;h++){var o=n[h],p=o.split(/\s+/);this.compoundRules.push(p[1])}r+=u}else if("REP"===l){var p=o.split(/\s+/);3===p.length&&this.replacementTable.push([p[1],p[2]])}else this.flags[l]=a[1]}return t},_removeAffixComments:function(e){return e=e.replace(/#.*$/gm,""),e=e.replace(/^\s\s*/m,"").replace(/\s\s*$/m,""),e=e.replace(/\n{2,}/g,"\n"),e=e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")},_parseDIC:function(e){function t(e,t){e in r&&"object"==typeof r[e]||(r[e]=[]),r[e].push(t)}e=this._removeDicComments(e);for(var n=e.split("\n"),r={},i=1,o=n.length;o>i;i++){var a=n[i],l=a.split("/",2),s=l[0];if(l.length>1){var c=this.parseRuleCodes(l[1]);"NEEDAFFIX"in this.flags&&-1!=c.indexOf(this.flags.NEEDAFFIX)||t(s,c);for(var u=0,f=c.length;f>u;u++){var h=c[u],d=this.rules[h];if(d)for(var p=this._applyRule(s,d),m=0,g=p.length;g>m;m++){var v=p[m];if(t(v,[]),d.combineable)for(var y=u+1;f>y;y++){var x=c[y],b=this.rules[x];if(b&&b.combineable&&d.type!=b.type)for(var w=this._applyRule(v,b),k=0,S=w.length;S>k;k++){var C=w[k];t(C,[])}}}h in this.compoundRuleCodes&&this.compoundRuleCodes[h].push(s)}}else t(s.trim(),[])}return r},_removeDicComments:function(e){return e=e.replace(/^\t.*$/gm,"")},parseRuleCodes:function(e){if(!e)return[];if(!("FLAG"in this.flags))return e.split("");if("long"===this.flags.FLAG){for(var t=[],n=0,r=e.length;r>n;n+=2)t.push(e.substr(n,2));return t}return"num"===this.flags.FLAG?textCode.split(","):void 0},_applyRule:function(e,t){for(var n=t.entries,r=[],i=0,o=n.length;o>i;i++){var a=n[i];if(!a.match||e.match(a.match)){var l=e;if(a.remove&&(l=l.replace(a.remove,"")),"SFX"===t.type?l+=a.add:l=a.add+l,r.push(l),"continuationClasses"in a)for(var s=0,c=a.continuationClasses.length;c>s;s++){var u=this.rules[a.continuationClasses[s]];u&&(r=r.concat(this._applyRule(l,u)))}}}return r},check:function(e){var t=e.replace(/^\s\s*/,"").replace(/\s\s*$/,"");if(this.checkExact(t))return!0;if(t.toUpperCase()===t){var n=t[0]+t.substring(1).toLowerCase();if(this.hasFlag(n,"KEEPCASE"))return!1;if(this.checkExact(n))return!0}var r=t.toLowerCase();if(r!==t){if(this.hasFlag(r,"KEEPCASE"))return!1;if(this.checkExact(r))return!0}return!1},checkExact:function(e){var t=this.dictionaryTable[e];if("undefined"==typeof t){if("COMPOUNDMIN"in this.flags&&e.length>=this.flags.COMPOUNDMIN)for(var n=0,r=this.compoundRules.length;r>n;n++)if(e.match(this.compoundRules[n]))return!0;return!1}if("object"==typeof t){for(var n=0,r=t.length;r>n;n++)if(!this.hasFlag(e,"ONLYINCOMPOUND",t[n]))return!0;return!1}},hasFlag:function(e,t,n){if(t in this.flags){if("undefined"==typeof n)var n=Array.prototype.concat.apply([],this.dictionaryTable[e]);if(n&&-1!==n.indexOf(this.flags[t]))return!0}return!1},alphabet:"",suggest:function(e,t){function n(e){for(var t=[],n=0,r=e.length;r>n;n++){for(var i=e[n],o=[],a=0,l=i.length+1;l>a;a++)o.push([i.substring(0,a),i.substring(a,i.length)]);for(var s=[],a=0,l=o.length;l>a;a++){var u=o[a];u[1]&&s.push(u[0]+u[1].substring(1))}for(var f=[],a=0,l=o.length;l>a;a++){var u=o[a];u[1].length>1&&f.push(u[0]+u[1][1]+u[1][0]+u[1].substring(2))}for(var h=[],a=0,l=o.length;l>a;a++){var u=o[a];if(u[1])for(var d=0,p=c.alphabet.length;p>d;d++)h.push(u[0]+c.alphabet[d]+u[1].substring(1))}for(var m=[],a=0,l=o.length;l>a;a++){var u=o[a];if(u[1])for(var d=0,p=c.alphabet.length;p>d;d++)h.push(u[0]+c.alphabet[d]+u[1])}t=t.concat(s),t=t.concat(f),t=t.concat(h),t=t.concat(m)}return t}function r(e){for(var t=[],n=0;nu;u++)l[u]in s?s[l[u]]+=1:s[l[u]]=1;var h=[];for(var u in s)h.push([u,s[u]]);h.sort(i).reverse();for(var d=[],u=0,f=Math.min(t,h.length);f>u;u++)c.hasFlag(h[u][0],"NOSUGGEST")||d.push(h[u][0]);return d}if(t||(t=5),this.check(e))return[];for(var o=0,a=this.replacementTable.length;a>o;o++){var l=this.replacementTable[o];if(-1!==e.indexOf(l[0])){var s=e.replace(l[0],l[1]);if(this.check(s))return[s]}}var c=this;return c.alphabet="abcdefghijklmnopqrstuvwxyz",i(e)}},"undefined"!=typeof t&&(t.exports=i)}).call(this,e("buffer").Buffer,"/node_modules/typo-js")},{buffer:3,fs:2}],19:[function(e,t,n){var r=e("codemirror");r.commands.tabAndIndentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentMore");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}},r.commands.shiftTabAndUnindentMarkdownList=function(e){var t=e.listSelections(),n=t[0].head,r=e.getStateAfter(n.line),i=r.list!==!1;if(i)return void e.execCommand("indentLess");if(e.options.indentWithTabs)e.execCommand("insertTab");else{var o=Array(e.options.tabSize+1).join(" ");e.replaceSelection(o)}}},{codemirror:10}],20:[function(e,t,n){"use strict";function r(e){return e=U?e.replace("Ctrl","Cmd"):e.replace("Cmd","Ctrl")}function i(e,t,n){e=e||{};var r=document.createElement("a");return t=void 0==t?!0:t,e.title&&t&&(r.title=a(e.title,e.action,n),U&&(r.title=r.title.replace("Ctrl","⌘"),r.title=r.title.replace("Alt","⌥"))),r.tabIndex=-1,r.className=e.className,r}function o(){var e=document.createElement("i");return e.className="separator",e.innerHTML="|",e}function a(e,t,n){var i,o=e;return t&&(i=Y(t),n[i]&&(o+=" ("+r(n[i])+")")),o}function l(e,t){t=t||e.getCursor("start");var n=e.getTokenAt(t);if(!n.type)return{};for(var r,i,o=n.type.split(" "),a={},l=0;l=0&&(d=c.getLineHandle(o),!t(d));o--);var v,y,x,b,w=c.getTokenAt({line:o,ch:1}),k=n(w).fencedChars;t(c.getLineHandle(u.line))?(v="",y=u.line):t(c.getLineHandle(u.line-1))?(v="",y=u.line-1):(v=k+"\n",y=u.line),t(c.getLineHandle(f.line))?(x="",b=f.line,0===f.ch&&(b+=1)):0!==f.ch&&t(c.getLineHandle(f.line+1))?(x="",b=f.line+1):(x=k+"\n",b=f.line+1),0===f.ch&&(b-=1),c.operation(function(){c.replaceRange(x,{line:b,ch:0},{line:b+(x?0:1),ch:0}),c.replaceRange(v,{line:y,ch:0},{line:y+(v?0:1),ch:0})}),c.setSelection({line:y+(v?1:0),ch:0},{line:b+(v?1:-1),ch:0}),c.focus()}else{var S=u.line;if(t(c.getLineHandle(u.line))&&("fenced"===r(c,u.line+1)?(o=u.line,S=u.line+1):(a=u.line,S=u.line-1)),void 0===o)for(o=S;o>=0&&(d=c.getLineHandle(o),!t(d));o--);if(void 0===a)for(l=c.lineCount(),a=S;l>a&&(d=c.getLineHandle(a),!t(d));a++);c.operation(function(){c.replaceRange("",{line:o,ch:0},{line:o+1,ch:0}),c.replaceRange("",{line:a-1,ch:0},{line:a,ch:0})}),c.focus()}else if("indented"===p){if(u.line!==f.line||u.ch!==f.ch)o=u.line,a=f.line,0===f.ch&&a--;else{for(o=u.line;o>=0;o--)if(d=c.getLineHandle(o),!d.text.match(/^\s*$/)&&"indented"!==r(c,o,d)){o+=1;break}for(l=c.lineCount(),a=u.line;l>a;a++)if(d=c.getLineHandle(a),!d.text.match(/^\s*$/)&&"indented"!==r(c,a,d)){a-=1;break}}var C=c.getLineHandle(a+1),L=C&&c.getTokenAt({line:a+1,ch:C.text.length-1}),T=L&&n(L).indentedCode;T&&c.replaceRange("\n",{line:a+1,ch:0});for(var M=o;a>=M;M++)c.indentLine(M,"subtract");c.focus()}else{var N=u.line===f.line&&u.ch===f.ch&&0===u.ch,A=u.line!==f.line;N||A?i(c,u,f,s):E(c,!1,["`","`"])}}function d(e){var t=e.codemirror;I(t,"quote")}function p(e){var t=e.codemirror;O(t,"smaller")}function m(e){var t=e.codemirror;O(t,"bigger")}function g(e){var t=e.codemirror;O(t,void 0,1)}function v(e){var t=e.codemirror;O(t,void 0,2)}function y(e){var t=e.codemirror;O(t,void 0,3)}function x(e){var t=e.codemirror;I(t,"unordered-list")}function b(e){var t=e.codemirror;I(t,"ordered-list")}function w(e){var t=e.codemirror;R(t)}function k(e){var t=e.codemirror,n=l(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.link),!i)?!1:void E(t,n.link,r.insertTexts.link,i)}function S(e){var t=e.codemirror,n=l(t),r=e.options,i="http://";return r.promptURLs&&(i=prompt(r.promptTexts.image),!i)?!1:void E(t,n.image,r.insertTexts.image,i)}function C(e){var t=e.codemirror,n=l(t),r=e.options;E(t,n.table,r.insertTexts.table)}function L(e){var t=e.codemirror,n=l(t),r=e.options;E(t,n.image,r.insertTexts.horizontalRule)}function T(e){var t=e.codemirror;t.undo(),t.focus()}function M(e){var t=e.codemirror;t.redo(),t.focus()}function N(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.nextSibling,i=e.toolbarElements["side-by-side"],o=!1;/editor-preview-active-side/.test(r.className)?(r.className=r.className.replace(/\s*editor-preview-active-side\s*/g,""),i.className=i.className.replace(/\s*active\s*/g,""),n.className=n.className.replace(/\s*CodeMirror-sided\s*/g," ")):(setTimeout(function(){t.getOption("fullScreen")||s(e),r.className+=" editor-preview-active-side"},1),i.className+=" active",n.className+=" CodeMirror-sided",o=!0);var a=n.lastChild;if(/editor-preview-active/.test(a.className)){a.className=a.className.replace(/\s*editor-preview-active\s*/g,"");var l=e.toolbarElements.preview,c=n.previousSibling;l.className=l.className.replace(/\s*active\s*/g,""),c.className=c.className.replace(/\s*disabled-for-preview*/g,"")}var u=function(){r.innerHTML=e.options.previewRender(e.value(),r)};t.sideBySideRenderingFunction||(t.sideBySideRenderingFunction=u),o?(r.innerHTML=e.options.previewRender(e.value(),r),t.on("update",t.sideBySideRenderingFunction)):t.off("update",t.sideBySideRenderingFunction),t.refresh()}function A(e){var t=e.codemirror,n=t.getWrapperElement(),r=n.previousSibling,i=e.options.toolbar?e.toolbarElements.preview:!1,o=n.lastChild;o&&/editor-preview/.test(o.className)||(o=document.createElement("div"),o.className="editor-preview",n.appendChild(o)),/editor-preview-active/.test(o.className)?(o.className=o.className.replace(/\s*editor-preview-active\s*/g,""),i&&(i.className=i.className.replace(/\s*active\s*/g,""),r.className=r.className.replace(/\s*disabled-for-preview*/g,""))):(setTimeout(function(){o.className+=" editor-preview-active"},1),i&&(i.className+=" active",r.className+=" disabled-for-preview")),o.innerHTML=e.options.previewRender(e.value(),o);var a=t.getWrapperElement().nextSibling;/editor-preview-active-side/.test(a.className)&&N(e)}function E(e,t,n,r){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){var i,o=n[0],a=n[1],l=e.getCursor("start"),s=e.getCursor("end");r&&(a=a.replace("#url#",r)),t?(i=e.getLine(l.line),o=i.slice(0,l.ch),a=i.slice(l.ch),e.replaceRange(o+a,{line:l.line,ch:0})):(i=e.getSelection(),e.replaceSelection(o+i+a),l.ch+=o.length,l!==s&&(s.ch+=o.length)),e.setSelection(l,s),e.focus()}}function O(e,t,n){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var r=e.getCursor("start"),i=e.getCursor("end"),o=r.line;o<=i.line;o++)!function(r){var i=e.getLine(r),o=i.search(/[^#]/);i=void 0!==t?0>=o?"bigger"==t?"###### "+i:"# "+i:6==o&&"smaller"==t?i.substr(7):1==o&&"bigger"==t?i.substr(2):"bigger"==t?i.substr(1):"#"+i:1==n?0>=o?"# "+i:o==n?i.substr(o+1):"# "+i.substr(o+1):2==n?0>=o?"## "+i:o==n?i.substr(o+1):"## "+i.substr(o+1):0>=o?"### "+i:o==n?i.substr(o+1):"### "+i.substr(o+1),e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(o);e.focus()}}function I(e,t){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className)){for(var n=l(e),r=e.getCursor("start"),i=e.getCursor("end"),o={quote:/^(\s*)\>\s+/,"unordered-list":/^(\s*)(\*|\-|\+)\s+/,"ordered-list":/^(\s*)\d+\.\s+/},a={quote:"> ","unordered-list":"* ","ordered-list":"1. "},s=r.line;s<=i.line;s++)!function(r){var i=e.getLine(r);i=n[t]?i.replace(o[t],"$1"):a[t]+i,e.replaceRange(i,{line:r,ch:0},{line:r,ch:99999999999999})}(s);e.focus()}}function P(e,t,n,r){if(!/editor-preview-active/.test(e.codemirror.getWrapperElement().lastChild.className)){r="undefined"==typeof r?n:r;var i,o=e.codemirror,a=l(o),s=n,c=r,u=o.getCursor("start"),f=o.getCursor("end");a[t]?(i=o.getLine(u.line),s=i.slice(0,u.ch),c=i.slice(u.ch),"bold"==t?(s=s.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/,""),c=c.replace(/(\*\*|__)/,"")):"italic"==t?(s=s.replace(/(\*|_)(?![\s\S]*(\*|_))/,""),c=c.replace(/(\*|_)/,"")):"strikethrough"==t&&(s=s.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/,""),c=c.replace(/(\*\*|~~)/,"")),o.replaceRange(s+c,{line:u.line,ch:0},{line:u.line,ch:99999999999999}),"bold"==t||"strikethrough"==t?(u.ch-=2,u!==f&&(f.ch-=2)):"italic"==t&&(u.ch-=1,u!==f&&(f.ch-=1))):(i=o.getSelection(),"bold"==t?(i=i.split("**").join(""),i=i.split("__").join("")):"italic"==t?(i=i.split("*").join(""),i=i.split("_").join("")):"strikethrough"==t&&(i=i.split("~~").join("")),o.replaceSelection(s+i+c),u.ch+=n.length,f.ch=u.ch+i.length),o.setSelection(u,f),o.focus()}}function R(e){if(!/editor-preview-active/.test(e.getWrapperElement().lastChild.className))for(var t,n=e.getCursor("start"),r=e.getCursor("end"),i=n.line;i<=r.line;i++)t=e.getLine(i),t=t.replace(/^[ ]*([# ]+|\*|\-|[> ]+|[0-9]+(.|\)))[ ]*/,""),e.replaceRange(t,{line:i,ch:0},{line:i,ch:99999999999999})}function D(e,t){for(var n in t)t.hasOwnProperty(n)&&(t[n]instanceof Array?e[n]=t[n].concat(e[n]instanceof Array?e[n]:[]):null!==t[n]&&"object"==typeof t[n]&&t[n].constructor===Object?e[n]=D(e[n]||{},t[n]):e[n]=t[n]);return e}function H(e){for(var t=1;t=19968?n[i].length:1;return r}function B(e){e=e||{},e.parent=this;var t=!0;if(e.autoDownloadFontAwesome===!1&&(t=!1),e.autoDownloadFontAwesome!==!0)for(var n=document.styleSheets,r=0;r-1&&(t=!1);if(t){var i=document.createElement("link");i.rel="stylesheet",i.href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css",document.getElementsByTagName("head")[0].appendChild(i)}if(e.element)this.element=e.element;else if(null===e.element)return void console.log("SimpleMDE: Error. No element was found.");if(void 0===e.toolbar){e.toolbar=[];for(var o in K)K.hasOwnProperty(o)&&(-1!=o.indexOf("separator-")&&e.toolbar.push("|"),(K[o]["default"]===!0||e.showIcons&&e.showIcons.constructor===Array&&-1!=e.showIcons.indexOf(o))&&e.toolbar.push(o))}e.hasOwnProperty("status")||(e.status=["autosave","lines","words","cursor"]),e.previewRender||(e.previewRender=function(e){return this.parent.markdown(e)}),e.parsingConfig=H({highlightFormatting:!0},e.parsingConfig||{}),e.insertTexts=H({},X,e.insertTexts||{}),e.promptTexts=Z,e.blockStyles=H({},J,e.blockStyles||{}),e.shortcuts=H({},G,e.shortcuts||{}),void 0!=e.autosave&&void 0!=e.autosave.unique_id&&""!=e.autosave.unique_id&&(e.autosave.uniqueId=e.autosave.unique_id),this.options=e,this.render(),!e.initialValue||this.options.autosave&&this.options.autosave.foundSavedValue===!0||this.value(e.initialValue)}function _(){if("object"!=typeof localStorage)return!1;try{localStorage.setItem("smde_localStorage",1),localStorage.removeItem("smde_localStorage")}catch(e){return!1}return!0}var F=e("codemirror");e("codemirror/addon/edit/continuelist.js"),e("./codemirror/tablist"),e("codemirror/addon/display/fullscreen.js"),e("codemirror/mode/markdown/markdown.js"),e("codemirror/addon/mode/overlay.js"),e("codemirror/addon/display/placeholder.js"),e("codemirror/addon/selection/mark-selection.js"),e("codemirror/mode/gfm/gfm.js"),e("codemirror/mode/xml/xml.js");var z=e("codemirror-spell-checker"),j=e("marked"),U=/Mac/.test(navigator.platform),q={toggleBold:c,toggleItalic:u,drawLink:k,toggleHeadingSmaller:p,toggleHeadingBigger:m,drawImage:S,toggleBlockquote:d,toggleOrderedList:b,toggleUnorderedList:x,toggleCodeBlock:h,togglePreview:A,toggleStrikethrough:f,toggleHeading1:g,toggleHeading2:v,toggleHeading3:y,cleanBlock:w,drawTable:C,drawHorizontalRule:L,undo:T,redo:M,toggleSideBySide:N,toggleFullScreen:s},G={toggleBold:"Cmd-B",toggleItalic:"Cmd-I",drawLink:"Cmd-K",toggleHeadingSmaller:"Cmd-H",toggleHeadingBigger:"Shift-Cmd-H",cleanBlock:"Cmd-E",drawImage:"Cmd-Alt-I",toggleBlockquote:"Cmd-'",toggleOrderedList:"Cmd-Alt-L",toggleUnorderedList:"Cmd-L",toggleCodeBlock:"Cmd-Alt-C",togglePreview:"Cmd-P",toggleSideBySide:"F9",toggleFullScreen:"F11"},Y=function(e){for(var t in q)if(q[t]===e)return t;return null},$=function(){var e=!1;return function(t){(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(t)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(t.substr(0,4)))&&(e=!0); +}(navigator.userAgent||navigator.vendor||window.opera),e},V="",K={bold:{name:"bold",action:c,className:"fa fa-bold",title:"Bold","default":!0},italic:{name:"italic",action:u,className:"fa fa-italic",title:"Italic","default":!0},strikethrough:{name:"strikethrough",action:f,className:"fa fa-strikethrough",title:"Strikethrough"},heading:{name:"heading",action:p,className:"fa fa-header",title:"Heading","default":!0},"heading-smaller":{name:"heading-smaller",action:p,className:"fa fa-header fa-header-x fa-header-smaller",title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:m,className:"fa fa-header fa-header-x fa-header-bigger",title:"Bigger Heading"},"heading-1":{name:"heading-1",action:g,className:"fa fa-header fa-header-x fa-header-1",title:"Big Heading"},"heading-2":{name:"heading-2",action:v,className:"fa fa-header fa-header-x fa-header-2",title:"Medium Heading"},"heading-3":{name:"heading-3",action:y,className:"fa fa-header fa-header-x fa-header-3",title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:h,className:"fa fa-code",title:"Code"},quote:{name:"quote",action:d,className:"fa fa-quote-left",title:"Quote","default":!0},"unordered-list":{name:"unordered-list",action:x,className:"fa fa-list-ul",title:"Generic List","default":!0},"ordered-list":{name:"ordered-list",action:b,className:"fa fa-list-ol",title:"Numbered List","default":!0},"clean-block":{name:"clean-block",action:w,className:"fa fa-eraser fa-clean-block",title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:k,className:"fa fa-link",title:"Create Link","default":!0},image:{name:"image",action:S,className:"fa fa-picture-o",title:"Insert Image","default":!0},table:{name:"table",action:C,className:"fa fa-table",title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:L,className:"fa fa-minus",title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:A,className:"fa fa-eye no-disable",title:"Toggle Preview","default":!0},"side-by-side":{name:"side-by-side",action:N,className:"fa fa-columns no-disable no-mobile",title:"Toggle Side by Side","default":!0},fullscreen:{name:"fullscreen",action:s,className:"fa fa-arrows-alt no-disable no-mobile",title:"Toggle Fullscreen","default":!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://simplemde.com/markdown-guide",className:"fa fa-question-circle",title:"Markdown Guide","default":!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:T,className:"fa fa-undo no-disable",title:"Undo"},redo:{name:"redo",action:M,className:"fa fa-repeat no-disable",title:"Redo"}},X={link:["[","](#url#)"],image:["![](","#url#)"],table:["","\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"],horizontalRule:["","\n\n-----\n\n"]},Z={link:"URL for the link:",image:"URL of the image:"},J={bold:"**",code:"```",italic:"*"};B.prototype.markdown=function(e){if(j){var t={};return this.options&&this.options.renderingConfig&&this.options.renderingConfig.singleLineBreaks===!1?t.breaks=!1:t.breaks=!0,this.options&&this.options.renderingConfig&&this.options.renderingConfig.codeSyntaxHighlighting===!0&&window.hljs&&(t.highlight=function(e){return window.hljs.highlightAuto(e).value}),j.setOptions(t),j(e)}},B.prototype.render=function(e){if(e||(e=this.element||document.getElementsByTagName("textarea")[0]),!this._rendered||this._rendered!==e){this.element=e;var t=this.options,n=this,i={};for(var o in t.shortcuts)null!==t.shortcuts[o]&&null!==q[o]&&!function(e){i[r(t.shortcuts[e])]=function(){q[e](n)}}(o);i.Enter="newlineAndIndentContinueMarkdownList",i.Tab="tabAndIndentMarkdownList",i["Shift-Tab"]="shiftTabAndUnindentMarkdownList",i.Esc=function(e){e.getOption("fullScreen")&&s(n)},document.addEventListener("keydown",function(e){e=e||window.event,27==e.keyCode&&n.codemirror.getOption("fullScreen")&&s(n)},!1);var a,l;if(t.spellChecker!==!1?(a="spell-checker",l=t.parsingConfig,l.name="gfm",l.gitHubSpice=!1,z({codeMirrorInstance:F})):(a=t.parsingConfig,a.name="gfm",a.gitHubSpice=!1),this.codemirror=F.fromTextArea(e,{mode:a,backdrop:l,theme:"paper",tabSize:void 0!=t.tabSize?t.tabSize:2,indentUnit:void 0!=t.tabSize?t.tabSize:2,indentWithTabs:t.indentWithTabs!==!1,lineNumbers:!1,autofocus:t.autofocus===!0,extraKeys:i,lineWrapping:t.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:t.placeholder||e.getAttribute("placeholder")||"",styleSelectedText:void 0!=t.styleSelectedText?t.styleSelectedText:!0}),t.forceSync===!0){var c=this.codemirror;c.on("change",function(){c.save()})}this.gui={},t.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),t.status!==!1&&(this.gui.statusbar=this.createStatusbar()),void 0!=t.autosave&&t.autosave.enabled===!0&&this.autosave(),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element;var u=this.codemirror;setTimeout(function(){u.refresh()}.bind(u),0)}},B.prototype.autosave=function(){if(_()){var e=this;if(void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to use the autosave feature");null!=e.element.form&&void 0!=e.element.form&&e.element.form.addEventListener("submit",function(){localStorage.removeItem("smde_"+e.options.autosave.uniqueId)}),this.options.autosave.loaded!==!0&&("string"==typeof localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&""!=localStorage.getItem("smde_"+this.options.autosave.uniqueId)&&(this.codemirror.setValue(localStorage.getItem("smde_"+this.options.autosave.uniqueId)),this.options.autosave.foundSavedValue=!0),this.options.autosave.loaded=!0),localStorage.setItem("smde_"+this.options.autosave.uniqueId,e.value());var t=document.getElementById("autosaved");if(null!=t&&void 0!=t&&""!=t){var n=new Date,r=n.getHours(),i=n.getMinutes(),o="am",a=r;a>=12&&(a=r-12,o="pm"),0==a&&(a=12),i=10>i?"0"+i:i,t.innerHTML="Autosaved: "+a+":"+i+" "+o}this.autosaveTimeoutId=setTimeout(function(){e.autosave()},this.options.autosave.delay||1e4)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},B.prototype.clearAutosavedValue=function(){if(_()){if(void 0==this.options.autosave||void 0==this.options.autosave.uniqueId||""==this.options.autosave.uniqueId)return void console.log("SimpleMDE: You must set a uniqueId to clear the autosave value");localStorage.removeItem("smde_"+this.options.autosave.uniqueId)}else console.log("SimpleMDE: localStorage not available, cannot autosave")},B.prototype.createSideBySide=function(){var e=this.codemirror,t=e.getWrapperElement(),n=t.nextSibling;n&&/editor-preview-side/.test(n.className)||(n=document.createElement("div"),n.className="editor-preview-side",t.parentNode.insertBefore(n,t.nextSibling));var r=!1,i=!1;return e.on("scroll",function(e){if(r)return void(r=!1);i=!0;var t=e.getScrollInfo().height-e.getScrollInfo().clientHeight,o=parseFloat(e.getScrollInfo().top)/t,a=(n.scrollHeight-n.clientHeight)*o;n.scrollTop=a}),n.onscroll=function(){if(i)return void(i=!1);r=!0;var t=n.scrollHeight-n.clientHeight,o=parseFloat(n.scrollTop)/t,a=(e.getScrollInfo().height-e.getScrollInfo().clientHeight)*o;e.scrollTo(0,a)},n},B.prototype.createToolbar=function(e){if(e=e||this.options.toolbar,e&&0!==e.length){var t;for(t=0;t 3) { + args.pop(); // no capture + } + } else if (len === 3) { + args.push(false); + } + return args; + } + + function apply(args, sType) { + var element = args.shift(), + method = [evt[sType]]; + if (old) { + element[method](args[0], args[1]); + } else { + element[method].apply(element, args); + } + } + + function addEvent() { + apply(getArgs(arguments), 'add'); + } + + function removeEvent() { + apply(getArgs(arguments), 'remove'); + } + + return { + add: addEvent, + remove: removeEvent + }; + + }()); + + function rnd(n,min) { + if (isNaN(min)) { + min = 0; + } + return (Math.random()*n)+min; + } + + function plusMinus(n) { + return (parseInt(rnd(2),10)===1?n*-1:n); + } + + this.randomizeWind = function() { + var i; + vRndX = plusMinus(rnd(storm.vMaxX,0.2)); + vRndY = rnd(storm.vMaxY,0.2); + if (this.flakes) { + for (i=0; i=0 && s.vX<0.2) { + s.vX = 0.2; + } else if (s.vX<0 && s.vX>-0.2) { + s.vX = -0.2; + } + if (s.vY>=0 && s.vY<0.2) { + s.vY = 0.2; + } + }; + + this.move = function() { + var vX = s.vX*windOffset, yDiff; + s.x += vX; + s.y += (s.vY*s.vAmp); + if (s.x >= screenX || screenX-s.x < storm.flakeWidth) { // X-axis scroll check + s.x = 0; + } else if (vX < 0 && s.x-storm.flakeLeftOffset < -storm.flakeWidth) { + s.x = screenX-storm.flakeWidth-1; // flakeWidth; + } + s.refresh(); + yDiff = screenY+scrollY-s.y+storm.flakeHeight; + if (yDiff0.998) { + // ~1/1000 chance of melting mid-air, with each frame + s.melting = true; + s.melt(); + // only incrementally melt one frame + // s.melting = false; + } + if (storm.useTwinkleEffect) { + if (s.twinkleFrame < 0) { + if (Math.random() > 0.97) { + s.twinkleFrame = parseInt(Math.random() * 8, 10); + } + } else { + s.twinkleFrame--; + if (!opacitySupported) { + s.o.style.visibility = (s.twinkleFrame && s.twinkleFrame % 2 === 0 ? 'hidden' : 'visible'); + } else { + s.o.style.opacity = (s.twinkleFrame && s.twinkleFrame % 2 === 0 ? 0 : 1); + } + } + } + } + }; + + this.animate = function() { + // main animation loop + // move, check status, die etc. + s.move(); + }; + + this.setVelocities = function() { + s.vX = vRndX+rnd(storm.vMaxX*0.12,0.1); + s.vY = vRndY+rnd(storm.vMaxY*0.12,0.1); + }; + + this.setOpacity = function(o,opacity) { + if (!opacitySupported) { + return false; + } + o.style.opacity = opacity; + }; + + this.melt = function() { + if (!storm.useMeltEffect || !s.melting) { + s.recycle(); + } else { + if (s.meltFrame < s.meltFrameCount) { + s.setOpacity(s.o,s.meltFrames[s.meltFrame]); + s.o.style.fontSize = s.fontSize-(s.fontSize*(s.meltFrame/s.meltFrameCount))+'px'; + s.o.style.lineHeight = storm.flakeHeight+2+(storm.flakeHeight*0.75*(s.meltFrame/s.meltFrameCount))+'px'; + s.meltFrame++; + } else { + s.recycle(); + } + } + }; + + this.recycle = function() { + s.o.style.display = 'none'; + s.o.style.position = (fixedForEverything?'fixed':'absolute'); + s.o.style.bottom = 'auto'; + s.setVelocities(); + s.vCheck(); + s.meltFrame = 0; + s.melting = false; + s.setOpacity(s.o,1); + s.o.style.padding = '0px'; + s.o.style.margin = '0px'; + s.o.style.fontSize = s.fontSize+'px'; + s.o.style.lineHeight = (storm.flakeHeight+2)+'px'; + s.o.style.textAlign = 'center'; + s.o.style.verticalAlign = 'baseline'; + s.x = parseInt(rnd(screenX-storm.flakeWidth-20),10); + s.y = parseInt(rnd(screenY)*-1,10)-storm.flakeHeight; + s.refresh(); + s.o.style.display = 'block'; + s.active = 1; + }; + + this.recycle(); // set up x/y coords etc. + this.refresh(); + + }; + + this.snow = function() { + var active = 0, flake = null, i, j; + for (i=0, j=storm.flakes.length; istorm.flakesMaxActive) { + storm.flakes[storm.flakes.length-1].active = -1; + } + } + storm.targetElement.appendChild(docFrag); + }; + + this.timerInit = function() { + storm.timer = true; + storm.snow(); + }; + + this.init = function() { + var i; + for (i=0; i=0;o--)u(e(n[o]),t)}function u(t,n,o){var s=!(!o||!o.force)&&o.force;return!(!t||!s&&0!==e(":focus",t).length)&&(t[n.hideMethod]({duration:n.hideDuration,easing:n.hideEasing,complete:function(){h(t)}}),!0)}function d(t){return v=e("
    ").attr("id",t.containerId).addClass(t.positionClass),v.appendTo(e(t.target)),v}function p(){return{tapToDismiss:!0,toastClass:"toast",containerId:"toast-container",debug:!1,showMethod:"fadeIn",showDuration:300,showEasing:"swing",onShown:void 0,hideMethod:"fadeOut",hideDuration:1e3,hideEasing:"swing",onHidden:void 0,closeMethod:!1,closeDuration:!1,closeEasing:!1,closeOnHover:!0,extendedTimeOut:1e3,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},iconClass:"toast-info",positionClass:"toast-top-right",timeOut:5e3,titleClass:"toast-title",messageClass:"toast-message",escapeHtml:!1,target:".app",closeHtml:'',closeClass:"toast-close-button",newestOnTop:!0,preventDuplicates:!1,progressBar:!1,progressClass:"toast-progress",rtl:!1}}function f(e){C&&C(e)}function g(t){function o(e){return null==e&&(e=""),e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function s(){c(),u(),d(),p(),g(),C(),l(),i()}function i(){var e="";switch(t.iconClass){case"toast-success":case"toast-info":e="polite";break;default:e="assertive"}I.attr("aria-live",e)}function a(){E.closeOnHover&&I.hover(H,D),!E.onclick&&E.tapToDismiss&&I.click(b),E.closeButton&&j&&j.click(function(e){e.stopPropagation?e.stopPropagation():void 0!==e.cancelBubble&&e.cancelBubble!==!0&&(e.cancelBubble=!0),E.onCloseClick&&E.onCloseClick(e),b(!0)}),E.onclick&&I.click(function(e){E.onclick(e),b()})}function r(){I.hide(),I[E.showMethod]({duration:E.showDuration,easing:E.showEasing,complete:E.onShown}),E.timeOut>0&&(k=setTimeout(b,E.timeOut),F.maxHideTime=parseFloat(E.timeOut),F.hideEta=(new Date).getTime()+F.maxHideTime,E.progressBar&&(F.intervalId=setInterval(x,10)))}function c(){t.iconClass&&I.addClass(E.toastClass).addClass(y)}function l(){E.newestOnTop?v.prepend(I):v.append(I)}function u(){if(t.title){var e=t.title;E.escapeHtml&&(e=o(t.title)),M.append(e).addClass(E.titleClass),I.append(M)}}function d(){if(t.message){var e=t.message;E.escapeHtml&&(e=o(t.message)),B.append(e).addClass(E.messageClass),I.append(B)}}function p(){E.closeButton&&(j.addClass(E.closeClass).attr("role","button"),I.prepend(j))}function g(){E.progressBar&&(q.addClass(E.progressClass),I.prepend(q))}function C(){E.rtl&&I.addClass("rtl")}function O(e,t){if(e.preventDuplicates){if(t.message===w)return!0;w=t.message}return!1}function b(t){var n=t&&E.closeMethod!==!1?E.closeMethod:E.hideMethod,o=t&&E.closeDuration!==!1?E.closeDuration:E.hideDuration,s=t&&E.closeEasing!==!1?E.closeEasing:E.hideEasing;if(!e(":focus",I).length||t)return clearTimeout(F.intervalId),I[n]({duration:o,easing:s,complete:function(){h(I),clearTimeout(k),E.onHidden&&"hidden"!==P.state&&E.onHidden(),P.state="hidden",P.endTime=new Date,f(P)}})}function D(){(E.timeOut>0||E.extendedTimeOut>0)&&(k=setTimeout(b,E.extendedTimeOut),F.maxHideTime=parseFloat(E.extendedTimeOut),F.hideEta=(new Date).getTime()+F.maxHideTime)}function H(){clearTimeout(k),F.hideEta=0,I.stop(!0,!0)[E.showMethod]({duration:E.showDuration,easing:E.showEasing})}function x(){var e=(F.hideEta-(new Date).getTime())/F.maxHideTime*100;q.width(e+"%")}var E=m(),y=t.iconClass||E.iconClass;if("undefined"!=typeof t.optionsOverride&&(E=e.extend(E,t.optionsOverride),y=t.optionsOverride.iconClass||y),!O(E,t)){T++,v=n(E,!0);var k=null,I=e("
    "),M=e("
    "),B=e("
    "),q=e("
    "),j=e(E.closeHtml),F={intervalId:null,hideEta:null,maxHideTime:null},P={toastId:T,state:"visible",startTime:new Date,options:E,map:t};return s(),r(),a(),f(P),E.debug&&console&&console.log(P),I}}function m(){return e.extend({},p(),b.options)}function h(e){v||(v=n()),e.is(":visible")||(e.remove(),e=null,0===v.children().length&&(v.remove(),w=void 0))}var v,C,w,T=0,O={error:"error",info:"info",success:"success",warning:"warning"},b={clear:r,remove:c,error:t,getContainer:n,info:o,options:{},subscribe:s,success:i,version:"2.1.3",warning:a};return b}()})}("function"==typeof define&&define.amd?define:function(e,t){"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):window.toastr=t(window.jQuery)}); +//# sourceMappingURL=toastr.js.map \ No newline at end of file diff --git a/js/toastr.js.map b/js/toastr.js.map new file mode 100644 index 0000000..07b5237 --- /dev/null +++ b/js/toastr.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["toastr.js"],"names":["define","$","error","message","title","optionsOverride","notify","type","toastType","iconClass","getOptions","iconClasses","getContainer","options","create","$container","containerId","length","createContainer","info","subscribe","callback","listener","success","warning","clear","$toastElement","clearOptions","clearToast","clearContainer","remove","removeToast","children","toastsToClear","i","force","hideMethod","duration","hideDuration","easing","hideEasing","complete","attr","addClass","positionClass","appendTo","target","getDefaults","tapToDismiss","toastClass","debug","showMethod","showDuration","showEasing","onShown","undefined","onHidden","closeMethod","closeDuration","closeEasing","closeOnHover","extendedTimeOut","timeOut","titleClass","messageClass","escapeHtml","closeHtml","closeClass","newestOnTop","preventDuplicates","progressBar","progressClass","rtl","publish","args","map","source","replace","personalizeToast","setIcon","setTitle","setMessage","setCloseButton","setProgressBar","setRTL","setSequence","setAria","ariaValue","handleEvents","hover","stickAround","delayedHideToast","onclick","click","hideToast","closeButton","$closeElement","event","stopPropagation","cancelBubble","onCloseClick","displayToast","hide","intervalId","setTimeout","maxHideTime","parseFloat","hideEta","Date","getTime","setInterval","updateProgress","prepend","append","suffix","$titleElement","$messageElement","$progressElement","shouldExit","previousToast","override","method","clearTimeout","response","state","endTime","stop","percentage","width","extend","toastId","startTime","console","log","toastr","is","version","amd","deps","factory","module","exports","require","window","jQuery"],"mappings":"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","file":"toastr.js","sourcesContent":["/*\n * Toastr\n * Copyright 2012-2015\n * Authors: John Papa, Hans Fjällemark, and Tim Ferrell.\n * All Rights Reserved.\n * Use, reproduction, distribution, and modification of this code is subject to the terms and\n * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php\n *\n * ARIA Support: Greta Krafsig\n *\n * Project: https://github.com/CodeSeven/toastr\n */\n/* global define */\n(function (define) {\n define(['jquery'], function ($) {\n return (function () {\n var $container;\n var listener;\n var toastId = 0;\n var toastType = {\n error: 'error',\n info: 'info',\n success: 'success',\n warning: 'warning'\n };\n\n var toastr = {\n clear: clear,\n remove: remove,\n error: error,\n getContainer: getContainer,\n info: info,\n options: {},\n subscribe: subscribe,\n success: success,\n version: '2.1.3',\n warning: warning\n };\n\n var previousToast;\n\n return toastr;\n\n ////////////////\n\n function error(message, title, optionsOverride) {\n return notify({\n type: toastType.error,\n iconClass: getOptions().iconClasses.error,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function getContainer(options, create) {\n if (!options) { options = getOptions(); }\n $container = $('#' + options.containerId);\n if ($container.length) {\n return $container;\n }\n if (create) {\n $container = createContainer(options);\n }\n return $container;\n }\n\n function info(message, title, optionsOverride) {\n return notify({\n type: toastType.info,\n iconClass: getOptions().iconClasses.info,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function subscribe(callback) {\n listener = callback;\n }\n\n function success(message, title, optionsOverride) {\n return notify({\n type: toastType.success,\n iconClass: getOptions().iconClasses.success,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function warning(message, title, optionsOverride) {\n return notify({\n type: toastType.warning,\n iconClass: getOptions().iconClasses.warning,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function clear($toastElement, clearOptions) {\n var options = getOptions();\n if (!$container) { getContainer(options); }\n if (!clearToast($toastElement, options, clearOptions)) {\n clearContainer(options);\n }\n }\n\n function remove($toastElement) {\n var options = getOptions();\n if (!$container) { getContainer(options); }\n if ($toastElement && $(':focus', $toastElement).length === 0) {\n removeToast($toastElement);\n return;\n }\n if ($container.children().length) {\n $container.remove();\n }\n }\n\n // internal functions\n\n function clearContainer (options) {\n var toastsToClear = $container.children();\n for (var i = toastsToClear.length - 1; i >= 0; i--) {\n clearToast($(toastsToClear[i]), options);\n }\n }\n\n function clearToast ($toastElement, options, clearOptions) {\n var force = clearOptions && clearOptions.force ? clearOptions.force : false;\n if ($toastElement && (force || $(':focus', $toastElement).length === 0)) {\n $toastElement[options.hideMethod]({\n duration: options.hideDuration,\n easing: options.hideEasing,\n complete: function () { removeToast($toastElement); }\n });\n return true;\n }\n return false;\n }\n\n function createContainer(options) {\n $container = $('
    ')\n .attr('id', options.containerId)\n .addClass(options.positionClass);\n\n $container.appendTo($(options.target));\n return $container;\n }\n\n function getDefaults() {\n return {\n tapToDismiss: true,\n toastClass: 'toast',\n containerId: 'toast-container',\n debug: false,\n\n showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery\n showDuration: 300,\n showEasing: 'swing', //swing and linear are built into jQuery\n onShown: undefined,\n hideMethod: 'fadeOut',\n hideDuration: 1000,\n hideEasing: 'swing',\n onHidden: undefined,\n closeMethod: false,\n closeDuration: false,\n closeEasing: false,\n closeOnHover: true,\n\n extendedTimeOut: 1000,\n iconClasses: {\n error: 'toast-error',\n info: 'toast-info',\n success: 'toast-success',\n warning: 'toast-warning'\n },\n iconClass: 'toast-info',\n positionClass: 'toast-top-right',\n timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky\n titleClass: 'toast-title',\n messageClass: 'toast-message',\n escapeHtml: false,\n target: 'body',\n closeHtml: '',\n closeClass: 'toast-close-button',\n newestOnTop: true,\n preventDuplicates: false,\n progressBar: false,\n progressClass: 'toast-progress',\n rtl: false\n };\n }\n\n function publish(args) {\n if (!listener) { return; }\n listener(args);\n }\n\n function notify(map) {\n var options = getOptions();\n var iconClass = map.iconClass || options.iconClass;\n\n if (typeof (map.optionsOverride) !== 'undefined') {\n options = $.extend(options, map.optionsOverride);\n iconClass = map.optionsOverride.iconClass || iconClass;\n }\n\n if (shouldExit(options, map)) { return; }\n\n toastId++;\n\n $container = getContainer(options, true);\n\n var intervalId = null;\n var $toastElement = $('
    ');\n var $titleElement = $('
    ');\n var $messageElement = $('
    ');\n var $progressElement = $('
    ');\n var $closeElement = $(options.closeHtml);\n var progressBar = {\n intervalId: null,\n hideEta: null,\n maxHideTime: null\n };\n var response = {\n toastId: toastId,\n state: 'visible',\n startTime: new Date(),\n options: options,\n map: map\n };\n\n personalizeToast();\n\n displayToast();\n\n handleEvents();\n\n publish(response);\n\n if (options.debug && console) {\n console.log(response);\n }\n\n return $toastElement;\n\n function escapeHtml(source) {\n if (source == null) {\n source = '';\n }\n\n return source\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(//g, '>');\n }\n\n function personalizeToast() {\n setIcon();\n setTitle();\n setMessage();\n setCloseButton();\n setProgressBar();\n setRTL();\n setSequence();\n setAria();\n }\n\n function setAria() {\n var ariaValue = '';\n switch (map.iconClass) {\n case 'toast-success':\n case 'toast-info':\n ariaValue = 'polite';\n break;\n default:\n ariaValue = 'assertive';\n }\n $toastElement.attr('aria-live', ariaValue);\n }\n\n function handleEvents() {\n if (options.closeOnHover) {\n $toastElement.hover(stickAround, delayedHideToast);\n }\n\n if (!options.onclick && options.tapToDismiss) {\n $toastElement.click(hideToast);\n }\n\n if (options.closeButton && $closeElement) {\n $closeElement.click(function (event) {\n if (event.stopPropagation) {\n event.stopPropagation();\n } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) {\n event.cancelBubble = true;\n }\n\n if (options.onCloseClick) {\n options.onCloseClick(event);\n }\n\n hideToast(true);\n });\n }\n\n if (options.onclick) {\n $toastElement.click(function (event) {\n options.onclick(event);\n hideToast();\n });\n }\n }\n\n function displayToast() {\n $toastElement.hide();\n\n $toastElement[options.showMethod](\n {duration: options.showDuration, easing: options.showEasing, complete: options.onShown}\n );\n\n if (options.timeOut > 0) {\n intervalId = setTimeout(hideToast, options.timeOut);\n progressBar.maxHideTime = parseFloat(options.timeOut);\n progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime;\n if (options.progressBar) {\n progressBar.intervalId = setInterval(updateProgress, 10);\n }\n }\n }\n\n function setIcon() {\n if (map.iconClass) {\n $toastElement.addClass(options.toastClass).addClass(iconClass);\n }\n }\n\n function setSequence() {\n if (options.newestOnTop) {\n $container.prepend($toastElement);\n } else {\n $container.append($toastElement);\n }\n }\n\n function setTitle() {\n if (map.title) {\n var suffix = map.title;\n if (options.escapeHtml) {\n suffix = escapeHtml(map.title);\n }\n $titleElement.append(suffix).addClass(options.titleClass);\n $toastElement.append($titleElement);\n }\n }\n\n function setMessage() {\n if (map.message) {\n var suffix = map.message;\n if (options.escapeHtml) {\n suffix = escapeHtml(map.message);\n }\n $messageElement.append(suffix).addClass(options.messageClass);\n $toastElement.append($messageElement);\n }\n }\n\n function setCloseButton() {\n if (options.closeButton) {\n $closeElement.addClass(options.closeClass).attr('role', 'button');\n $toastElement.prepend($closeElement);\n }\n }\n\n function setProgressBar() {\n if (options.progressBar) {\n $progressElement.addClass(options.progressClass);\n $toastElement.prepend($progressElement);\n }\n }\n\n function setRTL() {\n if (options.rtl) {\n $toastElement.addClass('rtl');\n }\n }\n\n function shouldExit(options, map) {\n if (options.preventDuplicates) {\n if (map.message === previousToast) {\n return true;\n } else {\n previousToast = map.message;\n }\n }\n return false;\n }\n\n function hideToast(override) {\n var method = override && options.closeMethod !== false ? options.closeMethod : options.hideMethod;\n var duration = override && options.closeDuration !== false ?\n options.closeDuration : options.hideDuration;\n var easing = override && options.closeEasing !== false ? options.closeEasing : options.hideEasing;\n if ($(':focus', $toastElement).length && !override) {\n return;\n }\n clearTimeout(progressBar.intervalId);\n return $toastElement[method]({\n duration: duration,\n easing: easing,\n complete: function () {\n removeToast($toastElement);\n clearTimeout(intervalId);\n if (options.onHidden && response.state !== 'hidden') {\n options.onHidden();\n }\n response.state = 'hidden';\n response.endTime = new Date();\n publish(response);\n }\n });\n }\n\n function delayedHideToast() {\n if (options.timeOut > 0 || options.extendedTimeOut > 0) {\n intervalId = setTimeout(hideToast, options.extendedTimeOut);\n progressBar.maxHideTime = parseFloat(options.extendedTimeOut);\n progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime;\n }\n }\n\n function stickAround() {\n clearTimeout(intervalId);\n progressBar.hideEta = 0;\n $toastElement.stop(true, true)[options.showMethod](\n {duration: options.showDuration, easing: options.showEasing}\n );\n }\n\n function updateProgress() {\n var percentage = ((progressBar.hideEta - (new Date().getTime())) / progressBar.maxHideTime) * 100;\n $progressElement.width(percentage + '%');\n }\n }\n\n function getOptions() {\n return $.extend({}, getDefaults(), toastr.options);\n }\n\n function removeToast($toastElement) {\n if (!$container) { $container = getContainer(); }\n if ($toastElement.is(':visible')) {\n return;\n }\n $toastElement.remove();\n $toastElement = null;\n if ($container.children().length === 0) {\n $container.remove();\n previousToast = undefined;\n }\n }\n\n })();\n });\n}(typeof define === 'function' && define.amd ? define : function (deps, factory) {\n if (typeof module !== 'undefined' && module.exports) { //Node\n module.exports = factory(require('jquery'));\n } else {\n window.toastr = factory(window.jQuery);\n }\n}));\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..064568b --- /dev/null +++ b/login.php @@ -0,0 +1,111 @@ + false, "password" => false]; +$username = $password = false; +$studio = isset($_GET['embedded']); + +$returnurl_raw = $_GET['ReturnUrl'] ?? false; +$returnurl = str_starts_with($returnurl_raw, "https://".$_SERVER['HTTP_HOST']) ? $returnurl_raw : "https://".$_SERVER['HTTP_HOST'].$returnurl_raw; + +if($_SERVER['REQUEST_METHOD'] == 'POST') +{ + $username = $_POST['username'] ?? false; + $password = $_POST['password'] ?? false; + $pwresult = false; + $userInfo = Users::GetInfoFromName($username); + $auth = new Password($password); + + if (!$password) $errors["password"] = "Please enter your password"; + if (!$username) $errors["username"] = "Please enter your username"; + else if (!$userInfo) $errors["username"] = "That user doesn't exist"; + else if (!$auth->verify($userInfo->password)) $errors["password"] = "Incorrect password"; + else if (Polygon::IsDevSite() && !in_array($userInfo->id, SITE_CONFIG["DevWhitelist"])) $errors["password"] = "An unexpected error occurred"; + + if(!$errors["username"] && !$errors["password"]) + { + // upgrade password to argon2id w/ encryption if still using bcrypt + if(strpos($userInfo->password, "$2y$10") !== false) $auth->update($userInfo->id); + + Session::Create($userInfo->id); + + if($userInfo->twofa) + { + if($returnurl_raw) die(header("Location: /login/2fa?ReturnUrl=".$returnurl_raw.($studio?"&embedded":""))); + die(header("Location: /login/2fa")); + } + else + { + if($returnurl_raw) die(header("Location: ".$returnurl)); + die(header("Location: /")); + } + } +} + +$pageBuilder = new PageBuilder(["title" => "Login", "ShowNavbar" => !$studio]); +$pageBuilder->buildHeader(); +?> +

    Login to

    +
    +
    +
    +
    + +
    + " name="username" id="username" value="" autocomplete="username"> +

    >

    +
    +
    +
    + +
    + " name="password" id="password" autocomplete="current-password"> +

    >

    +
    +
    + + +
    +
    + +
    +
    +
    Not a member?
    +
    +
    +
    +

    Sign Up

    +

    - explore many old versions

    +

    - buy any item you want

    +

    - customize your character

    +

    - publish your creations

    +

    - more text goes here

    +
    +
    + Sign Up +
    +
    +
    +
    +
    +
    + +buildFooter(); ?> diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..bd5a153 --- /dev/null +++ b/logout.php @@ -0,0 +1,6 @@ +errorCode(404); + +if($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["reactivate"]) && Users::UndoUserModeration(SESSION["user"]["id"])) +{ + redirect("/"); +} + +$moderationInfo = Users::GetUserModeration(SESSION["user"]["id"]); +if (!$moderationInfo) PageBuilder::instance()->errorCode(404); + +$text = +[ + "title" => + [ + 1 => "Warning", + 2 => "Banned for ".timeSince("@".((($moderationInfo->timeEnds-$moderationInfo->timeStarted)+time())+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
    rules and abide by them to prevent yourself from facing a ban", + 2 => "Your ban end".($moderationInfo->timeEnds > time() ? "s":"ed")." at ".date('j/n/Y g:i:s A \G\M\T', $moderationInfo->timeEnds).($moderationInfo->timeEnds > time() ? ", or in ".timeSince("@".($moderationInfo->timeEnds+1), true, false)."

    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)" + ] +]; + +$pageBuilder = new PageBuilder(["title" => SITE_CONFIG["site"]["name"]." Moderation"]); +$pageBuilder->buildHeader(); +?> +
    +
    + Moderation +
    +
    +

    banType]?>

    +

    banType]?>

    +

    Done at: timeStarted)?>

    +

    Moderator note:

    +
    + ', '

    ', $markdown->text($moderationInfo->reason, true))?> +

    +
    +

    banType]?>

    + + banType == 1 || $moderationInfo->banType == 2 && $moderationInfo->timeEnds < time()) { ?> +
    + +
    + +
    +
    +buildFooter(); ?> \ No newline at end of file diff --git a/my/account.php b/my/account.php new file mode 100644 index 0000000..0ff3e10 --- /dev/null +++ b/my/account.php @@ -0,0 +1,375 @@ + "My Account"]); + +$panel = "Settings"; +$userinfo = (object)SESSION["user"]; +$discordinfo = (object) +[ + "info" => NULL, + "key" => $userinfo->discordKey, + "timeVerified" => $userinfo->discordVerifiedTime +]; + +if ($discordinfo->key == NULL) +{ + $discordinfo->key = generateUUID(); + Database::singleton()->run( + "UPDATE users SET discordKey = :key WHERE id = :id", + [":key" => $discordinfo->key, ":id" => $userinfo->id] + ); +} +else if ($userInfo->discordID != NULL) +{ + $discordinfo->info = Discord::GetUserInfo($userinfo->discordID); +} + +$gauth = new GoogleAuthenticator(); +$twofa = SESSION["user"]["twofa"]; +$twofaSecret = $userinfo->twofaSecret; + +$sessions = Database::singleton()->run( + "SELECT * FROM sessions WHERE userId = :uid AND valid AND created+157700000 > UNIX_TIMESTAMP() AND lastonline+432000 > UNIX_TIMESTAMP() ORDER BY created DESC", + [":uid" => $userinfo->id] +); + +$Fields = (object) +[ + "Code" => "", + "Password" => "" +]; + +$Errors = (object) +[ + "Code" => false, + "Password" => false +]; + +$RequestSent = false; + +//2fa stuff is not done via ajax cuz am lazy +if ($_SERVER["REQUEST_METHOD"] == "POST") +{ + $RequestSent = true; + $panel = "2FA"; + + $csrf = $_POST['polygon_csrf'] ?? false; + $Fields->Code = $_POST['code'] ?? "false"; + $Fields->Password = $_POST['password'] ?? "false"; + + $auth = new Password($Fields->Password); + + if($csrf != SESSION["csrfToken"]) $Errors->Password = "An unexpected error occurred"; + if(!$gauth->checkCode($twofaSecret, $Fields->Code, 1)) $Errors->Code = "Incorrect code"; + if(!$auth->verify($userInfo->password)) $Errors->Password = "Incorrect password"; + + if(!$Errors->Code && !$Errors->Password) + { + $twofa = !SESSION["user"]["twofa"]; + + Database::singleton()->run( + "UPDATE users SET twofa = :2fa WHERE id = :uid", + [":2fa" => (int)!$twofa, ":uid" => SESSION["user"]["id"]] + ); + + if ($twofa) + { + $recoveryCodes = str_split(bin2hex(random_bytes(60)), 12); + + Database::singleton()->run( + "UPDATE users SET twofaRecoveryCodes = :json WHERE id = :uid", + [":json" => json_encode(array_fill_keys($recoveryCodes, true)), ":uid" => SESSION["user"]["id"]] + ); + + ob_start(); +?> + +Congratulations! Your account is now more secure. But before you go, there's one last thing: +

    +If you can't get a code from your 2FA app for whatever reason, you can use a 2FA recovery code. +

    +
    + +
    + +
    + +
    +
    +These are a set of static, one-time use codes that never expire unless they are used or you disable 2FA. You can use these to get back into your account without the need of a 2FA app. +

    +This is the only time you'll ever see these here, so write them down somewhere now. + +showStaticModal([ + "header" => "Two-Factor Authentication is active", + "body" => ob_get_clean(), + "buttons" => [["class" => "btn btn-primary", "dismiss" => true, "text" => "I understand"]], + "options" => ["show" => true, "backdrop" => "static"] + ]); + } + } +} + +if (!$twofa) +{ + $twofaSecret = $gauth->generateSecret(); + Database::singleton()->run( + "UPDATE users SET twofaSecret = :secret WHERE id = :uid", + [":secret" => $twofaSecret, ":uid" => SESSION["user"]["id"]] + ); +} + +$pageBuilder->buildHeader(); +?> + + +

    My Account

    +
    + +
    +
    +
    " id="settings" role="tabpanel" aria-labelledby="settings-tab" style="max-width:36rem;"> +
    + +
    + + 1000 characters max +
    +
    +
    + +
    + + Note: the 2014 theme has some visual style issues +
    +
    +
    +
    Filter
    +
    + +
    +
    + +
    +
    +
    +
    + info == NULL) { ?> +

    Looks like you're not yet verified.

    +

    If you haven't joined the Discord server yet, join via the widget over by the side.

    +

    Once you join, the verification bot should DM you asking for your key, which is here:

    + PolygonVerify:key?> +

    Copy and send this to the bot in DMs, and you'll be in!

    +

    If the bot hasn't DMed you, it may be down. When it comes back online, just send a DM to the bot with your key.

    + +
    +
    +
    +
    + +
    +
    +

    info->username?>#info->tag?>

    +
    Verified timeVerified)?>
    + ID +
    +
    +
    +
    +

    If you wish to have your Discord account unverified so you can use another account, message an admin.

    + +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + + 8 - 64 characters, must have at least 6 characters and 2 numbers +
    +
    +
    + +
    + +
    +
    + +
    +
    " id="twofa" role="tabpanel" aria-labelledby="twofa-tab" style="max-width:36rem;"> + +

    Two-Factor Authentication is currently active. If you wish to disable it, just fill in the fields below.

    +

    Keep in mind that disabling 2FA will invalidate your recovery codes. If you re-enable it, use the new ones it gives you.

    + +

    Use a two-factor authentication app that has backups (Authy is a good one), so you don't have to worry about being unable to log in if you lose your device.

    +
    +
    + "> +
    +
    +

    Scan the QR code with your authenticator app of choice.

    +

    This changes with every page refresh, so be careful.

    +

    There's also a manual key here if you prefer that:

    +
    +
    + +
    +
    + +
    + + Code != false) { ?>Code?> +
    +
    +
    + +
    + + Password != false) { ?>Password?> +
    +
    + "> + + +
    +
    +
    +
    + fetch(\PDO::FETCH_OBJ)) { /* $ipInfo = Polygon::getIpInfo($session->loginIp); */ $browserInfo = get_browser($session->userAgent); ?> +
    +
    +
    +
    IsGameClient ? "Polygon Game Client" : "{$browserInfo->device_type} / {$browserInfo->browser}"?>
    + IsGameClient ? htmlspecialchars($session->userAgent) : $browserInfo->platform?>
    + Started created)?>
    + sessionKey == $_COOKIE['polygon_session']){ ?>This is your current session + Last seen lastonline)?> +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +buildFooter(); ?> diff --git a/my/character.php b/my/character.php new file mode 100644 index 0000000..335807b --- /dev/null +++ b/my/character.php @@ -0,0 +1,228 @@ + "Character Customizer"]); +$pageBuilder->addResource("polygonScripts", "/js/polygon/character.js"); + +$pageBuilder->addResource("polygonScripts", "/js/3D/ThumbnailView.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/ThreeDeeThumbnails.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/three.min.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/MTLLoader.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/OBJMTLLoader.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/tween.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/PolygonOrbitControls.js"); + +$pageBuilder->buildHeader(); +?> +

    Character Customizer

    +
    +
    +

    Avatar

    +
    "> + "> + <?=SESSION[" class="avatar img-fluid" src=""> + + +
    +

    Something wrong with your avatar?

    +

    Click here to re-draw it!

    +
    +
    + +
    +
    + +
    +
    +
    +

    +
    +
    + +
    + +
    +
    +
    +
    +

    Colors

    +
    +

    Click a body part to change its color:

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Currently Wearing

    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +buildFooter(); ?> diff --git a/my/creategroup.php b/my/creategroup.php new file mode 100644 index 0000000..19a4e7d --- /dev/null +++ b/my/creategroup.php @@ -0,0 +1,274 @@ + false, + "Description" => false, + "Entry" => false, + "Emblem" => false, + "General" => false +]; + +$Fields = (object) +[ + "Name" => "", + "Description" => "", + "Entry" => "Anyone" +]; + +// bit of a clunky way to do this but eh +$Ranks = +(object) [ + (object) [ + "Name" => "Guest", + "Description" => "A non-group member.", + "Rank" => 0, + "Permissions" => json_encode([ + "CanViewGroupWall" => true, + "CanViewGroupStatus" => true, + "CanPostOnGroupWall" => true, + "CanPostGroupStatus" => false, + "CanDeleteGroupWallPosts" => false, + + "CanAcceptJoinRequests" => false, + "CanKickLowerRankedMembers" => false, + "CanRoleLowerRankedMembers" => false, + "CanManageRelationships" => false, + + "CanCreateAssets" => false, + "CanConfigureAssets" => false, + "CanSpendFunds" => false, + "CanManageGames" => false, + + "CanManageGroupAdmin" => false, + "CanViewAuditLog" => false + ]) + ], + (object) [ + "Name" => "Member", + "Description" => "A regular group member.", + "Rank" => 25, + "Permissions" => json_encode([ + "CanViewGroupWall" => true, + "CanViewGroupStatus" => true, + "CanPostOnGroupWall" => true, + "CanPostGroupStatus" => false, + "CanDeleteGroupWallPosts" => false, + + "CanAcceptJoinRequests" => false, + "CanKickLowerRankedMembers" => false, + "CanRoleLowerRankedMembers" => false, + "CanManageRelationships" => false, + + "CanCreateAssets" => false, + "CanConfigureAssets" => false, + "CanSpendFunds" => false, + "CanManageGames" => false, + + "CanManageGroupAdmin" => false, + "CanViewAuditLog" => false + ]) + ], + (object) [ + "Name" => "Admin", + "Description" => "A group administrator.", + "Rank" => 100, + "Permissions" => json_encode([ + "CanViewGroupWall" => true, + "CanViewGroupStatus" => true, + "CanPostOnGroupWall" => true, + "CanPostGroupStatus" => true, + "CanDeleteGroupWallPosts" => true, + + "CanAcceptJoinRequests" => false, + "CanKickLowerRankedMembers" => true, + "CanRoleLowerRankedMembers" => true, + "CanManageRelationships" => false, + + "CanCreateAssets" => true, + "CanConfigureAssets" => true, + "CanSpendFunds" => false, + "CanManageGames" => false, + + "CanManageGroupAdmin" => true, + "CanViewAuditLog" => true + ]) + ], + (object) [ + "Name" => "Owner", + "Description" => "The group's owner.", + "Rank" => 255, + "Permissions" => json_encode([ + "CanViewGroupWall" => true, + "CanViewGroupStatus" => true, + "CanPostOnGroupWall" => true, + "CanPostGroupStatus" => true, + "CanDeleteGroupWallPosts" => true, + + "CanAcceptJoinRequests" => true, + "CanKickLowerRankedMembers" => true, + "CanRoleLowerRankedMembers" => true, + "CanManageRelationships" => true, + + "CanCreateAssets" => true, + "CanConfigureAssets" => true, + "CanSpendFunds" => true, + "CanManageGames" => false, + + "CanManageGroupAdmin" => true, + "CanViewAuditLog" => true + ]) + ], +]; + +if($_SERVER["REQUEST_METHOD"] == "POST") +{ + $Fields->Name = $_POST["Name"] ?? ""; + $Fields->Description = $_POST["Description"] ?? ""; + // $Fields->Entry = $_POST["Entry"] ?? ""; + $Emblem = $_FILES["Emblem"] ?? false; + + if(!strlen($Fields->Name)) $Errors->Name = "Group name cannot be empty"; + else if(strlen($Fields->Name) < 3) $Errors->Name = "Group name must be at least 3 characters long"; + else if(strlen($Fields->Name) > 48) $Errors->Name = "Group name cannot be longer than 48 characters"; + else if(Polygon::IsExplicitlyFiltered($Fields->Name)) $Errors->Name = "Group name contains inappropriate text"; + + if(strlen($Fields->Description) > 1000) $Errors->Description = "Group description cannot be longer than 1,000 characters"; + else if(Polygon::IsExplicitlyFiltered($Fields->Description)) $Errors->Description = "Group description contains inappropriate text"; + + // if(!in_array($Fields->Entry, ["Anyone", "Manual"])) $Errors->Entry = "Group entry setting is invalid"; + + if(!$Emblem || !$Emblem["size"]) $Errors->Emblem = "You must upload a group emblem"; + + // if(SESSION["user"]["currency"] < 500) $Errors->General = "You do not have the sufficient funds to create a group"; + + $GroupExists = Database::singleton()->run("SELECT COUNT(*) FROM groups WHERE name = :Name", [":Name" => $Fields->Name])->fetchColumn(); + if($GroupExists) $Errors->Name = "A group with that name already exists"; + + $CreatedGroups = Database::singleton()->run("SELECT COUNT(*) FROM groups WHERE owner = :UserID", [":UserID" => SESSION["user"]["id"]])->fetchColumn(); + if($CreatedGroups >= 3) $Errors->General = "You can only create a maximum of three groups"; + + if(Groups::GetUserGroups(SESSION["user"]["id"])->rowCount() >= 20) $Errors->General = "You have reached the maximum number of groups"; + + if(!$Errors->Name && !$Errors->Description && !$Errors->Entry && !$Errors->Emblem && !$Errors->General) + { + // the group emblem is uploaded as an image on the creator's account + $Image = new Upload($Emblem); + if(!$Image->uploaded) throw new Exception("Failed to upload image"); + $Image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; + $Image->image_convert = 'png'; + + $EmblemID = Catalog::CreateAsset(["type" => 22, "creator" => SESSION["user"]["id"], "name" => $Fields->Name, "description" => "Group Emblem"]); + $Processor = Image::Process($Image, ["name" => "$EmblemID", "resize" => false, "dir" => "assets/"]); + if($Processor !== true) $Errors->Emblem = $Processor; + + if(!$Errors->Emblem) + { + Thumbnails::UploadAsset($Image, $EmblemID, 60, 62, ["keepRatio" => true, "align" => "C"]); + Thumbnails::UploadAsset($Image, $EmblemID, 420, 420, ["keepRatio" => true, "align" => "C"]); + + // remove 500 pizzas from creator + // Database::singleton()->run( + // "UPDATE users SET currency = currency - 500 WHERE id = :UserID", + // [":UserID" => SESSION["user"]["id"]] + // ); + + // create group + Database::singleton()->run( + "INSERT INTO groups (creator, owner, emblem, name, description, entry, created) VALUES (:UserID, :UserID, :EmblemID, :Name, :Description, :Entry, UNIX_TIMESTAMP())", + [":UserID" => SESSION["user"]["id"], ":EmblemID" => $EmblemID, ":Name" => $Fields->Name, ":Description" => $Fields->Description, ":Entry" => $Fields->Entry] + ); + + $GroupID = Database::singleton()->lastInsertId(); + + // create initial ranks + foreach ($Ranks as $Rank) + { + Database::singleton()->run( + "INSERT INTO groups_ranks (GroupID, Name, Description, Rank, Permissions, Created) VALUES (:GroupID, :Name, :Description, :Rank, :Permissions, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, "Name" => $Rank->Name, ":Description" => $Rank->Description, ":Rank" => $Rank->Rank, ":Permissions" => $Rank->Permissions] + ); + } + + // instantiate creator as owner + Database::singleton()->run( + "INSERT INTO groups_members (GroupID, UserID, Rank, Joined) VALUES (:GroupID, :UserID, 255, UNIX_TIMESTAMP())", + [":GroupID" => $GroupID, ":UserID" => SESSION["user"]["id"]] + ); + + redirect("/groups?gid=$GroupID"); + } + } +} + +$pageBuilder = new PageBuilder(); +$pageBuilder->buildHeader(); +?> +

    Create A Group

    +
    +
    +
    +
    + +
    + +
    Name?>
    +
    +
    +
    + +
    + +
    Description?>
    +
    +
    +
    + +
    + +
    Emblem?>
    +
    +
    +
    +
    + +

    General?>

    +
    +
    + + +
    +
    + General?> +
    +
    + +
    +
    +
    +buildFooter(); \ No newline at end of file diff --git a/my/groupadmin.php b/my/groupadmin.php new file mode 100644 index 0000000..f4c42ad --- /dev/null +++ b/my/groupadmin.php @@ -0,0 +1,363 @@ +errorCode(404); + +$MyRank = Groups::GetUserRank(SESSION["user"]["id"], $GroupInfo->id); +if(!$MyRank->Permissions->CanManageGroupAdmin) PageBuilder::instance()->errorCode(404); + +$Panel = "Members"; +$ShowInfoAlert = false; + +$Errors = (object) +[ + "Name" => false, + "Description" => false, + "Emblem" => false +]; + +$Fields = (object) +[ + "Name" => $GroupInfo->name, + "Description" => $GroupInfo->description +]; + +if($_SERVER["REQUEST_METHOD"] == "POST" && $MyRank->Level == 255) +{ + $Panel = "Info"; + + $Fields->Name = $_POST["Name"] ?? ""; + $Fields->Description = $_POST["Description"] ?? ""; + $Emblem = $_FILES["Emblem"] ?? false; + + if(!strlen($Fields->Name)) $Errors->Name = "Group name cannot be empty"; + else if(strlen($Fields->Name) < 3) $Errors->Name = "Group name must be at least 3 characters long"; + else if(strlen($Fields->Name) > 48) $Errors->Name = "Group name cannot be longer than 48 characters"; + else if(Polygon::IsExplicitlyFiltered($Fields->Name)) $Errors->Name = "Group name contains inappropriate text"; + + if(strlen($Fields->Description) > 1000) $Errors->Description = "Group description cannot be longer than 1,000 characters"; + else if(Polygon::IsExplicitlyFiltered($Fields->Description)) $Errors->Description = "Group description contains inappropriate text"; + + $GroupExists = Database::singleton()->run("SELECT COUNT(*) FROM groups WHERE name = :Name", [":Name" => $Fields->Name])->fetchColumn(); + if($GroupExists && $GroupInfo->name != $Fields->Name) $Errors->Name = "A group with that name already exists"; + + if(!$Errors->Name && !$Errors->Description) + { + if($Emblem && $Emblem["size"] !== 0) + { + // the group emblem is uploaded as an image on the creator's account + $Image = new Upload($Emblem); + if($Image->uploaded) + { + $Image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; + $Image->image_convert = 'png'; + + $Processed = Image::Process($Image, ["name" => $GroupInfo->emblem, "keepRatio" => true, "align" => "T", "x" => 128, "y" => 128, "dir" => "assets/"]); + + if ($Processed === true) + { + Thumbnails::UploadAsset($Image, $GroupInfo->emblem, 60, 62, ["keepRatio" => true, "align" => "C"]); + Thumbnails::UploadAsset($Image, $GroupInfo->emblem, 420, 420, ["keepRatio" => true, "align" => "C"]); + + Database::singleton()->run("UPDATE assets SET approved = 0 WHERE id = :EmblemID", [":EmblemID" => $GroupInfo->emblem]); + } + else + { + $Errors->Emblem = "Image processing failed: $Processed"; + } + } + else + { + $Errors->Emblem = "Failed to upload image"; + } + } + + if($GroupInfo->name != $Fields->Name) + { + Groups::LogAction( + $GroupInfo->id, "Rename", + sprintf( + "%s renamed the group to: %s", + SESSION["user"]["id"], SESSION["user"]["username"], htmlspecialchars($Fields->Name) + ) + ); + } + + if($GroupInfo->description != $Fields->Description) + { + Groups::LogAction( + $GroupInfo->id, "Change Description", + sprintf( + "%s changed the group description to: %s", + SESSION["user"]["id"], SESSION["user"]["username"], htmlspecialchars($Fields->Description) + ) + ); + } + + // create group + Database::singleton()->run( + "UPDATE groups SET name = :Name, description = :Description WHERE id = :GroupID", + [":GroupID" => $GroupInfo->id, ":Name" => $Fields->Name, ":Description" => $Fields->Description] + ); + + $GroupInfo->name = $Fields->Name; + $GroupInfo->description = $Fields->Description; + + $ShowInfoAlert = true; + } +} + +$pageBuilder = new PageBuilder(); +$pageBuilder->addAppAttribute("data-group-id", $GroupInfo->id); +$pageBuilder->addResource("stylesheets", "https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.6.1/css/bootstrap4-toggle.min.css"); +$pageBuilder->addResource("scripts", "https://cdn.jsdelivr.net/gh/gitbrent/bootstrap4-toggle@3.6.1/js/bootstrap4-toggle.min.js"); +$pageBuilder->addResource("scripts", "http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.min.js"); +$pageBuilder->addResource("polygonScripts", "/js/polygon/groups.js"); +$pageBuilder->buildHeader(); +?> +

    name)?>

    +

    Owned By: owner)?>

    +
    + +
    +
    +
    " id="members" role="tabpanel" aria-labelledby="members-tab"> +

    Members

    +
    + +
    +

    +
    + +
    +
    +
    + $UserName +
    + $UserName + Permissions->CanRoleLowerRankedMembers) { ?> + + +
    +
    +
    +
    +
    + Level == 255) { ?> +
    " id="info" role="tabpanel" aria-labelledby="info-tab"> + + + +
    +
    +
    +

    Group Information

    +
    +
    + +
    +
    +
    +
    +

    Emblem

    + + +
    Emblem?>
    +
    +
    +

    Name

    + +
    Name?>
    +

    Description

    + +
    Description?>
    +
    +
    +
    +
    +
    " id="settings" role="tabpanel" aria-labelledby="settings-tab"> +

    Settings

    +
    + Permissions->CanManageRelationships) { ?> +
    +
    +
    +

    Allies

    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    +

    +
    + +
    +
    +
    + $Name +
    +

    $Name

    + Remove +
    +
    +
    +
    +
    +
    +

    Ally Requests

    +
    + +
    +

    +
    + +
    +
    +
    + $Name +
    +

    $Name

    +
    + Accept + Decline +
    +
    +
    +
    +
    +
    +
    +
    +

    Enemies

    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    +

    +
    + +
    +
    +
    + $Name +
    +

    $Name

    + Remove +
    +
    +
    +
    +
    +
    + Level == 255) { ?> +
    +
    +
    +

    Roles

    +
    +
    + + +
    +
    + + + + + + + + + + + + + + + + + +
    NameDescriptionRankEdit
    +
    + +
    +
    +
    +buildFooter(); \ No newline at end of file diff --git a/my/groupaudit.php b/my/groupaudit.php new file mode 100644 index 0000000..ddd3056 --- /dev/null +++ b/my/groupaudit.php @@ -0,0 +1,90 @@ +errorCode(404); + +$MyRank = Groups::GetUserRank(SESSION["user"]["id"] ?? 0, $GroupInfo->id); +if(!$MyRank->Permissions->CanViewAuditLog) PageBuilder::instance()->errorCode(404); + +$pageBuilder = new PageBuilder(["title" => "Group Audit Log"]); +$pageBuilder->addAppAttribute("data-group-id", $GroupInfo->id); +$pageBuilder->addResource("polygonScripts", "/js/polygon/groups.js"); +$pageBuilder->buildHeader(); +?> +
    +
    +
    +

    Audit Log

    +
    +
    + + +
    +
    +
    + + + + + + + + + + +
    DateUserRankDescription
    +
    +
    + +

    + More logs +
    +
    +buildFooter(); ?> diff --git a/my/invites.php b/my/invites.php new file mode 100644 index 0000000..cf9bf9f --- /dev/null +++ b/my/invites.php @@ -0,0 +1,164 @@ + "2 weeks"]); + +if ($CreatableTickets) +{ + Database::singleton()->run( + "UPDATE users SET CreatableInviteTickets = :CreatableTickets, LastInviteTicketCheck = UNIX_TIMESTAMP() WHERE id = :UserID", + [":UserID" => SESSION["user"]["id"], ":CreatableTickets" => $CreatableTickets] + ); +} + +if ($_SERVER["REQUEST_METHOD"] == "POST" && SESSION["user"]["CreatableInviteTickets"] > 0) +{ + $Ticket = sprintf("PolygonTicket(%s)", bin2hex(random_bytes(16))); + + Database::singleton()->run( + "INSERT INTO InviteTickets (Ticket, TimeCreated, CreatedBy) VALUES (:Ticket, UNIX_TIMESTAMP(), :UserID)", + [":Ticket" => $Ticket, ":UserID" => SESSION["user"]["id"]] + ); + + Database::singleton()->run( + "UPDATE users SET CreatableInviteTickets = CreatableInviteTickets - 1 WHERE id = :UserID", + [":UserID" => SESSION["user"]["id"]] + ); + + $Alert = ["Text" => sprintf("Your key has been created! %s", $Ticket), "Color" => "success"]; +} + +$Page = $_GET["Page"] ?? 1; +$TicketCount = Database::singleton()->run( + "SELECT COUNT(*) FROM InviteTickets WHERE CreatedBy = :UserID", + [":UserID" => SESSION["user"]["id"]] +)->fetchColumn(); + +$Pagination = Pagination($Page, $TicketCount, 20); + +$Tickets = Database::singleton()->run( + "SELECT InviteTickets.*, UsedBy.username AS UsedByName FROM InviteTickets + LEFT JOIN users AS UsedBy ON UsedBy.id = InviteTickets.UsedBy + WHERE CreatedBy = :UserID ORDER BY TimeCreated DESC LIMIT 20 OFFSET :Offset", + [":UserID" => SESSION["user"]["id"], ":Offset" => $Pagination->Offset] +); + +// idk y i decided to code this on my 15th birthday for mercury but i did +$InviteTree = []; + +function GenerateTree($UserName, $UserID, &$Node) +{ + $CurrentNode = ["UserName" => $UserName, "UserID" => $UserID, "Invited" => []]; + + // $query = $pdo->prepare("SELECT users.username, users.id FROM invitekeys INNER JOIN users ON users.id = usedById WHERE creatorId = :uid AND keyUsed"); + // $query->bindParam(":uid", $UserID, PDO::PARAM_INT); + // $query->execute(); + + $query = Database::singleton()->run( + "SELECT UsedBy.username, UsedBy.id FROM InviteTickets + INNER JOIN users AS UsedBy ON UsedBy.id = InviteTickets.UsedBy + WHERE CreatedBy = :UserID ORDER BY id DESC", + [":UserID" => $UserID] + ); + + while($row = $query->fetch(PDO::FETCH_OBJ)) + { + GenerateTree($row->username, $row->id, $CurrentNode["Invited"]); + } + + $Node[] = $CurrentNode; +} + +function DisplayTree($Tree) +{ + echo "
      "; + foreach($Tree as $Node) + { + // echo "
    • " . $Node["UserName"] . "
    • "; + printf("
    • %s
    • ", $Node["UserID"], $Node["UserName"]); + if (count($Node["Invited"]) > 0) DisplayTree($Node["Invited"]); + } + echo "
    "; +} + +GenerateTree(SESSION["user"]["username"], SESSION["user"]["id"], $InviteTree); + +$pageBuilder = new PageBuilder(["title" => "My Invitations"]); +$pageBuilder->buildHeader(); +?> +
    px-2 py-1" role="alert">
    +

    My Invitations

    + 0) { ?> +

    You currently have ticket creations remaining.

    + +

    You must wait before you can create a new invite ticket.

    + +
    +
    + +
    + 0) { ?> +
    + +
    + + + +
    +
    +
    +
    +
    + + + + + + + + + + fetch(\PDO::FETCH_OBJ)) { ?> + + + UsedBy == NULL) { ?> + + + + + + + + +
    TicketUsed ByCreated
    Ticket)?>No OneUsedByName?>TimeCreated)?>
    + Pages > 1) { ?> + + +
    +
    + +
    +
    +
    +
    +buildFooter(); ?> diff --git a/my/item.php b/my/item.php new file mode 100644 index 0000000..204e482 --- /dev/null +++ b/my/item.php @@ -0,0 +1,322 @@ +creator != SESSION["user"]["id"]) PageBuilder::instance()->errorCode(404); +if($item->type == 19) Catalog::$GearAttributes = json_decode($item->gear_attributes, true); + +$alert = false; +$itemLocation = Polygon::GetSharedResource("assets/{$item->id}"); +$ItemURL = encode_asset_name($item->name) . "-item?id={$item->id}"; + +if ($item->approved == 2 || !file_exists($itemLocation)) +{ + $AssetData = "This asset is deleted."; +} +else if ($item->type == 10) +{ + $AssetData = Gzip::Decompress($itemLocation); +} +else +{ + $AssetData = file_get_contents($itemLocation); +} + +if($_SERVER['REQUEST_METHOD'] == 'POST') +{ + $name = $_POST['name'] ?? ""; + $description = $_POST['description'] ?? ""; + $comments = isset($_POST['comments']) && $_POST['comments'] == "on"; + $sale = isset($_POST['sell']) && $_POST['sell'] == "on"; + $sell_for_price = isset($_POST['sell-for-currency']) && $_POST['sell-for-currency'] == "on"; + $price = $sell_for_price && isset($_POST['sell-price']) ? $_POST['sell-price'] : false; + $file = $_FILES["file"] ?? false; + Catalog::ParseGearAttributes(); + + if($sale && $sell_for_price && $price === "") $sell_for_price = $price = false; + + if(!strlen($name)) + { + $alert = ["text" => "Item name cannot be empty", "color" => "danger"]; + } + else if(Polygon::IsExplicitlyFiltered($name)) + { + $alert = ["text" => "The name contains inappropriate text", "color" => "danger"]; + } + else if(mb_strlen($name, "utf-8") > 50) + { + $alert = ["text" => "Item name cannot be any longer than 50 characters", "color" => "danger"]; + } + else if(strlen($description) > 1000) + { + $alert = ["text" => "Item description cannot be any longer than 1000 characters", "color" => "danger"]; + } + else if(Polygon::IsExplicitlyFiltered($description)) + { + $alert = ["text" => "The description contains inappropriate text", "color" => "danger"]; + } + else if($sale && $sell_for_price && !is_numeric($price)) + { + $alert = ["text" => "Item price is invalid", "color" => "danger"]; + } + else if($sale && $sell_for_price && $price < 0) + { + $alert = ["text" => "Item price cannot be less than zero", "color" => "danger"]; + } + else if($sale && $sell_for_price && $price > (2**31)) + { + $alert = ["text" => "Item price is too large", "color" => "danger"]; + } + else + { + $item->name = $name; + $item->description = $description; + $item->comments = $comments; + + if($item->type != 1) $item->sale = $sale; + if(in_array($item->type, [2, 8, 11, 12, 17, 18, 19])) $item->price = $price; + if($item->type == 10) $item->publicDomain = $item->sale; + if($item->type == 19) $item->gear_attributes = json_encode(Catalog::$GearAttributes); + + if($file && $file["size"]) + { + if ($item->approved == 2) + { + $alert = ["text" => "You cannot update the asset data of deleted assets", "color" => "danger"]; + } + else if($isAdmin && !in_array($item->type, [1, 3, 10])) + { + copy($file["tmp_name"], $itemLocation); + if($item->type == 10) Gzip::Compress($itemLocation); + } + else if($item->type == 3) + { + $image = new Upload($file); + if(!$image->uploaded) + { + $alert = ["text" => "Failed to process image - please contact an admin", "color" => "danger"]; + } + else + { + $image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; + $image->image_convert = 'png'; + + Thumbnails::UploadAsset($image, $item->id, 420, 420); + } + } + else if($item->type == 1) + { + $image = new Upload($file); + if(!$image->uploaded) + { + $alert = ["text" => "Failed to process image - please contact an admin", "color" => "danger"]; + } + else + { + $image->allowed = ['image/png', 'image/jpg', 'image/jpeg']; + $image->image_convert = 'png'; + + Image::Process($image, ["name" => $item->id, "resize" => false, "dir" => "assets/"]); + Thumbnails::UploadAsset($image, $item->id, 420, 420, ["keepRatio" => true, "align" => "C"]); + } + } + } + + Database::singleton()->run( + "UPDATE assets SET name = :name, description = :description, comments = :comments, sale = :sale, price = :price, gear_attributes = :gear, updated = UNIX_TIMESTAMP() + WHERE id = :id", + [ + ":name" => $item->name, + ":description" => $item->description, + ":comments" => $item->comments ? 1 : 0, + ":sale" => $item->sale ? 1 : 0, + ":price" => is_numeric($item->price) ? $item->price : 0, + ":gear" => $item->gear_attributes, + ":id" => $item->id + ] + ); + + if (SESSION["user"]["id"] != $item->creator && $item->creator != 1) + { + Users::LogStaffAction("[ Asset Modification ] Updated \"{$item->name}\" [" . Catalog::GetTypeByNum($item->type) . " ID {$item->id}]"); + } + + $alert = ["text" => "Your changes to this item have been saved (".date('h:i:s A').")", "color" => "primary"]; + } +} + +$pageBuilder = new PageBuilder(["title" => "Configure ".Catalog::GetTypeByNum($item->type)]); +$pageBuilder->buildHeader(); +?> +

    Configure type)?>

    +Back +
    +
    px-2 py-1" role="alert">
    +
    +
    + + +
    +
    + <?=htmlspecialchars($item->name)?> +
    +
    + + +
    + type, [1, 3, 10])) { ?> +
    +
    Update asset data Download
    +
    + + Preview: + +
    +
    + type == 1 || $item->type == 3) { ?> +
    +
    Set thumbnail
    +
    + + type == 3) { ?>Note: thumbnail should be in a 1:1 aspect ratio, like an album cover +
    +
    + +
    +
    Turn comments on/off
    +
    +

    Choose whether or not this item is open for comments.

    +
    + comments?' checked="checked"':''?>> + +
    +
    +
    + type == 19) { ?> + +
    +
    +
    +
    +
    + > + +
    +
    +
    +
    + > + +
    +
    +
    +
    + > + +
    +
    +
    +
    + > + +
    +
    +
    +
    + > + +
    +
    +
    +
    + > + +
    +
    +
    +
    + > + +
    +
    +
    +
    + > + +
    +
    +
    +
    + > + +
    +
    +
    +
    +
    + type, [2, 8, 11, 12, 17, 18, 19])) { //clothing ?> +
    +
    Sell this Item
    +
    +

    Check the box below and enter a price if you want to sell this item in the catalog.

    +

    Uncheck the box to remove the item from the catalog.

    +
    +
    +
    + sale?' checked="checked"':''?>> + +
    +
    +
    sale?'':' style="display:none"'?>> +
    +
    + price?' checked="checked"':''?>> + +
    +
    +
    +
    +
    + price?' value="'.$item->price.'"':' disabled="disabled"'?>> +
    +
    +
    +
    +
    +
    + type, [13, 3, 5, 10])) { //decal ?> +
    +
    Make Free
    +
    +

    Choose whether or not this item is freely available.

    +
    + sale?' checked="checked"':''?>> + +
    +
    +
    + +
    + + Cancel +
    +
    +
    +Back + +buildFooter(); ?> diff --git a/my/money.php b/my/money.php new file mode 100644 index 0000000..5c15dbe --- /dev/null +++ b/my/money.php @@ -0,0 +1,40 @@ + "Transactions"]); +$pageBuilder->addResource("polygonScripts", "/js/polygon/money.js"); +$pageBuilder->buildHeader(); +?> +
    +

    My Transactions

    +
    + + +
    +
    + + + + + + + + + + +
    DateMemberDescriptionAmount
    +
    + +
    +buildFooter(); ?> diff --git a/my/stuff.php b/my/stuff.php new file mode 100644 index 0000000..49d3d62 --- /dev/null +++ b/my/stuff.php @@ -0,0 +1,82 @@ + "Inventory"]); +$pageBuilder->addAppAttribute("data-user-id", SESSION["user"]["id"]); +$pageBuilder->addResource("polygonScripts", "/js/polygon/inventory.js"); +$pageBuilder->buildHeader(); +?> +
    +

    Inventory

    +
    +
    + +
    + +
    +
    +buildFooter(); ?> diff --git a/placeitem.php b/placeitem.php new file mode 100644 index 0000000..b7613cc --- /dev/null +++ b/placeitem.php @@ -0,0 +1,235 @@ +errorCode(404); + +if ($Asset->type != 9) +{ + redirect("/".encode_asset_name($Asset->name)."-item?id=".$Asset->id); +} + +$AssetThumbnail = Thumbnails::GetAsset($Asset, 768, 432); +$Gears = json_decode($Asset->gear_attributes, true); + +$IsCreator = SESSION && $Asset->creator == SESSION["user"]["id"]; +$IsStaff = Users::IsAdmin(); +$IsAdmin = Users::IsAdmin([Users::STAFF_CATALOG, Users::STAFF_ADMINISTRATOR]); +$CanConfigure = $IsCreator || $IsAdmin; + +if($_SERVER['REQUEST_URI'] != "/".encode_asset_name($Asset->name)."-place?id=".$Asset->id) +{ + redirect("/".encode_asset_name($Asset->name)."-place?id=".$Asset->id); +} + +$pageBuilder = new PageBuilder(["title" => Polygon::FilterText($Asset->name).", ".vowel(Catalog::GetTypeByNum($Asset->type))." by ".$Asset->username]); +$pageBuilder->addAppAttribute("data-asset-id", $Asset->id); +$pageBuilder->addAppAttribute("data-owns-asset", $CanConfigure ? "true" : "false"); +$pageBuilder->addMetaTag("og:image", $AssetThumbnail); +$pageBuilder->addMetaTag("og:description", Polygon::FilterText($Asset->description)); +$pageBuilder->addMetaTag("twitter:image", $AssetThumbnail); +$pageBuilder->addMetaTag("twitter:card", "summary_large_image"); + +if (Polygon::IsEmbed()) +{ + $pageBuilder->buildHeader(); + echo "

    wtf are you doing

    "; + $pageBuilder->buildFooter(); + die(); +} + +$pageBuilder->addResource("scripts", "/js/protocolcheck.js"); +$pageBuilder->addResource("polygonScripts", "/js/polygon/games.js"); +$pageBuilder->addResource("polygonScripts", "/js/polygon/item.js"); +if($IsStaff) $pageBuilder->addResource("polygonScripts", "/js/polygon/admin/asset-moderation.js"); + +$pageBuilder->buildHeader(); +?> +
    + + + +

    name)?>

    +
    +
    + + description)) { ?> +

    description))?>

    + +
    +
    +
    +
    + +
    +
    +
    Builder:
    +
    username?>
    +

    Joined: jointime)?>

    +
    +
    + + + publicDomain || $IsCreator || $IsStaff) { ?> + + + + +
    +

    Sorry, this place is currently only open to the creator's friends.

    +
    + +
    +

    Created: created)?>

    +

    Updated: updated)?>

    +

    Visited: Visits)?>

    +

    Version: Version?>

    +

    Max Players: MaxPlayers)?>

    +

    Allowed Gear Types:

    + + + $Enabled) { if (!$Enabled) continue; ?> + text-primary" data-toggle="tooltip" data-placement="bottom" title=""> + +
    +
    +
    + +
    +
    +
    + +
    +
    +

    + +
    +
    +
    +
    +

    $PlayerCount of $MaximumPlayers players max

    + + +
    +
    +
    +
    +
    +
    +
    + comments) { ?> +
    +
    +
    + +
    + +
    +
    +
    Write a comment!
    +
    + +
    +
    + Please wait 60 seconds before posting another comment + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + Nobody has posted any comments for this item +
    +
    +

    Come and share your thoughts about it!

    +
    +
    +
    +
    +
    +
    + +
    +
    + Show More +
    +
    +
    +
    + +
    +
    +
    +
    + Posted $time by $commenter_name +
    +
    +

    $content

    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +buildFooter(); ?> diff --git a/rbxclient/analytics/gameperfmonitor.php b/rbxclient/analytics/gameperfmonitor.php new file mode 100644 index 0000000..fb24642 --- /dev/null +++ b/rbxclient/analytics/gameperfmonitor.php @@ -0,0 +1,215 @@ + + +local jobid, placeid, userId, immediate = ... + +function enableStats() + game:SetJobsExtendedStatsWindow(30); +end + +function disableStats() + game:SetJobsExtendedStatsWindow(0); +end + +function collectStats() + + local measures = {} + + local appendstring = function(name, value) + local row = "string\t" .. name .. "\t" .. value; + measures[#measures+1] = row; + end + + local appenddouble = function(name, value) + local row = "double\t" .. name .. "\t" .. value; + measures[#measures+1] = row; + end + + local extendedstats = game:GetJobsExtendedStats(); + -- some known column headers. + local NAME = 1 + local columnnames = extendedstats[1]; + + -- setup index of running jobs. + -- fist row is header. start at 2. + local taskmap = {}; + for i = 2, #(extendedstats) do + taskmap[extendedstats[i][NAME]] = i; + end + + local appendtask = function(taskname) + if taskmap[taskname] then + local taskdata = extendedstats[taskmap[taskname]]; + + for col = 2, #taskdata do + appenddouble(taskname .. "." .. columnnames[col], taskdata[col]) + end + end + end + + appendtask("Physics") + appendtask("Render") + appendtask("Heartbeat") + + local player = game:GetService("Players").LocalPlayer; + if player then + appenddouble("UserId", game:GetService("Players").LocalPlayer.userId); + end + + if jobid then + appendstring("JobId", jobid) + end + + if placeid then + appenddouble("PlaceId", placeid) + end + + appenddouble("DistributedGameTime", game.Workspace.DistributedGameTime); + + if taskmap["Render"] then + appenddouble("Render.interval.peakAbove40ms", game:GetJobIntervalPeakFraction("Render", 0.040)); + appenddouble("Render.time.peakAbove50ms", game:GetJobTimePeakFraction("Render", 0.050)); + end + if taskmap["Physics"] then + appenddouble("Physics.interval.peakAbove40ms", game:GetJobIntervalPeakFraction("Physics", 0.040)); + appenddouble("Physics.time.peakAbove50ms", game:GetJobTimePeakFraction("Physics", 0.050)); + end + + if stats():FindFirstChild("Network") then + for k, child in pairs(stats().Network:GetChildren()) do + local rpackets = child:FindFirstChild("Received Physics Packets"); + if rpackets then + appenddouble("Network." .. tostring(child) .. ".ReceivedPhysicsPackets", rpackets:GetValue()) + end + end + end + + if stats():FindFirstChild("Workspace") then + appenddouble("Workspace.FPS", stats().Workspace.FPS:GetValue()) + appenddouble("Workspace.World.Primitives", stats().Workspace.World.Primitives:GetValue()) + end + + appenddouble("ElapsedTime", settings().Diagnostics.ElapsedTime); + appenddouble("InstanceCount", settings().Diagnostics.InstanceCount); + appenddouble("JobCount", settings().Diagnostics.JobCount); + appenddouble("PrivateBytes", settings().Diagnostics.PrivateBytes); + appenddouble("ProcessCores", settings().Diagnostics.ProcessCores); + appendstring("RobloxVersion", settings().Diagnostics.RobloxVersion); + + -- these can be gleaned from the machine config as well, but they are put here for convenience. + appenddouble("RAM", settings().Diagnostics.RAM); + appendstring("CPU", settings().Diagnostics.CPU); + appenddouble("CpuCount", settings().Diagnostics.CpuCount); + + -- TODO: remove pcall and OsPlatformId once OsPlatform is deployed everywhere + pcall(function() appendstring("OsPlatform", settings().Diagnostics.OsPlatform) end) + appenddouble("OsPlatformId", settings().Diagnostics.OsPlatformId) + + appenddouble("PlayerCount", game.Players.NumPlayers); + + return measures +end + +function postStats(t) + local poststring = table.concat(t, "\n") + local player = game:GetService("Players").LocalPlayer; + local id = 0; + local idtype = "None"; + if player then + idtype = "PlayerId" + id = game:GetService("Players").LocalPlayer.userId; + end + if placeid then + idtype = "PlaceId" + id = placeid + end + + local url = "http:///Analytics/Measurement.ashx?Type=Game.Performance&IPFilter=primary&SecondaryFilterName=" .. idtype .. "&SecondaryFilterValue=" .. tostring(id); + --print(url) + --print(poststring) + game:HttpPost(url, poststring, false) +end + + +enableStats(); + +-- lua doesn't have built in xor! +-- use this multiplication-based munger instead. +function munge_words(a, b) + return ((a * b) / 0x100) % 0x10000; +end + +function collectAndPostStatsMaybe() + local shouldPostProb = 1.0/64.0; + local shouldPost = (math.random() < shouldPostProb) + + if jobid then + -- generate a "random" number from the guid. + local s = {string.match(jobid, "(%x%x%x%x)(%x%x%x%x)-(%x%x%x%x)-(%x%x%x%x)-(%x%x%x%x)-(%x%x%x%x)(%x%x%x%x)(%x%x%x%x)")}; + local w = 0x0100; -- this is the identity value for munge_words + for i = 1, #s do + w = munge_words(w, tonumber(s[i], 16)) + end + shouldPost = w < 0x10000 * shouldPostProb; + end + + if shouldPost then + postStats(collectStats()) + end +end + +if immediate then + postStats(collectStats()) +else + delay(1 * 60, function() + while true do + collectAndPostStatsMaybe() + wait(10 * 60) + end + end) +end + + +function postFrmStats(t, id) + local poststring = table.concat(t, "\n") + local idtype = "PlayerId" + + local url = "http:///Analytics/Measurement.ashx?Type=Game.Performance.FrameRateManager&IPFilter=primary&SecondaryFilterName=" .. idtype .. "&SecondaryFilterValue=" .. tostring(id); + --print(url) + --print(poststring) + game:HttpPost(url, poststring, false) +end + +-- Some sampling condition based on userId +if (userId % 100) == 0 then + local frm = stats():FindFirstChild("FrameRateManager") + local closeConnection = game.Close:connect(function() + pcall(function() + local measures = {} + + local appenddouble = function(name, value) + local row = "double\t" .. name .. "\t" .. value; + measures[#measures+1] = row; + end + + if frm then + frm:GetValue() + for k, child in pairs(frm:GetChildren()) do + appenddouble(child.Name, child:GetValue()) + end + end + postFrmStats(measures, userId) + postStats(collectStats()) + end) + end) +end + + + +errorCode(404); +$bodycolors = json_decode($info->bodycolors); + +header("content-type: application/xml"); +header("Pragma: no-cache"); +header("Cache-Control: no-cache"); +?> + + + null + nil + + + Head?> + {'Left Arm'}?> + {'Left Leg'}?> + Body Colors + {'Right Arm'}?> + {'Right Leg'}?> + Torso?> + true + + + \ No newline at end of file diff --git a/rbxclient/asset/characterfetch.php b/rbxclient/asset/characterfetch.php new file mode 100644 index 0000000..a398893 --- /dev/null +++ b/rbxclient/asset/characterfetch.php @@ -0,0 +1,79 @@ +errorCode(404); + +header("Pragma: no-cache"); +header("Cache-Control: no-cache"); +header("content-type: text/plain; charset=utf-8"); + +$charapp = "http://{$_SERVER['HTTP_HOST']}/Asset/BodyColors.ashx?userId={$UserID}"; + +// to start off, get everything the user's wearing +$querystring = +"SELECT * FROM ownedAssets +INNER JOIN assets ON assets.id = assetId +WHERE userId = :UserID AND wearing"; + +if ($ServerID == -1) //thumbnail server - only get the last gear the user equipped +{ + // get everything they're wearing except their gears + $querystring .= " AND type != 19"; + + // get the last gear the user equipped + $LastGearID = Database::singleton()->run( + "SELECT assetId FROM ownedAssets + INNER JOIN assets ON assets.id = assetId + WHERE userId = :UserID AND wearing AND assets.type = 19 + ORDER BY last_toggle DESC LIMIT 1", + [":UserID" => $UserID] + )->fetchColumn(); + + // add the last gear to their character appearance + $charapp .= ";http://{$_SERVER['HTTP_HOST']}/Asset/?id={$LastGearID}"; +} +else if ($ServerID || $PlaceID) +{ + // get the server's allowed gears + if ($ServerID) + { + $gears = Database::singleton()->run("SELECT allowed_gears FROM selfhosted_servers WHERE id = :ServerID", [":ServerID" => $ServerID])->fetchColumn(); + } + else + { + $gears = Database::singleton()->run("SELECT gear_attributes FROM assets WHERE id = :PlaceID", [":PlaceID" => $PlaceID])->fetchColumn(); + } + + // get everything they're wearing, and the allowed gears + if($gears) + { + $gears = json_decode($gears, true); + $querystring .= " AND (gear_attributes IS NULL"; + + foreach($gears as $GearAttribute => $GearEnabled) + { + if(!$GearEnabled) continue; + $querystring .= " OR JSON_EXTRACT(gear_attributes, \"$.{$GearAttribute}\")"; + } + + $querystring .= ")"; + } +} + +// and get them all +$assets = Database::singleton()->run($querystring, [":UserID" => $UserID]); +while ($asset = $assets->fetch(\PDO::FETCH_OBJ)) +{ + $charapp .= ";http://{$_SERVER['HTTP_HOST']}/Asset/?id={$asset->assetId}"; +} + +echo $charapp; + +// echo Users::GetCharacterAppearance($uid, $sid, $host); \ No newline at end of file diff --git a/rbxclient/asset/fetch.php b/rbxclient/asset/fetch.php new file mode 100644 index 0000000..6f3e8f6 --- /dev/null +++ b/rbxclient/asset/fetch.php @@ -0,0 +1,244 @@ + 2599, // stamper rock + 60051616 => 2600, // stamper funk + 60049010 => 2601, // stamper electronic + + // all these audios are archived roblox audios + // following the audio privatization update that happened on march 23 2022 + + 1034065 => 9043, + 1077604 => 9044, + 1280414 => 9045, + 1280463 => 9046, + 1280470 => 9047, + 1280473 => 9048, + 1372257 => 9049, + 1372259 => 9050, + 1372261 => 9051, + 1372262 => 9052, + 2676305 => 9053, + 2692801 => 9054, + 3086666 => 9055, + 5982975 => 9056, + 5985787 => 9057, + 5986151 => 9058, + 9413294 => 9059, + 9650822 => 9060, + 11420922 => 9061, + 11420933 => 9062, + 25641508 => 9063, + 25641879 => 9064, + 25874790 => 9065, + 27697267 => 9066, + 27697277 => 9067, + 27697707 => 9068, + 27697713 => 9069, + 27697719 => 9070, + 27697735 => 9071, + 27697743 => 9072, + 133346243 => 9073, + 142305036 => 9074, + 142305279 => 9075, + 142373086 => 9076, + 142521090 => 9077, + 142835753 => 9078, + 157578373 => 9079, + 157668328 => 9080, + 157668360 => 9081, + 157682042 => 9082, + 158285293 => 9083, + 158285396 => 9084, + 158430058 => 9085, + 162950163 => 9086, + 167882400 => 9087, + 173607522 => 9088, + 173607557 => 9089, + 173607581 => 9090, + 173607633 => 9091, + 173607688 => 9092, + 179260188 => 9093, + 188285624 => 9094, + 189636267 => 9095, + 189636326 => 9096, + 272471086 => 9097, + 274690929 => 9098, + 285118686 => 9099, + 350358126 => 9100, + 380180953 => 9101, + 525617971 => 9102, + 555674149 => 9103, + 627901899 => 9104, + 743848052 => 9105, + 785043144 => 9106, + 901513885 => 9107, + 1234948361 => 9108, + 1293213302 => 9109, + 1438888974 => 9110, + 2049081250 => 9111, + 3026949263 => 9112, + 4770256529 => 9113, + 4784439779 => 9114, + 5421717438 => 9115, + 5476362039 => 9116, + 6165493032 => 9117, + 6271868642 => 9118, + 6338746851 => 9119, + 6486640462 => 9120, + 6490234609 => 9121, + 7040099874 => 9122, + 7303836083 => 9123, + 8438332663 => 9124, + 8474936251 => 9125, + + 511287639 => 9137, + 198833724 => 9138, + 434620676 => 9139, + 1326247206 => 9141, + 2214369247 => 9142, + 1326247206 => 9145, + 142376088 => 9146, + + 5124272528 => 9174, + 646618754 => 9183, + 4634865303 => 9184, + 9057221828 => 9185, + 6293684320 => 9186 +]; + +$AssetID = $_GET['ID'] ?? $_GET['id'] ?? false; +$AssetHost = $_SERVER['HTTP_HOST']; + +$RobloxAsset = false; + +$AssetID = $SwapIDs[$AssetID] ?? $AssetID; +$Asset = Database::singleton()->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 +{ + $AssetLocation = Polygon::GetSharedResource("assets/{$Asset->id}"); + + if (!file_exists($AssetLocation)) die(http_response_code(404)); + + // we shouldn't authorize gameservers based on ip as that will allow any asset request to go through + // which is an issue as someone could load an offsale model ingame, save the place and have the model + // this might pose an issue for the insert tool(?) as players may not be able to insert models they own + // idk how it dealed with asset ownership back then but for now this will do + + if (isset($_GET["force"]) || Polygon::IsThumbnailServerIP() || Polygon::IsGameserverAuthorized()) + { + if ($Asset->approved == 2) die(http_response_code(403)); + } + else if (SESSION && SESSION["user"]["id"] != 1) + { + if ($Asset->approved != 1) die(http_response_code(403)); + if ($Asset->Access == "Friends" && !Games::CanPlayGame($Asset)) die(http_response_code(403)); + if (!$Asset->publicDomain && (!SESSION || !Catalog::OwnsAsset(SESSION["user"]["id"], $Asset->id))) die(http_response_code(403)); + } + + $AssetData = file_get_contents($AssetLocation); + + if($Asset->type == 9 || $Asset->type == 10 && (!stripos($AssetData, 'roblox'))) + { + $StartTime = time(); + $AssetData = Gzip::Decompress($AssetLocation); + $EndTime = time(); + header("Decompression-Time: " . ($EndTime - $StartTime)); + } + + if (SESSION && SESSION["user"]["id"] != $Asset->creator && ($Asset->type == 9 || $Asset->type == 10)) + { + Users::LogStaffAction("[ Asset Download ] Downloaded \"{$Asset->name}\" [" . Catalog::GetTypeByNum($Asset->type) . " ID {$Asset->id}]"); + } +} + +// replace asset urls +$AssetData = str_replace("%ASSETURL%", "http://{$AssetHost}/asset/?id=", $AssetData); +$AssetData = str_replace("%ROBLOXASSETURL%", "https://assetdelivery.roblox.com/v1/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", "video/ogg" => ".ogg", "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: application/octet-stream'); + if($RobloxAsset) header('Content-Disposition: attachment; filename="'.sha1($AssetData).'"'); + else header('Content-Disposition: attachment; filename="'.sha1_file($AssetLocation).'"'); +} + +if(!$RobloxAsset && $Asset->type == 5) $AssetData = RBXClient::CryptSignScript($AssetData, $Asset->id); + +header('Content-Length: '.strlen($AssetData)); +die($AssetData); \ No newline at end of file diff --git a/rbxclient/asset/getscriptstate.html b/rbxclient/asset/getscriptstate.html new file mode 100644 index 0000000..7abb2ca --- /dev/null +++ b/rbxclient/asset/getscriptstate.html @@ -0,0 +1 @@ +0 0 0 0 \ No newline at end of file diff --git a/rbxclient/error/dump.php b/rbxclient/error/dump.php new file mode 100644 index 0000000..33fd3d5 --- /dev/null +++ b/rbxclient/error/dump.php @@ -0,0 +1,5 @@ +run("SELECT * FROM friends WHERE :uid IN (requesterId, receiverId) AND status = 1", [":uid" => $userId]); +while($row = $query->fetch(\PDO::FETCH_OBJ)) +{ + $friendId = $row->requesterId == $userId ? $row->receiverId : $row->requesterId; + $response .= $friendId.","; +} +echo $response; \ No newline at end of file diff --git a/rbxclient/friend/breakfriend.php b/rbxclient/friend/breakfriend.php new file mode 100644 index 0000000..16eab50 --- /dev/null +++ b/rbxclient/friend/breakfriend.php @@ -0,0 +1,27 @@ +errorCode(404); + +header("Pragma: no-cache"); +header("Cache-Control: no-cache"); +header("content-type: text/plain; charset=utf-8"); + +$RequesterID = API::GetParameter("GET", "firstUserId", "int"); +$ReceiverID = API::GetParameter("GET", "secondUserId", "int"); +$JobID = API::GetParameter("GET", "jobId", "string"); + +if ($RequesterID == $ReceiverID) throw new Exception("firstUserId {$RequesterID} and secondUserId {$ReceiverID} are the same"); +if (!Games::CheckIfPlayerInGame($RequesterID, $JobID)) throw new Exception("firstUserId {$RequesterID} is not in the game"); +if (!Games::CheckIfPlayerInGame($ReceiverID, $JobID)) throw new Exception("secondUserId {$ReceiverID} is not in the game"); + +Database::singleton()->run( + "UPDATE friends SET status = 2 WHERE :RequesterID IN (requesterId, receiverId) AND :ReceiverID IN (requesterId, receiverId)", + [":RequesterID" => $RequesterID, ":ReceiverID" => $ReceiverID] +); + +die("OK"); \ No newline at end of file diff --git a/rbxclient/friend/createfriend.php b/rbxclient/friend/createfriend.php new file mode 100644 index 0000000..73e2f97 --- /dev/null +++ b/rbxclient/friend/createfriend.php @@ -0,0 +1,43 @@ +errorCode(404); + +header("Pragma: no-cache"); +header("Cache-Control: no-cache"); +header("content-type: text/plain; charset=utf-8"); + +$RequesterID = API::GetParameter("GET", "firstUserId", "int"); +$ReceiverID = API::GetParameter("GET", "secondUserId", "int"); +$JobID = API::GetParameter("GET", "jobId", "string"); + +if ($RequesterID == $ReceiverID) throw new Exception("firstUserId {$RequesterID} and secondUserId {$ReceiverID} are the same"); +if (!Games::CheckIfPlayerInGame($RequesterID, $JobID)) throw new Exception("firstUserId {$RequesterID} is not in the game"); +if (!Games::CheckIfPlayerInGame($ReceiverID, $JobID)) throw new Exception("secondUserId {$ReceiverID} is not in the game"); + +$FriendConnection = Database::singleton()->run( + "SELECT * FROM friends WHERE :RequesterID IN (requesterId, receiverId) AND :ReceiverID IN (requesterId, receiverId) AND NOT status = 2", + [":RequesterID" => $RequesterID, ":ReceiverID" => $ReceiverID] +)->fetch(\PDO::FETCH_OBJ); + +if (!$FriendConnection) // friend connection does not exist +{ + Database::singleton()->run( + "INSERT INTO friends (requesterId, receiverId, timeSent, status) VALUES (:RequesterID, :ReceiverID, UNIX_TIMESTAMP(), 1)", + [":RequesterID" => $RequesterID, ":ReceiverID" => $ReceiverID] + ); +} +else if ($FriendConnection->status == 0) // friend request pending +{ + Database::singleton()->run("UPDATE friends SET status = 1 WHERE id = :FriendID", [":FriendID" => $FriendConnection->id]); +} +else if ($FriendConnection->Status == 1) // friend connection already exists +{ + throw new Exception("friend connection already exists between firstUserId {$RequesterID} and secondUserId {$ReceiverID}"); +} + +die("OK"); \ No newline at end of file diff --git a/rbxclient/game/clientpresence.php b/rbxclient/game/clientpresence.php new file mode 100644 index 0000000..a05e9be --- /dev/null +++ b/rbxclient/game/clientpresence.php @@ -0,0 +1,63 @@ +run("UPDATE GameJobSessions SET Active = 1 WHERE SecurityTicket = :Ticket", [":Ticket" => $Ticket]); + Database::singleton()->run("UPDATE GameJobs SET PlayerCount = PlayerCount + 1 WHERE JobID = :JobID", [":JobID" => $TicketInfo->JobID]); + Database::singleton()->run("UPDATE assets SET ActivePlayers = ActivePlayers + 1 WHERE id = :PlaceID", [":PlaceID" => $TicketInfo->PlaceID]); + } + else if ($Action == "disconnect") + { + Database::singleton()->run("UPDATE GameJobSessions SET Active = 0 WHERE SecurityTicket = :Ticket", [":Ticket" => $Ticket]); + Database::singleton()->run("UPDATE GameJobs SET PlayerCount = PlayerCount - 1 WHERE JobID = :JobID", [":JobID" => $TicketInfo->JobID]); + Database::singleton()->run("UPDATE assets SET ActivePlayers = ActivePlayers - 1 WHERE id = :PlaceID", [":PlaceID" => $TicketInfo->PlaceID]); + Database::singleton()->run("UPDATE users SET ClientPresencePing = UNIX_TIMESTAMP() - 65 WHERE id = :UserID", [":UserID" => $TicketInfo->UserID]); + } +} +else if (SESSION) +{ + $PlaceID = $_GET["PlaceID"] ?? 0; + $LocationType = $_GET["LocationType"] ?? "Visit"; + $PlaceInfo = Catalog::GetAssetInfo($PlaceID); + + if (!$PlaceInfo) die("OK"); + + // check if they have ownership of the place + if ($LocationType == "Studio" && !$PlaceInfo->publicDomain && $PlaceInfo->creator != SESSION["user"]["id"]) + { + die("OK"); + } + + Database::singleton()->run( + "UPDATE users + SET ClientPresenceLocation = :Location, + ClientPresenceType = :LocationType, + ClientPresencePing = UNIX_TIMESTAMP() + WHERE id = :UserID", + [":Location" => (int)$PlaceID, ":LocationType" => $LocationType, ":UserID" => SESSION["user"]["id"]] + ); + + Users::UpdatePing(); +} + +die("OK"); \ No newline at end of file diff --git a/rbxclient/game/edit.php b/rbxclient/game/edit.php new file mode 100644 index 0000000..4998a00 --- /dev/null +++ b/rbxclient/game/edit.php @@ -0,0 +1,74 @@ + 0, + "AssetURL" => "", + "PingURL" => "", + "UploadURL" => "" +]; + +if(isset($_GET["PlaceID"]) && is_numeric($_GET["PlaceID"])) +{ + $Params->PlaceID = $_GET["PlaceID"]; + $Params->AssetURL = "http://{$_SERVER['HTTP_HOST']}/Asset/?id={$Params->PlaceID}"; + $Params->PingURL = "http://{$_SERVER['HTTP_HOST']}/Game/ClientPresence.ashx?PlaceID={$Params->PlaceID}&LocationType=Studio"; +} + +ob_start(); +?> +-- Prepended to Edit.lua and Visit.lua and Studio.lua and PlaySolo.lua-- + +if true then + pcall(function() game:SetPlaceID(PlaceID?>) end) +else + if PlaceID?>>0 then + pcall(function() game:SetPlaceID(PlaceID?>) end) + end +end + +visit = game:GetService("Visit") + +local message = Instance.new("Message") +message.Parent = workspace +message.archivable = false + +game:GetService("ScriptInformationProvider"):SetAssetUrl("http:///Asset/") +game:GetService("ContentProvider"):SetThreadPool(16) +pcall(function() game:GetService("InsertService"):SetFreeModelUrl("http:///Game/Tools/InsertAsset.ashx?type=fm&q=%s&pg=%d&rs=%d") end) -- Used for free model search (insert tool) +pcall(function() game:GetService("InsertService"):SetFreeDecalUrl("http:///Game/Tools/InsertAsset.ashx?type=fd&q=%s&pg=%d&rs=%d") end) -- Used for free decal search (insert tool) + +settings().Diagnostics:LegacyScriptMode() + +game:GetService("InsertService"):SetBaseSetsUrl("http:///Game/Tools/InsertAsset.ashx?nsets=10&type=base") +game:GetService("InsertService"):SetUserSetsUrl("http:///Game/Tools/InsertAsset.ashx?nsets=20&type=user&userid=%d") +game:GetService("InsertService"):SetCollectionUrl("http:///Game/Tools/InsertAsset.ashx?sid=%d") +game:GetService("InsertService"):SetAssetUrl("http:///Asset/?id=%d") +game:GetService("InsertService"):SetAssetVersionUrl("http:///Asset/?assetversionid=%d") + +pcall(function() game:GetService("SocialService"):SetFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetBestFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsBestFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsInGroup&playerid=%d&groupid=%d") end) +pcall(function() game:SetCreatorID(0, Enum.CreatorType.User) end) + +pcall(function() game:SetScreenshotInfo("") end) +pcall(function() game:SetVideoInfo("") end) + +message.Text = "Loading Place. Please wait..." +coroutine.yield() +game:Load("AssetURL?>") + +if #"" > 0 then + visit:SetUploadUrl("UploadURL?>") +end + +message.Parent = nil + +game:GetService("ChangeHistoryService"):SetEnabled(true) + +visit:SetPing("PingURL?>", 60) + +local placeId, port, sleeptime, access, url, killID, deathID, timeout, autosaveInterval, locationID, groupBuild, machineAddress, gsmInterval, gsmUrl, maxPlayers, maxSlotsUpperLimit, maxSlotsLowerLimit, gsmAccess, injectScriptAssetID, servicesUrl, permissionsServiceUrl, apiKey, libraryRegistrationScriptAssetID = ... + + +-- StartGame -- +pcall(function() game:GetService("ScriptContext"):AddStarterScript(injectScriptAssetID) end) +game:GetService("RunService"):Run() + + + +-- REQUIRES: StartGanmeSharedArgs.txt +-- REQUIRES: MonitorGameStatus.txt + +------------------- UTILITY FUNCTIONS -------------------------- + +function waitForChild(parent, childName) + while true do + local child = parent:findFirstChild(childName) + if child then + return child + end + parent.ChildAdded:wait() + end +end + +-- returns the player object that killed this humanoid +-- returns nil if the killer is no longer in the game +function getKillerOfHumanoidIfStillInGame(humanoid) + + -- check for kill tag on humanoid - may be more than one - todo: deal with this + local tag = humanoid:findFirstChild("creator") + + -- find player with name on tag + if tag then + local killer = tag.Value + if killer.Parent then -- killer still in game + return killer + end + end + + return nil +end + +-- send kill and death stats when a player dies +function onDied(victim, humanoid) + local killer = getKillerOfHumanoidIfStillInGame(humanoid) + local victorId = 0 + if killer then + victorId = killer.userId + print("STAT: kill by " .. victorId .. " of " .. victim.userId) + game:HttpGet(url .. "/Game/Knockouts.ashx?UserID=" .. victorId .. "&" .. access) + end + print("STAT: death of " .. victim.userId .. " by " .. victorId) + game:HttpGet(url .. "/Game/Wipeouts.ashx?UserID=" .. victim.userId .. "&" .. access) +end + +-- This code might move to C++ +function characterRessurection(player) + if player.Character then + local humanoid = player.Character.Humanoid + humanoid.Died:connect(function() wait(5) player:LoadCharacter() end) + end +end + +-----------------------------------END UTILITY FUNCTIONS ------------------------- + +-----------------------------------"CUSTOM" SHARED CODE---------------------------------- + +pcall(function() settings().Network.UseInstancePacketCache = true end) +pcall(function() settings().Network.UsePhysicsPacketCache = true end) +--pcall(function() settings()["Task Scheduler"].PriorityMethod = Enum.PriorityMethod.FIFO end) +pcall(function() settings()["Task Scheduler"].PriorityMethod = Enum.PriorityMethod.AccumulatedError end) + +--settings().Network.PhysicsSend = 1 -- 1==RoundRobin +pcall(function() settings().Network.PhysicsSend = Enum.PhysicsSendMethod.ErrorComputation2 end) +settings().Network.ExperimentalPhysicsEnabled = true +settings().Network.WaitingForCharacterLogRate = 100 +pcall(function() settings().Diagnostics:LegacyScriptMode() end) + +-----------------------------------START GAME SHARED SCRIPT------------------------------ + +local assetId = placeId -- might be able to remove this now + +local scriptContext = game:GetService('ScriptContext') +pcall(function() scriptContext:AddStarterScript(libraryRegistrationScriptAssetID) end) +scriptContext.ScriptsDisabled = true + +pcall(function() game:SetPlaceID(assetId, false) end) +game:GetService("ChangeHistoryService"):SetEnabled(false) + +-- establish this peer as the Server +local ns = game:GetService("NetworkServer") + +if url~=nil then + pcall(function() game:GetService("Players"):SetAbuseReportUrl(url .. "/AbuseReport/InGameChatHandler.ashx") end) + pcall(function() game:GetService("ScriptInformationProvider"):SetAssetUrl(url .. "/Asset/") end) + pcall(function() game:GetService("ContentProvider"):SetBaseUrl(url .. "/") end) + pcall(function() game:GetService("Players"):SetChatFilterUrl(url .. "/Game/ChatFilter.ashx") end) + + game:GetService("BadgeService"):SetPlaceId(placeId) + if access~=nil then + game:GetService("BadgeService"):SetAwardBadgeUrl(url .. "/Game/Badge/AwardBadge.ashx?UserID=%d&BadgeID=%d&PlaceID=%d&" .. access) + game:GetService("BadgeService"):SetHasBadgeUrl(url .. "/Game/Badge/HasBadge.ashx?UserID=%d&BadgeID=%d&" .. access) + game:GetService("BadgeService"):SetIsBadgeDisabledUrl(url .. "/Game/Badge/IsBadgeDisabled.ashx?BadgeID=%d&PlaceID=%d&" .. access) + + game:GetService("FriendService"):SetMakeFriendUrl(servicesUrl .. "/Friend/CreateFriend?firstUserId=%d&secondUserId=%d&" .. access) + game:GetService("FriendService"):SetBreakFriendUrl(servicesUrl .. "/Friend/BreakFriend?firstUserId=%d&secondUserId=%d&" .. access) + game:GetService("FriendService"):SetGetFriendsUrl(servicesUrl .. "/Friend/AreFriends?userId=%d&" .. access) + end + game:GetService("BadgeService"):SetIsBadgeLegalUrl("") + game:GetService("InsertService"):SetBaseSetsUrl(url .. "/Game/Tools/InsertAsset.ashx?nsets=10&type=base") + game:GetService("InsertService"):SetUserSetsUrl(url .. "/Game/Tools/InsertAsset.ashx?nsets=20&type=user&userid=%d") + game:GetService("InsertService"):SetCollectionUrl(url .. "/Game/Tools/InsertAsset.ashx?sid=%d") + game:GetService("InsertService"):SetAssetUrl(url .. "/Asset/?id=%d") + game:GetService("InsertService"):SetAssetVersionUrl(url .. "/Asset/?assetversionid=%d") + + pcall(function() loadfile(url .. "/Game/LoadPlaceInfo.ashx?PlaceId=" .. placeId)() end) + + pcall(function() + if access then + loadfile(url .. "/Game/PlaceSpecificScript.ashx?PlaceId=" .. placeId .. "&" .. access)() + end + end) +end + +pcall(function() game:GetService("NetworkServer"):SetIsPlayerAuthenticationRequired(false) end) +settings().Diagnostics.LuaRamLimit = 0 +--settings().Network:SetThroughputSensitivity(0.08, 0.01) +--settings().Network.SendRate = 35 +--settings().Network.PhysicsSend = 0 -- 1==RoundRobin + +--shared["__time"] = 0 +--game:GetService("RunService").Stepped:connect(function (time) shared["__time"] = time end) + + + + +if placeId~=nil and killID~=nil and deathID~=nil and url~=nil then + -- listen for the death of a Player + function createDeathMonitor(player) + -- we don't need to clean up old monitors or connections since the Character will be destroyed soon + if player.Character then + local humanoid = waitForChild(player.Character, "Humanoid") + humanoid.Died:connect( + function () + onDied(player, humanoid) + end + ) + end + end + + -- listen to all Players' Characters + game:GetService("Players").ChildAdded:connect( + function (player) + createDeathMonitor(player) + player.Changed:connect( + function (property) + if property=="Character" then + createDeathMonitor(player) + end + end + ) + end + ) +end + +game:GetService("Players").PlayerAdded:connect(function(player) + print("Player " .. player.userId .. " added") + + characterRessurection(player) + player.Changed:connect(function(name) + if name=="Character" then + characterRessurection(player) + end + end) + + if url and access and placeId and player and player.userId then + game:HttpGet(url .. "/Game/ClientPresence.ashx?action=connect&" .. access .. "&PlaceID=" .. placeId .. "&UserID=" .. player.userId) + game:HttpGet(url .. "/Game/PlaceVisit.ashx?UserID=" .. player.userId .. "&AssociatedPlaceID=" .. placeId .. "&" .. access) + end +end) + + +game:GetService("Players").PlayerRemoving:connect(function(player) + print("Player " .. player.userId .. " leaving") + + if url and access and placeId and player and player.userId then + game:HttpGet(url .. "/Game/ClientPresence.ashx?action=disconnect&" .. access .. "&PlaceID=" .. placeId .. "&UserID=" .. player.userId) + end +end) + +if placeId~=nil and url~=nil then + -- yield so that file load happens in the heartbeat thread + wait() + + -- load the game + game:Load(url .. "/asset/?id=" .. placeId) +end + +-- Now start the connection +ns:Start(port, sleeptime) + +if timeout then + scriptContext:SetTimeout(timeout) +end +scriptContext.ScriptsDisabled = false + +--delay(1, function() +-- loadfile(url .. "/analytics/GamePerfMonitor.ashx")(game.JobId, placeId) +--end) + +------------------------------END START GAME SHARED SCRIPT-------------------------- + + + + + + + + + + <?=SITE_CONFIG["site"]["name"]?> Help + + +
    + + +

    + Besides using simple blocks, you can insert Things that other people have built into your Place. Use the Insert... menu in the game to browse.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Action + Primary + Alternate
    + Move Character + Arrow keys + ASDW keys
    + Move Character (no tool selected) + Click location with green disk +
    + Jump + Space Bar +
    + Look up/down/left/right + Right-click and drag mouse +
    + Look side to side + Move mouse to the far right or far left + +
    + Zoom in/out and up/down + Mouse wheel + I (in) and O (out) keys
    + Change Tool / Toggle tool on off + Number keys 1, 2, 3, ... + Click on the tool
    + Drop Tool + Backspace key +
    + Drop Hat + Equal (=) key +
    + Regenerate dead or stuck character + Character regenerates automatically + Exit and then return to the Place
    + First Person Mode + Zoom all the way in +
    + + +
    + + +
    +
    + + diff --git a/rbxclient/game/join.php b/rbxclient/game/join.php new file mode 100644 index 0000000..b0c9305 --- /dev/null +++ b/rbxclient/game/join.php @@ -0,0 +1,367 @@ + "true", + + "JobID" => "", + "MachineAddress" => "localhost", + "ServerPort" => 53640, + + "PlaceID" => -1, + "CreatorID" => 0, + "IsTeleport" => "false", + + "Username" => "Player", + "UserID" => 0, + "ChatStyle" => "Classic", + "MembershipType" => "None", + "SafeChat" => "true", + "AccountAge" => 0, + "PolygonTicket" => "", + "ReplicatorTicket" => "", + "CharacterAppearance" => "http://{$_SERVER['HTTP_HOST']}/Asset/CharacterFetch.ashx?userId=2", + + "PingURL" => "" +]; + +if (!empty($JobTicket)) +{ + $JobSession = Database::singleton()->run( + "SELECT GameJobSessions.*, + GameJobs.Status, + GameJobs.Version, + GameJobs.PlaceID, + GameJobs.MachineAddress, + GameJobs.ServerPort, + users.username AS Username, + users.adminlevel AS AdminLevel, + assets.creator AS CreatorID, + assets.ChatType + FROM GameJobSessions + INNER JOIN GameJobs ON GameJobSessions.JobID = GameJobs.JobID + INNER JOIN users ON users.id = UserID + INNER JOIN assets ON assets.id = PlaceID + WHERE Ticket = :JobTicket AND GameJobs.Status = \"Ready\" AND NOT Verified AND GameJobSessions.TimeCreated + 300 > UNIX_TIMESTAMP()", + [":JobTicket" => $JobTicket] + )->fetch(\PDO::FETCH_OBJ); + + if ($JobSession !== false) + { + $Parameters->IsTestMode = "false"; + + $Parameters->JobID = $JobSession->JobID; + $Parameters->MachineAddress = $JobSession->MachineAddress; + $Parameters->ServerPort = $JobSession->ServerPort; + + $Parameters->PlaceID = $JobSession->PlaceID; + $Parameters->CreatorID = $JobSession->CreatorID; + $Parameters->IsTeleport = $JobSession->IsTeleport ? "true" : "false"; + + $Parameters->Username = $JobSession->Username; + $Parameters->UserID = $JobSession->UserID; + $Parameters->ChatStyle = $JobSession->ChatType == "Both" ? "ClassicAndBubble" : $JobSession->ChatType; + $Parameters->MembershipType = $JobSession->AdminLevel == 0 ? "None" : "OutrageousBuildersClub"; + $Parameters->SafeChat = "false"; + $Parameters->PolygonTicket = $JobSession->SecurityTicket; + $Parameters->CharacterAppearance = "http://{$_SERVER['HTTP_HOST']}/Asset/CharacterFetch.ashx?userId={$Parameters->UserID}&placeId={$Parameters->PlaceID}"; + + $Parameters->PingURL = "http://{$_SERVER['HTTP_HOST']}/Game/ClientPresence.ashx?PlaceID={$Parameters->PlaceID}"; + + if (is_null($JobSession->ReplicatorTicket)) + { + // 2010 tickets are formatted as '{date};{installRemotePlayerTicket}' + // 2011 and newer tickets are formatted as '{date};{installRemotePlayerTicket};{preauthenticateTicket}' + // so here we'll have to account for both + // if i can retrofit replicator tickets into 2012 then i might be able to retrofit preauthenticateTicket into 2010 + + $date = date('n/j/Y g:i:s A'); + + $installRemotePlayerTicket = RBXClient::CryptGetSignature(sprintf( + "%s\n%s\n%s\n%s\n%s", + $Parameters->UserID, + $Parameters->Username, + $Parameters->CharacterAppearance, + $Parameters->JobID, + $date + )); + + if ($JobSession->Version == 2010) + { + $Parameters->ReplicatorTicket = sprintf("%s;%s", $date, $installRemotePlayerTicket); + } + else + { + $preauthenticateTicket = RBXClient::CryptGetSignature(sprintf( + "%s\n%s\n%s", + $Parameters->UserID, + $Parameters->JobID, + $date + )); + + $Parameters->ReplicatorTicket = sprintf("%s;%s;%s", $date, $installRemotePlayerTicket, $preauthenticateTicket); + } + + Database::singleton()->run( + "UPDATE GameJobSessions SET ReplicatorTicket = :ReplicatorTicket WHERE Ticket = :JobTicket", + [":ReplicatorTicket" => $Parameters->ReplicatorTicket, ":JobTicket" => $JobTicket] + ); + } + else + { + $Parameters->ReplicatorTicket = $JobSession->ReplicatorTicket; + } + + // teleportservice cookie + setcookie("GameJobTicket", $JobTicket, time()+86400, "/"); + } +} +else if (SESSION) +{ + $Parameters->CharacterAppearance = "http://{$_SERVER['HTTP_HOST']}/Asset/CharacterFetch.ashx?userId=".SESSION['user']['id']; + if (SESSION["user"]["adminlevel"]) $Parameters->MembershipType = "OutrageousBuildersClub"; +} + +ob_start(); +?> + +-- functions -------------------------- +function onPlayerAdded(player) + -- override +end + +-- MultiplayerSharedScript.lua inserted here ------ Prepended to GroupBuild.lua and Join.lua -- +pcall(function() game:SetPlaceID(PlaceID?>, false) end) + +pcall(function() settings()["Game Options"].CollisionSoundEnabled = true end) +pcall(function() settings().Rendering.EnableFRM = true end) +pcall(function() settings().Physics.Is30FpsThrottleEnabled = true end) +pcall(function() settings()["Task Scheduler"].PriorityMethod = Enum.PriorityMethod.AccumulatedError end) + +-- arguments --------------------------------------- +local threadSleepTime = ... + +if threadSleepTime==nil then + threadSleepTime = 15 +end + +local test = IsTestMode?> + +print("! Joining game 'JobID?>' place PlaceID?> at MachineAddress?>") + +game:GetService("ChangeHistoryService"):SetEnabled(false) +game:GetService("ContentProvider"):SetThreadPool(16) +game:GetService("InsertService"):SetBaseSetsUrl("http:///Game/Tools/InsertAsset.ashx?nsets=10&type=base") +game:GetService("InsertService"):SetUserSetsUrl("http:///Game/Tools/InsertAsset.ashx?nsets=20&type=user&userid=%d") +game:GetService("InsertService"):SetCollectionUrl("http:///Game/Tools/InsertAsset.ashx?sid=%d") +game:GetService("InsertService"):SetAssetUrl("http:///Asset/?id=%d") +game:GetService("InsertService"):SetAssetVersionUrl("http:///Asset/?assetversionid=%d") + +pcall(function() game:GetService("SocialService"):SetFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetBestFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsBestFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsInGroup&playerid=%d&groupid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupRankUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=GetGroupRank&playerid=%d&groupid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupRoleUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=GetGroupRole&playerid=%d&groupid=%d") end) +pcall(function() game:SetCreatorID(CreatorID?>, Enum.CreatorType.User) end) + +-- Bubble chat. This is all-encapsulated to allow us to turn it off with a config setting +pcall(function() game:GetService("Players"):SetChatStyle(Enum.ChatStyle.ChatStyle?>) end) + +local waitingForCharacter = false +pcall( function() + if settings().Network.MtuOverride == 0 then + settings().Network.MtuOverride = 1400 + end +end) + + +-- globals ----------------------------------------- + +client = game:GetService("NetworkClient") +visit = game:GetService("Visit") + +-- functions --------------------------------------- +function setMessage(message) + -- todo: animated "..." + if not IsTeleport?> then + game:SetMessage(message) + else + -- hack, good enought for now + game:SetMessage("Teleporting ...") + end +end + +function showErrorWindow(message) + game:SetMessage(message) +end + +function reportError(err) + print("***ERROR*** " .. err) + if not test then visit:SetUploadUrl("") end + client:Disconnect() + wait(4) + showErrorWindow("Error: " .. err) +end + +-- called when the client connection closes +function onDisconnection(peer, lostConnection) + if lostConnection then + showErrorWindow("You have lost the connection to the game") + else + showErrorWindow("This game has shut down") + end +end + +function requestCharacter(replicator) + + -- prepare code for when the Character appears + local connection + connection = player.Changed:connect(function (property) + if property=="Character" then + game:ClearMessage() + waitingForCharacter = false + + connection:disconnect() + end + end) + + setMessage("Requesting character") + + local success, err = pcall(function() + replicator:RequestCharacter() + setMessage("Waiting for character") + waitingForCharacter = true + end) + + if not success then + reportError(err) + return + end +end + +-- called when the client connection is established +function onConnectionAccepted(url, replicator) + + local waitingForMarker = true + + local success, err = pcall(function() + if not test then + visit:SetPing("PingURL?>", 60) + end + + if not IsTeleport?> then + game:SetMessageBrickCount() + else + setMessage("Teleporting ...") + end + + replicator.Disconnection:connect(onDisconnection) + + -- Wait for a marker to return before creating the Player + local marker = replicator:SendMarker() + + marker.Received:connect(function() + waitingForMarker = false + requestCharacter(replicator) + end) + end) + + if not success then + reportError(err) + return + end + + -- TODO: report marker progress + + while waitingForMarker do + workspace:ZoomToExtents() + wait(0.5) + end +end + +-- called when the client connection fails +function onConnectionFailed(_, error) + showErrorWindow("Failed to connect to the Game. (ID=" .. error .. ")") +end + +-- called when the client connection is rejected +function onConnectionRejected() + connectionFailed:disconnect() + showErrorWindow("This game is not available. Please try another") +end + +idled = false +function onPlayerIdled(time) + if time > 20*60 then + showErrorWindow(string.format("You were disconnected for being idle %d minutes", time/60)) + client:Disconnect() + if not idled then + idled = true + end + end +end + + +-- main ------------------------------------------------------------ + +pcall(function() settings().Diagnostics:LegacyScriptMode() end) +local success, err = pcall(function() + + game:SetRemoteBuildMode(true) + + setMessage("Connecting to Server") + client.ConnectionAccepted:connect(onConnectionAccepted) + client.ConnectionRejected:connect(onConnectionRejected) + connectionFailed = client.ConnectionFailed:connect(onConnectionFailed) + client.Ticket = "ReplicatorTicket?>" + + playerConnectSucces, player = pcall(function() return client:PlayerConnect(UserID?>, "MachineAddress?>", ServerPort?>, 0, threadSleepTime) end) + if not playerConnectSucces then + --Old player connection scheme + player = game:GetService("Players"):CreateLocalPlayer(UserID?>) + client:Connect("MachineAddress?>", ServerPort?>, 0, threadSleepTime) + end + + IsTestMode == "false") { ?> + ticket = Instance.new("StringValue") + ticket.Name = "PolygonTicket" + ticket.Value = "PolygonTicket?>" + ticket.Parent = player + + + player:SetSuperSafeChat(SafeChat?>) + pcall(function() player:SetMembershipType(Enum.MembershipType.MembershipType?>) end) + pcall(function() player:SetAccountAge(AccountAge?>) end) + player.Idled:connect(onPlayerIdled) + + -- Overriden + onPlayerAdded(player) + + pcall(function() player.Name = [========[Username?>]========] end) + player.CharacterAppearance = "CharacterAppearance?>" + if not test then visit:SetUploadUrl("") end +end) + +if not success then + reportError(err) +end + +if not test then + -- TODO: Async get? + -- loadfile("")("", PlaceID?>, 0) +end + +pcall(function() game:SetScreenshotInfo("") end) +pcall(function() game:SetVideoInfo(']]>GamesProject Polygon, video, free game, online virtual world') end) +-- use single quotes here because the video info string may have unescaped double quotes + +errorCode(404); + +header("Pragma: no-cache"); +header("Cache-Control: no-cache"); +header("content-type: text/plain; charset=utf-8"); + +$UserID = API::GetParameter("GET", "UserID", "int"); +Database::singleton()->run("UPDATE users SET Knockouts = Knockouts + 1 WHERE id = :UserID", [":UserID" => $UserID]); \ No newline at end of file diff --git a/rbxclient/game/loadplaceinfo.php b/rbxclient/game/loadplaceinfo.php new file mode 100644 index 0000000..994c075 --- /dev/null +++ b/rbxclient/game/loadplaceinfo.php @@ -0,0 +1,31 @@ + 0, + "CreatorID" => 0 +]; + +if (isset($_GET["PlaceID"]) && is_numeric($_GET["PlaceID"])) +{ + $Params->PlaceID = $_GET["PlaceID"]; + + $CreatorID = Database::singleton()->run("SELECT creator FROM assets WHERE type = 9 AND id = :PlaceID", [":PlaceID" => $Params->PlaceID]); + if ($CreatorID->rowCount()) $Params->CreatorID = $CreatorID->fetchColumn(); +} +ob_start(); +?> +pcall(function() game:SetCreatorID(CreatorID?>, Enum.CreatorType.User) end) + +pcall(function() game:GetService("SocialService"):SetFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetBestFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsBestFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsInGroup&playerid=%d&groupid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupRankUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=GetGroupRank&playerid=%d&groupid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupRoleUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=GetGroupRole&playerid=%d&groupid=%d") end) +pcall(function() game:GetService("GamePassService"):SetPlayerHasPassUrl("http:///Game/GamePass/GamePassHandler.ashx?Action=HasPass&UserID=%d&PassID=%d") end) +run( + "SELECT COUNT(*) FROM friends WHERE :uid1 IN (requesterId, receiverId) AND :uid2 IN (requesterId, receiverId) AND status = 1", + [":uid1" => $userId, ":uid2" => $userId2] + )->rowCount(); + + if($IsFriends) die('true'); + else die('false'); +} +else if($method == "IsInGroup") +{ + $IsInGroup = Database::singleton()->run( + "SELECT * FROM groups_members WHERE GroupID = :GroupID AND UserID = :UserID", + [":GroupID" => $groupId, ":UserID" => $userId] + )->rowCount(); + + if($IsInGroup) die('true'); + else die('false'); +} +else if($method == "GetGroupRank") +{ + $GroupRank = Database::singleton()->run( + "SELECT Rank FROM groups_members WHERE GroupID = :GroupID AND UserID = :UserID", + [":GroupID" => $groupId, ":UserID" => $userId] + ); + + if($GroupRank->rowCount()) die('' . $GroupRank->fetchColumn() . ''); + else die('0'); +} +else if($method == "GetGroupRole") +{ + $GroupRole = Database::singleton()->run( + "SELECT groups_ranks.Name FROM groups_members + INNER JOIN groups_ranks ON groups_ranks.Rank = groups_members.Rank AND groups_ranks.GroupID = groups_members.GroupID + WHERE groups_members.GroupID = :GroupID AND groups_members.UserID = :UserID", + [":GroupID" => $groupId, ":UserID" => $userId] + ); + + if($GroupRole->rowCount()) die($GroupRole->fetchColumn()); + else die('Guest'); +} + +echo 'false'; \ No newline at end of file diff --git a/rbxclient/game/machineconfiguration.php b/rbxclient/game/machineconfiguration.php new file mode 100644 index 0000000..0a8f41c --- /dev/null +++ b/rbxclient/game/machineconfiguration.php @@ -0,0 +1,4 @@ +errorCode(404); + +header("Pragma: no-cache"); +header("Cache-Control: no-cache"); +header("content-type: text/plain; charset=utf-8"); + +$Ticket = API::GetParameter("GET", "Ticket", "string"); +$TicketInfo = Games::GetJobSession($Ticket); +if (!$TicketInfo) die("Ticket is not valid"); + +// increment place visits +Database::singleton()->run( + "UPDATE assets SET Visits = Visits + 1 WHERE id = (SELECT PlaceID FROM GameJobs WHERE JobID = :JobID)", + [":JobID" => $TicketInfo->JobID] +); + +// increment user place visits +Database::singleton()->run( + "UPDATE users SET PlaceVisits = PlaceVisits + 1 WHERE id = + ( + SELECT creator FROM assets WHERE id = + ( + SELECT PlaceID FROM GameJobs WHERE JobID = :JobID + ) + )", + [":JobID" => $TicketInfo->JobID] +); + +die("OK"); \ No newline at end of file diff --git a/rbxclient/game/studio.php b/rbxclient/game/studio.php new file mode 100644 index 0000000..75b6c9c --- /dev/null +++ b/rbxclient/game/studio.php @@ -0,0 +1,40 @@ + +-- Setup studio cmd bar & load core scripts + +pcall(function() game:GetService("InsertService"):SetFreeModelUrl("http:///Game/Tools/InsertAsset.ashx?type=fm&q=%s&pg=%d&rs=%d") end) +pcall(function() game:GetService("InsertService"):SetFreeDecalUrl("http:///Game/Tools/InsertAsset.ashx?type=fd&q=%s&pg=%d&rs=%d") end) + +game:GetService("ScriptInformationProvider"):SetAssetUrl("http:///Asset/") +game:GetService("InsertService"):SetBaseSetsUrl("http:///Game/Tools/InsertAsset.ashx?nsets=10&type=base") +game:GetService("InsertService"):SetUserSetsUrl("http:///Game/Tools/InsertAsset.ashx?nsets=20&type=user&userid=%d&t=2") +game:GetService("InsertService"):SetCollectionUrl("http:///Game/Tools/InsertAsset.ashx?sid=%d") +game:GetService("InsertService"):SetAssetUrl("http:///Asset/?id=%d") +game:GetService("InsertService"):SetAssetVersionUrl("http:///Asset/?assetversionid=%d") +game:GetService("InsertService"):SetTrustLevel(0) + +pcall(function() game:GetService("SocialService"):SetFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetBestFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsBestFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsInGroup&playerid=%d&groupid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupRankUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=GetGroupRank&playerid=%d&groupid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupRoleUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=GetGroupRole&playerid=%d&groupid=%d") end) + +local starterScriptID = -1 +if game.CoreGui.Version == 1 or game.CoreGui.Version == 2 then starterScriptID = 1036 --2011 +elseif game.CoreGui.Version == 7 then starterScriptID = 1083 end --2012 + +local result = pcall(function() game:GetService("ScriptContext"):AddStarterScript(starterScriptID) end) +if not result then + pcall(function() game:GetService("ScriptContext"):AddCoreScript(starterScriptID,game:GetService("ScriptContext"),"StarterScript") end) +end + +-- loadfile("http://chef.pizzaboxer.xyz/game/visit.ashx")() + 0, + "Username" => "Guest " . rand(1, 9999), + "UserID" => 0, + "Membership" => "None", + "Age" => 0, + "CharacterAppearance" => "http://{$_SERVER['HTTP_HOST']}/Asset/CharacterFetch.ashx?userId=2", + "AssetURL" => "", + "PingURL" => "", + "StatsURL" => "", + "UploadURL" => "" +]; + +if (SESSION) +{ + $Params->Username = SESSION["user"]["username"]; + $Params->UserID = SESSION["user"]["id"]; + $Params->CharacterAppearance = "http://{$_SERVER['HTTP_HOST']}/Asset/CharacterFetch.ashx?userId={$Params->UserID}"; + if(SESSION["user"]["adminlevel"]) $Params->Membership = "OutrageousBuildersClub"; +} + +if (isset($_GET["PlaceID"]) && is_numeric($_GET["PlaceID"])) +{ + $Params->PlaceID = $_GET["PlaceID"]; + $Params->AssetURL = "http://{$_SERVER['HTTP_HOST']}/asset/?id={$Params->PlaceID}"; + $Params->PingURL = "http://{$_SERVER['HTTP_HOST']}/Game/ClientPresence.ashx?version=old&PlaceID={$Params->PlaceID}"; + // $Params->StatsURL = "http://{$_SERVER['HTTP_HOST']}/Game/Statistics.ashx?TypeID=4&UserID={$Params->UserID}&AssociatedCreatorID=1&AssociatedCreatorType=User&AssociatedPlaceID={$Params->PlaceID}"; +} + +ob_start(); +?> +-- Prepended to Edit.lua and Visit.lua and Studio.lua and PlaySolo.lua-- + +if true then + pcall(function() game:SetPlaceID(PlaceID?>) end) +else + if PlaceID?>>0 then + pcall(function() game:SetPlaceID(PlaceID?>) end) + end +end + +visit = game:GetService("Visit") + +local message = Instance.new("Message") +message.Parent = workspace +message.archivable = false + +game:GetService("ScriptInformationProvider"):SetAssetUrl("http:///Asset/") +game:GetService("ContentProvider"):SetThreadPool(16) +pcall(function() game:GetService("InsertService"):SetFreeModelUrl("http:///Game/Tools/InsertAsset.ashx?type=fm&q=%s&pg=%d&rs=%d") end) -- Used for free model search (insert tool) +pcall(function() game:GetService("InsertService"):SetFreeDecalUrl("http:///Game/Tools/InsertAsset.ashx?type=fd&q=%s&pg=%d&rs=%d") end) -- Used for free decal search (insert tool) + +settings().Diagnostics:LegacyScriptMode() + +game:GetService("InsertService"):SetBaseSetsUrl("http:///Game/Tools/InsertAsset.ashx?nsets=10&type=base") +game:GetService("InsertService"):SetUserSetsUrl("http:///Game/Tools/InsertAsset.ashx?nsets=20&type=user&userid=%d") +game:GetService("InsertService"):SetCollectionUrl("http:///Game/Tools/InsertAsset.ashx?sid=%d") +game:GetService("InsertService"):SetAssetUrl("http:///Asset/?id=%d") +game:GetService("InsertService"):SetAssetVersionUrl("http:///Asset/?assetversionid=%d") + +pcall(function() game:GetService("SocialService"):SetFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetBestFriendUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsBestFriendsWith&playerid=%d&userid=%d") end) +pcall(function() game:GetService("SocialService"):SetGroupUrl("http:///Game/LuaWebService/HandleSocialRequest.ashx?method=IsInGroup&playerid=%d&groupid=%d") end) +pcall(function() game:SetCreatorID(0, Enum.CreatorType.User) end) + +pcall(function() game:SetScreenshotInfo("") end) +pcall(function() game:SetVideoInfo("") end) + +-- SingleplayerSharedScript.lua inserted here -- + +pcall(function() settings().Rendering.EnableFRM = true end) +pcall(function() settings()["Task Scheduler"].PriorityMethod = Enum.PriorityMethod.AccumulatedError end) + +game:GetService("ChangeHistoryService"):SetEnabled(false) +pcall(function() game:GetService("Players"):SetBuildUserPermissionsUrl("http:////Game/BuildActionPermissionCheck.ashx?assetId=0&userId=%d&isSolo=true") end) + +workspace:SetPhysicsThrottleEnabled(true) + +local addedBuildTools = false +local screenGui = game:GetService("CoreGui"):FindFirstChild("RobloxGui") + +-- This code might move to C++ +function characterRessurection(player) + if player.Character then + local humanoid = player.Character.Humanoid + humanoid.Died:connect(function() wait(5) player:LoadCharacter() end) + end +end +--[[game:GetService("Players").PlayerAdded:connect(function(player) + characterRessurection(player) + player.Changed:connect(function(name) + if name=="Character" then + characterRessurection(player) + end + end) +end)--]] + +function doVisit() + message.Text = "Loading Game" + if PlaceID == 0 ? "false" : "true"?> then + game:Load("AssetURL?>") + pcall(function() visit:SetUploadUrl("UploadURL?>") end) + else + pcall(function() visit:SetUploadUrl("UploadURL?>") end) + end + + + message.Text = "Running" + game:GetService("RunService"):Run() + + message.Text = "Creating Player" + if PlaceID == 0 ? "false" : "true"?> then + player = game:GetService("Players"):CreateLocalPlayer(UserID?>) + player.Name = [====[Username?>]====] + else + player = game:GetService("Players"):CreateLocalPlayer(0) + end + player.CharacterAppearance = "CharacterAppearance?>" + local propExists, canAutoLoadChar = false + propExists = pcall(function() canAutoLoadChar = game.Players.CharacterAutoLoads end) + + if (propExists and canAutoLoadChar) or (not propExists) then + player:LoadCharacter() + end + + + message.Text = "Setting GUI" + player:SetSuperSafeChat(true) + pcall(function() player:SetMembershipType(Enum.MembershipType.Membership?>) end) + pcall(function() player:SetAccountAge(Age?>) end) + + if PlaceID == 0 ? "false" : "true"?>s then + message.Text = "Setting Ping" + visit:SetPing("PingURL?>", 120) + + message.Text = "Sending Stats" + game:HttpGet("StatsURL?>") + end + +end + +success, err = pcall(doVisit) + +if not addedBuildTools then + + local playerName = Instance.new("StringValue") + playerName.Name = "PlayerName" + playerName.Value = player.Name + playerName.RobloxLocked = true + playerName.Parent = screenGui + + local BuildToolsScriptID = -1 + + pcall(function() + --if game.CoreGui.Version == 1 or game.CoreGui.Version == 2 then BuildToolsScriptID = 1179 + --else if game.CoreGui.Version == 7 then BuildToolsScriptID = 1568 end + game:GetService("ScriptContext"):AddCoreScript(1568,screenGui,"BuildToolsScript") + end) + addedBuildTools = true +end + +if success then + message.Parent = nil +else + print(err) + if PlaceID == 0 ? "false" : "true"?> then + pcall(function() visit:SetUploadUrl("") end) + end + wait(5) + message.Text = "Error on visit: " .. err + if true then + game:HttpPost("http:///Error/Lua.ashx?", "Visit.lua: " .. err) + end +end +errorCode(404); + +header("Pragma: no-cache"); +header("Cache-Control: no-cache"); +header("content-type: text/plain; charset=utf-8"); + +$UserID = API::GetParameter("GET", "UserID", "int"); +Database::singleton()->run("UPDATE users SET Wipeouts = Wipeouts + 1 WHERE id = :UserID", [":UserID" => $UserID]); \ No newline at end of file diff --git a/rbxclient/login/negotiate.php b/rbxclient/login/negotiate.php new file mode 100644 index 0000000..863c894 --- /dev/null +++ b/rbxclient/login/negotiate.php @@ -0,0 +1,65 @@ +errorCode(400); + +$ticket = $_GET["suggest"] ?? ""; + +if (!isset($_SERVER["HTTP_RBXAUTHENTICATIONNEGOTIATION"])) +{ + http_response_code(403); + echo "Missing custom Roblox header."; + die(); +} + +if (!strlen($ticket)) +{ + http_response_code(403); + echo "Authentication ticket was not sent."; + die(); +} + +if (!isset($_COOKIE['polygon_session'])) +{ + // the ticket is formatted as [{username}:{id}:{timestamp}]:{signature} + // the part in square brackets is what the signature represents + + $ticket = explode(":", $_GET["suggest"]); + + if (count($ticket) == 4) + { + $username = $ticket[0]; + $userid = $ticket[1]; + $timestamp = (int)$ticket[2]; + $signature = $ticket[3]; + + // reconstruct the signed message + $ticketRecon = sprintf("%s:%s:%d", $username, $userid, $timestamp); + + // check if signature matches and if ticket is 3 minutes old max + if (RBXClient::CryptVerifySignature($ticketRecon, $signature) && $timestamp + 180 > time()) + { + // before we create the session, let's just quickly check to make sure we don't create any duplicate sessions + $lastSession = Database::singleton()->run( + "SELECT created FROM sessions + WHERE userId = :UserID AND IsGameClient + ORDER BY created DESC LIMIT 1", + [":UserID" => $userid] + )->fetchColumn(); + + if ($lastSession + 180 < $timestamp) + { + $session = Session::Create($userid, true); + + // this might be a war crime + // $_COOKIE["polygon_session"] = $session; + } + } + } +} + +echo "OK"; \ No newline at end of file diff --git a/rbxclient/studio/publish-model.php b/rbxclient/studio/publish-model.php new file mode 100644 index 0000000..d02f31b --- /dev/null +++ b/rbxclient/studio/publish-model.php @@ -0,0 +1,267 @@ +run("SELECT type FROM assets WHERE id = :id AND creator = :uid", [":id" => $modelId, ":uid" => $userid]); + if(!$query->rowCount()) error("You do not own this Model"); + if($query->fetchColumn() != 10) error("Not a Model"); + } + else + { + if(!strlen($name)) error("Model Name cannot be empty"); + if(strlen($name) > 50) error("Model Name cannot longer than 50 characters"); + if(strlen($desc) > 1000) error("Model Description cannot longer than 1000 characters"); + if(!strlen($desc)) $desc = "Model"; + } + + // the roblox client gzencodes the xml but fiddler automatically decodes it + // so if we can't find xml then assume its gzencoded + $xml = file_get_contents('php://input'); + if(!stripos($xml, 'roblox')) $xml = gzdecode(file_get_contents('php://input')); + try { @new SimpleXMLElement($xml); } catch(Exception $e) { error("Invalid XML"); } + if(strlen($xml) > 16000000) error("Model cannot be larger than 16 megabytes"); + + // $xml = str_ireplace("http://www.roblox.com/asset/?id=", "%ROBLOXASSETURL%", $xml); + // $xml = str_ireplace("http://www.roblox.com/asset?id=", "%ROBLOXASSETURL%", $xml); + $xml = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset/?id=", "%ASSETURL%", $xml); + $xml = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset?id=", "%ASSETURL%", $xml); + $isScript = stripos($xml, 'class="Script" referent="RBX0"'); + + if($modelId) + unlink(Polygon::GetSharedResource("assets/{$modelId}")); + else + $modelId = Catalog::CreateAsset([ + "type" => 10, + "creator" => SESSION["user"]["id"], + "name" => $name, + "description" => $desc, + "sale" => (int)$free, + "PublicDomain" => (int)$free, + "approved" => (int)$isScript + ]); + + file_put_contents(Polygon::GetSharedResource("assets/{$modelId}"), $xml); + Gzip::Compress(Polygon::GetSharedResource("assets/{$modelId}")); + + if(!$postModelId && $isScript) + { + //put script image as thumbnail + Image::RenderFromStaticImage("Script", $modelId); + } + elseif(!$isScript) + { + // user uploaded models are rendered as "usermodels" - this is just normal model rendering except there's no alpha + // no roblox thumbnails had transparency up until like 2013 anyway so its not that big of a deal + Polygon::RequestRender("UserModel", $modelId); + } +} + +$models = Database::singleton()->run("SELECT * from assets WHERE creator = :uid AND type = 10 ORDER BY created DESC", [":uid" => $userid]); +?> + + + + Save + + + + + +
    + + + + + + + + + rowCount()) { ?> + + + + + + + + + +
    +

    You are about to publish this Model to Project Polygon. Please choose how you would like to save your work:

    +
    +
    +
    +
    Create a new Model on Project Polygon.
    Choose this to create a brand new Model. Your existing Models will not be changed.
    +
    Update an existing Model on Project Polygon.
    Choose this to make changes to a Model you have previously created. You will have the opportunity to select which Model you wish to update.
    Keep playing and exit later.
    +
    + + + + + + + \ No newline at end of file diff --git a/rbxclient/studio/publish-place.php b/rbxclient/studio/publish-place.php new file mode 100644 index 0000000..2cf2676 --- /dev/null +++ b/rbxclient/studio/publish-place.php @@ -0,0 +1,289 @@ +run("SELECT COUNT(*) FROM assets WHERE creator = :UserID AND type = 9", [":UserID" => SESSION["user"]["id"]])->fetchColumn(); +$AllSlotsFilled = $PlaceCount >= SESSION["user"]["PlaceSlots"]; + +if ($_SERVER['REQUEST_METHOD'] == "POST") +{ + if (!SESSION) error("You are not logged in"); + $PlaceID = $_GET['PlaceID'] ?? false; + $Name = $_GET['Name'] ?? ""; + $Description = $_GET['Description'] ?? ""; + $Uncopylocked = isset($_GET['PublicDomain']) && $_GET['PublicDomain'] == "true"; + + if($PlaceID) + { + $PlaceCheck = Database::singleton()->run( + "SELECT type FROM assets WHERE id = :PlaceID AND creator = :UserID", + [":PlaceID" => $PlaceID, ":UserID" => SESSION["user"]["id"]] + ); + + if (!$PlaceCheck->rowCount()) error("You do not own this Place"); + if ($PlaceCheck->fetchColumn() != 9) error("Not a Place"); + } + else + { + if ($AllSlotsFilled) error("You have used up all your place slots"); + if (strlen($Name) == 0) error("Place Name cannot be empty"); + if (strlen($Name) > 50) error("Place Name cannot longer than 50 characters"); + if (strlen($Description) > 1000) error("Place Description cannot longer than 1000 characters"); + } + + // the roblox client gzencodes the xml but fiddler automatically decodes it + // so if we can't find xml then assume its gzencoded + $xml = file_get_contents('php://input'); + if (!stripos($xml, 'roblox')) $xml = gzdecode(file_get_contents('php://input')); + try { @new SimpleXMLElement($xml); } catch(Exception $e) { error("Invalid XML"); } + if (strlen($xml) > 32000000) error("Place cannot be larger than 32 megabytes"); + + // $xml = str_ireplace("http://www.roblox.com/asset/?id=", "%ROBLOXASSETURL%", $xml); + // $xml = str_ireplace("http://www.roblox.com/asset?id=", "%ROBLOXASSETURL%", $xml); + $xml = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset/?id=", "%ASSETURL%", $xml); + $xml = str_ireplace("http://".$_SERVER['HTTP_HOST']."/asset?id=", "%ASSETURL%", $xml); + $xml = preg_replace("/rbxasset:\/\/..\/[^<]*/", "", $xml); + + if ($PlaceID) + { + unlink(Polygon::GetSharedResource("assets/{$PlaceID}")); + Database::singleton()->run("UPDATE assets SET updated = UNIX_TIMESTAMP() WHERE id = :PlaceID", [":PlaceID" => $PlaceID]); + } + else + { + $PlaceID = Catalog::CreateAsset([ + "type" => 9, + "creator" => SESSION["user"]["id"], + "name" => $Name, + "description" => $Description, + "PublicDomain" => $Uncopylocked ? 1 : 0, + "ServerRunning" => 0, + "ActivePlayers" => 0, + "MaxPlayers" => 10, + "Access" => "Friends", + "Visits" => 0, + "Version" => 2010, + "ChatType" => "Classic", + "gear_attributes" => "{\"melee\":false,\"powerup\":false,\"ranged\":false,\"navigation\":false,\"explosive\":false,\"musical\":false,\"social\":false,\"transport\":false,\"building\":false}", + "approved" => 1 + ]); + } + + file_put_contents(Polygon::GetSharedResource("assets/{$PlaceID}"), $xml); + Gzip::Compress(Polygon::GetSharedResource("assets/{$PlaceID}")); + + Polygon::RequestRender("Place", $PlaceID); +} + +$Places = Database::singleton()->run( + "SELECT * from assets WHERE creator = :UserID AND type = 9 ORDER BY created DESC", + [":UserID" => SESSION["user"]["id"]] +); +?> + + + + Save + + + + + +
    + + + + + + + + + + rowCount()) { ?> + + + + + + + + + +
    +

    You are about to publish this Place to Project Polygon. Please choose how you would like to save your work:

    +
    +
    +
    +
    Create a new Place on Project Polygon.
    Choose this to create a brand new Place. Your existing Places will not be changed.
    +
    Update an existing Place on Project Polygon.
    Choose this to make changes to a Place you have previously created. You will have the opportunity to select which Place you wish to update.
    Keep playing and exit later.
    +
    + + + + + + + \ No newline at end of file diff --git a/rbxclient/studio/toolbox.css b/rbxclient/studio/toolbox.css new file mode 100644 index 0000000..e69de29 diff --git a/rbxclient/studio/toolbox.php b/rbxclient/studio/toolbox.php new file mode 100644 index 0000000..c6e25ef --- /dev/null +++ b/rbxclient/studio/toolbox.php @@ -0,0 +1,101 @@ + + + + + + Toolbox + + + + + +
    +
    +
    + +
    + +
    +
    +
    + + \ No newline at end of file diff --git a/rbxclient/studio/welcome.php b/rbxclient/studio/welcome.php new file mode 100644 index 0000000..27e1e4a --- /dev/null +++ b/rbxclient/studio/welcome.php @@ -0,0 +1,104 @@ +run( + "SELECT * FROM assets WHERE type = 9 AND creator = :UserID ORDER BY created DESC", + [":UserID" => SESSION["user"]["id"]] + ); +} + +$TemplatePlaces = Database::singleton()->run( + "SELECT * FROM assets WHERE TemplateOrder IS NOT NULL ORDER BY TemplateOrder" +)->fetchAll(); + +$pageBuilder = new PageBuilder(["title" => "Welcome", "ShowNavbar" => false, "ShowFooter" => false]); +$pageBuilder->addAppAttribute("class", "app nav-content"); +$pageBuilder->addResource("scripts", "/js/protocolcheck.js"); +$pageBuilder->addResource("polygonScripts", "/js/polygon/games.js"); +$pageBuilder->buildHeader(); +?> + +
    +
    + +
    +
    +
    +
    +

    Place Templates

    +
    + +
    +
    " placeversion="2012"> + " alt="" src=""> +
    +

    ">

    +
    +
    +
    + +
    +
    +
    +

    My Published Projects

    + +
    + fetch(\PDO::FETCH_OBJ)) { ?> +
    +
    + <?=Polygon::FilterText($Project->name)?> +
    +

    name)?>

    +
    +
    +
    + + +

    You must be logged in to view your published projects!

    + +
    +
    +
    +
    +
    +buildFooter(); ?> \ No newline at end of file diff --git a/rbxclient/uploadmedia/screenshot.php b/rbxclient/uploadmedia/screenshot.php new file mode 100644 index 0000000..a6308fa --- /dev/null +++ b/rbxclient/uploadmedia/screenshot.php @@ -0,0 +1,20 @@ + false, "ShowFooter" => false]); +$pageBuilder->buildHeader(); +?> +

    Screenshot

    +

    Hey, you just took a screenshot in ! You could:

    + +
    +Not interested, don't bother me again +buildFooter(); ?> \ No newline at end of file diff --git a/rbxclient/uploadmedia/video.php b/rbxclient/uploadmedia/video.php new file mode 100644 index 0000000..60043de --- /dev/null +++ b/rbxclient/uploadmedia/video.php @@ -0,0 +1,16 @@ + false, "ShowFooter" => false]); +$pageBuilder->buildHeader(); +?> +

    Video

    +

    Hey, you just recorded a video in ! You could:

    + +
    +Not interested, don't bother me again +buildFooter(); ?> \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..76ecf8f --- /dev/null +++ b/robots.txt @@ -0,0 +1 @@ +# Ratio \ No newline at end of file diff --git a/thumbs/Script.png b/thumbs/Script.png new file mode 100644 index 0000000..77e00f1 Binary files /dev/null and b/thumbs/Script.png differ diff --git a/thumbs/asset.php b/thumbs/asset.php new file mode 100644 index 0000000..cc85101 --- /dev/null +++ b/thumbs/asset.php @@ -0,0 +1,73 @@ +run( + "SELECT * FROM assets WHERE id = :AssetID", + [":AssetID" => $AssetID] +)->fetch(\PDO::FETCH_OBJ); + +if (!$AssetInfo) +{ + if ($AssetVersionID && is_numeric($AssetVersionID)) + { + // thumbnailasset.ashx died and it was like the only endpoint + // that supported fetching from asset version ids so here we are + + $Context = stream_context_create( + [ + "http" => + [ + "method" => "GET", + "header" => "User-Agent: Roblox/WinInet", + "ignore_errors" => true + ] + ]); + + $ServiceRequestParameters = http_build_query( + [ + "assetId" => 0, + "assetVersionId" => $AssetVersionID, + "width" => $ResX, + "height" => $ResY, + "imageFormat" => "Png", + "thumbnailFormatId" => 296, + "overrideModeration" => "false" + ]); + + $ServiceRequest = file_get_contents("https://assetgame.roblox.com/Thumbs/Asset.asmx/RequestThumbnail_v2?{$ServiceRequestParameters}", false, $Context); + + if ($http_response_header[0] == "HTTP/1.1 200 OK") + { + $ServiceResponse = json_decode($ServiceRequest); + http_response_code(302); + header("Location: {$ServiceResponse->d->url}"); + } + else + { + header($http_response_header[0]); + die(); + } + } + else if ($AssetID && is_numeric($AssetID)) // thumbnailasset.ashx / asset.ashx + { + die(header("Location: https://assetgame.roblox.com/Thumbs/Asset.ashx?format=png&width={$ResX}&height={$ResY}&assetId={$AssetID}")); + } + + die(); +} + +if ([$ResX, $ResY] == [420, 230]) +{ + $ResX = 768; + $ResY = 432; +} + +redirect(Thumbnails::GetAsset($AssetInfo, $ResX, $ResY)); \ No newline at end of file diff --git a/thumbs/asset3d.php b/thumbs/asset3d.php new file mode 100644 index 0000000..87da6ba --- /dev/null +++ b/thumbs/asset3d.php @@ -0,0 +1,17 @@ + Thumbnails::GetCDNLocation($Location, "json"), "Final" => true]); \ No newline at end of file diff --git a/thumbs/audio.png b/thumbs/audio.png new file mode 100644 index 0000000..37f8b37 Binary files /dev/null and b/thumbs/audio.png differ diff --git a/thumbs/avatar.php b/thumbs/avatar.php new file mode 100644 index 0000000..5f7307c --- /dev/null +++ b/thumbs/avatar.php @@ -0,0 +1,15 @@ + Thumbnails::GetCDNLocation($Location, "json"), "Final" => true]); \ No newline at end of file diff --git a/thumbs/rawavatar.php b/thumbs/rawavatar.php new file mode 100644 index 0000000..a81068b --- /dev/null +++ b/thumbs/rawavatar.php @@ -0,0 +1,18 @@ +run( + "SELECT renderStatus FROM renderqueue WHERE renderType = 'Avatar' AND assetID = :id ORDER BY timestampRequested DESC LIMIT 1", + [":id" => $id] +); +$status = $query->fetchColumn(); +if($query->rowCount() && in_array($status, [0, 1, 4])) die("PENDING"); + +echo Thumbnails::GetAvatar($id, 420, 420); \ No newline at end of file diff --git a/thumbs/resolvehash.php b/thumbs/resolvehash.php new file mode 100644 index 0000000..201fb27 --- /dev/null +++ b/thumbs/resolvehash.php @@ -0,0 +1,9 @@ + "https://polygoncdn.pizzaboxer.xyz/{$Filename}", "Final" => true]); \ No newline at end of file diff --git a/user.php b/user.php new file mode 100644 index 0000000..0d3fbfb --- /dev/null +++ b/user.php @@ -0,0 +1,330 @@ +id ?? false); + if(!$info || $moderation && !$isModerator) PageBuilder::instance()->errorCode(404); + $selfProfile = false; + $pronouns = ["your" => $info->username."'s", "do_not" => $info->username." doesn't", "have_not" => $info->username." hasn't"]; +} +else +{ + Users::RequireLogin(); + $info = Users::GetInfoFromID(SESSION["user"]["id"]); + $moderation = false; + $selfProfile = true; + $pronouns = ["your" => "Your", "do_not" => "You don't", "have_not" => "You haven't"]; +} + +$statistics = (object) +[ + "friends" => Users::GetFriendCount($info->id), + "posts" => $info->ForumThreads + $info->ForumReplies, + "joined" => date("F j Y", $info->jointime) +]; + +if(SESSION) $friendship = Users::CheckIfFriends(SESSION["user"]["id"], $info->id); + +$pageBuilder = new PageBuilder(["title" => $info->username]); +$pageBuilder->addAppAttribute("data-user-id", $info->id); +$pageBuilder->addAppAttribute("data-self-profile", $selfProfile ? "true" : "false"); +$pageBuilder->addMetaTag("og:image", Thumbnails::GetAvatar($info->id)); +$pageBuilder->addMetaTag("og:description", Polygon::FilterText($info->blurb)); + +if(SESSION) +{ + $pageBuilder->addResource("scripts", "http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.min.js"); + $pageBuilder->addResource("scripts", "/js/protocolcheck.js"); + $pageBuilder->addResource("polygonScripts", "/js/polygon/games.js"); + $pageBuilder->addResource("polygonScripts", "/js/polygon/inventory.js"); +} + +$pageBuilder->addResource("polygonScripts", "/js/polygon/profile.js"); +$pageBuilder->addResource("polygonScripts", "/js/polygon/friends.js"); + +$pageBuilder->addResource("polygonScripts", "/js/3D/ThumbnailView.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/ThreeDeeThumbnails.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/three.min.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/MTLLoader.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/OBJMTLLoader.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/tween.js"); +$pageBuilder->addResource("polygonScripts", "/js/3D/PolygonOrbitControls.js"); + +$pageBuilder->buildHeader(); +?> + +
    +
    +
    +

    Profile

    +
    + + (View Public Profile) + id, true); ?> + Attributes?>>[ Text?> ]

    + https:// + +
    +
    + + <?=$info->username?>" data-src="id)?>"> + + +
    +

    blurb))?>

    + +
    + id != SESSION["user"]["id"]) { if(!$friendship) { ?> + Send Friend Request + status == 0 && $friendship->receiverId == SESSION["user"]["id"]) { ?> +
    + Accept + Decline +
    + status == 0) { ?> + Friend Request Pending + + Unfriend + + Send Friend Request + +
    + + + +
    + +
    +
    +

    Alternate Accounts

    + id) as $alt) { ?> +

    "> (Created )

    + +
    + + +
    +
    +

    Badges

    + +
    +

    +
    + +
    +
    +
    + $name +
    $name
    +
    + +
    +
    +
    + +
    +
    +

    Groups

    +
    +

    +
    + +
    +
    + +
    +
    +
    +

    Rank: $Role

    +

    Members: $MemberCount

    +
    +
    +
    +
    +
    +
    +
    +
    +

    Statistics

    +
    +
    +

    Friends:

    +

    forum.">Forum Posts:

    +

    Place Visits:

    +

    Knockouts:

    +

    Wipeouts:

    +

    account.">Joined:

    +
    +
    +

    friends?>

    +

    posts?>

    +

    PlaceVisits)?>

    +

    Knockouts)?>

    +

    Wipeouts)?>

    +

    joined?>

    +
    +
    +
    +
    +
    + +
    +

    Active Places

    +
    +
    have any active places
    +
    + +
    + +
    +

    Visited $Visits times

    + " data-src="$Thumbnail" class="img-fluid"> +

    $Description

    + + + +
    +
    +
    +
    + + +
    + +
    +
    +
    +

    Inventory

    +
    +
    + +
    + +
    +
    +
    + +
    +buildFooter(); ?>