diff --git a/code/__DEFINES/text.dm b/code/__DEFINES/text.dm index 58c24747e01..58ab98b132b 100644 --- a/code/__DEFINES/text.dm +++ b/code/__DEFINES/text.dm @@ -3,3 +3,9 @@ /// Macro from Lummox used to get height from a MeasureText proc #define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1)) + +/// Removes characters incompatible with file names. +#define SANITIZE_FILENAME(text) (GLOB.filename_forbidden_chars.Replace(text, "")) + +/// Simply removes the < and > characters, and limits the length of the message. +#define STRIP_HTML_SIMPLE(text, limit) (GLOB.angular_brackets.Replace(copytext(text, 1, limit), "")) diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 48362120ec6..775086f40d3 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1120,7 +1120,7 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0 if (!isicon(I)) if (isfile(thing)) //special snowflake - var/name = sanitize_filename("[generate_asset_name(thing)].png") + var/name = SANITIZE_FILENAME("[generate_asset_name(thing)].png") if (!SSassets.cache[name]) SSassets.transport.register_asset(name, thing) for (var/thing2 in targets) diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index ed0c6b1bf34..f86d92705ae 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -20,28 +20,6 @@ * Text sanitization */ -//Simply removes < and > and limits the length of the message -/proc/strip_html_simple(t,limit=MAX_MESSAGE_LEN) - var/list/strip_chars = list("<",">") - t = copytext(t,1,limit) - for(var/char in strip_chars) - var/index = findtext(t, char) - while(index) - t = copytext(t, 1, index) + copytext(t, index+1) - index = findtext(t, char) - return t - -//Removes a few problematic characters -/proc/sanitize_simple(t,list/repl_chars = list("\n"="#","\t"="#")) - for(var/char in repl_chars) - var/index = findtext(t, char) - while(index) - t = copytext(t, 1, index) + repl_chars[char] + copytext(t, index + length(char)) - index = findtext(t, char, index + length(char)) - return t - -/proc/sanitize_filename(t) - return sanitize_simple(t, list("\n"="", "\t"="", "/"="", "\\"="", "?"="", "%"="", "*"="", ":"="", "|"="", "\""="", "<"="", ">"="")) ///returns nothing with an alert instead of the message if it contains something in the ic filter, and sanitizes normally if the name is fine. It returns nothing so it backs out of the input the same way as if you had entered nothing. /proc/sanitize_name(t,allow_numbers=FALSE) @@ -54,19 +32,22 @@ return "" return sanitize(r) -//Runs byond's sanitization proc along-side sanitize_simple -/proc/sanitize(t,list/repl_chars = null) - return html_encode(sanitize_simple(t,repl_chars)) -//Runs sanitize and strip_html_simple -//I believe strip_html_simple() is required to run first to prevent '<' from displaying as '<' after sanitize() calls byond's html_encode() -/proc/strip_html(t,limit=MAX_MESSAGE_LEN) - return copytext((sanitize(strip_html_simple(t))),1,limit) +/// Runs byond's html encoding sanitization proc, after replacing new-lines and tabs for the # character. +/proc/sanitize(text) + var/static/regex/regex = regex(@"[\n\t]", "g") + return html_encode(regex.Replace(text, "#")) + + +/// Runs STRIP_HTML_SIMPLE and sanitize. +/proc/strip_html(text, limit = MAX_MESSAGE_LEN) + return sanitize(STRIP_HTML_SIMPLE(text, limit)) + + +/// Runs STRIP_HTML_SIMPLE and byond's sanitization proc. +/proc/adminscrub(text, limit = MAX_MESSAGE_LEN) + return html_encode(STRIP_HTML_SIMPLE(text, limit)) -//Runs byond's sanitization proc along-side strip_html_simple -//I believe strip_html_simple() is required to run first to prevent '<' from displaying as '<' that html_encode() would cause -/proc/adminscrub(t,limit=MAX_MESSAGE_LEN) - return copytext((html_encode(strip_html_simple(t))),1,limit) /** * Perform a whitespace cleanup on the text, similar to what HTML renderers do @@ -91,31 +72,34 @@ return t -//Returns null if there is any bad text in the string + +/** + * Returns the text if properly formatted, or null else. + * + * Things considered improper: + * * Larger than max_length. + * * Presence of non-ASCII characters if asci_only is set to TRUE. + * * Only whitespaces, tabs and/or line breaks in the text. + * * Presence of the <, >, \ and / characters. + * * Presence of ASCII special control characters (horizontal tab and new line not included). + * */ /proc/reject_bad_text(text, max_length = 512, ascii_only = TRUE) - var/char_count = 0 - var/non_whitespace = FALSE - var/lenbytes = length(text) - var/char = "" - for(var/i = 1, i <= lenbytes, i += length(char)) - char = text[i] - char_count++ - if(char_count > max_length) - return - switch(text2ascii(char)) - if(62, 60, 92, 47) // <, >, \, / - return - if(0 to 31) - return - if(32) - continue - if(127 to INFINITY) - if(ascii_only) - return - else - non_whitespace = TRUE - if(non_whitespace) - return text //only accepts the text if it has some non-spaces + if(ascii_only) + if(length(text) > max_length) + return null + var/static/regex/non_ascii = regex(@"[^\x20-\x7E\t\n]") + if(non_ascii.Find(text)) + return null + else if(length_char(text) > max_length) + return null + var/static/regex/non_whitespace = regex(@"\S") + if(!non_whitespace.Find(text)) + return null + var/static/regex/bad_chars = regex(@"[\\<>/\x00-\x08\x11-\x1F]") + if(bad_chars.Find(text)) + return null + return text + /// Used to get a properly sanitized input, of max_length /// no_trim is self explanatory but it prevents the input from being trimed if you intend to parse newlines or whitespace. diff --git a/code/_globalvars/regexes.dm b/code/_globalvars/regexes.dm index a2e317e60d4..cfe7586044d 100644 --- a/code/_globalvars/regexes.dm +++ b/code/_globalvars/regexes.dm @@ -9,3 +9,10 @@ GLOBAL_DATUM_INIT(is_color, /regex, regex("^#\[0-9a-fA-F]{6}$")) //finds text strings recognized as links on discord. Mainly used to stop embedding. GLOBAL_DATUM_INIT(has_discord_embeddable_links, /regex, regex("(https?://\[^\\s|<\]{2,})")) + +//All < and > characters +GLOBAL_DATUM_INIT(angular_brackets, /regex, regex(@"[<>]", "g")) + +//All characters forbidden by filenames: ", \, \n, \t, /, ?, %, *, :, |, <, > +GLOBAL_DATUM_INIT(filename_forbidden_chars, /regex, regex(@{""|[\\\n\t/?%*:|<>]"}, "g")) +// had to use the OR operator for quotes instead of putting them in the character class because it breaks the syntax highlighting otherwise. diff --git a/code/game/objects/structures/artstuff.dm b/code/game/objects/structures/artstuff.dm index 9e85f13aa07..289e9131b4e 100644 --- a/code/game/objects/structures/artstuff.dm +++ b/code/game/objects/structures/artstuff.dm @@ -376,7 +376,7 @@ /obj/structure/sign/painting/proc/save_persistent() if(!persistence_id || !current_canvas || current_canvas.no_save) return - if(sanitize_filename(persistence_id) != persistence_id) + if(SANITIZE_FILENAME(persistence_id) != persistence_id) stack_trace("Invalid persistence_id - [persistence_id]") return if(!current_canvas.painting_name) diff --git a/code/modules/asset_cache/asset_list.dm b/code/modules/asset_cache/asset_list.dm index f88f57eacc3..89eec3e700d 100644 --- a/code/modules/asset_cache/asset_list.dm +++ b/code/modules/asset_cache/asset_list.dm @@ -261,7 +261,7 @@ GLOBAL_LIST_EMPTY(asset_datums) var/item_filename /datum/asset/changelog_item/New(date) - item_filename = sanitize_filename("[date].yml") + item_filename = SANITIZE_FILENAME("[date].yml") SSassets.transport.register_asset(item_filename, file("html/changelogs/archive/" + item_filename)) /datum/asset/changelog_item/send(client) @@ -302,7 +302,7 @@ GLOBAL_LIST_EMPTY(asset_datums) continue asset = fcopy_rsc(asset) //dedupe var/prefix2 = (directions.len > 1) ? "[dir2text(direction)]." : "" - var/asset_name = sanitize_filename("[prefix].[prefix2][icon_state_name].png") + var/asset_name = SANITIZE_FILENAME("[prefix].[prefix2][icon_state_name].png") if (generic_icon_names) asset_name = "[generate_asset_name(asset)].png"