//! Parámetro `waypoint`, URL de destino transportada entre pantallas. use serde::Deserialize; use crate::AutoDefault; use crate::html::RoutePath; /// URL de destino transportada por el parámetro `waypoint`. /// /// Cualquier pantalla alcanzada desde un listado (alta, edición, confirmación, o una cadena de /// varias de ellas) puede recibir en su *query string* un parámetro `waypoint` con la URL de la /// página a la que ir para continuar. /// /// `Waypoint` transporta ese valor a través de la cadena de pantallas. Se extrae de la petición /// entrante con [`web::Query`](crate::web::Query), igual que cualquier otro parámetro, y se repone /// en cada enlace o acción de formulario intermedia con [`append_to()`](Self::append_to) para que /// sobreviva a la siguiente petición. Se resuelve con [`or()`](Self::or) al decidir el destino de /// un enlace de vuelta o de una redirección. /// /// # Ejemplo /// /// ```rust,no_run /// use pagetop::prelude::*; /// /// const ITEMS_PATH: &str = "/items"; /// /// /// GET /items/{id}/edit - Formulario de edición, alcanzado desde el listado. /// async fn edit_get( /// web::Path(id): web::Path, /// web::Query(waypoint): web::Query, /// ) -> Markup { /// // El listado de origen (si lo hay) viaja en la acción del formulario, para volver después. /// let action = waypoint.append_to(format!("{ITEMS_PATH}/{id}/edit")); /// html! { form action=(action) method="post" { /* ... */ } } /// } /// /// /// POST /items/{id}/edit - Guarda los cambios y vuelve al listado de origen, o a `ITEMS_PATH`. /// async fn edit_post( /// web::Path(id): web::Path, /// web::Query(waypoint): web::Query, /// ) -> Response { /// Redirect::see_other(waypoint.or(ITEMS_PATH)) /// } /// ``` #[derive(AutoDefault, Clone, Debug, Deserialize)] #[serde(from = "RawWaypoint")] pub struct Waypoint { waypoint: Option, } impl Waypoint { /// Crea un valor explícito. /// /// Sólo acepta una ruta local que empiece por `/`, pero no por `//` ni `/\` que el navegador /// interpretaría como una URL *protocol-relative*. Una cadena vacía, una URL absoluta /// (`https://...`), un esquema arbitrario (`javascript:...`) o cualquier otro valor que no /// cumpla esa forma se trata igual que `None`. /// /// Este filtro protege contra *open redirect*; el `waypoint` viaja en la *query string* de la /// petición, así que un cliente malicioso lo controla por completo. Por eso se aplica siempre, /// también al deserializar con [`web::Query`](crate::web::Query), el punto de entrada habitual /// donde el valor procede de fuera de la aplicación. pub fn new(waypoint: impl Into>) -> Self { Self { waypoint: waypoint.into().filter(|d| { // Sólo acepta `/algo`. Rechaza cadenas vacías, esquemas (`https:`, `javascript:`) y // las URLs *protocol-relative* (`//evil.example`, `/\evil.example`). let mut chars = d.chars(); match chars.next() { Some('/') => !matches!(chars.next(), Some('/') | Some('\\')), _ => false, } }), } } /// Devuelve la URL de destino, si se proporcionó una y es una ruta local válida. pub fn as_deref(&self) -> Option<&str> { self.waypoint.as_deref() } /// Añade `?waypoint=` a `route`, si hay una URL de destino propia. Si no, /// devuelve `route` sin modificar. /// /// Es la operación habitual para que un enlace o la acción de un formulario transporten el /// waypoint a la siguiente pantalla. Igual que [`or()`](Self::or), devuelve un [`RoutePath`] /// convertible directamente en [`Route`](crate::core::component::Route) sin volver a /// procesarse. A diferencia de [`or()`](Self::or), nunca sustituye `route`; úsalo para propagar /// el waypoint a un enlace intermedio, no para resolver un destino final. /// /// Aparece en dos momentos típicos: el `action` de un formulario, para que un envío no pierda /// el waypoint (ver el ejemplo del módulo), y el `href` de los enlaces que un listado genera /// hacia otras pantallas (ver el ejemplo). /// /// Preserva cualquier *query string* ya presente en `route` sin romperla (como `lang=...` si /// `route` se construyó con [`Context::route()`](crate::core::component::Context::route)). /// /// # Ejemplo /// /// ```rust,no_run /// use pagetop::prelude::*; /// /// const ITEMS_PATH: &str = "/items"; /// /// fn edit_href(cx: &Context, waypoint: &Waypoint, item_id: i32) -> RoutePath { /// waypoint.append_to(cx.route(format!("{ITEMS_PATH}/{item_id}/edit"))) /// } /// ``` pub fn append_to(&self, route: impl Into) -> RoutePath { let mut route = route.into(); if let Some(d) = self.as_deref() { route.alter_param("waypoint", d); } route } /// Devuelve la URL de destino, o `fallback` si no se proporcionó ninguna. /// /// Es la operación habitual al decidir el destino de un enlace de vuelta o de una redirección: /// el waypoint transportado si lo hay, o la URL del listado por defecto si se llegó a esta /// pantalla sin pasar por ninguno. A diferencia de [`append_to()`](Self::append_to), sustituye /// por completo el destino. Úsalo cuando necesites un único `RoutePath` final, no para propagar /// el waypoint a otro enlace intermedio. /// /// Aparece en dos momentos típicos de una misma pantalla: el `href` del enlace "volver" al /// renderizarla (ver el ejemplo) y el destino de `Redirect::see_other(...)` tras guardar con /// éxito en un POST, como en el ejemplo del módulo. /// /// Devuelve un [`RoutePath`], no una cadena ya renderizada, para que el resultado pueda /// convertirse directamente en una [`Route`](crate::core::component::Route) (por ejemplo al /// pasarlo a [`Form::with_action()`](crate::base::component::Form::with_action)) sin volver a /// procesarse. `fallback` acepta cualquier tipo convertible a `RoutePath` (un literal, un /// `String`, o un `RoutePath` ya construido); preservando los parámetros que correspondan. /// /// # Ejemplo /// /// ```rust,no_run /// use pagetop::prelude::*; /// /// const ITEMS_PATH: &str = "/items"; /// /// fn back_href(cx: &Context, waypoint: &Waypoint) -> RoutePath { /// waypoint.or(cx.route(ITEMS_PATH)) /// } /// ``` pub fn or(&self, fallback: impl Into) -> RoutePath { match self.as_deref() { Some(d) => RoutePath::new(d.to_owned()), None => fallback.into(), } } } impl From> for Waypoint { fn from(waypoint: Option) -> Self { Self::new(waypoint) } } impl From for Waypoint { fn from(waypoint: String) -> Self { Self::new(Some(waypoint)) } } // Valor tal y como llega en la deserialización (p. ej. desde `web::Query`), antes de ser filtrado. #[derive(Deserialize)] struct RawWaypoint { #[serde(default)] waypoint: Option, } impl From for Waypoint { fn from(raw: RawWaypoint) -> Self { Self::new(raw.waypoint) } }