1use std::sync::{Arc, Mutex, PoisonError};
2
3use http::Method;
4use url::Url;
5
6use crate::core::error::{Error, Kind, Result};
7use crate::core::into_url::IntoUrl;
8use crate::core::request::{IntoParamPair, RequestBuilder};
9use crate::core::response::Response;
10use crate::core::session::Session;
11use crate::profile::Preset;
12
13#[derive(Clone)]
14pub struct Tab {
15 session: Session,
16 page: Arc<Mutex<Option<Url>>>,
17}
18
19impl std::fmt::Debug for Tab {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 f.debug_struct("Tab")
22 .field("current", &self.current())
23 .finish_non_exhaustive()
24 }
25}
26
27impl Tab {
28 pub(crate) fn new(session: Session) -> Self {
29 Self {
30 session,
31 page: Arc::new(Mutex::new(None)),
32 }
33 }
34
35 pub fn session(&self) -> &Session {
36 &self.session
37 }
38
39 pub fn current(&self) -> Option<Url> {
40 self.page
41 .lock()
42 .unwrap_or_else(PoisonError::into_inner)
43 .clone()
44 }
45
46 pub fn set_current(&self, page: Option<Url>) {
47 *self.page.lock().unwrap_or_else(PoisonError::into_inner) = page;
48 }
49
50 pub(crate) fn request(
51 &self,
52 method: Method,
53 url: impl IntoUrl,
54 preset: Preset,
55 ) -> RequestBuilder {
56 let page = self.current();
57 let builder = self.build(method, url, page.as_ref()).preset(preset);
58 match page {
59 Some(page) => builder.initiator(page),
60 None => builder,
61 }
62 }
63
64 pub fn fetch(&self, method: Method, url: impl IntoUrl) -> RequestBuilder {
65 self.script(method, url, Preset::Xhr)
66 }
67
68 pub fn xhr(&self, url: impl IntoUrl) -> RequestBuilder {
69 self.fetch(Method::GET, url)
70 }
71
72 pub fn post_json(&self, url: impl IntoUrl, body: &impl serde::Serialize) -> RequestBuilder {
73 self.fetch(Method::POST, url).json(body)
74 }
75
76 pub fn subresource(&self, url: impl IntoUrl, preset: Preset) -> RequestBuilder {
77 self.script(Method::GET, url, preset)
78 }
79
80 fn script(&self, method: Method, url: impl IntoUrl, preset: Preset) -> RequestBuilder {
81 let mut builder = self.request(method, url, preset);
82 if self.current().is_none() {
83 builder.fail(
84 Error::new(Kind::Request).with_message("the tab has no page; open one first"),
85 );
86 }
87 builder
88 }
89
90 pub async fn open(&self, url: impl IntoUrl) -> Result<Response> {
91 let page = self.current();
92 let builder = self
93 .build(Method::GET, url, page.as_ref())
94 .preset(Preset::Navigate);
95 self.navigate(builder).await
96 }
97
98 pub async fn follow(&self, url: impl IntoUrl) -> Result<Response> {
99 self.navigate(self.trusted(self.request(Method::GET, url, Preset::Navigate)))
100 .await
101 }
102
103 pub async fn submit<I, P>(&self, url: impl IntoUrl, fields: I) -> Result<Response>
104 where
105 I: IntoIterator<Item = P>,
106 P: IntoParamPair,
107 {
108 let builder = self
109 .request(Method::POST, url, Preset::FormNavigate)
110 .form(fields);
111 self.navigate(builder).await
112 }
113
114 #[cfg(feature = "html")]
115 pub async fn submit_form(&self, form: &crate::html::Form) -> Result<Response> {
116 let mut url = self.form_action(form)?;
117 let fields = normalized_fields(form);
118 let builder = match form.method() {
119 crate::html::FormMethod::Post => encode_form(
120 self.request(Method::POST, url, Preset::FormNavigate),
121 form.enctype(),
122 fields,
123 ),
124 crate::html::FormMethod::Get => {
125 url.query_pairs_mut().clear().extend_pairs(&fields);
126 self.request(Method::GET, url, Preset::Navigate)
127 }
128 };
129 self.navigate(self.trusted(builder)).await
130 }
131
132 #[cfg(feature = "html")]
133 fn form_action(&self, form: &crate::html::Form) -> Result<Url> {
134 let page = self.current().or_else(|| self.session.base_url().cloned());
135 let base = form
136 .base()
137 .and_then(|href| href.into_url_with_base(page.as_ref()).ok())
138 .or_else(|| page.clone());
139 let against = if form.action().is_empty() {
140 page.as_ref()
141 } else {
142 base.as_ref()
143 };
144 form.action().into_url_with_base(against)
145 }
146
147 pub(crate) async fn navigate(&self, builder: RequestBuilder) -> Result<Response> {
148 let response = builder.send().await?;
149 self.set_current(Some(response.url().clone()));
150 Ok(response)
151 }
152
153 fn trusted(&self, mut builder: RequestBuilder) -> RequestBuilder {
154 builder.trusted_origin = self.current();
155 builder
156 }
157
158 fn build(&self, method: Method, url: impl IntoUrl, page: Option<&Url>) -> RequestBuilder {
159 let Some(page) = page else {
160 return self.session.request(method, url);
161 };
162 match url.into_url_with_base(Some(page)) {
163 Ok(url) => self.session.request(method, url),
164 Err(err) => {
165 let mut builder = self.session.request(method, page.clone());
166 builder.fail(err);
167 builder
168 }
169 }
170 }
171}
172
173#[cfg(feature = "html")]
174fn normalized_fields(form: &crate::html::Form) -> Vec<(String, String)> {
175 form.fields()
176 .iter()
177 .map(|(name, value)| (crlf(name), crlf(value)))
178 .collect()
179}
180
181#[cfg(feature = "html")]
182fn crlf(text: &str) -> String {
183 text.replace("\r\n", "\n")
184 .replace('\r', "\n")
185 .replace('\n', "\r\n")
186}
187
188#[cfg(feature = "html")]
189fn encode_form(
190 builder: RequestBuilder,
191 enctype: crate::html::FormEnctype,
192 fields: Vec<(String, String)>,
193) -> RequestBuilder {
194 use crate::html::FormEnctype;
195 match enctype {
196 FormEnctype::UrlEncoded => builder.form(fields),
197 FormEnctype::TextPlain => {
198 let body: String = fields
199 .iter()
200 .map(|(name, value)| format!("{name}={value}\r\n"))
201 .collect();
202 builder
203 .header(http::header::CONTENT_TYPE, FormEnctype::TextPlain.as_str())
204 .body(body)
205 }
206 FormEnctype::Multipart => builder.multipart(fields.into_iter().fold(
207 crate::core::multipart::Form::new(),
208 |body, (name, value)| body.text(name, value),
209 )),
210 }
211}