@@ -8,7 +8,7 @@ grant execute on function url.url, url.encode to anon;
88
99-- drop table if exists post cascade;
1010create 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;
2525create policy " published" on post for all to anon
2626using (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;
2942insert into post (title, content, published_at)
3043select i::text , xmlelement(name h3, ' hello ' || i)::text , case when i > 6 then null else now() end
3144from 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)
3564with (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
44137from 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;
50143create 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;
66161create or replace view blog (html)
67162with (security_invoker)
68163as
164+ with httpg (error) as (
165+ select nullif(current_setting(' httpg .errors ' , true), ' ' )::jsonb->>' error'
166+ )
69167table head
70168union 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
71184select body::text
72- from html
185+ from post_html
73186;
74187
75188grant 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
0 commit comments