1#![allow(clippy::cmp_owned)]
2
3use std::collections::HashMap;
4
5use crate::server::ResponseExt;
7use crate::subreddit::join_until_size_limit;
8use crate::utils::{deflate_decompress, redirect, template, Preferences};
9use cookie::Cookie;
10use futures_lite::StreamExt;
11use hyper::{Body, Request, Response};
12use rinja::Template;
13use time::{Duration, OffsetDateTime};
14use tokio::time::timeout;
15use url::form_urlencoded;
16
17#[derive(Template)]
19#[template(path = "settings.html")]
20struct SettingsTemplate {
21 prefs: Preferences,
22 url: String,
23}
24
25const PREFS: [&str; 19] = [
28 "theme",
29 "front_page",
30 "layout",
31 "wide",
32 "comment_sort",
33 "post_sort",
34 "blur_spoiler",
35 "show_nsfw",
36 "blur_nsfw",
37 "use_hls",
38 "hide_hls_notification",
39 "autoplay_videos",
40 "hide_sidebar_and_summary",
41 "fixed_navbar",
42 "hide_awards",
43 "hide_score",
44 "disable_visit_reddit_confirmation",
45 "video_quality",
46 "remove_default_feeds",
47];
48
49pub async fn get(req: Request<Body>) -> Result<Response<Body>, String> {
53 let url = req.uri().to_string();
54 Ok(template(&SettingsTemplate {
55 prefs: Preferences::new(&req),
56 url,
57 }))
58}
59
60pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
62 let (parts, mut body) = req.into_parts();
64
65 let _cookies: Vec<Cookie<'_>> = parts
67 .headers
68 .get_all("Cookie")
69 .iter()
70 .filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
71 .collect();
72
73 let body_bytes = body
76 .try_fold(Vec::new(), |mut data, chunk| {
77 data.extend_from_slice(&chunk);
78 Ok(data)
79 })
80 .await
81 .map_err(|e| e.to_string())?;
82
83 let form = url::form_urlencoded::parse(&body_bytes).collect::<HashMap<_, _>>();
84
85 let mut response = redirect("/settings");
86
87 for &name in &PREFS {
88 match form.get(name) {
89 Some(value) => response.insert_cookie(
90 Cookie::build((name.to_owned(), value.clone()))
91 .path("/")
92 .http_only(true)
93 .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
94 .into(),
95 ),
96 None => response.remove_cookie(name.to_string()),
97 };
98 }
99
100 Ok(response)
101}
102
103fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body> {
104 let (parts, _) = req.into_parts();
106
107 let _cookies: Vec<Cookie<'_>> = parts
109 .headers
110 .get_all("Cookie")
111 .iter()
112 .filter_map(|header| Cookie::parse(header.to_str().unwrap_or_default()).ok())
113 .collect();
114
115 let query = parts.uri.query().unwrap_or_default().as_bytes();
116
117 let form = url::form_urlencoded::parse(query).collect::<HashMap<_, _>>();
118
119 let path = match form.get("redirect") {
120 Some(value) => format!("/{}", value.replace("%26", "&").replace("%23", "#")),
121 None => "/".to_string(),
122 };
123
124 let mut response = redirect(&path);
125
126 for name in PREFS {
127 match form.get(name) {
128 Some(value) => response.insert_cookie(
129 Cookie::build((name.to_owned(), value.clone()))
130 .path("/")
131 .http_only(true)
132 .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
133 .into(),
134 ),
135 None => {
136 if remove_cookies {
137 response.remove_cookie(name.to_string());
138 }
139 }
140 };
141 }
142
143 let subscriptions = form.get("subscriptions");
145 let filters = form.get("filters");
146
147 let cookies_string = parts
149 .headers
150 .get("cookie")
151 .map(|hv| hv.to_str().unwrap_or("").to_string()) .unwrap_or_else(String::new); if subscriptions.is_some() {
156 let sub_list: Vec<String> = subscriptions.expect("Subscriptions").split('+').map(str::to_string).collect();
157
158 let mut subscriptions_number_to_delete_from = 0;
160
161 for (subscriptions_number, list) in join_until_size_limit(&sub_list).into_iter().enumerate() {
163 let subscriptions_cookie = if subscriptions_number == 0 {
164 "subscriptions".to_string()
165 } else {
166 format!("subscriptions{}", subscriptions_number)
167 };
168
169 response.insert_cookie(
170 Cookie::build((subscriptions_cookie, list))
171 .path("/")
172 .http_only(true)
173 .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
174 .into(),
175 );
176
177 subscriptions_number_to_delete_from += 1;
178 }
179
180 while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) {
182 response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}"));
184
185 subscriptions_number_to_delete_from += 1;
187 }
188 } else {
189 response.remove_cookie("subscriptions".to_string());
191
192 let mut subscriptions_number_to_delete_from = 1;
194
195 while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) {
197 response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}"));
199
200 subscriptions_number_to_delete_from += 1;
202 }
203 }
204
205 if filters.is_some() {
207 let filters_list: Vec<String> = filters.expect("Filters").split('+').map(str::to_string).collect();
208
209 let mut filters_number_to_delete_from = 0;
211
212 for (filters_number, list) in join_until_size_limit(&filters_list).into_iter().enumerate() {
214 let filters_cookie = if filters_number == 0 {
215 "filters".to_string()
216 } else {
217 format!("filters{}", filters_number)
218 };
219
220 response.insert_cookie(
221 Cookie::build((filters_cookie, list))
222 .path("/")
223 .http_only(true)
224 .expires(OffsetDateTime::now_utc() + Duration::weeks(52))
225 .into(),
226 );
227
228 filters_number_to_delete_from += 1;
229 }
230
231 while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) {
233 response.remove_cookie(format!("filters{filters_number_to_delete_from}"));
235
236 filters_number_to_delete_from += 1;
238 }
239 } else {
240 response.remove_cookie("filters".to_string());
242
243 let mut filters_number_to_delete_from = 1;
245
246 while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) {
248 response.remove_cookie(format!("filters{filters_number_to_delete_from}"));
250
251 filters_number_to_delete_from += 1;
253 }
254 }
255
256 response
257}
258
259pub async fn restore(req: Request<Body>) -> Result<Response<Body>, String> {
261 Ok(set_cookies_method(req, true))
262}
263
264pub async fn update(req: Request<Body>) -> Result<Response<Body>, String> {
265 Ok(set_cookies_method(req, false))
266}
267
268pub async fn encoded_restore(req: Request<Body>) -> Result<Response<Body>, String> {
269 let body = hyper::body::to_bytes(req.into_body())
270 .await
271 .map_err(|e| format!("Failed to get bytes from request body: {}", e))?;
272
273 if body.len() > 1024 * 1024 {
274 return Err("Request body too large".to_string());
275 }
276
277 let encoded_prefs = form_urlencoded::parse(&body)
278 .find(|(key, _)| key == "encoded_prefs")
279 .map(|(_, value)| value)
280 .ok_or_else(|| "encoded_prefs parameter not found in request body".to_string())?;
281
282 let bytes = base2048::decode(&encoded_prefs).ok_or_else(|| "Failed to decode base2048 encoded preferences".to_string())?;
283
284 let out = timeout(std::time::Duration::from_secs(1), async { deflate_decompress(bytes) })
285 .await
286 .map_err(|e| format!("Failed to decompress bytes: {}", e))??;
287
288 let mut prefs: Preferences = timeout(std::time::Duration::from_secs(1), async { bincode::deserialize(&out) })
289 .await
290 .map_err(|e| format!("Failed to deserialize preferences: {}", e))?
291 .map_err(|e| format!("Failed to deserialize bytes into Preferences struct: {}", e))?;
292
293 prefs.available_themes = vec![];
294
295 let url = format!("/settings/restore/?{}", prefs.to_urlencoded()?);
296
297 Ok(redirect(&url))
298}