<%@ WebHandler Language="C#" Class="ScoresHandler" %> /* * API du classement Memory (pictogrammes de sécurité) — ASP.NET + SQL Server. * * GET scores.ashx?difficulty=easy|normal|hard|all[&limit=10] * -> renvoie le classement (top N) trié par score décroissant. * * POST scores.ashx (corps JSON) * { playerName, userId, score, moves, timeSeconds, difficulty, isPerfect } * -> enregistre le score, ne conserve que le top N par difficulté, * renvoie { ok, saved, rank, isTop }. * * Seul le top N (défaut 10) de chaque mode est conservé : les scores plus * faibles sont supprimés après chaque insertion. * * Fichier compilé au vol par IIS : il suffit de le déposer, aucune build. * La chaîne de connexion est lue dans web.config (clé "MemoryDb"). */ using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.IO; using System.Text; using System.Web; using System.Web.Configuration; using System.Web.Script.Serialization; public class ScoresHandler : IHttpHandler { static readonly string[] Difficulties = { "easy", "normal", "hard" }; const int TopN = 10; public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext ctx) { ctx.Response.ContentType = "application/json; charset=utf-8"; ctx.Response.AddHeader("X-Content-Type-Options", "nosniff"); var connString = WebConfigurationManager.ConnectionStrings["MemoryDb"] != null ? WebConfigurationManager.ConnectionStrings["MemoryDb"].ConnectionString : null; if (string.IsNullOrEmpty(connString)) { Fail(ctx, 500, "Configuration manquante (connectionString 'MemoryDb' dans web.config)."); return; } try { using (var conn = new SqlConnection(connString)) { conn.Open(); EnsureTable(conn); if (ctx.Request.HttpMethod == "GET") { HandleGet(ctx, conn); } else if (ctx.Request.HttpMethod == "POST") { HandlePost(ctx, conn); } else { ctx.Response.StatusCode = 405; ctx.Response.AddHeader("Allow", "GET, POST"); Write(ctx, new Dictionary { { "ok", false }, { "error", "Méthode non autorisée." } }); } } } catch (Exception) { Fail(ctx, 500, "Erreur serveur."); } } // --- Création de la table si besoin ------------------------------------- void EnsureTable(SqlConnection conn) { const string sql = "IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'memory_scores') " + "CREATE TABLE memory_scores (" + " id INT IDENTITY(1,1) PRIMARY KEY," + " player_name NVARCHAR(40) NOT NULL," + " user_id NVARCHAR(64) NOT NULL," + " score INT NOT NULL," + " moves INT NOT NULL," + " time_seconds INT NOT NULL," + " difficulty NVARCHAR(16) NOT NULL," + " is_perfect BIT NOT NULL DEFAULT 0," + " created_at DATETIME NOT NULL" + ");"; using (var cmd = new SqlCommand(sql, conn)) { cmd.ExecuteNonQuery(); } } // --- GET : renvoie le classement ---------------------------------------- void HandleGet(HttpContext ctx, SqlConnection conn) { var difficulty = (ctx.Request.QueryString["difficulty"] ?? "all").Trim(); int limit; if (!int.TryParse(ctx.Request.QueryString["limit"], out limit)) limit = TopN; if (limit < 1) limit = 1; if (limit > TopN) limit = TopN; var rows = new List>(); if (difficulty == "all") { foreach (var diff in Difficulties) rows.AddRange(FetchTop(conn, diff, TopN)); rows.Sort(CompareRows); if (rows.Count > limit) rows = rows.GetRange(0, limit); } else { if (Array.IndexOf(Difficulties, difficulty) < 0) { Fail(ctx, 400, "Difficulté inconnue."); return; } rows = FetchTop(conn, difficulty, limit); } Write(ctx, new Dictionary { { "ok", true }, { "scores", rows } }); } // --- POST : enregistre un score puis élague au top N -------------------- void HandlePost(HttpContext ctx, SqlConnection conn) { string body; using (var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8)) body = reader.ReadToEnd(); Dictionary data = null; try { data = new JavaScriptSerializer().Deserialize>(body); } catch { data = null; } if (data == null) { Fail(ctx, 400, "Corps JSON invalide."); return; } var difficulty = Str(data, "difficulty"); if (Array.IndexOf(Difficulties, difficulty) < 0) { Fail(ctx, 400, "Difficulté inconnue."); return; } // Nettoyage / bornage des entrées var playerName = Str(data, "playerName").Trim(); playerName = StripControlChars(playerName); if (playerName.Length == 0) playerName = "Anonyme"; if (playerName.Length > 40) playerName = playerName.Substring(0, 40); var userId = OnlyIdChars(Str(data, "userId")); if (userId.Length > 64) userId = userId.Substring(0, 64); if (userId.Length == 0) userId = "anon"; int score = Clamp(Int(data, "score"), 0, 100000000); int moves = Clamp(Int(data, "moves"), 0, 100000); int time = Clamp(Int(data, "timeSeconds"), 0, 1000000); int isPerfect = Bool(data, "isPerfect") ? 1 : 0; int insertedId; using (var cmd = new SqlCommand( "INSERT INTO memory_scores " + "(player_name, user_id, score, moves, time_seconds, difficulty, is_perfect, created_at) " + "OUTPUT INSERTED.id " + "VALUES (@name, @uid, @score, @moves, @time, @diff, @perfect, GETUTCDATE())", conn)) { cmd.Parameters.Add("@name", SqlDbType.NVarChar, 40).Value = playerName; cmd.Parameters.Add("@uid", SqlDbType.NVarChar, 64).Value = userId; cmd.Parameters.Add("@score", SqlDbType.Int).Value = score; cmd.Parameters.Add("@moves", SqlDbType.Int).Value = moves; cmd.Parameters.Add("@time", SqlDbType.Int).Value = time; cmd.Parameters.Add("@diff", SqlDbType.NVarChar, 16).Value = difficulty; cmd.Parameters.Add("@perfect", SqlDbType.Bit).Value = isPerfect; insertedId = (int)cmd.ExecuteScalar(); } // Ne conserver que le top N pour ce mode using (var cmd = new SqlCommand( "WITH ranked AS (" + " SELECT id, ROW_NUMBER() OVER (" + " PARTITION BY difficulty ORDER BY score DESC, time_seconds ASC, id ASC) AS rn" + " FROM memory_scores WHERE difficulty = @diff" + ") DELETE FROM memory_scores WHERE id IN (SELECT id FROM ranked WHERE rn > @topN)", conn)) { cmd.Parameters.Add("@diff", SqlDbType.NVarChar, 16).Value = difficulty; cmd.Parameters.Add("@topN", SqlDbType.Int).Value = TopN; cmd.ExecuteNonQuery(); } // Le score inséré a-t-il survécu à l'élagage ? Quel rang ? var top = FetchTop(conn, difficulty, TopN); int? rank = null; for (int i = 0; i < top.Count; i++) { if ((int)top[i]["id"] == insertedId) { rank = i + 1; break; } } Write(ctx, new Dictionary { { "ok", true }, { "saved", true }, { "isTop", rank.HasValue }, { "rank", rank.HasValue ? (object)rank.Value : null } }); } // --- Accès données ------------------------------------------------------- List> FetchTop(SqlConnection conn, string difficulty, int limit) { var result = new List>(); using (var cmd = new SqlCommand( "SELECT TOP (@limit) id, player_name, user_id, score, moves, time_seconds, " + "difficulty, is_perfect, created_at " + "FROM memory_scores WHERE difficulty = @diff " + "ORDER BY score DESC, time_seconds ASC, id ASC", conn)) { cmd.Parameters.Add("@limit", SqlDbType.Int).Value = limit; cmd.Parameters.Add("@diff", SqlDbType.NVarChar, 16).Value = difficulty; using (var r = cmd.ExecuteReader()) { while (r.Read()) { result.Add(new Dictionary { { "id", (int)r["id"] }, { "playerName", r["player_name"].ToString() }, { "userId", r["user_id"].ToString() }, { "score", (int)r["score"] }, { "moves", (int)r["moves"] }, { "timeSeconds", (int)r["time_seconds"] }, { "difficulty", r["difficulty"].ToString() }, { "isPerfect", Convert.ToBoolean(r["is_perfect"]) }, { "date", ((DateTime)r["created_at"]).ToString("yyyy-MM-ddTHH:mm:ss") + "Z" } }); } } } return result; } int CompareRows(Dictionary a, Dictionary b) { int sa = (int)a["score"], sb = (int)b["score"]; if (sa != sb) return sb - sa; return (int)a["timeSeconds"] - (int)b["timeSeconds"]; } // --- Sérialisation / helpers -------------------------------------------- void Write(HttpContext ctx, object payload) { // On retire la clé "id" (interne) des lignes avant l'envoi. var scrubbed = Scrub(payload); ctx.Response.Write(new JavaScriptSerializer().Serialize(scrubbed)); } object Scrub(object payload) { var dict = payload as Dictionary; if (dict != null && dict.ContainsKey("scores")) { var list = dict["scores"] as List>; if (list != null) foreach (var row in list) row.Remove("id"); } return payload; } void Fail(HttpContext ctx, int status, string message) { ctx.Response.StatusCode = status; ctx.Response.Write(new JavaScriptSerializer().Serialize( new Dictionary { { "ok", false }, { "error", message } })); } static string Str(Dictionary d, string key) { object v; return (d.TryGetValue(key, out v) && v != null) ? v.ToString() : ""; } static int Int(Dictionary d, string key) { object v; int n; if (d.TryGetValue(key, out v) && v != null && int.TryParse(v.ToString(), out n)) return n; return 0; } static bool Bool(Dictionary d, string key) { object v; if (!d.TryGetValue(key, out v) || v == null) return false; if (v is bool) return (bool)v; var s = v.ToString().ToLowerInvariant(); return s == "true" || s == "1"; } static int Clamp(int value, int min, int max) { if (value < min) return min; if (value > max) return max; return value; } static string StripControlChars(string s) { var sb = new StringBuilder(s.Length); foreach (var c in s) if (!char.IsControl(c)) sb.Append(c); return sb.ToString(); } static string OnlyIdChars(string s) { var sb = new StringBuilder(s.Length); foreach (var c in s) if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') sb.Append(c); return sb.ToString(); } }