blitz_dom/
stash_policy.rs1use std::collections::BTreeSet;
2
3use crate::{BaseDocument, NodeId, QualName};
4
5#[derive(Default)]
7pub struct StashPolicy {
8 sensitive: BTreeSet<NodeId>,
9 no_stash: bool,
10}
11
12fn forbidden_autocomplete(value: &str) -> bool {
13 value.split_ascii_whitespace().any(|token| {
14 token.eq_ignore_ascii_case("off")
15 || token.eq_ignore_ascii_case("one-time-code")
16 || token.eq_ignore_ascii_case("current-password")
17 || token.eq_ignore_ascii_case("new-password")
18 || token.eq_ignore_ascii_case("cc-name")
19 || token.eq_ignore_ascii_case("cc-given-name")
20 || token.eq_ignore_ascii_case("cc-additional-name")
21 || token.eq_ignore_ascii_case("cc-family-name")
22 || token.eq_ignore_ascii_case("cc-type")
23 || token.eq_ignore_ascii_case("cc-number")
24 || token.eq_ignore_ascii_case("cc-csc")
25 || token.eq_ignore_ascii_case("cc-exp")
26 || token.eq_ignore_ascii_case("cc-exp-month")
27 || token.eq_ignore_ascii_case("cc-exp-year")
28 })
29}
30
31impl BaseDocument {
32 fn mark_stash_sensitive(&mut self, id: NodeId) {
33 if self.stash_policy.no_stash {
34 return;
35 }
36 if self.stash_policy.sensitive.len() >= 4096 {
37 self.stash_policy.no_stash = true;
38 self.stash_policy.sensitive.clear();
39 } else {
40 self.stash_policy.sensitive.insert(id);
41 }
42 }
43
44 pub(crate) fn inherit_stash_classification(&mut self, from: NodeId, to: NodeId) {
45 if self.stash_sensitive(from) {
46 self.mark_stash_sensitive(to);
47 }
48 }
49
50 pub(crate) fn note_stash_attribute(&mut self, id: NodeId, name: &QualName, value: &str) {
51 if !name.ns.as_ref().is_empty() {
52 return;
53 }
54 match name.local.as_ref() {
55 "data-chuzz-no-stash" => self.stash_policy.no_stash = true,
56 "data-chuzz-sensitive" => self.mark_stash_sensitive(id),
57 "autocomplete" if forbidden_autocomplete(value) => self.mark_stash_sensitive(id),
58 "type"
59 if ["password", "file", "hidden"]
60 .iter()
61 .any(|kind| value.eq_ignore_ascii_case(kind)) =>
62 {
63 self.mark_stash_sensitive(id)
64 }
65 _ => {}
66 }
67 self.note_stash_subtree(id);
68 }
69
70 fn note_stash_node(&mut self, id: NodeId) {
71 if self.stash_policy.no_stash {
72 return;
73 }
74 let Some(element) = self.get_node(id).and_then(|node| node.element_data()) else {
75 return;
76 };
77 let sensitive = element.attrs.iter().any(|attr| {
78 attr.name.ns.as_ref().is_empty()
79 && match attr.name.local.as_ref() {
80 "data-chuzz-sensitive" => true,
81 "autocomplete" => forbidden_autocomplete(&attr.value),
82 "type" => ["password", "file", "hidden"]
83 .iter()
84 .any(|kind| str::eq_ignore_ascii_case(attr.value.as_ref(), kind)),
85 _ => false,
86 }
87 });
88 let no_stash = element.attrs.iter().any(|attr| {
89 attr.name.ns.as_ref().is_empty() && attr.name.local.as_ref() == "data-chuzz-no-stash"
90 });
91 self.stash_policy.no_stash |= no_stash;
92 if sensitive || self.stash_sensitive(id) {
93 self.mark_stash_sensitive(id);
94 }
95 }
96
97 pub(crate) fn note_stash_subtree(&mut self, id: NodeId) {
99 if self.stash_policy.no_stash {
100 return;
101 }
102 let associated: Vec<_> = self
103 .get_node(id)
104 .filter(|node| {
105 node.element_data()
106 .is_some_and(|element| element.name.local.as_ref() == "form")
107 })
108 .and_then(|node| node.attr(crate::local_name!("id")))
109 .map_or_else(Vec::new, |name| {
110 self.nodes
111 .iter()
112 .filter_map(|(control, node)| {
113 (node.attr(crate::local_name!("form")) == Some(name)
114 || self.controls_to_form.get(&control) == Some(&id))
115 .then_some(control)
116 })
117 .collect()
118 });
119 let mut pending = vec![id];
120 pending.extend(associated);
121 let mut visited = BTreeSet::new();
122 while let Some(id) = pending.pop() {
123 if !visited.insert(id) {
124 continue;
125 }
126 self.note_stash_node(id);
127 if let Some(node) = self.get_node(id) {
128 pending.extend(node.children.iter().copied());
129 }
130 }
131 }
132
133 pub fn classify_stash(&mut self) {
135 if self.stash_policy.no_stash {
136 return;
137 }
138 let ids: Vec<_> = self.nodes.iter().map(|(id, _)| id).collect();
139 for id in ids {
140 self.note_stash_node(id);
141 }
142 }
143
144 pub fn stash_disabled(&self) -> bool {
145 self.stash_policy.no_stash
146 }
147
148 pub fn stash_sensitive(&self, id: NodeId) -> bool {
149 if self.stash_policy.no_stash {
150 return true;
151 }
152 let mut cursor = Some(id);
153 while let Some(id) = cursor {
154 if self.stash_policy.sensitive.contains(&id) {
155 return true;
156 }
157 cursor = self.get_node(id).and_then(|node| node.parent);
158 }
159 let Some(element) = self.get_node(id).and_then(|node| node.element_data()) else {
160 return false;
161 };
162 let form = element
163 .attr(crate::local_name!("form"))
164 .and_then(|name| self.get_element_by_id(name))
165 .or_else(|| self.controls_to_form.get(&id).copied());
166 form.is_some_and(|form| self.stash_policy.sensitive.contains(&form))
167 }
168}