Skip to content

Commit e7c8e70

Browse files
committed
comments
1 parent b403a70 commit e7c8e70

5 files changed

Lines changed: 137 additions & 18 deletions

File tree

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
devShells.default = pkgs.mkShell {
189189
packages = with pkgs; [
190190
postgresql_18
191+
postgresql_18.pg_config
191192
cargo cargo-watch cargo-shear clippy rustc rust-analyzer openssl.dev pkg-config
192193
mold-wrapped clang
193194
biscuit-cli

public/cpres/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ input[is="cpres-map"] {
169169
box-shadow: 0 0 0.5rem hsl(0 0% 0% / 35%);
170170
border-radius: 0.5rem;
171171
padding: 1rem;
172+
margin: 1rem;
172173

173174
& img, & object {
174175
width: 100%;

sql/blog/index.sql

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ grant execute on function url.url, url.encode to anon;
88

99
-- drop table if exists post cascade;
1010
create table if not exists post (
11-
id uuid primary key default uuidv7(),
11+
post_id uuid primary key default uuidv7(),
1212
title text not null,
1313
content text not null,
1414
published_at timestamptz default now(),
@@ -25,26 +25,119 @@ drop policy if exists "published" on post;
2525
create policy "published" on post for all to anon
2626
using (published_at is not null);
2727

28-
truncate post;
28+
-- drop table if exists comment cascade;
29+
create table if not exists comment (
30+
comment_id uuid primary key default uuidv7(),
31+
author text not null check (trim(author) <> ''),
32+
content text not null check (length(content) <= 10000 and trim(content) <> ''),
33+
post_id uuid not null references post (post_id) on delete cascade,
34+
published_at timestamptz default now()
35+
);
36+
37+
create index if not exists post_id on comment (post_id);
38+
39+
grant select, insert on table comment to anon;
40+
41+
truncate post cascade;
2942
insert into post (title, content, published_at)
3043
select i::text, xmlelement(name h3, 'hello '||i)::text, case when i > 6 then null else now() end
3144
from generate_series(1, 100) i;
3245

33-
-- drop view if exists html cascade;
34-
create or replace view html (id, body)
46+
insert into comment (author, content, post_id)
47+
select 'example@example.org', xmlconcat(
48+
xmlelement(name h3, 'comment '||i),
49+
xmlelement(name script, 'alert(1)'),
50+
xmlelement(name iframe, xmlattributes('https://wikipedia.fr' as src), ''),
51+
xmlelement(name base, xmlattributes('https://wikipedia.fr' as href)),
52+
xmlelement(name form, xmlattributes('https://wikipedia.fr' as action), xmlelement(name input, xmlattributes('submit' as type))),
53+
xmlelement(name div,
54+
xmlelement(name script, 'alert(2)'),
55+
xmlelement(name style, 'body {color: red !important;}'),
56+
xmlelement(name h4, 'sub h4 '||i)
57+
),
58+
xmlelement(name p, 'test')
59+
)::text, post_id
60+
from generate_series(1, 5) i, post;
61+
62+
-- drop view if exists post_html cascade;
63+
create or replace view post_html (post_id, body)
3564
with (security_invoker)
36-
as with entry (id, xml) as (
37-
select id, xmlelement(name div,
38-
xmlelement(name h1, title),
39-
xmlelement(name article, content::xml)
65+
as with entry (post_id, xml) as (
66+
select post_id, xmlelement(name div,
67+
xmlelement(name article, xmlattributes('card' as class),
68+
xmlelement(name h2, post.title),
69+
post.content::xml,
70+
xmlelement(name hr),
71+
xmlelement(name form, xmlattributes(
72+
'POST' as method,
73+
'/blog/query' as action
74+
),
75+
xmlelement(name input, xmlattributes(
76+
'hidden' as type,
77+
'sql' as name,
78+
'insert into blog.comment (author, content, post_id) values ($1, $2, $3::uuid)' as value
79+
)),
80+
xmlelement(name input, xmlattributes(
81+
'hidden' as type,
82+
'on_error' as name,
83+
'select * from blog.blog' as value
84+
)),
85+
xmlelement(name input, xmlattributes(
86+
'hidden' as type,
87+
'redirect' as name,
88+
url('/blog/query', jsonb_build_object('sql', 'select * from blog.blog')) as value
89+
)),
90+
xmlelement(name input, xmlattributes(
91+
'text' as type,
92+
'params[0]' as name,
93+
'author' as placeholder
94+
)),
95+
xmlelement(name textarea, xmlattributes(
96+
'params[1]' as name,
97+
'comment' as placeholder
98+
), ''),
99+
xmlelement(name input, xmlattributes(
100+
'hidden' as type,
101+
'params[2]' as name,
102+
post_id as value
103+
)),
104+
xmlelement(name input, xmlattributes(
105+
'submit' as type,
106+
'Comment' as value
107+
))
108+
),
109+
xmlelement(name div, xmlattributes('messages' as class), xmlagg(
110+
xmlelement(name article, xmlattributes('card' as class),
111+
comment.content,
112+
-- (
113+
-- with recursive n (comment_id, n, i, ordinality) as (
114+
-- select comment_id, r.n, 0, ordinality
115+
-- from unnest(xpath('/root/*[name() != ''script'']', xmlelement(name root, comment.content::xml))) with ordinality r(n)
116+
-- union all
117+
-- select comment_id, c.n, i + 1, c.ordinality
118+
-- from n, unnest(xpath('/root/*/child::*[name() != ''script'']', xmlelement(name root, n.n))) with ordinality c(n)
119+
-- -- where i < 20
120+
-- )
121+
-- select xmlagg(n.n order by i, ordinality)
122+
-- from n
123+
-- group by comment_id
124+
-- -- order by i, ordinality
125+
-- )),
126+
xmlelement(name address, comment.author)
127+
)
128+
order by comment.published_at
129+
))
130+
)
40131
)
41132
from post
133+
left join comment using (post_id)
134+
group by post_id
42135
)
43-
select id, xml
136+
select post_id, xml
44137
from entry
45138
;
46139

47-
grant select on table html to anon;
140+
grant select on table post_html to anon;
48141

49142
-- drop view if exists blog cascade;
50143
create or replace view head (html)
@@ -57,6 +150,8 @@ select $html$<!DOCTYPE html>
57150
<title>docteurklein's blog</title>
58151
<meta name="color-scheme" content="dark light" />
59152
<meta name="viewport" content="width=device-width, initial-scale=1" />
153+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; form-action 'self'; " />
154+
<link rel="stylesheet" href="/cpres/index.css?v=4" />
60155
</head>
61156
$html$;
62157
@@ -66,10 +161,28 @@ grant select on table head to anon;
66161
create or replace view blog (html)
67162
with (security_invoker)
68163
as
164+
with httpg (error) as (
165+
select nullif(current_setting('httpg.errors', true), '')::jsonb->>'error'
166+
)
69167
table head
70168
union all
169+
select xmlelement(name h1, 'docteurklein''s blog')::text
170+
union all
171+
select xmlelement(name article, xmlattributes(
172+
'card error' as class
173+
), coalesce(
174+
pg_get_constraintdef((
175+
select oid
176+
from pg_constraint
177+
where conname = substring(error, 'violates check constraint "(\w+)"')
178+
)),
179+
error
180+
))::text
181+
from httpg
182+
where error is not null
183+
union all
71184
select body::text
72-
from html
185+
from post_html
73186
;
74187
75188
grant select on table blog to anon;
@@ -88,10 +201,10 @@ entry (xml) as (
88201
select xmlagg(xmlelement(name entry,
89202
xmlelement(name title, title),
90203
xmlelement(name link, xmlattributes(url(format('%s://%s/query', scheme, host), jsonb_build_object(
91-
'sql', 'select * from blog.head union all select body::text from blog.html where id = $1::uuid',
92-
'params[]', id
204+
'sql', 'select * from blog.head union all select body::text from blog.post_html where post_id = $1::uuid',
205+
'params[]', post_id
93206
)) as href)),
94-
xmlelement(name id, 'urn:uuid:' || id),
207+
xmlelement(name id, 'urn:uuid:' || post_id),
95208
xmlelement(name content, xmlattributes('html' as type), content::xml)
96209
) order by published_at desc)
97210
from httpg, post

src/extract/query.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,6 @@ where
240240
let qs = serde_json::from_value::<QueryPart>(serde_json::json!(raw_qs)).unwrap_or_default();
241241
let body = serde_json::from_value::<QueryPart>(serde_json::json!(raw_body)).unwrap_or_default();
242242

243-
let referer_header = headers.get(REFERER);
244-
let referer = referer_header.and_then(|value| value.to_str().ok());
245-
246243
let host = match uri.authority() {
247244
Some(authority) => Some(authority.as_str()),
248245
None => headers
@@ -275,6 +272,9 @@ where
275272
Err(HttpgError::RefusedSql {query: sql.to_string()}.into_response()),
276273
}?;
277274

275+
let referer_header = headers.get(REFERER);
276+
let referer = referer_header.and_then(|value| value.to_str().ok());
277+
278278
let redirect = match qs.redirect.as_ref().or(body.redirect.as_ref()) {
279279
Some(a) if a == "referer" => referer,
280280
Some(a) => Some(a.as_str()),

src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,17 +585,21 @@ async fn post_query(
585585
let mut tx = conn.build_transaction().read_only(true).isolation_level(IsolationLevel::RepeatableRead).start().await?;
586586

587587
let guard = pre(&mut tx, &biscuit, anon_role, &query).await?;
588+
588589
tx.query_typed_raw(
589590
"select set_config('httpg.errors', $1, true)",
590591
vec![(serde_json::to_string(&errors)?, Type::TEXT)]
591592
).await?;
592593

593594
let rows = tx.query_typed_raw(on_error.as_ref(), sql_params).await?;
594595

596+
let mut query = query.clone();
597+
query.redirect = None;
598+
595599
return Ok((
596600
StatusCode::BAD_REQUEST,
597601
response::HttpResult {
598-
query: query.to_owned(),
602+
query: query,
599603
rows: CancelStream::new(rows, guard),
600604
}
601605
).into_response());

0 commit comments

Comments
 (0)