/* * Holds procs designed to help with filtering text * Contains groups: * SQL sanitization * Text sanitization * Text searches * Text modification * Misc */ /* * Text sanitization */ // Can be used almost the same way as normal input for text /proc/clean_input(Message, Title, Default, mob/user=usr) var/txt = input(user, Message, Title, html_decode(Default)) as text | null if(txt) return html_encode(txt) //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) t = replacetext(t, char, repl_chars[char]) return t //Runs byond's sanitization proc along-side sanitize_simple /proc/sanitize(t, list/repl_chars = null) return html_encode(sanitize_simple(t,repl_chars)) // Gut ANYTHING that isnt alphanumeric, or brackets /proc/paranoid_sanitize(t) var/regex/alphanum_only = regex("\[^a-zA-Z0-9# ,.?!:;()]", "g") return alphanum_only.Replace(t, "#") // Less agressive, to allow discord features, such as <>, / and @ /proc/not_as_paranoid_sanitize(t) var/regex/alphanum_slashes_only = regex("\[^a-zA-Z0-9# ,.?!:;()/<>@]", "g") return alphanum_slashes_only.Replace(t, "#") //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) // Used to get a properly sanitized multiline input, of max_length /proc/stripped_multiline_input(mob/user, message = "", title = "", default = "", max_length=MAX_MESSAGE_LEN, no_trim=FALSE) var/name = input(user, message, title, html_decode(default)) as message|null if(no_trim) return copytext(html_encode(name), 1, max_length) else return trim(html_encode(name), max_length) //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) //Returns null if there is any bad text in the string /proc/reject_bad_text(text, max_length=512) if(length(text) > max_length) return //message too long var/non_whitespace = 0 for(var/i=1, i<=length(text), i++) switch(text2ascii(text,i)) if(62,60,92,47) return //rejects the text if it contains these bad characters: <, >, \ or / if(127 to 255) return //rejects weird letters like � if(0 to 31) return //more weird stuff if(32) continue //whitespace else non_whitespace = 1 if(non_whitespace) return text //only accepts the text if it has some non-spaces // Used to get a sanitized input. /proc/stripped_input(mob/user, message = "", title = "", default = "", max_length=MAX_MESSAGE_LEN, no_trim=FALSE) var/name = sanitize(input(user, message, title, html_decode(default)) as text|null) if(!no_trim) name = trim(name) //trim is "outside" because html_encode can expand single symbols into multiple symbols (such as turning < into <) return copytext(name, 1, max_length) // Uses client.typing to check if the popup should appear or not /proc/typing_input(mob/user, message = "", title = "", default = "") if(user.client.checkTyping()) // Prevent double windows return null var/client/C = user.client // Save it in a var in case the client disconnects from the mob C.typing = TRUE var/msg = input(user, message, title, html_decode(default)) as text|null if(!C) return null C.typing = FALSE if(!user || C != user.client) // User got out of the mob for some reason or the mob is gone return null return msg //Filters out undesirable characters from names /proc/reject_bad_name(t_in, allow_numbers=0, max_length=MAX_NAME_LEN) // Decode so that names with characters like < are still rejected t_in = html_decode(t_in) if(!t_in || length(t_in) > max_length) return //Rejects the input if it is null or if it is longer than the max length allowed var/number_of_alphanumeric = 0 var/last_char_group = 0 var/t_out = "" for(var/i=1, i<=length(t_in), i++) var/ascii_char = text2ascii(t_in,i) switch(ascii_char) // A .. Z if(65 to 90) //Uppercase Letters t_out += ascii2text(ascii_char) number_of_alphanumeric++ last_char_group = 4 // a .. z if(97 to 122) //Lowercase Letters if(last_char_group<2) t_out += ascii2text(ascii_char-32) //Force uppercase first character else t_out += ascii2text(ascii_char) number_of_alphanumeric++ last_char_group = 4 // 0 .. 9 if(48 to 57) //Numbers if(!last_char_group) continue //suppress at start of string if(!allow_numbers) continue t_out += ascii2text(ascii_char) number_of_alphanumeric++ last_char_group = 3 // ' - . , if(39, 45, 46, 44) //Common name punctuation if(!last_char_group) continue t_out += ascii2text(ascii_char) last_char_group = 2 // ~ | @ : # $ % & * + ! if(126, 124, 64, 58, 35, 36, 37, 38, 42, 43, 33) //Other symbols that we'll allow (mainly for AI) if(!last_char_group) continue //suppress at start of string if(!allow_numbers) continue t_out += ascii2text(ascii_char) last_char_group = 2 //Space if(32) if(last_char_group <= 1) continue //suppress double-spaces and spaces at start of string t_out += ascii2text(ascii_char) last_char_group = 1 else return if(number_of_alphanumeric < 2) return //protects against tiny names like "A" and also names like "' ' ' ' ' ' ' '" if(last_char_group == 1) t_out = copytext(t_out,1,length(t_out)) //removes the last character (in this case a space) for(var/bad_name in list("space","floor","wall","r-wall","monkey","unknown","inactive ai","plating")) //prevents these common metagamey names if(cmptext(t_out,bad_name)) return //(not case sensitive) return t_out //checks text for html tags //if tag is not in whitelist (var/list/paper_tag_whitelist in global.dm) //relpaces < with < /proc/checkhtml(t) t = sanitize_simple(t, list(""=".")) var/p = findtext(t,"<",1) while(p) //going through all the tags var/start = p++ var/tag = copytext(t,p, p+1) if(tag != "/") while(reject_bad_text(copytext(t, p, p+1), 1)) tag = copytext(t,start, p) p++ tag = copytext(t,start+1, p) if(!(tag in GLOB.paper_tag_whitelist)) //if it's unkown tag, disarming it t = copytext(t,1,start-1) + "<" + copytext(t,start+1) p = findtext(t,"<",p) return t /* * Text searches */ //Checks the beginning of a string for a specified sub-string //Returns the position of the substring or 0 if it was not found /proc/dd_hasprefix(text, prefix) var/start = 1 var/end = length(prefix) + 1 return findtext(text, prefix, start, end) //Checks the beginning of a string for a specified sub-string. This proc is case sensitive //Returns the position of the substring or 0 if it was not found /proc/dd_hasprefix_case(text, prefix) var/start = 1 var/end = length(prefix) + 1 return findtextEx(text, prefix, start, end) //Checks the end of a string for a specified substring. //Returns the position of the substring or 0 if it was not found /proc/dd_hassuffix(text, suffix) var/start = length(text) - length(suffix) if(start) return findtext(text, suffix, start, null) return //Checks the end of a string for a specified substring. This proc is case sensitive //Returns the position of the substring or 0 if it was not found /proc/dd_hassuffix_case(text, suffix) var/start = length(text) - length(suffix) if(start) return findtextEx(text, suffix, start, null) /* * Text modification */ // See bygex.dm /proc/replace_characters(t, list/repl_chars) for(var/char in repl_chars) t = replacetext(t, char, repl_chars[char]) return t //Strips the first char and returns it and the new string as a list /proc/strip_first(t) return list(copytext(t, 1, 2), copytext(t, 2, 0)) //Strips the last char and returns it and the new string as a list /proc/strip_last(t) return list(copytext(t, 1, length(t)), copytext(t, length(t))) //Adds 'u' number of zeros ahead of the text 't' /proc/add_zero(t, u) while(length(t) < u) t = "0[t]" return t //Adds 'u' number of spaces ahead of the text 't' /proc/add_lspace(t, u) while(length(t) < u) t = " [t]" return t //Adds 'u' number of spaces behind the text 't' /proc/add_tspace(t, u) while(length(t) < u) t = "[t] " return t //Returns a string with reserved characters and spaces before the first letter removed /proc/trim_left(text) for(var/i = 1 to length(text)) if(text2ascii(text, i) > 32) return copytext(text, i) return "" //Returns a string with reserved characters and spaces after the last letter removed /proc/trim_right(text) for(var/i = length(text), i > 0, i--) if(text2ascii(text, i) > 32) return copytext(text, 1, i + 1) return "" //Returns a string with reserved characters and spaces before the first word and after the last word removed. /proc/trim(text) return trim_left(trim_right(text)) //Returns a string with the first element of the string capitalized. /proc/capitalize(t as text) return uppertext(copytext(t, 1, 2)) + copytext(t, 2) //Centers text by adding spaces to either side of the string. /proc/dd_centertext(message, length) var/new_message = message var/size = length(message) var/delta = length - size if(size == length) return new_message if(size > length) return copytext(new_message, 1, length + 1) if(delta == 1) return new_message + " " if(delta % 2) new_message = " " + new_message delta-- var/spaces = add_lspace("",delta/2-1) return spaces + new_message + spaces //Limits the length of the text. Note: MAX_MESSAGE_LEN and MAX_NAME_LEN are widely used for this purpose /proc/dd_limittext(message, length) var/size = length(message) if(size <= length) return message return copytext(message, 1, length + 1) /proc/stringmerge(text, compare,replace = "*") //This proc fills in all spaces with the "replace" var (* by default) with whatever //is in the other string at the same spot (assuming it is not a replace char). //This is used for fingerprints var/newtext = text if(length(text) != length(compare)) return 0 for(var/i = 1, i < length(text), i++) var/a = copytext(text,i,i+1) var/b = copytext(compare,i,i+1) //if it isn't both the same letter, or if they are both the replacement character //(no way to know what it was supposed to be) if(a != b) if(a == replace) //if A is the replacement char newtext = copytext(newtext,1,i) + b + copytext(newtext, i+1) else if(b == replace) //if B is the replacement char newtext = copytext(newtext,1,i) + a + copytext(newtext, i+1) else //The lists disagree, Uh-oh! return 0 return newtext /proc/stringpercent(text, character = "*") //This proc returns the number of chars of the string that is the character //This is used for detective work to determine fingerprint completion. if(!text || !character) return 0 var/count = 0 for(var/i = 1, i <= length(text), i++) var/a = copytext(text,i,i+1) if(a == character) count++ return count /proc/reverse_text(text = "") var/new_text = "" for(var/i = length(text); i > 0; i--) new_text += copytext(text, i, i+1) return new_text //This proc strips html properly, but it's not lazy like the other procs. //This means that it doesn't just remove < and > and call it a day. //Also limit the size of the input, if specified. /proc/strip_html_properly(input, max_length = MAX_MESSAGE_LEN, allow_lines = 0) if(!input) return var/opentag = 1 //These store the position of < and > respectively. var/closetag = 1 while(1) opentag = findtext(input, "<") closetag = findtext(input, ">") if(closetag && opentag) if(closetag < opentag) input = copytext(input, (closetag + 1)) else input = copytext(input, 1, opentag) + copytext(input, (closetag + 1)) else if(closetag || opentag) if(opentag) input = copytext(input, 1, opentag) else input = copytext(input, (closetag + 1)) else break if(max_length) input = copytext(input,1,max_length) return sanitize(input, allow_lines ? list("\t" = " ") : list("\n" = " ", "\t" = " ")) /proc/trim_strip_html_properly(input, max_length = MAX_MESSAGE_LEN, allow_lines = 0) return trim(strip_html_properly(input, max_length, allow_lines)) //Used in preferences' SetFlavorText and human's set_flavor verb //Previews a string of len or less length /proc/TextPreview(string, len=40) if(length(string) <= len) if(!length(string)) return "\[...\]" else return html_encode(string) //NO DECODED HTML YOU CHUCKLEFUCKS else return "[copytext_preserve_html(string, 1, 37)]..." //alternative copytext() for encoded text, doesn't break html entities (" and other) /proc/copytext_preserve_html(text, first, last) return html_encode(copytext(html_decode(text), first, last)) //Run sanitize(), but remove <, >, " first to prevent displaying them as > < &34; in some places, after html_encode(). //Best used for sanitize object names, window titles. //If you have a problem with sanitize() in chat, when quotes and >, < are displayed as html entites - //this is a problem of double-encode(when & becomes &), use sanitize() with encode=0, but not the sanitizeSafe()! /proc/sanitizeSafe(input, max_length = MAX_MESSAGE_LEN, encode = 1, trim = 1, extra = 1) return sanitize(replace_characters(input, list(">"=" ","<"=" ", "\""="'")), max_length, encode, trim, extra) //Replace BYOND text macros with span classes for to_chat /proc/replace_text_macro(match, code, rest) var/regex/text_macro = new("(\\xFF.)(.*)$") return text_macro.Replace(rest, /proc/replace_text_macro) /proc/macro2html(text) var/static/regex/text_macro = new("(\\xFF.)(.*)$") return text_macro.Replace(text, /proc/replace_text_macro) /proc/dmm_encode(text) // First, go through and nix out any of our escape sequences so we don't leave ourselves open to some escape sequence attack // Some coder will probably despise me for this, years down the line var/list/repl_chars = list("#?qt;", "#?lbr;", "#?rbr;") for(var/char in repl_chars) var/index = findtext(text, char) var/keylength = length(char) while(index) stack_trace("Bad string given to dmm encoder! [text]") // Replace w/ underscore to prevent "{4;" from cheesing the radar // Should probably also use canon text replacing procs text = copytext(text, 1, index) + "_" + copytext(text, index+keylength) index = findtext(text, char) // Then, replace characters as normal var/list/repl_chars_2 = list("\"" = "#?qt;", "{" = "#?lbr;", "}" = "#?rbr;") for(var/char in repl_chars_2) var/index = findtext(text, char) var/keylength = length(char) while(index) text = copytext(text, 1, index) + repl_chars_2[char] + copytext(text, index+keylength) index = findtext(text, char) return text /proc/dmm_decode(text) // Replace what we extracted above var/list/repl_chars = list("#?qt;" = "\"", "#?lbr;" = "{", "#?rbr;" = "}") for(var/char in repl_chars) var/index = findtext(text, char) var/keylength = length(char) while(index) text = copytext(text, 1, index) + repl_chars[char] + copytext(text, index+keylength) index = findtext(text, char) return text //Checks if any of a given list of needles is in the haystack /proc/text_in_list(haystack, list/needle_list, start=1, end=0) for(var/needle in needle_list) if(findtext(haystack, needle, start, end)) return 1 return 0 //Like above, but case sensitive /proc/text_in_list_case(haystack, list/needle_list, start=1, end=0) for(var/needle in needle_list) if(findtextEx(haystack, needle, start, end)) return 1 return 0 // Pencode /proc/pencode_to_html(text, mob/user, obj/item/pen/P = null, format = 1, sign = 1, fields = 1, deffont = PEN_FONT, signfont = SIGNFONT, crayonfont = CRAYON_FONT, no_font = FALSE) text = replacetext(text, "\[b\]", "") text = replacetext(text, "\[/b\]", "") text = replacetext(text, "\[i\]", "") text = replacetext(text, "\[/i\]", "") text = replacetext(text, "\[u\]", "") text = replacetext(text, "\[/u\]", "") if(findtext(text, "\[signfont\]") || findtext(text, "\[/signfont\]")) // Make sure the text is there before giving off an error if(check_rights(R_EVENT)) text = replacetext(text, "\[signfont\]", "") text = replacetext(text, "\[/signfont\]", "") if(sign) text = replacetext(text, "\[sign\]", "[user ? user.real_name : "Anonymous"]") if(fields) text = replacetext(text, "\[field\]", "") if(format) text = replacetext(text, "\[h1\]", "
")
text = replacetext(text, "\[syndielogo\]", "​
")
text = replacetext(text, "\[time\]", "[station_time_timestamp()]") // TO DO
text = replacetext(text, "\[date\]", "[GLOB.current_date_string]")
text = replacetext(text, "\[station\]", "[SSmapping.map_datum.fluff_name]")
if(!no_font)
if(P)
text = "[text]"
else
text = "[text]"
text = copytext(text, 1, MAX_PAPER_MESSAGE_LEN)
return text
/proc/convert_pencode_arg(text, tag, arg)
arg = sanitize_simple(html_encode(arg), list("''"="","\""="", "?"=""))
// https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-4---css-escape-and-strictly-validate-before-inserting-untrusted-data-into-html-style-property-values
var/list/style_attacks = list("javascript:", "expression", "byond:", "file:")
for(var/style_attack in style_attacks)
if(findtext(arg, style_attack))
// Do not attempt to render dangerous things
return text
if(tag == "class")
return ""
if(tag == "style")
return ""
if(tag == "img")
var/list/img_props = splittext(arg, ";")
if(img_props.len == 3)
return "
", "\[logo\]")
text = replacetext(text, "
", "\[syndielogo\]")
return text
/datum/html/split_holder
var/list/opening
var/inner_text
var/list/closing
/datum/html/split_holder/New()
opening = list()
inner_text = ""
closing = list()
/proc/split_html(raw_text="")
// gently borrowed and re-purposed from code/modules/pda/utilities.dm
// define a datum to hold our result
var/datum/html/split_holder/s = new()
// copy the raw_text to get started
var/text = copytext_char(raw_text, 1)
// search for tag brackets
var/tag_start = findtext_char(text, "<")
var/tag_stop = findtext_char(text, ">")
// until we run out of opening tags
while((tag_start != 0) && (tag_stop != 0))
// if the tag isn't at the beginning of the string
if(tag_start > 1)
// we've found our text, so copy it out
s.inner_text = copytext_char(text, 1, tag_start)
// and chop the text for the next round
text = copytext_char(text, tag_start)
break
// otherwise, we found an opening tag, so add it to the list
var/tag = copytext_char(text, tag_start, tag_stop+1)
s.opening.Add(tag)
// and chop the text for the next round
text = copytext_char(text, tag_stop+1)
// look for the next tag in what's left
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")
// search for tag brackets
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")
// until we run out of closing tags
while((tag_start != 0) && (tag_stop != 0))
// we found a closing tag, so add it to the list
var/tag = copytext_char(text, tag_start, tag_stop+1)
s.closing.Add(tag)
// and chop the text for the next round
text = copytext_char(text, tag_stop+1)
// look for the next tag in what's left
tag_start = findtext(text, "<")
tag_stop = findtext(text, ">")
// return the split html object to the caller
return s
//Used for applying byonds text macros to strings that are loaded at runtime
/proc/apply_text_macros(string)
var/next_backslash = findtext(string, "\\")
if(!next_backslash)
return string
var/leng = length(string)
var/next_space = findtext(string, " ", next_backslash + length(string[next_backslash]))
if(!next_space)
next_space = leng - next_backslash
if(!next_space) //trailing bs
return string
var/base = next_backslash == 1 ? "" : copytext(string, 1, next_backslash)
var/macro = lowertext(copytext(string, next_backslash + length(string[next_backslash]), next_space))
var/rest = next_backslash > leng ? "" : copytext(string, next_space + length(string[next_space]))
//See https://secure.byond.com/docs/ref/info.html#/DM/text/macros
switch(macro)
//prefixes/agnostic
if("the")
rest = "\the [rest]"
if("a")
rest = "\a [rest]"
if("an")
rest = "\an [rest]"
if("proper")
rest = "\proper [rest]"
if("improper")
rest = "\improper [rest]"
if("roman")
rest = "\roman [rest]"
//postfixes
if("th")
base = "[rest]\th"
if("s")
base = "[rest]\s"
if("he")
base = "[rest]\he"
if("she")
base = "[rest]\she"
if("his")
base = "[rest]\his"
if("himself")
base = "[rest]\himself"
if("herself")
base = "[rest]\herself"
if("hers")
base = "[rest]\hers"
. = base
if(rest)
. += .(rest)
/**
* Proc to generate a "rank colour" from a client
*
* This takes the client and looks at various factors in order, such as patreon status, staff rank, and more
* Arguments:
* * C - The client were looking up
*/
/proc/client2rankcolour(client/C)
// First check if end user is an admin
if(C.holder)
if(C.holder.rank in GLOB.configuration.admin.rank_colour_map)
// Return their rank colour if they are in here
return GLOB.configuration.admin.rank_colour_map[C.holder.rank]
// If they arent an admin, see if they are a patreon. Just accept any level
if(C.donator_level)
return "#e67e22" // Patreon orange
return null
// Removes HTML tags, preserving text
/proc/strip_html_tags(the_text)
var/static/regex/html_replacer = regex("<\[^>]*>", "g")
return html_replacer.Replace(the_text, "")
/proc/starts_with_vowel(text)
var/start_char = copytext(text, 1, 2)
switch(lowertext(start_char))
if("a", "e", "i", "o", "u")
return TRUE
else
return FALSE
/**
* Formats num with an SI prefix.
*
* Returns a string formatted with a multiple of num and an SI prefix corresponding to an exponent of 10.
* Only considers exponents that are multiples of 3 (deca, deci, hecto, and centi are not included).
* A unit is not included in the string, the prefix is placed after the number with no spacing added anywhere.
* Listing of prefixes: https://en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes
*/
/proc/format_si_suffix(num)
if(num == 0)
return "[num]"
var/exponent = round_down(log(10, abs(num)))
var/ofthree = exponent / 3
if(exponent < 0)
ofthree = round(ofthree)
else
ofthree = round_down(ofthree)
if(ofthree == 0)
return "[num]"
return "[num / (10 ** (ofthree * 3))][GLOB.si_suffixes[round(length(GLOB.si_suffixes) / 2) + ofthree + 1]]"
/**
* Creates a hyperlink for a specified wiki article.
*/
/proc/wiki_link(article_name, link_text = null)
var/url = "[GLOB.configuration.url.wiki_url]/index.php?title=[article_name]"
return "[link_text ? link_text : url]"