From 4d4de00df587f36fc4f78da61863ac06b7feaa54 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Sat, 11 May 2024 19:14:27 +0200 Subject: [PATCH] sqlpage.hash_password(NULL) now returns NULL --- CHANGELOG.md | 4 ++++ src/webserver/database/sql.rs | 1 + .../sqlpage_functions/function_traits.rs | 19 +++++++++++++------ .../database/sqlpage_functions/functions.rs | 13 ++++++++++--- .../database/sqlpage_functions/mod.rs | 1 + src/webserver/database/syntax_tree.rs | 2 ++ .../it_works_hash_password_null.sql | 3 +++ 7 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 tests/sql_test_files/it_works_hash_password_null.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4c1533..579d5281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG.md +## unreleased + + - `sqlpage.hash_password(NULL)` now returns `NULL` instead of throwing an error. This behavior was changed unintentionally in 0.20.5 and could have broken existing SQLPage websites. + ## 0.20.5 (2024-05-07) - Searchable multi-valued selects in the form component diff --git a/src/webserver/database/sql.rs b/src/webserver/database/sql.rs index 8edd0bbb..ade7795d 100644 --- a/src/webserver/database/sql.rs +++ b/src/webserver/database/sql.rs @@ -400,6 +400,7 @@ fn expr_to_stmt_param(arg: &mut Expr) -> Option { Expr::Value(Value::Number(param_value, _is_long)) => { Some(StmtParam::Literal(param_value.clone())) } + Expr::Value(Value::Null) => Some(StmtParam::Null), Expr::BinaryOp { // 'str1' || 'str2' left, diff --git a/src/webserver/database/sqlpage_functions/function_traits.rs b/src/webserver/database/sqlpage_functions/function_traits.rs index d59fbb5b..74bbbe22 100644 --- a/src/webserver/database/sqlpage_functions/function_traits.rs +++ b/src/webserver/database/sqlpage_functions/function_traits.rs @@ -49,6 +49,13 @@ impl<'a> FunctionParamType<'a> for String { } } +impl<'a> FunctionParamType<'a> for Option { + type TargetType = Self; + fn from_args(arg: &mut std::vec::IntoIter>>) -> anyhow::Result { + >>::from_args(arg).map(|x| x.map(Cow::into_owned)) + } +} + /// similar to `FromStr`, but borrows the input string pub(super) trait BorrowFromStr<'a>: Sized { fn borrow_from_str(s: Cow<'a, str>) -> anyhow::Result; @@ -97,12 +104,6 @@ trait IntoCow<'a> { fn into_cow(self) -> Option>; } -impl<'a> IntoCow<'a> for Option> { - fn into_cow(self) -> Option> { - self - } -} - impl<'a> IntoCow<'a> for Cow<'a, str> { fn into_cow(self) -> Option> { Some(self) @@ -120,3 +121,9 @@ impl<'a> IntoCow<'a> for &'a str { Some(Cow::Borrowed(self)) } } + +impl<'a, T: IntoCow<'a>> IntoCow<'a> for Option { + fn into_cow(self) -> Option> { + self.and_then(IntoCow::into_cow) + } +} diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index 5c1335a7..da392fef 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -16,7 +16,7 @@ super::function_definition_macro::sqlpage_functions! { fetch(http_request: SqlPageFunctionParam>); - hash_password(password: String); + hash_password(password: Option); header((&RequestInfo), name: Cow); path((&RequestInfo)); @@ -159,7 +159,10 @@ async fn fetch( Ok(response_str) } -pub(crate) async fn hash_password(password: String) -> anyhow::Result { +pub(crate) async fn hash_password(password: Option) -> anyhow::Result> { + let Some(password) = password else { + return Ok(None); + }; actix_web::rt::task::spawn_blocking(move || { // Hashes a password using Argon2. This is a CPU-intensive blocking operation. let phf = argon2::Argon2::default(); @@ -169,6 +172,7 @@ pub(crate) async fn hash_password(password: String) -> anyhow::Result { Ok(password_hash.to_string()) }) .await? + .map(Some) } async fn header<'a>(request: &'a RequestInfo, name: Cow<'a, str>) -> Option> { @@ -390,7 +394,10 @@ async fn run_sql<'a>( #[tokio::test] async fn test_hash_password() { - let s = hash_password("password".to_string()).await.unwrap(); + let s = hash_password(Some("password".to_string())) + .await + .unwrap() + .unwrap(); assert!(s.starts_with("$argon2")); } diff --git a/src/webserver/database/sqlpage_functions/mod.rs b/src/webserver/database/sqlpage_functions/mod.rs index 82c08d76..48bf84a8 100644 --- a/src/webserver/database/sqlpage_functions/mod.rs +++ b/src/webserver/database/sqlpage_functions/mod.rs @@ -45,6 +45,7 @@ pub(super) async fn extract_req_param<'a>( .map(SingleOrVec::as_json_str), StmtParam::Error(x) => anyhow::bail!("{}", x), StmtParam::Literal(x) => Some(Cow::Owned(x.to_string())), + StmtParam::Null => None, StmtParam::Concat(args) => concat_params(&args[..], request).await?, StmtParam::FunctionCall(func) => func.evaluate(request).await.with_context(|| { format!( diff --git a/src/webserver/database/syntax_tree.rs b/src/webserver/database/syntax_tree.rs index f913d170..2337dea7 100644 --- a/src/webserver/database/syntax_tree.rs +++ b/src/webserver/database/syntax_tree.rs @@ -21,6 +21,7 @@ pub(crate) enum StmtParam { GetOrPost(String), Error(String), Literal(String), + Null, Concat(Vec), FunctionCall(SqlPageFunctionCall), } @@ -32,6 +33,7 @@ impl std::fmt::Display for StmtParam { StmtParam::Post(name) => write!(f, ":{name}"), StmtParam::GetOrPost(name) => write!(f, "${name}"), StmtParam::Literal(x) => write!(f, "'{}'", x.replace('\'', "''")), + StmtParam::Null => write!(f, "NULL"), StmtParam::Concat(items) => { write!(f, "CONCAT(")?; for item in items { diff --git a/tests/sql_test_files/it_works_hash_password_null.sql b/tests/sql_test_files/it_works_hash_password_null.sql new file mode 100644 index 00000000..9c52f351 --- /dev/null +++ b/tests/sql_test_files/it_works_hash_password_null.sql @@ -0,0 +1,3 @@ +SELECT 'text' as component, + case when sqlpage.hash_password(null) is null then 'It works !' else 'Error !' end + as contents;