Index: ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js (revision 19880) +++ ps/trunk/binaries/data/mods/public/gui/common/functions_utility.js (revision 19881) @@ -1,260 +1,242 @@ /** * Used by notifyUser() to limit the number of pings */ var g_LastNickNotification = -1; // Get list of XML files in pathname with recursion, excepting those starting with _ function getXMLFileList(pathname) { var files = Engine.BuildDirEntList(pathname, "*.xml", true); var result = []; // Get only subpath from filename and discard extension for (var i = 0; i < files.length; ++i) { var file = files[i]; file = file.substring(pathname.length, file.length-4); // Split path into directories so we can check for beginning _ character var tokens = file.split("/"); if (tokens[tokens.length-1][0] != "_") result.push(file); } return result; } function getJSONFileList(pathname) { // Remove the path and extension from each name, since we just want the filename return Engine.BuildDirEntList(pathname, "*.json", false).map( filename => filename.substring(pathname.length, filename.length-5)); } // A sorting function for arrays of objects with 'name' properties, ignoring case function sortNameIgnoreCase(x, y) { var lowerX = x.name.toLowerCase(); var lowerY = y.name.toLowerCase(); if (lowerX < lowerY) return -1; else if (lowerX > lowerY) return 1; else return 0; } /** * Escape tag start and escape characters, so users cannot use special formatting. * Also limit string length to 256 characters (not counting escape characters). */ function escapeText(text, limitLength = true) { if (!text) return text; if (limitLength) text = text.substr(0, 255); return text.replace(/\\/g, "\\\\").replace(/\[/g, "\\["); } function unescapeText(text) { if (!text) return text; return text.replace(/\\\\/g, "\\").replace(/\\\[/g, "\["); } /** * Merge players by team to remove duplicate Team entries, thus reducing the packet size of the lobby report. */ function playerDataToStringifiedTeamList(playerData) { let teamList = {}; for (let pData of playerData) { let team = pData.Team === undefined ? -1 : pData.Team; if (!teamList[team]) teamList[team] = []; teamList[team].push(pData); delete teamList[team].Team; } return escapeText(JSON.stringify(teamList), false); } function stringifiedTeamListToPlayerData(stringifiedTeamList) { let teamList = JSON.parse(unescapeText(stringifiedTeamList)); let playerData = []; for (let team in teamList) for (let pData of teamList[team]) { pData.Team = team; playerData.push(pData); } return playerData; } function translateMapTitle(mapTitle) { return mapTitle == "random" ? translateWithContext("map selection", "Random") : translate(mapTitle); } /** * Convert time in milliseconds to [hh:]mm:ss string representation. * @param time Time period in milliseconds (integer) * @return String representing time period */ function timeToString(time) { return Engine.FormatMillisecondsIntoDateStringGMT(time, time < 1000 * 60 * 60 ? translate("mm:ss") : translate("HH:mm:ss")); } function removeDupes(array) { // loop backwards to make splice operations cheaper var i = array.length; while (i--) { if (array.indexOf(array[i]) != i) array.splice(i, 1); } } -// Filter out conflicting characters and limit the length of a given name. -// @param name Name to be filtered. -// @param stripUnicode Whether or not to remove unicode characters. -// @param stripSpaces Whether or not to remove whitespace. -function sanitizePlayerName(name, stripUnicode, stripSpaces) -{ - // We delete the '[', ']' characters (GUI tags) and delete the ',' characters (player name separators) by default. - var sanitizedName = name.replace(/[\[\],]/g, ""); - // Optionally strip unicode - if (stripUnicode) - sanitizedName = sanitizedName.replace(/[^\x20-\x7f]/g, ""); - // Optionally strip whitespace - if (stripSpaces) - sanitizedName = sanitizedName.replace(/\s/g, ""); - // Limit the length to 20 characters - return sanitizedName.substr(0,20); -} - function singleplayerName() { return Engine.ConfigDB_GetValue("user", "playername.singleplayer") || Engine.GetSystemUsername(); } function multiplayerName() { return Engine.ConfigDB_GetValue("user", "playername.multiplayer") || Engine.GetSystemUsername(); } function tryAutoComplete(text, autoCompleteList) { if (!text.length) return text; var wordSplit = text.split(/\s/g); if (!wordSplit.length) return text; var lastWord = wordSplit.pop(); if (!lastWord.length) return text; for (var word of autoCompleteList) { if (word.toLowerCase().indexOf(lastWord.toLowerCase()) != 0) continue; text = wordSplit.join(" "); if (text.length > 0) text += " "; text += word; break; } return text; } function autoCompleteNick(guiObject, playernames) { let text = guiObject.caption; if (!text.length) return; let bufferPosition = guiObject.buffer_position; let textTillBufferPosition = text.substring(0, bufferPosition); let newText = tryAutoComplete(textTillBufferPosition, playernames); guiObject.caption = newText + text.substring(bufferPosition); guiObject.buffer_position = bufferPosition + (newText.length - textTillBufferPosition.length); } function clearChatMessages() { g_ChatMessages.length = 0; Engine.GetGUIObjectByName("chatText").caption = ""; try { for (let timer of g_ChatTimers) clearTimeout(timer); g_ChatTimers.length = 0; } catch (e) { } } /** * Plays a sound if user's nick is mentioned in chat */ function notifyUser(userName, msgText) { if (Engine.ConfigDB_GetValue("user", "sound.notify.nick") != "true" || msgText.toLowerCase().indexOf(userName.toLowerCase()) == -1) return; let timeNow = Date.now(); if (!g_LastNickNotification || timeNow > g_LastNickNotification + 3000) Engine.PlayUISound("audio/interface/ui/chat_alert.ogg", false); g_LastNickNotification = timeNow; } /** * Horizontally spaces objects within a parent * * @param margin The gap, in px, between the objects */ function horizontallySpaceObjects(parentName, margin=0) { let objects = Engine.GetGUIObjectByName(parentName).children; for (let i = 0; i < objects.length; ++i) { let size = objects[i].size; let width = size.right - size.left; size.left = i * (width + margin) + margin; size.right = (i + 1) * (width + margin); objects[i].size = size; } } /** * Hide all children after a certain index */ function hideRemaining(parentName, start = 0) { let objects = Engine.GetGUIObjectByName(parentName).children; for (let i = start; i < objects.length; ++i) objects[i].hidden = true; } Index: ps/trunk/binaries/data/mods/public/gui/lobby/prelobby.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/prelobby.js (revision 19880) +++ ps/trunk/binaries/data/mods/public/gui/lobby/prelobby.js (revision 19881) @@ -1,271 +1,274 @@ var g_LobbyIsConnecting = false; var g_EncryptedPassword = ""; var g_PasswordInputIsHidden = false; var g_TermsOfServiceRead = false; var g_TermsOfUseRead = false; var g_DisplayingSystemMessage = false; function init() { g_EncryptedPassword = Engine.ConfigDB_GetValue("user", "lobby.password"); if (Engine.ConfigDB_GetValue("user", "lobby.login") && g_EncryptedPassword) switchPage("connect"); } function lobbyStop() { Engine.GetGUIObjectByName("feedback").caption = ""; if (!g_LobbyIsConnecting) return; g_LobbyIsConnecting = false; Engine.StopXmppClient(); } function lobbyStartConnect() { if (g_LobbyIsConnecting) return; if (Engine.HasXmppClient()) Engine.StopXmppClient(); let username = Engine.GetGUIObjectByName("connectUsername").caption; let password = Engine.GetGUIObjectByName("connectPassword").caption; let feedback = Engine.GetGUIObjectByName("feedback"); let room = Engine.ConfigDB_GetValue("user", "lobby.room"); let history = Number(Engine.ConfigDB_GetValue("user", "lobby.history")); feedback.caption = translate("Connecting..."); // If they enter a different password, re-encrypt. if (password != g_EncryptedPassword.substring(0, 10)) g_EncryptedPassword = Engine.EncryptPassword(password, username); // We just use username as nick for simplicity. Engine.StartXmppClient(username, g_EncryptedPassword, room, username, history); g_LobbyIsConnecting = true; Engine.ConnectXmppClient(); } function lobbyStartRegister() { if (g_LobbyIsConnecting) return; if (Engine.HasXmppClient()) Engine.StopXmppClient(); let account = Engine.GetGUIObjectByName("registerUsername").caption; let password = Engine.GetGUIObjectByName("registerPassword").caption; let feedback = Engine.GetGUIObjectByName("feedback"); feedback.caption = translate("Registering..."); g_EncryptedPassword = Engine.EncryptPassword(password, account); Engine.StartRegisterXmppClient(account, g_EncryptedPassword); g_LobbyIsConnecting = true; Engine.ConnectXmppClient(); } function onTick() { let pageRegisterHidden = Engine.GetGUIObjectByName("pageRegister").hidden; let username = Engine.GetGUIObjectByName(pageRegisterHidden ? "connectUsername" : "registerUsername").caption; let password = Engine.GetGUIObjectByName(pageRegisterHidden ? "connectPassword" : "registerPassword").caption; let passwordAgain = Engine.GetGUIObjectByName("registerPasswordAgain").caption; let agreeTerms = Engine.GetGUIObjectByName("registerAgreeTerms"); let feedback = Engine.GetGUIObjectByName("feedback"); let continueButton = Engine.GetGUIObjectByName("continue"); // Do not change feedback while connecting. if (g_LobbyIsConnecting) {} // Do not show feedback on the welcome screen. else if (!Engine.GetGUIObjectByName("pageWelcome").hidden) { feedback.caption = ""; g_DisplayingSystemMessage = false; } // Check that they entered a username. else if (!username) { continueButton.enabled = false; feedback.caption = translate("Please enter your username"); } - // Check that they are using a valid username. - else if (username != sanitizePlayerName(username, true, true)) + + // Prevent registation (but not login) with non-alphanumerical characters + if (!pageRegisterHidden && (!username.match(/^[a-z0-9._-]*$/i) || username.length > 20)) { continueButton.enabled = false; feedback.caption = translate("Usernames can't contain \\[, ], unicode, whitespace, or commas"); } // Check that they entered a password. else if (!password) { continueButton.enabled = false; feedback.caption = translate("Please enter your password"); } // Allow them to connect if tests pass up to this point. else if (pageRegisterHidden) { if (!g_DisplayingSystemMessage) feedback.caption = ""; continueButton.enabled = true; } // Check that they entered their password again. else if (!passwordAgain) { continueButton.enabled = false; feedback.caption = translate("Please enter your password again"); } // Check that the passwords match. else if (passwordAgain != password) { continueButton.enabled = false; feedback.caption = translate("Passwords do not match"); } // Check that they read the Terms of Service. else if (!g_TermsOfServiceRead) { continueButton.enabled = false; feedback.caption = translate("Please read the Terms of Service"); } // Check that they read the Terms of Use. else if (!g_TermsOfUseRead) { continueButton.enabled = false; feedback.caption = translate("Please read the Terms of Use"); } // Check that they agree to the terms of service and use. else if (!agreeTerms.checked) { continueButton.enabled = false; feedback.caption = translate("Please agree to the Terms of Service and Terms of Use"); } // Allow them to register. else { if (!g_DisplayingSystemMessage) feedback.caption = ""; continueButton.enabled = true; } // Handle queued messages from the XMPP client (if running and if any) let message; while ((message = Engine.LobbyGuiPollMessage()) != undefined) { // TODO: Properly deal with unrecognized messages if (message.type != "system" || !message.level) continue; g_LobbyIsConnecting = false; switch(message.level) { case "error": case "disconnected": { Engine.GetGUIObjectByName("feedback").caption = message.text || translate("Unknown error. This usually occurs because the same IP address is not allowed to register more than one account within one hour."); g_DisplayingSystemMessage = true; Engine.StopXmppClient(); break; } case "registered": Engine.GetGUIObjectByName("feedback").caption = translate("Registered"); g_DisplayingSystemMessage = true; Engine.GetGUIObjectByName("connectUsername").caption = username; Engine.GetGUIObjectByName("connectPassword").caption = password; Engine.StopXmppClient(); switchPage("connect"); break; case "connected": { Engine.PopGuiPage(); Engine.SwitchGuiPage("page_lobby.xml"); - Engine.ConfigDB_CreateValue("user", "playername.multiplayer", sanitizePlayerName(username, true, true)); - Engine.ConfigDB_WriteValueToFile("user", "playername.multiplayer", sanitizePlayerName(username, true, true), "config/user.cfg"); + Engine.ConfigDB_CreateValue("user", "playername.multiplayer", username); + Engine.ConfigDB_WriteValueToFile("user", "playername.multiplayer", username, "config/user.cfg"); Engine.ConfigDB_CreateValue("user", "lobby.login", username); Engine.ConfigDB_WriteValueToFile("user", "lobby.login", username, "config/user.cfg"); // We only store the encrypted password, so make sure to re-encrypt it if changed before saving. if (password != g_EncryptedPassword.substring(0, 10)) g_EncryptedPassword = Engine.EncryptPassword(password, username); Engine.ConfigDB_CreateValue("user", "lobby.password", g_EncryptedPassword); Engine.ConfigDB_WriteValueToFile("user", "lobby.password", g_EncryptedPassword, "config/user.cfg"); break; } } } } function switchPage(page) { // First hide everything. if (!Engine.GetGUIObjectByName("pageWelcome").hidden) { Engine.GetGUIObjectByName("pageWelcome").hidden = true; } else if (!Engine.GetGUIObjectByName("pageRegister").hidden) { Engine.GetGUIObjectByName("pageRegister").hidden = true; Engine.GetGUIObjectByName("continue").hidden = true; let dialog = Engine.GetGUIObjectByName("dialog"); let newSize = dialog.size; newSize.bottom -= 150; dialog.size = newSize; } else if (!Engine.GetGUIObjectByName("pageConnect").hidden) { Engine.GetGUIObjectByName("pageConnect").hidden = true; Engine.GetGUIObjectByName("continue").hidden = true; } // Then show appropriate page. switch(page) { case "welcome": Engine.GetGUIObjectByName("pageWelcome").hidden = false; break; case "register": { let dialog = Engine.GetGUIObjectByName("dialog"); let newSize = dialog.size; newSize.bottom += 150; dialog.size = newSize; Engine.GetGUIObjectByName("pageRegister").hidden = false; Engine.GetGUIObjectByName("continue").caption = translate("Register"); Engine.GetGUIObjectByName("continue").hidden = false; break; } case "connect": Engine.GetGUIObjectByName("pageConnect").hidden = false; Engine.GetGUIObjectByName("continue").caption = translate("Connect"); Engine.GetGUIObjectByName("continue").hidden = false; break; } } function openTermsOfService() { g_TermsOfServiceRead = true; Engine.PushGuiPage("page_manual.xml", { "page": "lobby/Terms_of_Service", "title": translate("Terms of Service"), }); } function openTermsOfUse() { g_TermsOfUseRead = true; Engine.PushGuiPage("page_manual.xml", { "page": "lobby/Terms_of_Use", "title": translate("Terms of Use"), }); } function prelobbyCancel() { lobbyStop(); + Engine.GetGUIObjectByName("feedback").caption = ""; + if (Engine.GetGUIObjectByName("pageWelcome").hidden) switchPage("welcome"); else Engine.PopGuiPage(); }