individual level pages!!!!!!!!!

This commit is contained in:
Jill 2023-05-24 11:27:44 +03:00
parent 543a959829
commit dc6d5767bb
Signed by: oat
GPG Key ID: 33489AA58A955108
8 changed files with 303 additions and 23 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -128,7 +128,7 @@
</div>
</div>
</div>
<a href="/accounts/notifications/" title="Notifiations" class="circle-button notifications <%= unread_notifications ? "notifications-unread" : "" %>">
<a href="/accounts/notifications" title="Notifiations" class="circle-button notifications <%= unread_notifications ? "notifications-unread" : "" %>">
<%= File.read("public/assets/icons/bell.svg") %>
</a>
</div>

View File

@ -38,7 +38,7 @@
<li>The <a href="https://git.oat.zone/oat/crystal-gauntlet">Git repository</a></li>
<li><a href="/accounts">Account stuff</a></li>
<li><a href="/tools/song_upload">Song reuploading</a> and <a href="/tools/song_search">searching</a></li>
<li><a href="/tools/levels">Levels</a> and <a href="/tools/reupload">level reuploading</a></li>
<li><a href="/levels">Levels</a> and <a href="/tools/reupload">level reuploading</a></li>
<%- if config_get("sessions.allow").as(Bool | Nil) -%>
<li>The <a href="/tools/create_session">session creation page</a> (for accessing features in 1.9)</li>
<%- end -%>

258
public/template/level.ecr Normal file
View File

@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
<title><%= name %> by <%= username %></title>
<style>
body {
max-width: 800px;
margin: auto;
padding: 1em;
}
.card {
background-color: var(--background-color-2);
border-radius: 1.5em;
padding: 1em;
display: flex;
flex-direction: row;
gap: 1rem;
margin-bottom: 1em;
}
.card.bright {
background-color: var(--accent-color);
color: #000;
}
.card.bright ::selection {
background-color: #000;
color: #fff;
}
.card.bright a {
color: #000;
}
.card-l {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5em;
}
.level-img {
display: block;
height: 3rem;
}
.stars {
font-size: 1.1em;
font-weight: bold;
}
.stars img {
width: auto;
height: 1em;
vertical-align: middle;
}
.card-r {
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
.card-top {
display: flex;
}
.label-left {
flex: 1 1 auto;
font-size: 1.5em;
font-weight: bold;
}
.label-right {
flex: 0 0 auto;
}
.card-header {
font-weight: bold;
color: var(--text-color-dark);
text-transform: uppercase;
font-size: 0.8em;
user-select: none;
}
.favicon {
margin: auto;
display: block;
}
.description {
flex-direction: column;
gap: 0.5em;
}
.leaderboard {
min-width: 100%;
overflow: auto;
font-size: 0.9em;
border-collapse: separate;
border-spacing: 0;
}
/* honestly quite horrendous */
.leaderboard tr:nth-child(2) td:first-child {
border-top-left-radius: 0.5em;
}
.leaderboard tr:nth-child(2) td:last-child {
border-top-right-radius: 0.5em;
}
.leaderboard tr:last-child td:first-child {
border-bottom-left-radius: 0.5em;
}
.leaderboard tr:last-child td:last-child {
border-bottom-right-radius: 0.5em;
}
.leaderboard tr:not(.leaderboard-header):nth-child(even) td {
background-color: var(--background-color-2);
}
.leaderboard th, .leaderboard td {
padding: 0.2em 0.5em;
text-align: left;
}
.leaderboard th {
font-size: 0.8em;
user-select: none;
color: var(--text-color-dark);
text-transform: uppercase;
}
.leaderboard td.rank {
width: 30px;
font-weight: bold;
}
.leaderboard td.percent {
width: 75px;
}
.leaderboard td.coins {
width: 3em;
}
.leaderboard td.icon {
width: 2.5em;
text-align: center;
}
.leaderboard td.name {
width: 180px
}
.leaderboard td.name a {
text-decoration: none;
}
.leaderboard .player-icon {
height: 1.2em;
width: auto;
vertical-align: middle;
}
</style>
</head>
<body>
<a href="/"><img src="/favicon.png" width="64" height="auto" class="spinny favicon"></a><br>
<div class="card bright">
<div class="card-l">
<%=
difficulty = (difficulty_set || difficulty_community).try { |n| LevelDifficulty.new(n) }
demon_difficulty = demon_difficulty_int.try { |n| DemonDifficulty.new(n) }
"<img src='#{Templates.get_difficulty_icon(difficulty, featured, epic, demon_difficulty)}' class='level-img'>"
%>
<%- if stars -%>
<div class="stars">
<%= stars %> <img src="/assets/icons/gd/star.png">
</div>
<%- end -%>
</div>
<div class="card-r">
<div class="card-top">
<div class="label-left">
<%= name %>
</div>
<div class="label-right">
<%= downloads %> downloads <%= likes %> likes
</div>
</div>
<div class="card-bottom">
by <a href="/user/<%= username %>"><%= username %></a>
</div>
</div>
</div>
<div class="card description">
<div class="card-header">DESCRIPTION</div>
<div>
<%= description != "" ? HTML.escape(description) : "<i>No description provided</i>" %>
</div>
</div>
<div class="card description">
<div class="card-header">SONG</div>
<div>
<%- if song_name -%>
<!--
song_name, song_author, song_url, song_author_url
-->
<div><a href="<%= song_url %>" target="_blank" rel="noopener"><%= song_name %></a></div>
<%- if song_author && song_author != "" -%>
<div>by <a href="<%= song_author_url %>" target="_blank" rel="noopener"><%= song_author %></a></div>
<%- else -%>
<div><i>unknown artist</i></div>
<%- end -%>
<%- else -%>
idk
<%- end -%>
</div>
</div>
<table class="leaderboard">
<tr class="leaderboard-header">
<th class="rank">RANK</td>
<th class="percent">PERCENT</td>
<th class="coins">COINS</td>
<th class="icon"></td>
<th class="name">PLAYER</td>
<th class="date">TIME</td>
</tr>
<%
rank = 0
scores.each do |percent, coins, username, icon_type, color1, color2, cube, ship, ball, ufo, wave, robot, spider, special, set_at|
rank = rank + 1
icon_value = [cube, ship, ball, ufo, wave, robot, spider][icon_type]
type_str = ["cube", "ship", "ball", "ufo", "wave", "robot", "spider"][icon_type]
set_at_date = Time.parse(set_at, Format::TIME_FORMAT, Time::Location::UTC)
%>
<tr>
<td class="rank">#<%= rank %></td>
<td class="percent"><%= percent %>%</td>
<td class="coins"></td>
<td class="icon">
<img src="https://gdicon.oat.zone/icon.png?type=<%=type_str%>&value=<%=icon_value%>&color1=<%=color1%>&color2=<%=color2%><%=special ? "&glow=1" : ""%>" class="player-icon">
</td>
<td class="name">
<a href="/user/<%= username %>"><%= username %></a>
</td>
<td class="date">
<time datetime="<%= Time::Format::RFC_3339.format(set_at_date) %>" title="<%= Time::Format::RFC_2822.format(set_at_date) %>"><%= Format.fmt_timespan(Time.utc - set_at_date) %></time>
</td>
</tr>
<%- end -%>
<%- if scores.size == 0 -%>
<tr>
<td colspan="6" style="text-align: center">
<i>No scores</i>
</td>
</tr>
<%- end -%>
</table>
<p>comments go here etc etc</p>
</body>
</html>

View File

@ -55,7 +55,10 @@
display: flex;
flex-direction: column;
align-items: center;
gap: 1em
gap: 1em;
}
.level a {
text-decoration: none;
}
</style>
</head>
@ -69,8 +72,8 @@
"<img src='#{Templates.get_difficulty_icon(difficulty, featured, epic, demon_difficulty)}' class='level-img'>"
%>
<div class="level-right">
<span class="line"><span class="name"><%= name %></span><span class="id dim">#<%= id %></span></span>
<small>by <%= username %></small><br>
<span class="line"><span class="name"><a href="/levels/<%= id %>"><%= name %></a></span><span class="id dim">#<%= id %></span></span>
<small>by <a href="/user/<%= username %>"><%= username %></a></small><br>
</div>
</div>
<%- end -%>

View File

@ -64,18 +64,16 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
LOG.debug { "allowed objects: #{allowed_objects.inspect}" }
if forbidden_obj = level_objects.find do |obj|
if !obj.has_key?("1")
false
end
id = obj["1"].to_i
if allowed_objects.size > 0
if !allowed_objects.includes?(id)
true
end
else
if forbidden_objects.includes?(id)
true
if obj.has_key?("1")
id = obj["1"].to_i
if allowed_objects.size > 0
if !allowed_objects.includes?(id)
true
end
else
if forbidden_objects.includes?(id)
true
end
end
end
end
@ -83,12 +81,12 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
return "-1"
end
if exploit_obj = level_objects.find do |obj|
if exploit_obj = level_objects.find { |obj|
# target color ID
(obj.has_key?("23") && obj["23"].to_i < 0 || obj["23"].to_i > 1100) ||
(obj.has_key?("23") && (obj["23"].to_i < 0 || obj["23"].to_i > 1100)) ||
# target group ID
(obj.has_key?("51") && obj["51"].to_i < 0 || obj["51"].to_i > 1100)
end
(obj.has_key?("51") && (obj["51"].to_i < 0 || obj["51"].to_i > 1100))
}
LOG.info { "preventing upload of level attempting to exploit invalid color/group IDs" }
return "-1"
end
@ -147,7 +145,7 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C
# create new level
next_id = IDs.get_next_id("levels")
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, extra_data, level_info, editor_time, editor_time_copies, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, Clean.clean_special(params["levelName"])[..20-1], user_id, description[..140-1], params["original"].to_i, params["gameVersion"].to_i, (params["binaryVersion"]? || "0").to_i, params["password"] == "0" ? nil : params["password"].to_i, requested_stars, (params["unlisted"]? || "0").to_i == 1, params["levelVersion"].to_i, Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]? || ""), (params["wt"]? || "0").to_i, (params["wt2"]? || "0").to_i, song_id.to_i, level_length, objects, coins, (params["ldm"]? || "0").to_i == 1, two_player)
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, extra_data, level_info, editor_time, editor_time_copies, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, Clean.clean_basic(params["levelName"])[..20-1], user_id, description[..140-1], params["original"].to_i, params["gameVersion"].to_i, (params["binaryVersion"]? || "0").to_i, params["password"] == "0" ? nil : params["password"].to_i, requested_stars, (params["unlisted"]? || "0").to_i == 1, params["levelVersion"].to_i, Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]? || ""), (params["wt"]? || "0").to_i, (params["wt2"]? || "0").to_i, song_id.to_i, level_length, objects, coins, (params["ldm"]? || "0").to_i == 1, two_player)
File.write(DATA_FOLDER / "levels" / "#{next_id.to_s}.lvl", Base64.decode(params["levelString"]))

View File

@ -6,7 +6,7 @@ levels_per_page = 10
CrystalGauntlet.template_endpoints[{
name: "list_levels",
path: "/tools/levels",
path: "/levels",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
@ -15,3 +15,24 @@ CrystalGauntlet.template_endpoints[{
levels = DATABASE.query_all("select levels.id, name, users.username, levels.community_difficulty, levels.difficulty, levels.demon_difficulty, levels.featured, levels.epic from levels left join users on levels.user_id = users.id order by levels.id desc limit #{levels_per_page} offset #{page * levels_per_page}", as: {Int32, String, String, Int32?, Int32?, Int32?, Bool, Bool})
ECR.embed("./public/template/levels.ecr", context.response)
}
CrystalGauntlet.template_endpoints[{
name: "level_page",
path: "/levels/{id<\\d+>}",
methods: ["get"]
}] = ->(context : HTTP::Server::Context, params : Hash(String, String?)) {
context.response.content_type = "text/html"
id = params["id"].as(String).to_i
begin
name, username, difficulty_community, difficulty_set, demon_difficulty_int, featured, epic, downloads, likes, stars, description, song_id, song_name, song_author, song_url, song_author_url = DATABASE.query_one("select levels.name, users.username, community_difficulty, difficulty, demon_difficulty, featured, epic, downloads, likes, levels.stars, description, song_id, song_data.name, song_authors.name, songs.url, song_authors.source from levels left join users on levels.user_id = users.id left join songs on songs.id = levels.song_id left join song_data on song_data.id = levels.song_id left join song_authors on song_data.author_id = song_authors.id where levels.id = ?", id, as: {String, String, Int32?, Int32?, Int32?, Bool, Bool, Int32, Int32, Int32?, String, Int32, String?, String?, String?, String?})
rescue err
LOG.error {"whar.... #{err}"}
context.response.status = HTTP::Status::NOT_FOUND
return
end
scores = DATABASE.query_all("select distinct percent, level_scores.coins, users.username, users.icon_type, users.color1, users.color2, users.cube, users.ship, users.ball, users.ufo, users.wave, users.robot, users.spider, users.special, set_at from level_scores join users on level_scores.account_id = users.account_id where level_id = ? order by percent desc, level_scores.coins desc, set_at limit 25", id, as: {Int32, Int32, String, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, String})
ECR.embed("./public/template/level.ecr", context.response)
}