https_everywhere_lib_wasm/
lib.rs

1use wasm_bindgen::prelude::*;
2use js_sys::{Array, Reflect, Boolean, Set, Object};
3use std::rc::Rc;
4
5mod debugging;
6
7pub use https_everywhere_lib_core::{Rule, CookieRule, RuleSet};
8use https_everywhere_lib_core::RuleSets as CoreRuleSets;
9
10const ERR: &str = "could not convert property to JS";
11
12#[derive(Debug)]
13struct StaticJsStrings {
14    active: JsValue,
15    cookierules: JsValue,
16    default_off: JsValue,
17    default_state: JsValue,
18    exclusion: JsValue,
19    exclusions: JsValue,
20    from: JsValue,
21    host: JsValue,
22    mixed_content: JsValue,
23    name: JsValue,
24    note: JsValue,
25    platform: JsValue,
26    rule: JsValue,
27    rules: JsValue,
28    scope: JsValue,
29    securecookie: JsValue,
30    state: JsValue,
31    target: JsValue,
32    to: JsValue,
33    user_rule: JsValue,
34}
35
36thread_local! {
37    static JS_STRINGS: StaticJsStrings = StaticJsStrings {
38        active: JsValue::from("active"),
39        cookierules: JsValue::from("cookierules"),
40        default_off: JsValue::from("default_off"),
41        default_state: JsValue::from("default_state"),
42        exclusion: JsValue::from("exclusion"),
43        exclusions: JsValue::from("exclusions"),
44        from: JsValue::from("from"),
45        host: JsValue::from("host"),
46        mixed_content: JsValue::from("mixedcontent"),
47        name: JsValue::from("name"),
48        note: JsValue::from("note"),
49        platform: JsValue::from("platform"),
50        rule: JsValue::from("rule"),
51        rules: JsValue::from("rules"),
52        scope: JsValue::from("scope"),
53        securecookie: JsValue::from("securecookie"),
54        state: JsValue::from("state"),
55        target: JsValue::from("target"),
56        to: JsValue::from("to"),
57        user_rule: JsValue::from("user rule"),
58    };
59}
60
61pub trait ToJavaScript {
62    fn to_javascript(&self) -> JsValue;
63}
64
65impl ToJavaScript for Vec<Rc<RuleSet>>{
66    /// Convert a vector of rulesets to a JS value
67    fn to_javascript(&self) -> JsValue {
68        let results = Set::new(&Array::new());
69        for rs in self {
70            results.add(&rs.to_javascript());
71        }
72        results.into()
73    }
74}
75
76impl ToJavaScript for Rule {
77    /// Convert a rule to a JS value
78    fn to_javascript(&self) -> JsValue {
79        let object = Object::new();
80        JS_STRINGS.with(|jss| {
81            match &self {
82                Rule::Trivial => {
83                    Reflect::set(&object, &jss.from, &JsValue::from("^http:")).expect(ERR);
84                    Reflect::set(&object, &jss.to, &JsValue::from("https:")).expect(ERR);
85                },
86                Rule::NonTrivial(from_regex, to) => {
87                    Reflect::set(&object, &jss.from, &JsValue::from(from_regex)).expect(ERR);
88                    Reflect::set(&object, &jss.to, &JsValue::from(to)).expect(ERR);
89                }
90            }
91        });
92        object.into()
93    }
94}
95
96impl ToJavaScript for CookieRule {
97    /// Convert a ruleset to a JS value
98    fn to_javascript(&self) -> JsValue {
99        let object = Object::new();
100        JS_STRINGS.with(|jss| {
101            Reflect::set(&object, &jss.host, &JsValue::from(&self.host_regex)).expect(ERR);
102            Reflect::set(&object, &jss.name, &JsValue::from(&self.name_regex)).expect(ERR);
103        });
104        object.into()
105    }
106}
107
108impl ToJavaScript for RuleSet {
109    /// Convert a ruleset to a JS object
110    fn to_javascript(&self) -> JsValue {
111        let object = Object::new();
112        JS_STRINGS.with(|jss| {
113            Reflect::set(&object, &jss.name, &JsValue::from(&self.name)).expect(ERR);
114            Reflect::set(&object, &jss.active, &JsValue::from_bool(self.active.clone())).expect(ERR);
115            Reflect::set(&object, &jss.default_state, &JsValue::from_bool(self.default_state.clone())).expect(ERR);
116            match self.scope.as_ref() {
117                Some(scope) => { Reflect::set(&object, &jss.scope, &JsValue::from(scope)).expect(ERR); },
118                None => {}
119            }
120            match &self.note {
121                Some(note) => { Reflect::set(&object, &jss.note, &JsValue::from(note)).expect(ERR); },
122                None => {}
123            }
124
125            let rules = Array::new();
126            for rule in &self.rules {
127                rules.push(&rule.to_javascript());
128            }
129            Reflect::set(&object, &jss.rules, &rules).expect(ERR);
130
131            match &self.exclusions {
132                Some(exclusions) => {
133                    Reflect::set(&object, &jss.exclusions, &JsValue::from(exclusions)).expect(ERR);
134                },
135                None => {}
136            }
137
138            match &self.cookierules {
139                Some(cookierules) => {
140                    let cookierules_array = Array::new();
141                    for cookierule in cookierules {
142                        cookierules_array.push(&cookierule.to_javascript());
143                    }
144                    Reflect::set(&object, &jss.cookierules, &cookierules_array).expect(ERR);
145                },
146                None => {}
147            }
148        });
149        object.into()
150    }
151}
152
153pub trait JsRuleSet {
154    fn add_rules(&mut self, rules_array: &Array);
155    fn add_exclusions(&mut self, exclusions_array: &Array);
156    fn add_cookierules(&mut self, cookierules_array: &Array);
157    fn is_equivalent_to(&self, ruleset_jsval: &JsValue) -> bool;
158}
159
160impl JsRuleSet for RuleSet {
161    /// Add rules, specified in JS array
162    fn add_rules(&mut self, rules_array: &Array){
163        for rule_result in rules_array.values() {
164            let rule_obj = rule_result.unwrap();
165            if rule_obj.is_object() {
166                JS_STRINGS.with(|jss| {
167                    let from_string = match Reflect::get(&rule_obj, &jss.from) {
168                        Ok(from) => {
169                            if from.is_string() {
170                                from.as_string().unwrap()
171                            } else {
172                                String::new()
173                            }
174                        },
175                        _ => String::new()
176                    };
177                    let to_string = match Reflect::get(&rule_obj, &jss.to) {
178                        Ok(to) => {
179                            if to.is_string() {
180                                to.as_string().unwrap()
181                            } else {
182                                String::new()
183                            }
184                        },
185                        _ => String::new()
186                    };
187
188                    self.rules.push(Rule::new(from_string, to_string));
189                });
190            }
191        };
192    }
193
194    /// Add exclusions to the ruleset from an exclusions JS array
195    fn add_exclusions(&mut self, exclusions_array: &Array){
196        let mut exclusions = vec![];
197        for exclusion_result in exclusions_array.values() {
198            let exclusion_string = exclusion_result.unwrap();
199            if exclusion_string.is_string() {
200                exclusions.push(exclusion_string.as_string().unwrap());
201            }
202        }
203
204        self.exclusions = Some(exclusions.join("|"));
205    }
206
207    /// Add cookierules to the ruleset from a cookierules array
208    fn add_cookierules(&mut self, cookierules_array: &Array){
209        let mut cookierules = vec![];
210        for cookierule_result in cookierules_array.values() {
211            let cookierule_obj = cookierule_result.unwrap();
212            if cookierule_obj.is_object() {
213                JS_STRINGS.with(|jss| {
214                    let host_string = Reflect::get(&cookierule_obj, &jss.host).unwrap();
215                    let name_string = Reflect::get(&cookierule_obj, &jss.name).unwrap();
216                    if host_string.is_string() && name_string.is_string() {
217                        cookierules.push(
218                            CookieRule::new(
219                                host_string.as_string().unwrap(),
220                                name_string.as_string().unwrap()));
221                    }
222                });
223            }
224        }
225
226        self.cookierules = Some(cookierules);
227    }
228
229    /// Return a bool indicating whether the given JS ruleset is equivalent
230    fn is_equivalent_to(&self, ruleset_jsval: &JsValue) -> bool {
231        let mut result = false;
232
233        if ruleset_jsval.is_object() {
234            JS_STRINGS.with(|jss| {
235                let name_jsval = Reflect::get(&ruleset_jsval, &jss.name).unwrap();
236                let note_jsval = Reflect::get(&ruleset_jsval, &jss.note).unwrap();
237                let active_jsval = Reflect::get(&ruleset_jsval, &jss.active).unwrap();
238                let rules_jsval = Reflect::get(&ruleset_jsval, &jss.rules).unwrap();
239
240                let name_is_equiv = name_jsval.is_string() && name_jsval.as_string().unwrap() == self.name;
241                let note_is_equiv = (note_jsval.is_null() && self.note == None) ||
242                    (note_jsval.is_string() && self.note == Some(note_jsval.as_string().unwrap()));
243                let active_is_equiv = Boolean::from(active_jsval) == self.active;
244
245                if name_is_equiv && note_is_equiv && active_is_equiv && Array::is_array(&rules_jsval) {
246                    let rules_array = Array::from(&rules_jsval);
247                    if rules_array.length() == self.rules.len() as u32 {
248                        let mut each_rule_equiv = true;
249                        let mut counter = 0;
250                        for rule_result in rules_array.values() {
251                            let rule_jsval = rule_result.unwrap();
252                            if rule_jsval.is_object() {
253                                let to_jsval = Reflect::get(&rule_jsval, &jss.to).unwrap();
254                                if let Some(to_string) = to_jsval.as_string() {
255                                    match &self.rules[counter] {
256                                        Rule::Trivial => {
257                                            each_rule_equiv = to_string == "https:";
258                                        },
259                                        Rule::NonTrivial(_, this_to_val) => {
260                                            each_rule_equiv = &to_string == this_to_val;
261                                        }
262                                    }
263                                }
264                                counter += 1;
265                            } else {
266                                each_rule_equiv = false;
267                            }
268                        }
269                        result = each_rule_equiv;
270                    }
271                }
272            });
273        }
274        result
275    }
276
277}
278
279
280/// A newtype for rulesets, wrapping all the JS functionality
281#[wasm_bindgen]
282#[derive(Debug)]
283pub struct RuleSets(CoreRuleSets);
284
285#[wasm_bindgen]
286impl RuleSets {
287
288    /// Returns a new JsRulesets struct
289    pub fn new() -> RuleSets {
290        RuleSets(CoreRuleSets::new())
291    }
292
293    /// Returns the number of targets in the current JsRuleSets struct as a `usize`
294    pub fn count_targets(&self) -> usize {
295        self.0.count_targets()
296    }
297
298    /// Construct and add new rulesets given a JS array of values
299    ///
300    /// # Arguments
301    ///
302    /// * `array` - A JS Array object of rulesets
303    /// * `enable_mixed_rulesets` - A JS Boolean indicating whether rulesets which trigger mixed
304    /// content blocking should be enabled
305    /// * `rule_active_states` - A JS object which lets us know whether rulesets have been disabled
306    /// or enabled
307    /// * `scope` - An optional JS string which indicates the scope of the current batch of
308    /// rulesets being added (see the [ruleset update channels](https://github.com/EFForg/https-everywhere/blob/master/docs/en_US/ruleset-update-channels.md) documentation)
309    pub fn add_all_from_js_array(&mut self, array: &Array, enable_mixed_rulesets: &Boolean, rule_active_states: &JsValue, scope: &JsValue) {
310
311        let scope: Rc<Option<String>> = if scope.is_string() {
312            Rc::new(Some(scope.as_string().unwrap()))
313        } else {
314            Rc::new(None)
315        };
316
317        let mut add_one_from_js = |jsval| {
318            JS_STRINGS.with(|jss| {
319                let mut ruleset_name: String;
320                let mut default_state = true;
321                let mut note = String::new();
322
323                let default_off = Reflect::get(&jsval, &jss.default_off).unwrap();
324                if default_off.is_string() {
325                    if default_off != jss.user_rule {
326                        default_state = false;
327                    }
328                    if let Some(default_off_string) = default_off.as_string() {
329                        note += &(default_off_string + "\n");
330                    }
331                }
332
333                let platform = Reflect::get(&jsval, &jss.platform).unwrap();
334                if platform.is_string() {
335                    if platform == jss.mixed_content {
336                         if !enable_mixed_rulesets.value_of() {
337                            default_state = false;
338                        }
339                    } else if !platform.is_undefined() {
340                        default_state = false;
341                    }
342                    if let Some(platform_string) = platform.as_string() {
343                        note.push_str("Platform(s): ");
344                        note += &(platform_string + "\n");
345                    }
346                }
347
348                let mut active = default_state;
349                let name = Reflect::get(&jsval, &jss.name).unwrap();
350                if name.is_string() {
351                    ruleset_name = name.as_string().unwrap();
352                    if rule_active_states.is_object() {
353                        let active_state = Reflect::get(&rule_active_states, &JsValue::from_str(&ruleset_name)).unwrap();
354                        match active_state.as_bool() {
355                            Some(false) => { active = false; }
356                            Some(true) => { active = true; }
357                            _ => {}
358                        }
359                    }
360
361                    let mut rs = RuleSet::new(ruleset_name, Rc::clone(&scope));
362                    rs.default_state = default_state;
363                    rs.note = match note.len() {
364                        0 => None,
365                        _ => Some(note.trim().to_string())
366                    };
367                    rs.active = active;
368
369                    if let Ok(rules_jsval) = Reflect::get(&jsval, &jss.rule) {
370                        if Array::is_array(&rules_jsval) {
371                            rs.add_rules(&Array::from(&rules_jsval));
372                        }
373                    }
374
375                    if let Ok(exclusion_jsval) = Reflect::get(&jsval, &jss.exclusion) {
376                        if Array::is_array(&exclusion_jsval) {
377                            rs.add_exclusions(&Array::from(&exclusion_jsval));
378                        }
379                    }
380
381                    if let Ok(securecookie_jsval) = Reflect::get(&jsval, &jss.securecookie) {
382                        if Array::is_array(&securecookie_jsval) {
383                            rs.add_cookierules(&Array::from(&securecookie_jsval));
384                        }
385                    }
386
387                    let rs_rc = Rc::new(rs);
388                    let target_jsval = Reflect::get(&jsval, &jss.target).unwrap();
389                    if Array::is_array(&target_jsval) {
390                        for target_result in &Array::from(&target_jsval).values() {
391                            let target = target_result.unwrap();
392                            if target.is_string() {
393                                let target = target.as_string().unwrap();
394                                match (self.0).0.get_mut(&target) {
395                                    Some(rs_vector) => {
396                                        rs_vector.push(Rc::clone(&rs_rc));
397                                    },
398                                    None => {
399                                        (self.0).0.insert(target, vec![Rc::clone(&rs_rc)]);
400                                    }
401                                }
402                            }
403                        }
404                    }
405                }
406            });
407        };
408
409        for val in array.values() {
410            let jsval = val.unwrap();
411            if jsval.is_object() {
412                add_one_from_js(jsval);
413            }
414        }
415    }
416
417    #[cfg(debug_assertions)]
418    /// Print the entire RuleSets struct
419    pub fn print_targets (&self) {
420        console_log!("{:?}", (self.0).0);
421    }
422
423    /// Remove a RuleSet from the RuleSets struct
424    pub fn remove_ruleset (&mut self, ruleset_jsval: &JsValue) {
425        if ruleset_jsval.is_object() {
426            JS_STRINGS.with(|jss| {
427                if let Ok(name) = Reflect::get(&ruleset_jsval, &jss.name) {
428                    if name.is_string() {
429                        let name = name.as_string().unwrap();
430                        if (self.0).0.contains_key(&name) {
431                            let ruleset_vec = (self.0).0.remove(&name).unwrap();
432                            let mut new_ruleset_vec = vec![];
433
434                            for ruleset in ruleset_vec {
435                                if !ruleset.is_equivalent_to(ruleset_jsval) {
436                                    new_ruleset_vec.push(Rc::clone(&ruleset));
437                                }
438                            }
439
440                            if new_ruleset_vec.len() > 0 {
441                                (self.0).0.insert(name, new_ruleset_vec);
442                            }
443                        }
444                    }
445                }
446            });
447        }
448    }
449
450    /// Return a JS set of rulesets that apply to the given host
451    ///
452    /// # Arguments
453    ///
454    /// * `host` - A JS string which indicates the host to search for potentially applicable rulesets
455    pub fn potentially_applicable (&self, host: &JsValue) -> JsValue {
456        if host.is_string() {
457            let host = host.as_string().unwrap();
458            self.0.potentially_applicable(&host).to_javascript()
459        } else {
460            Set::new(&Array::new()).into()
461        }
462    }
463}