Skip to main content

leyline/core/
tab.rs

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}