redlib/
settings.rs

1#![allow(clippy::cmp_owned)]
2
3use std::collections::HashMap;
4
5// CRATES
6use 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// STRUCTS
18#[derive(Template)]
19#[template(path = "settings.html")]
20struct SettingsTemplate {
21	prefs: Preferences,
22	url: String,
23}
24
25// CONSTANTS
26
27const 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
49// FUNCTIONS
50
51// Retrieve cookies from request "Cookie" header
52pub 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
60// Set cookies using response "Set-Cookie" header
61pub async fn set(req: Request<Body>) -> Result<Response<Body>, String> {
62	// Split the body into parts
63	let (parts, mut body) = req.into_parts();
64
65	// Grab existing cookies
66	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	// Aggregate the body...
74	// let whole_body = hyper::body::aggregate(req).await.map_err(|e| e.to_string())?;
75	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	// Split the body into parts
105	let (parts, _) = req.into_parts();
106
107	// Grab existing cookies
108	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	// Get subscriptions/filters to restore from query string
144	let subscriptions = form.get("subscriptions");
145	let filters = form.get("filters");
146
147	// We can't search through the cookies directly like in subreddit.rs, so instead we have to make a string out of the request's headers to search through
148	let cookies_string = parts
149		.headers
150		.get("cookie")
151		.map(|hv| hv.to_str().unwrap_or("").to_string()) // Return String
152		.unwrap_or_else(String::new); // Return an empty string if None
153
154	// If there are subscriptions to restore set them and delete any old subscriptions cookies, otherwise delete them all
155	if subscriptions.is_some() {
156		let sub_list: Vec<String> = subscriptions.expect("Subscriptions").split('+').map(str::to_string).collect();
157
158		// Start at 0 to keep track of what number we need to start deleting old subscription cookies from
159		let mut subscriptions_number_to_delete_from = 0;
160
161		// Starting at 0 so we handle the subscription cookie without a number first
162		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 subscriptionsNUMBER= is in the string of cookies add a response removing that cookie
181		while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) {
182			// Remove that subscriptions cookie
183			response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}"));
184
185			// Increment subscriptions cookie number
186			subscriptions_number_to_delete_from += 1;
187		}
188	} else {
189		// Remove unnumbered subscriptions cookie
190		response.remove_cookie("subscriptions".to_string());
191
192		// Starts at one to deal with the first numbered subscription cookie and onwards
193		let mut subscriptions_number_to_delete_from = 1;
194
195		// While subscriptionsNUMBER= is in the string of cookies add a response removing that cookie
196		while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) {
197			// Remove that subscriptions cookie
198			response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}"));
199
200			// Increment subscriptions cookie number
201			subscriptions_number_to_delete_from += 1;
202		}
203	}
204
205	// If there are filters to restore set them and delete any old filters cookies, otherwise delete them all
206	if filters.is_some() {
207		let filters_list: Vec<String> = filters.expect("Filters").split('+').map(str::to_string).collect();
208
209		// Start at 0 to keep track of what number we need to start deleting old subscription cookies from
210		let mut filters_number_to_delete_from = 0;
211
212		// Starting at 0 so we handle the subscription cookie without a number first
213		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 filtersNUMBER= is in the string of cookies add a response removing that cookie
232		while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) {
233			// Remove that filters cookie
234			response.remove_cookie(format!("filters{filters_number_to_delete_from}"));
235
236			// Increment filters cookie number
237			filters_number_to_delete_from += 1;
238		}
239	} else {
240		// Remove unnumbered filters cookie
241		response.remove_cookie("filters".to_string());
242
243		// Starts at one to deal with the first numbered subscription cookie and onwards
244		let mut filters_number_to_delete_from = 1;
245
246		// While filtersNUMBER= is in the string of cookies add a response removing that cookie
247		while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) {
248			// Remove that sfilters cookie
249			response.remove_cookie(format!("filters{filters_number_to_delete_from}"));
250
251			// Increment filters cookie number
252			filters_number_to_delete_from += 1;
253		}
254	}
255
256	response
257}
258
259// Set cookies using response "Set-Cookie" header
260pub 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}