Skip to main content

robinpath_modules/modules/
sanitize_mod.rs

1use 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('&', "&amp;")
123        .replace('<', "&lt;")
124        .replace('>', "&gt;")
125        .replace('"', "&quot;")
126        .replace('\'', "&#39;")
127}
128
129fn unescape_html(s: &str) -> String {
130    s.replace("&amp;", "&")
131        .replace("&lt;", "<")
132        .replace("&gt;", ">")
133        .replace("&quot;", "\"")
134        .replace("&#39;", "'")
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}