From 65fabe5209df92b53c07bdbff4ecbd2aa4932997 Mon Sep 17 00:00:00 2001 From: neon_arch Date: Sat, 2 Nov 2024 21:52:02 +0300 Subject: [PATCH] :sparkles: feat(routes): add new route `/download` & update the `/settings` route to handle post requests (#427) - Add a new route `/download` which handles the export functionality of the user preferences as a `json` file. - Update the `/settings` route to handle post requests for handling the import functionality of the user preferences. Also, taking care of the sanitization of the user provided `json` values. --- src/lib.rs | 1 + src/server/routes/export_import.rs | 180 +++++++++++++++++++++++++++++ src/server/routes/mod.rs | 1 + 3 files changed, 182 insertions(+) create mode 100644 src/server/routes/export_import.rs diff --git a/src/lib.rs b/src/lib.rs index b7f0d71..ad69ded 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,6 +110,7 @@ pub fn run( .service(server::routes::search::search) // search page .service(router::about) // about page .service(router::settings) // settings page + .service(server::routes::export_import::download) // download page .default_service(web::route().to(router::not_found)) // error page }) .workers(config.threads as usize) diff --git a/src/server/routes/export_import.rs b/src/server/routes/export_import.rs new file mode 100644 index 0000000..6fc445b --- /dev/null +++ b/src/server/routes/export_import.rs @@ -0,0 +1,180 @@ +//! This module handles the settings and download route of the search engine website. + +use crate::{ + handler::{file_path, FileType}, + models::{self, server_models}, + Config, +}; +use actix_multipart::form::{tempfile::TempFile, MultipartForm}; +use actix_web::{ + cookie::{ + time::{Duration, OffsetDateTime}, + Cookie, + }, + get, post, web, HttpRequest, HttpResponse, +}; +use std::io::Read; +use tokio::fs::read_dir; + +/// A helper function that helps in building the list of all available colorscheme/theme/animation +/// names present in the colorschemes, animations and themes folder respectively by excluding the +/// ones that have already been selected via the config file. +/// +/// # Arguments +/// +/// * `style_type` - It takes the style type of the values `theme` and `colorscheme` as an +/// argument. +/// +/// # Error +/// +/// Returns a list of colorscheme/theme names as a vector of tuple strings on success otherwise +/// returns a standard error message. +async fn style_option_list(style_type: &str) -> Result, Box> { + let mut style_options: Vec = Vec::new(); + let mut dir = read_dir(format!( + "{}static/{}/", + file_path(FileType::Theme)?, + style_type, + )) + .await?; + while let Some(file) = dir.next_entry().await? { + let style_name = file.file_name().to_str().unwrap().replace(".css", ""); + style_options.push(style_name.clone()); + } + + if style_type == "animations" { + style_options.push(String::default()) + } + + Ok(style_options.into_boxed_slice()) +} + +/// A helper function which santizes user provided json data from the input file. +/// +/// # Arguments +/// +/// * `config` - It takes the config struct as an argument. +/// * `setting_value` - It takes the cookie struct as an argument. +/// +/// # Error +/// +/// returns a standard error message on failure otherwise it returns the unit type. +async fn sanitize( + config: web::Data<&'static Config>, + setting_value: &mut models::server_models::Cookie, +) -> Result<(), Box> { + // Check whether the theme, colorscheme and animation option is valid by matching it against + // the available option list. If the option provided by the user via the JSON file is invalid + // then replace the user provided by the default one used by the server via the config file. + if !style_option_list("themes") + .await? + .contains(&setting_value.theme.to_string()) + { + setting_value.theme = config.style.theme.clone() + } else if !style_option_list("colorschemes") + .await? + .contains(&setting_value.colorscheme.to_string()) + { + setting_value.colorscheme = config.style.colorscheme.clone() + } else if !style_option_list("animations") + .await? + .contains(&setting_value.animation.clone().unwrap_or_default()) + { + setting_value.animation = config.style.animation.clone() + } + + // Filters out any engines in the list that are invalid by matching each engine against the + // available engine list. + setting_value.engines = setting_value + .engines + .clone() + .into_iter() + .filter_map(|engine| { + config + .upstream_search_engines + .keys() + .any(|other_engine| &engine == other_engine) + .then_some(engine) + }) + .collect(); + + setting_value.safe_search_level = match setting_value.safe_search_level { + 0..2 => setting_value.safe_search_level, + _ => u8::default(), + }; + + Ok(()) +} + +/// A multipart struct which stores user provided input file data in memory. +#[derive(MultipartForm)] +struct File { + /// It stores the input file data in memory. + file: TempFile, +} + +/// Handles the route of the post settings page. +#[post("/settings")] +pub async fn set_settings( + config: web::Data<&'static Config>, + MultipartForm(mut form): MultipartForm, +) -> Result> { + if let Some(file_name) = form.file.file_name { + let file_name_parts = file_name.split("."); + if let 2 = file_name_parts.clone().count() { + if let Some("json") = file_name_parts.last() { + if let 0 = form.file.size { + return Ok(HttpResponse::BadRequest().finish()); + } else { + let mut data = String::new(); + form.file.file.read_to_string(&mut data).unwrap(); + + let mut unsanitized_json_data: models::server_models::Cookie = + serde_json::from_str(&data)?; + + sanitize(config, &mut unsanitized_json_data).await?; + + let sanitized_json_data: String = + serde_json::json!(unsanitized_json_data).to_string(); + + return Ok(HttpResponse::Ok() + .cookie( + Cookie::build("appCookie", sanitized_json_data) + .expires( + OffsetDateTime::now_utc().saturating_add(Duration::weeks(52)), + ) + .finish(), + ) + .finish()); + } + } + } + } + Ok(HttpResponse::Ok().finish()) +} + +/// Handles the route of the download page. +#[get("/download")] +pub async fn download( + config: web::Data<&'static Config>, + req: HttpRequest, +) -> Result> { + let cookie = req.cookie("appCookie"); + + // Get search settings using the user's cookie or from the server's config + let preferences: server_models::Cookie = cookie + .and_then(|cookie_value| serde_json::from_str(cookie_value.value()).ok()) + .unwrap_or_else(|| { + server_models::Cookie::build( + config.style.clone(), + config + .upstream_search_engines + .iter() + .filter_map(|(engine, enabled)| enabled.then_some(engine.clone())) + .collect(), + u8::default(), + ) + }); + + Ok(HttpResponse::Ok().json(preferences)) +} diff --git a/src/server/routes/mod.rs b/src/server/routes/mod.rs index 6bc5750..1723bda 100644 --- a/src/server/routes/mod.rs +++ b/src/server/routes/mod.rs @@ -1,3 +1,4 @@ //! This module provides modules to handle various routes in the search engine website. +pub mod export_import; pub mod search;