neon_arch commited on
Commit
dc34d51
·
unverified ·
1 Parent(s): b911eae

:sparkles: feat(routes): add new route `/download` & update the `/settings` route to handle post requests (#427)

Browse files

- 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 CHANGED
@@ -110,6 +110,7 @@ pub fn run(
110
  .service(server::routes::search::search) // search page
111
  .service(router::about) // about page
112
  .service(router::settings) // settings page
 
113
  .default_service(web::route().to(router::not_found)) // error page
114
  })
115
  .workers(config.threads as usize)
 
110
  .service(server::routes::search::search) // search page
111
  .service(router::about) // about page
112
  .service(router::settings) // settings page
113
+ .service(server::routes::export_import::download) // download page
114
  .default_service(web::route().to(router::not_found)) // error page
115
  })
116
  .workers(config.threads as usize)
src/server/routes/export_import.rs ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! This module handles the settings and download route of the search engine website.
2
+
3
+ use crate::{
4
+ handler::{file_path, FileType},
5
+ models::{self, server_models},
6
+ Config,
7
+ };
8
+ use actix_multipart::form::{tempfile::TempFile, MultipartForm};
9
+ use actix_web::{
10
+ cookie::{
11
+ time::{Duration, OffsetDateTime},
12
+ Cookie,
13
+ },
14
+ get, post, web, HttpRequest, HttpResponse,
15
+ };
16
+ use std::io::Read;
17
+ use tokio::fs::read_dir;
18
+
19
+ /// A helper function that helps in building the list of all available colorscheme/theme/animation
20
+ /// names present in the colorschemes, animations and themes folder respectively by excluding the
21
+ /// ones that have already been selected via the config file.
22
+ ///
23
+ /// # Arguments
24
+ ///
25
+ /// * `style_type` - It takes the style type of the values `theme` and `colorscheme` as an
26
+ /// argument.
27
+ ///
28
+ /// # Error
29
+ ///
30
+ /// Returns a list of colorscheme/theme names as a vector of tuple strings on success otherwise
31
+ /// returns a standard error message.
32
+ async fn style_option_list(style_type: &str) -> Result<Box<[String]>, Box<dyn std::error::Error>> {
33
+ let mut style_options: Vec<String> = Vec::new();
34
+ let mut dir = read_dir(format!(
35
+ "{}static/{}/",
36
+ file_path(FileType::Theme)?,
37
+ style_type,
38
+ ))
39
+ .await?;
40
+ while let Some(file) = dir.next_entry().await? {
41
+ let style_name = file.file_name().to_str().unwrap().replace(".css", "");
42
+ style_options.push(style_name.clone());
43
+ }
44
+
45
+ if style_type == "animations" {
46
+ style_options.push(String::default())
47
+ }
48
+
49
+ Ok(style_options.into_boxed_slice())
50
+ }
51
+
52
+ /// A helper function which santizes user provided json data from the input file.
53
+ ///
54
+ /// # Arguments
55
+ ///
56
+ /// * `config` - It takes the config struct as an argument.
57
+ /// * `setting_value` - It takes the cookie struct as an argument.
58
+ ///
59
+ /// # Error
60
+ ///
61
+ /// returns a standard error message on failure otherwise it returns the unit type.
62
+ async fn sanitize(
63
+ config: web::Data<&'static Config>,
64
+ setting_value: &mut models::server_models::Cookie,
65
+ ) -> Result<(), Box<dyn std::error::Error>> {
66
+ // Check whether the theme, colorscheme and animation option is valid by matching it against
67
+ // the available option list. If the option provided by the user via the JSON file is invalid
68
+ // then replace the user provided by the default one used by the server via the config file.
69
+ if !style_option_list("themes")
70
+ .await?
71
+ .contains(&setting_value.theme.to_string())
72
+ {
73
+ setting_value.theme = config.style.theme.clone()
74
+ } else if !style_option_list("colorschemes")
75
+ .await?
76
+ .contains(&setting_value.colorscheme.to_string())
77
+ {
78
+ setting_value.colorscheme = config.style.colorscheme.clone()
79
+ } else if !style_option_list("animations")
80
+ .await?
81
+ .contains(&setting_value.animation.clone().unwrap_or_default())
82
+ {
83
+ setting_value.animation = config.style.animation.clone()
84
+ }
85
+
86
+ // Filters out any engines in the list that are invalid by matching each engine against the
87
+ // available engine list.
88
+ setting_value.engines = setting_value
89
+ .engines
90
+ .clone()
91
+ .into_iter()
92
+ .filter_map(|engine| {
93
+ config
94
+ .upstream_search_engines
95
+ .keys()
96
+ .any(|other_engine| &engine == other_engine)
97
+ .then_some(engine)
98
+ })
99
+ .collect();
100
+
101
+ setting_value.safe_search_level = match setting_value.safe_search_level {
102
+ 0..2 => setting_value.safe_search_level,
103
+ _ => u8::default(),
104
+ };
105
+
106
+ Ok(())
107
+ }
108
+
109
+ /// A multipart struct which stores user provided input file data in memory.
110
+ #[derive(MultipartForm)]
111
+ struct File {
112
+ /// It stores the input file data in memory.
113
+ file: TempFile,
114
+ }
115
+
116
+ /// Handles the route of the post settings page.
117
+ #[post("/settings")]
118
+ pub async fn set_settings(
119
+ config: web::Data<&'static Config>,
120
+ MultipartForm(mut form): MultipartForm<File>,
121
+ ) -> Result<HttpResponse, Box<dyn std::error::Error>> {
122
+ if let Some(file_name) = form.file.file_name {
123
+ let file_name_parts = file_name.split(".");
124
+ if let 2 = file_name_parts.clone().count() {
125
+ if let Some("json") = file_name_parts.last() {
126
+ if let 0 = form.file.size {
127
+ return Ok(HttpResponse::BadRequest().finish());
128
+ } else {
129
+ let mut data = String::new();
130
+ form.file.file.read_to_string(&mut data).unwrap();
131
+
132
+ let mut unsanitized_json_data: models::server_models::Cookie =
133
+ serde_json::from_str(&data)?;
134
+
135
+ sanitize(config, &mut unsanitized_json_data).await?;
136
+
137
+ let sanitized_json_data: String =
138
+ serde_json::json!(unsanitized_json_data).to_string();
139
+
140
+ return Ok(HttpResponse::Ok()
141
+ .cookie(
142
+ Cookie::build("appCookie", sanitized_json_data)
143
+ .expires(
144
+ OffsetDateTime::now_utc().saturating_add(Duration::weeks(52)),
145
+ )
146
+ .finish(),
147
+ )
148
+ .finish());
149
+ }
150
+ }
151
+ }
152
+ }
153
+ Ok(HttpResponse::Ok().finish())
154
+ }
155
+
156
+ /// Handles the route of the download page.
157
+ #[get("/download")]
158
+ pub async fn download(
159
+ config: web::Data<&'static Config>,
160
+ req: HttpRequest,
161
+ ) -> Result<HttpResponse, Box<dyn std::error::Error>> {
162
+ let cookie = req.cookie("appCookie");
163
+
164
+ // Get search settings using the user's cookie or from the server's config
165
+ let preferences: server_models::Cookie = cookie
166
+ .and_then(|cookie_value| serde_json::from_str(cookie_value.value()).ok())
167
+ .unwrap_or_else(|| {
168
+ server_models::Cookie::build(
169
+ config.style.clone(),
170
+ config
171
+ .upstream_search_engines
172
+ .iter()
173
+ .filter_map(|(engine, enabled)| enabled.then_some(engine.clone()))
174
+ .collect(),
175
+ u8::default(),
176
+ )
177
+ });
178
+
179
+ Ok(HttpResponse::Ok().json(preferences))
180
+ }
src/server/routes/mod.rs CHANGED
@@ -1,3 +1,4 @@
1
  //! This module provides modules to handle various routes in the search engine website.
2
 
 
3
  pub mod search;
 
1
  //! This module provides modules to handle various routes in the search engine website.
2
 
3
+ pub mod export_import;
4
  pub mod search;