diff --git a/README.md b/README.md index 6c75665..58b70b0 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en ## Client usage: +**For Web UI connect to http://localhost:8000** + sudo apt update sudo apt install curl build-essential git tor sudo systemctl enable --now tor.service diff --git a/README_TR.md b/README_TR.md index 752bc25..36b473b 100644 --- a/README_TR.md +++ b/README_TR.md @@ -70,6 +70,8 @@ Amnezichat, hiçbir kayıt tutulmamasını ve tüm mesaj verilerinin yalnızca s ## İstemci kullanımı: +**Web UI için http://localhost:8000 adresine bağlanın** + sudo apt update sudo apt install curl build-essential git tor sudo systemctl enable --now tor.service diff --git a/client/Cargo.toml b/client/Cargo.toml index 5029a00..71b3747 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -21,7 +21,4 @@ rpassword = "7.3.1" sha3 = "0.10.8" x25519-dalek = "2.0.1" ed25519-dalek = "2.1.1" -eframe = "0.24.1" -image = "0.24" -rfd = "0.12" -winapi = { version = "0.3", features = ["winuser", "windef"] } +rocket = { version = "0.5", features = ["json"] } \ No newline at end of file diff --git a/client/Rocket.toml b/client/Rocket.toml new file mode 100644 index 0000000..a07514f --- /dev/null +++ b/client/Rocket.toml @@ -0,0 +1,8 @@ +[default] +address = "127.0.0.1" +port = 8000 + +[default.limits] +forms = "2 MiB" +json = "2 MiB" +file = "2 MiB" diff --git a/client/src/gui.rs b/client/src/gui.rs index a5c3d68..8aa1302 100644 --- a/client/src/gui.rs +++ b/client/src/gui.rs @@ -2,32 +2,35 @@ use crate::encrypt_data; use crate::receive_and_fetch_messages; use crate::send_encrypted_message; use crate::pad_message; -use eframe::egui; -use image::GenericImageView; -use rfd::FileDialog; -use std::fs; +use rocket::{get, post, routes, serde::json::Json}; +use rocket::fs::NamedFile; +use rocket::tokio; +use rocket::fs::FileServer; +use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; -use std::thread; use std::time::Duration; -use regex::Regex; -use base64; +use std::path::PathBuf; +#[derive(Clone)] pub struct MessagingApp { username: String, - message_input: String, messages: Arc>>, - shared_hybrid_secret: Arc, + shared_hybrid_secret: Arc, shared_room_id: Arc, shared_url: Arc, - image_data: Option, +} + +#[derive(Serialize, Deserialize)] +struct MessageInput { + message: String, } impl MessagingApp { pub fn new( username: String, - shared_hybrid_secret: Arc, - shared_room_id: Arc, - shared_url: Arc, + shared_hybrid_secret: Arc, + shared_room_id: Arc>, + shared_url: Arc>, ) -> Self { let messages = Arc::new(Mutex::new(vec![])); let messages_clone = Arc::clone(&messages); @@ -35,160 +38,125 @@ impl MessagingApp { let shared_room_id_clone = Arc::clone(&shared_room_id); let shared_url_clone = Arc::clone(&shared_url); - thread::spawn(move || loop { - match receive_and_fetch_messages( - &shared_room_id_clone, - &shared_hybrid_secret_clone, - &shared_url_clone, - true, - ) { - Ok(new_messages) => { - let mut msgs = messages_clone.lock().unwrap(); - msgs.clear(); - msgs.extend(new_messages); - } - Err(e) => { - eprintln!("Error fetching messages: {}", e); + let room_id = Arc::new(shared_room_id_clone.lock().unwrap_or_else(|_| panic!("Failed to lock room_id")).clone()); + let url = Arc::new(shared_url_clone.lock().unwrap_or_else(|_| panic!("Failed to lock url")).clone()); + + tokio::spawn(async move { + loop { + let room_id_str = shared_room_id_clone.lock().unwrap_or_else(|_| panic!("Failed to lock room_id")).clone(); + let url_str = shared_url_clone.lock().unwrap_or_else(|_| panic!("Failed to lock url")).clone(); + + match receive_and_fetch_messages( + &room_id_str, + &shared_hybrid_secret_clone, + &url_str, + true, + ) { + Ok(new_messages) => { + let mut msgs = messages_clone.lock().unwrap_or_else(|_| panic!("Failed to lock messages")); + msgs.clear(); + msgs.extend(new_messages); + } + Err(e) => { + eprintln!("Error fetching messages: {}", e); + } } + tokio::time::sleep(Duration::from_secs(10)).await; } - thread::sleep(Duration::from_secs(10)); }); MessagingApp { username, - message_input: String::new(), messages, shared_hybrid_secret, - shared_room_id, - shared_url, - image_data: None, + shared_room_id: room_id, + shared_url: url, } } +} - fn handle_image_upload(&mut self) { - if let Some(file_path) = FileDialog::new().add_filter("Image files", &["png", "jpg", "jpeg", "bmp", "gif"]).pick_file() { - match fs::read(&file_path) { - Ok(data) => { - let encoded = base64::encode(data); - self.image_data = Some(format!("[IMAGE_DATA]:{}[END DATA]", encoded)); - } - Err(e) => eprintln!("Error reading image file: {}", e), +#[get("/messages")] +async fn get_messages(app: &rocket::State) -> Json> { + let result = fetch_and_update_messages(&app).await; + + match result { + Ok(msgs) => Json(msgs), + Err(e) => { + eprintln!("Error fetching messages: {}", e); + + // Return current messages if fetching fails + let msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages")); + Json(msgs.clone()) + } + } +} + +async fn fetch_and_update_messages(app: &rocket::State) -> Result, String> { + let room_id_str = app.shared_room_id.clone(); + let url_str = app.shared_url.clone(); + + let new_messages = tokio::task::block_in_place(move || { + receive_and_fetch_messages( + &room_id_str, + &app.shared_hybrid_secret, + &url_str, + true, + ) + }).map_err(|e| format!("Error fetching messages: {}", e))?; + + // Update the in-memory message storage + let mut msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages")); + msgs.clear(); + msgs.extend(new_messages.clone()); + + Ok(new_messages) +} + +#[post("/send", data = "")] +async fn post_message( + input: Json, + app: &rocket::State +) -> Result<&'static str, rocket::http::Status> { + // Create the formatted message once + let formatted_message = format!("{}: {}", app.username, input.message); + let padded_message = pad_message(&formatted_message, 2048); + + let result = tokio::task::block_in_place(|| { + // Encrypt the message + let encrypted = encrypt_data(&padded_message, &app.shared_hybrid_secret) + .map_err(|e| { + eprintln!("Encryption error: {}", e); + rocket::http::Status::InternalServerError + })?; + + // Send the encrypted message + send_encrypted_message(&encrypted, &app.shared_room_id, &app.shared_url) + .map_err(|e| { + eprintln!("Error sending message: {}", e); + rocket::http::Status::InternalServerError + }) + }); + + match result { + Ok(_) => { + { + let mut msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages")); + msgs.push(formatted_message); } + Ok("Message sent") } + Err(e) => Err(e), } } -impl eframe::App for MessagingApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.vertical(|ui| { - let chat_area_height = ui.available_height() - 80.0; - - egui::Frame::none() - .fill(egui::Color32::from_black_alpha(50)) - .rounding(10.0) - .inner_margin(egui::style::Margin::same(10.0)) - .show(ui, |ui| { - ui.set_height(chat_area_height); - egui::ScrollArea::vertical() - .auto_shrink([false, true]) - .show(ui, |ui| { - let messages = self.messages.lock().unwrap(); - let re = Regex::new(r"").unwrap(); - - for message in messages.iter() { - if message.contains("[IMAGE_DATA]:") { - if let Some(encoded) = message.split("[IMAGE_DATA]:").nth(1) { - if let Some(end_idx) = encoded.find("[END DATA]") { - let image_data = &encoded[..end_idx]; - match base64::decode(image_data) { - Ok(decoded) => { - if let Ok(image) = image::load_from_memory(&decoded) { - let size = image.dimensions(); - let color_image = egui::ColorImage::from_rgba_unmultiplied( - [size.0 as usize, size.1 as usize], - &image.to_rgba8(), - ); - let texture = ctx.load_texture( - "received_image", - color_image, - egui::TextureOptions::LINEAR, - ); - ui.image(&texture); - } else { - eprintln!("Failed to decode image format"); - } - } - Err(e) => eprintln!("Error decoding base64 image: {}", e), - } - } - } - } else { - let cleaned_message = re.replace_all(message, ""); - ui.label( - egui::RichText::new(cleaned_message.as_ref()) - .size(16.0) - .color(egui::Color32::WHITE), - ); - } - } - }); - }); - - ui.horizontal(|ui| { - let input_box_width = ui.available_width() * 0.65; - let button_width = ui.available_width() * 0.15; - - let text_edit = egui::TextEdit::singleline(&mut self.message_input) - .hint_text("Type a message...") - .text_color(egui::Color32::WHITE) - .frame(true); - ui.add_sized([input_box_width, 40.0], text_edit); - - if ui.add_sized([button_width, 40.0], egui::Button::new("Send")).clicked() { - let mut message = format!("{}: {}", self.username, self.message_input); - if let Some(image) = &self.image_data { - message.push_str(image); - self.image_data = None; - } - - // Pad the message to a fixed length (e.g., 2048 bytes) - let padded_message = pad_message(&message, 2048); - - if let Err(e) = send_encrypted_message( - &encrypt_data(&padded_message, &self.shared_hybrid_secret).unwrap(), - &self.shared_room_id, - &self.shared_url, - ) { - eprintln!("Error sending message: {}", e); - } else { - self.message_input.clear(); - } - } - - if ui.add_sized([button_width, 40.0], egui::Button::new("Upload Image")).clicked() { - self.handle_image_upload(); - } - }); - }); - }); - } +#[get("/")] +async fn serve_webpage() -> Option { + NamedFile::open(PathBuf::from("static/index.html")).await.ok() } -pub fn run_gui( - username: String, - shared_hybrid_secret: Arc, - shared_room_id: Arc, - shared_url: Arc, -) -> Result<(), eframe::Error> { - let app = MessagingApp::new( - username, - shared_hybrid_secret, - shared_room_id, - shared_url, - ); - let native_options = eframe::NativeOptions { - ..Default::default() - }; - eframe::run_native("Amnezichat", native_options, Box::new(|_| Box::new(app))) -} +pub fn create_rocket(app: MessagingApp) -> rocket::Rocket { + rocket::build() + .manage(app) + .mount("/", routes![get_messages, post_message, serve_webpage]) + .mount("/static", FileServer::from("static")) +} \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index 93af3db..93b0a41 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -4,7 +4,8 @@ mod network_operations; mod key_exchange; mod authentication; mod encryption; -use gui::run_gui; +use gui::create_rocket; +use gui::MessagingApp; use key_operations::key_operations_dilithium; use key_operations::key_operations_eddsa; use network_operations::create_client_with_proxy; @@ -469,21 +470,29 @@ fn main() -> Result<(), Box> { // Handle GUI or CLI messaging if interface_choice.to_lowercase() == "gui" { - let shared_hybrid_secret_for_gui = shared_hybrid_secret; - let shared_room_id_for_gui: Arc = { - let locked = shared_room_id.lock().unwrap(); - Arc::new(locked.clone()) - }; - let shared_url_for_gui: Arc = { - let locked = shared_url.lock().unwrap(); - Arc::new(locked.clone()) - }; - let _ = run_gui( - username.clone(), - shared_hybrid_secret_for_gui, - shared_room_id_for_gui, - shared_url_for_gui, - ); + let rt = rocket::tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + // Wrap only for passing to run_gui + let shared_hybrid_secret_for_gui = shared_hybrid_secret; + + // Correctly clone Arc> instead of Arc + let shared_room_id_for_gui: Arc> = Arc::clone(&shared_room_id); + let shared_url_for_gui: Arc> = Arc::clone(&shared_url); + + // Pass the arguments + let app = MessagingApp::new( + username, + shared_hybrid_secret_for_gui, + shared_room_id_for_gui, + shared_url_for_gui, + ); + + // Await the async launch function + if let Err(e) = create_rocket(app).launch().await { + eprintln!("Rocket server failed: {}", e); + } + }); + } else { loop { let mut message = String::new(); @@ -741,26 +750,28 @@ fn main() -> Result<(), Box> { if interface_choice.to_lowercase() == "gui" { - // Wrap only for passing to run_gui - let shared_hybrid_secret_for_gui = shared_hybrid_secret; + let rt = rocket::tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + // Wrap only for passing to run_gui + let shared_hybrid_secret_for_gui = shared_hybrid_secret; - let shared_room_id_for_gui: Arc = { - let locked = shared_room_id.lock().unwrap(); - Arc::new(locked.clone()) - }; + // Correctly clone Arc> instead of Arc + let shared_room_id_for_gui: Arc> = Arc::clone(&shared_room_id); + let shared_url_for_gui: Arc> = Arc::clone(&shared_url); - let shared_url_for_gui: Arc = { - let locked = shared_url.lock().unwrap(); - Arc::new(locked.clone()) - }; + // Pass the arguments + let app = MessagingApp::new( + username, + shared_hybrid_secret_for_gui, + shared_room_id_for_gui, + shared_url_for_gui, + ); - // Pass the arguments - let _ = run_gui( - username.clone(), - shared_hybrid_secret_for_gui, - shared_room_id_for_gui, - shared_url_for_gui, - ); + // Await the async launch function + if let Err(e) = create_rocket(app).launch().await { + eprintln!("Rocket server failed: {}", e); + } + }); } else { loop { diff --git a/client/src/network_operations.rs b/client/src/network_operations.rs index 32783cd..476a3b2 100644 --- a/client/src/network_operations.rs +++ b/client/src/network_operations.rs @@ -487,8 +487,13 @@ pub fn receive_and_fetch_messages( continue; } - // If gui is false, skip messages containing `[IMAGE_DATA]:` - if !gui && unpadded_message.contains("[IMAGE_DATA]:") { + // If gui is false, skip messages containing `` + if !gui && unpadded_message.contains("") { + continue; + } + + // If gui is false, skip messages containing `` + if !gui && unpadded_message.contains("") { continue; } diff --git a/client/static/index.html b/client/static/index.html new file mode 100644 index 0000000..0eb5f55 --- /dev/null +++ b/client/static/index.html @@ -0,0 +1,147 @@ + + + + + + Amnezichat + + + + + +
+
+
+ + + +
+
+ +
+
+

Settings

+ + + + + + + + + + + +
+
+ + diff --git a/client/static/purify.min.js b/client/static/purify.min.js new file mode 100644 index 0000000..a144b0e --- /dev/null +++ b/client/static/purify.min.js @@ -0,0 +1,3 @@ +/*! @license DOMPurify 2.3.10 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.10/LICENSE */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,n){return(t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,n)}function n(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function r(e,o,a){return(r=n()?Reflect.construct:function(e,n,r){var o=[null];o.push.apply(o,n);var a=new(Function.bind.apply(e,o));return r&&t(a,r.prototype),a}).apply(null,arguments)}function o(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return a(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?n-1:0),o=1;o/gm),q=f(/^data-[\-\w.\u00B7-\uFFFF]/),Y=f(/^aria-[\-\w]+$/),K=f(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),V=f(/^(?:\w+script|data):/i),$=f(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),X=f(/^html$/i),Z=function(){return"undefined"==typeof window?null:window},J=function(t,n){if("object"!==e(t)||"function"!=typeof t.createPolicy)return null;var r=null,o="data-tt-policy-suffix";n.currentScript&&n.currentScript.hasAttribute(o)&&(r=n.currentScript.getAttribute(o));var a="dompurify"+(r?"#"+r:"");try{return t.createPolicy(a,{createHTML:function(e){return e},createScriptURL:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+a+" could not be created."),null}};return function t(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Z(),r=function(e){return t(e)};if(r.version="2.3.10",r.removed=[],!n||!n.document||9!==n.document.nodeType)return r.isSupported=!1,r;var a=n.document,i=n.document,l=n.DocumentFragment,c=n.HTMLTemplateElement,u=n.Node,s=n.Element,f=n.NodeFilter,p=n.NamedNodeMap,d=void 0===p?n.NamedNodeMap||n.MozNamedAttrMap:p,h=n.HTMLFormElement,g=n.DOMParser,y=n.trustedTypes,_=s.prototype,Q=R(_,"cloneNode"),ee=R(_,"nextSibling"),te=R(_,"childNodes"),ne=R(_,"parentNode");if("function"==typeof c){var re=i.createElement("template");re.content&&re.content.ownerDocument&&(i=re.content.ownerDocument)}var oe=J(y,a),ae=oe?oe.createHTML(""):"",ie=i,le=ie.implementation,ce=ie.createNodeIterator,ue=ie.createDocumentFragment,se=ie.getElementsByTagName,me=a.importNode,fe={};try{fe=D(i).documentMode?i.documentMode:{}}catch(e){}var pe={};r.isSupported="function"==typeof ne&&le&&void 0!==le.createHTMLDocument&&9!==fe;var de,he,ge=G,ye=W,be=q,ve=Y,Te=V,Ne=$,Ae=K,Ee=null,we=O({},[].concat(o(L),o(M),o(C),o(F),o(U))),xe=null,Se=O({},[].concat(o(z),o(j),o(B),o(P))),ke=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),_e=null,Oe=null,De=!0,Re=!0,Le=!1,Me=!1,Ce=!1,Ie=!1,Fe=!1,He=!1,Ue=!1,ze=!1,je=!0,Be=!0,Pe=!1,Ge={},We=null,qe=O({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ye=null,Ke=O({},["audio","video","img","source","image","track"]),Ve=null,$e=O({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Xe="http://www.w3.org/1998/Math/MathML",Ze="http://www.w3.org/2000/svg",Je="http://www.w3.org/1999/xhtml",Qe=Je,et=!1,tt=["application/xhtml+xml","text/html"],nt="text/html",rt=null,ot=i.createElement("form"),at=function(e){return e instanceof RegExp||e instanceof Function},it=function(t){rt&&rt===t||(t&&"object"===e(t)||(t={}),t=D(t),de=de=-1===tt.indexOf(t.PARSER_MEDIA_TYPE)?nt:t.PARSER_MEDIA_TYPE,he="application/xhtml+xml"===de?function(e){return e}:N,Ee="ALLOWED_TAGS"in t?O({},t.ALLOWED_TAGS,he):we,xe="ALLOWED_ATTR"in t?O({},t.ALLOWED_ATTR,he):Se,Ve="ADD_URI_SAFE_ATTR"in t?O(D($e),t.ADD_URI_SAFE_ATTR,he):$e,Ye="ADD_DATA_URI_TAGS"in t?O(D(Ke),t.ADD_DATA_URI_TAGS,he):Ke,We="FORBID_CONTENTS"in t?O({},t.FORBID_CONTENTS,he):qe,_e="FORBID_TAGS"in t?O({},t.FORBID_TAGS,he):{},Oe="FORBID_ATTR"in t?O({},t.FORBID_ATTR,he):{},Ge="USE_PROFILES"in t&&t.USE_PROFILES,De=!1!==t.ALLOW_ARIA_ATTR,Re=!1!==t.ALLOW_DATA_ATTR,Le=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Me=t.SAFE_FOR_TEMPLATES||!1,Ce=t.WHOLE_DOCUMENT||!1,He=t.RETURN_DOM||!1,Ue=t.RETURN_DOM_FRAGMENT||!1,ze=t.RETURN_TRUSTED_TYPE||!1,Fe=t.FORCE_BODY||!1,je=!1!==t.SANITIZE_DOM,Be=!1!==t.KEEP_CONTENT,Pe=t.IN_PLACE||!1,Ae=t.ALLOWED_URI_REGEXP||Ae,Qe=t.NAMESPACE||Je,t.CUSTOM_ELEMENT_HANDLING&&at(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(ke.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&at(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(ke.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(ke.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Me&&(Re=!1),Ue&&(He=!0),Ge&&(Ee=O({},o(U)),xe=[],!0===Ge.html&&(O(Ee,L),O(xe,z)),!0===Ge.svg&&(O(Ee,M),O(xe,j),O(xe,P)),!0===Ge.svgFilters&&(O(Ee,C),O(xe,j),O(xe,P)),!0===Ge.mathMl&&(O(Ee,F),O(xe,B),O(xe,P))),t.ADD_TAGS&&(Ee===we&&(Ee=D(Ee)),O(Ee,t.ADD_TAGS,he)),t.ADD_ATTR&&(xe===Se&&(xe=D(xe)),O(xe,t.ADD_ATTR,he)),t.ADD_URI_SAFE_ATTR&&O(Ve,t.ADD_URI_SAFE_ATTR,he),t.FORBID_CONTENTS&&(We===qe&&(We=D(We)),O(We,t.FORBID_CONTENTS,he)),Be&&(Ee["#text"]=!0),Ce&&O(Ee,["html","head","body"]),Ee.table&&(O(Ee,["tbody"]),delete _e.tbody),m&&m(t),rt=t)},lt=O({},["mi","mo","mn","ms","mtext"]),ct=O({},["foreignobject","desc","title","annotation-xml"]),ut=O({},["title","style","font","a","script"]),st=O({},M);O(st,C),O(st,I);var mt=O({},F);O(mt,H);var ft=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Je,tagName:"template"});var n=N(e.tagName),r=N(t.tagName);return e.namespaceURI===Ze?t.namespaceURI===Je?"svg"===n:t.namespaceURI===Xe?"svg"===n&&("annotation-xml"===r||lt[r]):Boolean(st[n]):e.namespaceURI===Xe?t.namespaceURI===Je?"math"===n:t.namespaceURI===Ze?"math"===n&&ct[r]:Boolean(mt[n]):e.namespaceURI===Je&&(!(t.namespaceURI===Ze&&!ct[r])&&(!(t.namespaceURI===Xe&&!lt[r])&&(!mt[n]&&(ut[n]||!st[n]))))},pt=function(e){T(r.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ae}catch(t){e.remove()}}},dt=function(e,t){try{T(r.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){T(r.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!xe[e])if(He||Ue)try{pt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},ht=function(e){var t,n;if(Fe)e=""+e;else{var r=A(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===de&&(e=''+e+"");var o=oe?oe.createHTML(e):e;if(Qe===Je)try{t=(new g).parseFromString(o,de)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(Qe,"template",null);try{t.documentElement.innerHTML=et?"":o}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(i.createTextNode(n),a.childNodes[0]||null),Qe===Je?se.call(t,Ce?"html":"body")[0]:Ce?t.documentElement:a},gt=function(e){return ce.call(e.ownerDocument||e,e,f.SHOW_ELEMENT|f.SHOW_COMMENT|f.SHOW_TEXT,null,!1)},yt=function(e){return e instanceof h&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof d)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},bt=function(t){return"object"===e(u)?t instanceof u:t&&"object"===e(t)&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},vt=function(e,t,n){pe[e]&&b(pe[e],(function(e){e.call(r,t,n,rt)}))},Tt=function(e){var t;if(vt("beforeSanitizeElements",e,null),yt(e))return pt(e),!0;if(S(/[\u0080-\uFFFF]/,e.nodeName))return pt(e),!0;var n=he(e.nodeName);if(vt("uponSanitizeElement",e,{tagName:n,allowedTags:Ee}),e.hasChildNodes()&&!bt(e.firstElementChild)&&(!bt(e.content)||!bt(e.content.firstElementChild))&&S(/<[/\w]/g,e.innerHTML)&&S(/<[/\w]/g,e.textContent))return pt(e),!0;if("select"===n&&S(/