robinpath_modules/modules/
sanitize_mod.rs1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4 rp.register_builtin("sanitize.html", |args, _| {
5 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
6 let mode = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "strip".to_string());
7 match mode.as_str() {
8 "escape" => Ok(Value::String(escape_html(&s))),
9 _ => Ok(Value::String(strip_tags(&s, &[]))),
10 }
11 });
12
13 rp.register_builtin("sanitize.xss", |args, _| {
14 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
15 let re = regex::Regex::new(r"(?i)<script[^>]*>[\s\S]*?</script>|javascript:|on\w+\s*=").unwrap();
16 let cleaned = re.replace_all(&s, "").to_string();
17 Ok(Value::String(strip_tags(&cleaned, &[])))
18 });
19
20 rp.register_builtin("sanitize.sql", |args, _| {
21 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
22 let escaped = s
23 .replace('\'', "''")
24 .replace('\\', "\\\\")
25 .replace('\0', "")
26 .replace('\n', "\\n")
27 .replace('\r', "\\r");
28 Ok(Value::String(escaped))
29 });
30
31 rp.register_builtin("sanitize.regex", |args, _| {
32 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
33 Ok(Value::String(regex::escape(&s)))
34 });
35
36 rp.register_builtin("sanitize.filename", |args, _| {
37 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
38 let replacement = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "_".to_string());
39 let re = regex::Regex::new(r#"[<>:"/\\|?*\x00-\x1f]"#).unwrap();
40 let cleaned = re.replace_all(&s, replacement.as_str()).to_string();
41 Ok(Value::String(cleaned))
42 });
43
44 rp.register_builtin("sanitize.path", |args, _| {
45 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
46 let cleaned = s.replace("..", "").replace('\0', "");
47 Ok(Value::String(cleaned))
48 });
49
50 rp.register_builtin("sanitize.url", |args, _| {
51 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
52 let re = regex::Regex::new(r"(?i)^(javascript|data|vbscript):").unwrap();
53 if re.is_match(&s) {
54 Ok(Value::String(String::new()))
55 } else {
56 Ok(Value::String(s))
57 }
58 });
59
60 rp.register_builtin("sanitize.email", |args, _| {
61 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
62 Ok(Value::String(s.trim().to_lowercase()))
63 });
64
65 rp.register_builtin("sanitize.stripTags", |args, _| {
66 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
67 let allowed = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
68 let allowed_tags: Vec<&str> = allowed.split(',').map(|t| t.trim()).filter(|t| !t.is_empty()).collect();
69 Ok(Value::String(strip_tags(&s, &allowed_tags)))
70 });
71
72 rp.register_builtin("sanitize.escapeHtml", |args, _| {
73 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
74 Ok(Value::String(escape_html(&s)))
75 });
76
77 rp.register_builtin("sanitize.unescapeHtml", |args, _| {
78 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
79 Ok(Value::String(unescape_html(&s)))
80 });
81
82 rp.register_builtin("sanitize.trim", |args, _| {
83 let val = args.first().cloned().unwrap_or(Value::Null);
84 Ok(deep_trim(&val))
85 });
86
87 rp.register_builtin("sanitize.truncate", |args, _| {
88 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
89 let max_len = args.get(1).map(|v| v.to_number() as usize).unwrap_or(100);
90 let suffix = args.get(2).map(|v| v.to_display_string()).unwrap_or_else(|| "...".to_string());
91 if s.len() <= max_len {
92 Ok(Value::String(s))
93 } else {
94 let end = max_len.saturating_sub(suffix.len()).min(s.len());
95 Ok(Value::String(format!("{}{}", &s[..end], suffix)))
96 }
97 });
98
99 rp.register_builtin("sanitize.alphanumeric", |args, _| {
100 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
101 let allow_spaces = args.get(1).map(|v| v.is_truthy()).unwrap_or(false);
102 let result: String = s.chars().filter(|c| c.is_alphanumeric() || (allow_spaces && *c == ' ')).collect();
103 Ok(Value::String(result))
104 });
105
106 rp.register_builtin("sanitize.slug", |args, _| {
107 let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
108 let sep = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "-".to_string());
109 let slug: String = s
110 .to_lowercase()
111 .chars()
112 .map(|c| if c.is_alphanumeric() || c == ' ' { c } else { ' ' })
113 .collect::<String>()
114 .split_whitespace()
115 .collect::<Vec<_>>()
116 .join(&sep);
117 Ok(Value::String(slug))
118 });
119}
120
121fn escape_html(s: &str) -> String {
122 s.replace('&', "&")
123 .replace('<', "<")
124 .replace('>', ">")
125 .replace('"', """)
126 .replace('\'', "'")
127}
128
129fn unescape_html(s: &str) -> String {
130 s.replace("&", "&")
131 .replace("<", "<")
132 .replace(">", ">")
133 .replace(""", "\"")
134 .replace("'", "'")
135}
136
137fn strip_tags(s: &str, _allowed: &[&str]) -> String {
138 let re = regex::Regex::new(r"<[^>]*>").unwrap();
139 re.replace_all(s, "").to_string()
140}
141
142fn deep_trim(val: &Value) -> Value {
143 match val {
144 Value::String(s) => Value::String(s.trim().to_string()),
145 Value::Array(arr) => Value::Array(arr.iter().map(deep_trim).collect()),
146 Value::Object(obj) => {
147 let trimmed: indexmap::IndexMap<String, Value> = obj
148 .iter()
149 .map(|(k, v)| (k.clone(), deep_trim(v)))
150 .collect();
151 Value::Object(trimmed)
152 }
153 _ => val.clone(),
154 }
155}