1#[cfg(test)]
36mod tests;
37
38use crate::application::Application;
39use crate::cookie::{CookieJar, SetCookie};
40use crate::error::IntoResponse;
41use crate::extract::FromRequest;
42use crate::header::Header;
43use crate::middleware::Middleware;
44use crate::request::Request;
45use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
46use crate::router::names::encode;
47use crate::server::ConnectionInfo;
48
49pub const COOKIE_NAME: &str = "rws_flash";
51
52pub const MAX_COOKIE_BYTES: usize = 3800;
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct FlashMessage {
58 pub kind: String,
59 pub text: String,
60}
61
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
65pub struct Flash {
66 messages: Vec<FlashMessage>,
67}
68
69impl Flash {
70 pub fn read(request: &Request) -> Flash {
72 let Some(header) = request.header("cookie") else {
73 return Flash::default();
74 };
75 match CookieJar::parse(&header.value).get(COOKIE_NAME) {
76 Some(cookie) => Flash { messages: decode_messages(&cookie.value) },
77 None => Flash::default(),
78 }
79 }
80
81 pub fn messages(&self) -> &[FlashMessage] {
83 &self.messages
84 }
85
86 pub fn get(&self, kind: &str) -> Option<&str> {
88 self.messages.iter().find(|m| m.kind == kind).map(|m| m.text.as_str())
89 }
90
91 pub fn notice(&self) -> Option<&str> {
93 self.get("notice")
94 }
95
96 pub fn alert(&self) -> Option<&str> {
98 self.get("alert")
99 }
100
101 pub fn is_empty(&self) -> bool {
102 self.messages.is_empty()
103 }
104}
105
106impl FromRequest for Flash {
107 fn from_request(request: &Request) -> Result<Self, Response> {
108 Ok(Flash::read(request))
109 }
110}
111
112pub fn set(response: &mut Response, kind: &str, text: &str) {
114 let mut messages = Vec::new();
115 response.headers.retain(|h| {
116 let ours = h.name.eq_ignore_ascii_case(Header::_SET_COOKIE) && h.value.starts_with(&format!("{COOKIE_NAME}="));
117 if ours {
118 let value = h.value[COOKIE_NAME.len() + 1..].split(';').next().unwrap_or("");
119 messages = decode_messages(value);
120 }
121 !ours
122 });
123 messages.push(FlashMessage { kind: kind.to_string(), text: text.to_string() });
124 response.headers.push(Header::new(Header::_SET_COOKIE, cookie(&encode_messages(&messages))));
125}
126
127fn cookie(value: &str) -> String {
128 SetCookie::new(COOKIE_NAME, value).path("/").http_only().same_site("Lax").build()
129}
130
131fn clear_cookie() -> String {
132 SetCookie::new(COOKIE_NAME, "").path("/").http_only().same_site("Lax").max_age(0).build()
133}
134
135fn encode_messages(messages: &[FlashMessage]) -> String {
137 let mut out = String::new();
138 for message in messages {
139 let pair = format!("{}={}", encode(&message.kind, false), encode(&message.text, false));
140 if out.len() + pair.len() + 1 > MAX_COOKIE_BYTES {
141 break;
142 }
143 if !out.is_empty() {
144 out.push('&');
145 }
146 out.push_str(&pair);
147 }
148 out
149}
150
151fn decode_messages(value: &str) -> Vec<FlashMessage> {
152 value
153 .split('&')
154 .filter(|pair| !pair.is_empty())
155 .filter_map(|pair| {
156 let (kind, text) = pair.split_once('=')?;
157 Some(FlashMessage { kind: percent_decode(kind)?, text: percent_decode(text)? })
158 })
159 .collect()
160}
161
162pub(crate) fn percent_decode(text: &str) -> Option<String> {
164 let bytes = text.as_bytes();
165 let mut out = Vec::with_capacity(bytes.len());
166 let mut i = 0;
167 while i < bytes.len() {
168 if bytes[i] == b'%' {
169 let hex = text.get(i + 1..i + 3)?;
170 out.push(u8::from_str_radix(hex, 16).ok()?);
171 i += 3;
172 } else {
173 out.push(bytes[i]);
174 i += 1;
175 }
176 }
177 String::from_utf8(out).ok()
178}
179
180#[derive(Clone, Copy, Debug, Default)]
183pub struct FlashLayer;
184
185impl FlashLayer {
186 pub fn new() -> Self {
187 FlashLayer
188 }
189}
190
191fn carries_flash(request: &Request) -> bool {
192 request.header("cookie").is_some_and(|h| CookieJar::parse(&h.value).get(COOKIE_NAME).is_some())
193}
194
195impl Middleware for FlashLayer {
196 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
197 let mut response = next.execute(request, connection)?;
198 if carries_flash(request) {
199 let sets_flash = response.headers.iter().any(|h| h.name.eq_ignore_ascii_case(Header::_SET_COOKIE) && h.value.starts_with(&format!("{COOKIE_NAME}=")));
200 if !sets_flash {
201 response.headers.push(Header::new(Header::_SET_COOKIE, clear_cookie()));
202 }
203 }
204 Ok(response)
205 }
206
207 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
208 true
209 }
210
211 fn is_passthrough(&self, request: &Request, _connection: &ConnectionInfo) -> bool {
213 !carries_flash(request)
214 }
215
216 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
217 true
218 }
219
220 fn layer_name(&self) -> &'static str {
221 "FlashLayer"
222 }
223}
224
225#[derive(Debug, Clone)]
228pub struct Redirect {
229 location: String,
230 status: &'static crate::response::StatusCodeReasonPhrase,
231 flash: Vec<FlashMessage>,
232}
233
234impl Redirect {
235 pub fn to(location: &str) -> Self {
237 Redirect { location: location.replace(['\r', '\n'], ""), status: STATUS_CODE_REASON_PHRASE.n303_see_other, flash: Vec::new() }
238 }
239
240 pub fn found(location: &str) -> Self {
242 Redirect { status: STATUS_CODE_REASON_PHRASE.n302_found, ..Redirect::to(location) }
243 }
244
245 pub fn permanent(location: &str) -> Self {
247 Redirect { status: STATUS_CODE_REASON_PHRASE.n301_moved_permanently, ..Redirect::to(location) }
248 }
249
250 pub fn to_route(name: &str, params: &[(&str, &str)]) -> Self {
253 Redirect::to(&crate::router::url_for(name, params).unwrap_or_else(|| "/".to_string()))
254 }
255
256 pub fn flash(mut self, kind: &str, text: &str) -> Self {
258 self.flash.push(FlashMessage { kind: kind.to_string(), text: text.to_string() });
259 self
260 }
261
262 pub fn notice(self, text: &str) -> Self {
264 self.flash("notice", text)
265 }
266
267 pub fn alert(self, text: &str) -> Self {
269 self.flash("alert", text)
270 }
271
272 pub fn location(&self) -> &str {
274 &self.location
275 }
276}
277
278impl IntoResponse for Redirect {
279 fn into_response(self) -> Response {
280 let mut response = Response::get_response(self.status, None, None);
281 response.headers.push(Header::new(Header::_LOCATION, self.location));
282 for message in &self.flash {
283 set(&mut response, &message.kind, &message.text);
284 }
285 response
286 }
287}