Skip to content

Commit c1edb9a

Browse files
committed
add a new "sqlpage.header" function and improve error handling in functions
1 parent 91d6f79 commit c1edb9a

File tree

7 files changed

+216
-30
lines changed

7 files changed

+216
-30
lines changed

build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ fn hashed_filename(path: &Path) -> String {
5555
loop {
5656
let bytes_read = file
5757
.read(&mut buf)
58-
.expect(&format!("error reading {}", path.display()));
58+
.unwrap_or_else(|e| panic!("error reading '{}': {}", path.display(), e));
5959
if bytes_read == 0 {
6060
break;
6161
}

examples/official-site/functions.sql

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1;
2+
3+
select 'text' as component, 'SQLPage built-in functions' as title;
4+
select '
5+
In addition to normal SQL functions supported by your database,
6+
SQLPage provides a few special functions to help you extract data from user requests.
7+
8+
These functions are special, because they are not executed inside your database,
9+
but by SQLPage itself before sending the query to your database.
10+
Thus, they require all the parameters to be known at the time the query is sent to your database.
11+
Function parameters cannot reference columns from the rest of your query.
12+
' as contents_md;
13+
14+
select 'list' as component, 'SQLPage functions' as title;
15+
select name as title,
16+
icon,
17+
'?function=' || name || '#function' as link,
18+
$function = name as active
19+
from sqlpage_functions
20+
order by name;
21+
22+
select 'text' as component,
23+
'The sqlpage.' || $function || ' function' as title,
24+
'function' as id
25+
where $function IS NOT NULL;
26+
27+
SELECT description_md as contents_md FROM sqlpage_functions WHERE name = $function;
28+
29+
select 'title' as component, 3 as level, 'Parameters' as contents where $function IS NOT NULL AND EXISTS (SELECT 1 from sqlpage_function_parameters where "function" = $function);
30+
select 'card' as component, 3 AS columns where $function IS NOT NULL;
31+
select
32+
name as title,
33+
description_md as description,
34+
type as footer,
35+
'azure' as color
36+
from sqlpage_function_parameters where "function" = $function
37+
ORDER BY "index";

examples/official-site/sqlpage/migrations/05_cookie.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ SELECT ''cookie'' as component,
8888
''John Doe'' as value;
8989
```
9090
91-
and then display the value of the cookie:
91+
and then display the value of the cookie using the [`sqlpage.cookie`](functions.sql?function=cookie) function:
9292
9393
```sql
9494
SELECT ''text'' as component,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
-- Insert the http_header component into the component table
2+
INSERT INTO component (name, description, icon)
3+
VALUES (
4+
'authentication',
5+
'An advanced component that can be used to create pages with password-restricted access.
6+
When used, this component has to be at the top of your page, because once the page has begun being sent to the browser, it is too late to restrict access to it.
7+
The authentication component checks if the user has sent the correct password, and if not, redirects them to the URL specified in the link parameter.
8+
If you don''t want to re-check the password on every page (which is an expensive operation),
9+
you can check the password only once and store a session token in your database.
10+
You can use the cookie component to set the session token cookie in the client browser,
11+
and then check whether the token matches what you stored in subsequent pages.',
12+
'lock'
13+
);
14+
-- Insert the parameters for the http_header component into the parameter table
15+
INSERT INTO parameter (
16+
component,
17+
name,
18+
description,
19+
type,
20+
top_level,
21+
optional
22+
)
23+
VALUES (
24+
'authentication',
25+
'link',
26+
'The URL to redirect the user to if they are not logged in.',
27+
'TEXT',
28+
TRUE,
29+
TRUE
30+
),
31+
(
32+
'authentication',
33+
'password',
34+
'The password that was sent by the user. You can set this to :password if you have a login form leading to your page.',
35+
'TEXT',
36+
TRUE,
37+
TRUE
38+
),
39+
(
40+
'authentication',
41+
'password_hash',
42+
'The hash of the password that you stored for the user that is currently trying to log in. These hashes can be generated ahead of time using a tool like https://argon2.online/.',
43+
'TEXT',
44+
TRUE,
45+
TRUE
46+
);
47+
48+
-- Insert an example usage of the http_header component into the example table
49+
INSERT INTO example (component, description)
50+
VALUES (
51+
'authentication',
52+
'
53+
The most basic usage of the authentication component is to simply check if the user has sent the correct password, and if not, redirect them to a login page:
54+
55+
```sql
56+
SELECT ''authentication'' AS component,
57+
''/login'' AS link,
58+
''$argon2id$v=19$m=16,t=2,p=1$TERTd0lIcUpraWFTcmRQYw$+bjtag7Xjb6p1dsuYOkngw'' AS password_hash, -- generated using https://argon2.online/
59+
:password AS password; -- this is the password that the user sent through our form
60+
```
61+
62+
and in `login.sql` :
63+
64+
```sql
65+
SELECT ''form'' AS component, ''Login'' AS title, ''my_protected_page.sql'' AS action;
66+
SELECT ''password'' AS type, ''password'' AS name, ''Password'' AS label;
67+
```
68+
');
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
CREATE TABLE IF NOT EXISTS sqlpage_functions (
2+
"name" TEXT PRIMARY KEY,
3+
"icon" TEXT,
4+
"description_md" TEXT,
5+
"return_type" TEXT
6+
);
7+
CREATE TABLE IF NOT EXISTS sqlpage_function_parameters (
8+
"function" TEXT REFERENCES sqlpage_functions("name"),
9+
"index" INTEGER,
10+
"name" TEXT,
11+
"description_md" TEXT,
12+
"type" TEXT
13+
);
14+
INSERT INTO sqlpage_functions ("name", "icon", "description_md")
15+
VALUES (
16+
'cookie',
17+
'cookie',
18+
'Reads a [cookie](https://en.wikipedia.org/wiki/HTTP_cookie) with the given name from the request.
19+
Returns the value of the cookie as text, or NULL if the cookie is not present.
20+
21+
### Example
22+
23+
Read a cookie called `username` and greet the user by name:
24+
25+
```sql
26+
SELECT ''text'' as component,
27+
''Hello, '' || sqlpage.cookie(''username'') || ''!'' as contents;
28+
```
29+
'
30+
);
31+
INSERT INTO sqlpage_function_parameters (
32+
"function",
33+
"index",
34+
"name",
35+
"description_md",
36+
"type"
37+
)
38+
VALUES (
39+
'cookie',
40+
1,
41+
'name',
42+
'The name of the cookie to read.',
43+
'TEXT'
44+
);
45+
INSERT INTO sqlpage_functions ("name", "icon", "description_md")
46+
VALUES (
47+
'header',
48+
'heading',
49+
'Reads a [header](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields) with the given name from the request.
50+
Returns the value of the header as text, or NULL if the header is not present.
51+
52+
### Example
53+
54+
Log the [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) of the browser making the request in the database:
55+
56+
```sql
57+
INSERT INTO user_agent_log (user_agent) VALUES (sqlpage.header(''user-agent''));
58+
```
59+
'
60+
);
61+
INSERT INTO sqlpage_function_parameters (
62+
"function",
63+
"index",
64+
"name",
65+
"description_md",
66+
"type"
67+
)
68+
VALUES (
69+
'header',
70+
1,
71+
'name',
72+
'The name of the HTTP header to read.',
73+
'TEXT'
74+
);

src/webserver/database/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ pub async fn stream_query_results_direct<'a>(
117117
for res in &sql_file.statements {
118118
match res {
119119
Ok(stmt)=>{
120-
let query = bind_parameters(stmt, request);
120+
let query = bind_parameters(stmt, request)?;
121121
let mut stream = query.fetch_many(&db.connection);
122122
while let Some(elem) = stream.next().await {
123123
yield elem.with_context(|| format!("Error while running SQL: {stmt}"))
@@ -141,7 +141,7 @@ fn clone_anyhow_err(err: &anyhow::Error) -> anyhow::Error {
141141
fn bind_parameters<'a>(
142142
stmt: &'a PreparedStatement,
143143
request: &'a RequestInfo,
144-
) -> Query<'a, sqlx::Any, AnyArguments<'a>> {
144+
) -> anyhow::Result<Query<'a, sqlx::Any, AnyArguments<'a>>> {
145145
let mut arguments = AnyArguments::default();
146146
for param in &stmt.parameters {
147147
let argument = match param {
@@ -152,6 +152,8 @@ fn bind_parameters<'a>(
152152
.get(x)
153153
.or_else(|| request.get_variables.get(x)),
154154
StmtParam::Cookie(x) => request.cookies.get(x),
155+
StmtParam::Header(x) => request.headers.get(x),
156+
StmtParam::Error(x) => anyhow::bail!("{}", x),
155157
};
156158
log::debug!("Binding value {:?} in statement {}", &argument, stmt);
157159
match argument {
@@ -162,7 +164,7 @@ fn bind_parameters<'a>(
162164
}
163165
}
164166
}
165-
stmt.statement.query_with(arguments)
167+
Ok(stmt.statement.query_with(arguments))
166168
}
167169

168170
#[derive(Debug)]
@@ -300,6 +302,8 @@ enum StmtParam {
300302
Post(String),
301303
GetOrPost(String),
302304
Cookie(String),
305+
Header(String),
306+
Error(String),
303307
}
304308

305309
#[actix_web::test]

src/webserver/database/sql.rs

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -148,32 +148,35 @@ impl ParameterExtractor {
148148
mut arguments: Vec<FunctionArg>,
149149
) -> Expr {
150150
#[allow(clippy::single_match_else)]
151-
match (func_name, arguments.as_mut_slice()) {
152-
(
153-
"cookie",
154-
[FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(
155-
Value::SingleQuotedString(cookie_name),
156-
)))],
157-
) => {
158-
let placeholder = self.make_placeholder();
159-
self.parameters
160-
.push(StmtParam::Cookie(std::mem::take(cookie_name)));
161-
placeholder
151+
let placeholder = self.make_placeholder();
152+
let param = match func_name {
153+
"cookie" => extract_single_quoted_string("cookie", &mut arguments)
154+
.map_or_else(StmtParam::Error, StmtParam::Cookie),
155+
"header" => extract_single_quoted_string("header", &mut arguments)
156+
.map_or_else(StmtParam::Error, StmtParam::Header),
157+
unknown_name => {
158+
StmtParam::Error(format!("Unknown function {unknown_name}({arguments:#?})"))
162159
}
163-
_ => {
164-
log::warn!(
165-
"Unsupported SQLPage function: {func_name} with arguments {arguments:#?}"
166-
);
167-
Expr::Function(Function {
168-
name: ObjectName(vec![Ident::new("unsupported_sqlpage_function")]),
169-
args: arguments,
170-
special: false,
171-
distinct: false,
172-
over: None,
173-
order_by: vec![],
174-
})
175-
}
176-
}
160+
};
161+
self.parameters.push(param);
162+
placeholder
163+
}
164+
}
165+
166+
fn extract_single_quoted_string(
167+
func_name: &'static str,
168+
arguments: &mut [FunctionArg],
169+
) -> Result<String, String> {
170+
if let [FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(Value::SingleQuotedString(
171+
param_value,
172+
))))] = arguments
173+
{
174+
Ok(std::mem::take(param_value))
175+
} else {
176+
Err(format!(
177+
"{func_name}({args}) is not a valid call. Expected a literal single quoted string.",
178+
args = arguments.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
179+
))
177180
}
178181
}
179182

0 commit comments

Comments
 (0)