]> git.proxmox.com Git - pve-http-server.git/blame - src/PVE/APIServer/Formatter/HTML.pm
html formatter: encode href attributes
[pve-http-server.git] / src / PVE / APIServer / Formatter / HTML.pm
CommitLineData
63307beb
DM
1package PVE::APIServer::Formatter::HTML;
2
3use strict;
4use warnings;
5
6use PVE::APIServer::Formatter;
7use HTTP::Status;
8use JSON;
9use HTML::Entities;
10use PVE::JSONSchema;
11use PVE::APIServer::Formatter::Bootstrap;
12use PVE::APIServer::Formatter::Standard;
13
14my $portal_format = 'html';
15my $portal_ct = 'text/html;charset=UTF-8';
16
ca304f91
DM
17my $get_portal_base_url = sub {
18 my ($config) = @_;
19 return "$config->{base_uri}/$portal_format";
20};
21
22my $get_portal_login_url = sub {
23 my ($config) = @_;
24 return "$config->{base_uri}/$portal_format/access/ticket";
25};
63307beb
DM
26
27sub render_page {
ca304f91 28 my ($doc, $html, $config) = @_;
63307beb
DM
29
30 my $items = [];
31
32 push @$items, {
33 tag => 'li',
34 cn => {
35 tag => 'a',
ca304f91 36 href => $get_portal_login_url->($config),
63307beb
DM
37 onClick => "PVE.delete_auth_cookie();",
38 text => "Logout",
39 }};
40
ca304f91 41 my $base_url = $get_portal_base_url->($config);
63307beb 42
63307beb
DM
43 my $nav = $doc->el(
44 class => "navbar navbar-inverse navbar-fixed-top",
45 role => "navigation", cn => {
46 class => "container", cn => [
47 {
48 class => "navbar-header", cn => [
49 {
50 tag => 'button',
51 type => 'button',
52 class => "navbar-toggle",
53 'data-toggle' => "collapse",
54 'data-target' => ".navbar-collapse",
55 cn => [
56 { tag => 'span', class => 'sr-only', text => "Toggle navigation" },
57 { tag => 'span', class => 'icon-bar' },
58 { tag => 'span', class => 'icon-bar' },
59 { tag => 'span', class => 'icon-bar' },
60 ],
61 },
62 {
63 tag => 'a',
64 class => "navbar-brand",
ca304f91
DM
65 href => $base_url,
66 text => $config->{title},
63307beb
DM
67 },
68 ],
69 },
70 {
71 class => "collapse navbar-collapse",
72 cn => {
73 tag => 'ul',
74 class => "nav navbar-nav",
75 cn => $items,
76 },
77 },
78 ],
79 });
80
81 $items = [];
82 my @pcomp = split('/', $doc->{url});
83 shift @pcomp; # empty
84 shift @pcomp; # api2
85 shift @pcomp; # $format
86
ca304f91 87 my $href = $base_url;
63307beb
DM
88 push @$items, { tag => 'li', cn => {
89 tag => 'a',
90 href => $href,
91 text => 'Home'}};
92
93 foreach my $comp (@pcomp) {
67817350 94 $href .= "/".encode_entities($comp);
63307beb
DM
95 push @$items, { tag => 'li', cn => {
96 tag => 'a',
97 href => $href,
98 text => $comp}};
99 }
100
101 my $breadcrumbs = $doc->el(tag => 'ol', class => 'breadcrumb container', cn => $items);
102
103 return $doc->body($nav . $breadcrumbs . $html);
104}
105
106my $login_form = sub {
ca304f91 107 my ($config, $doc, $param, $errmsg) = @_;
63307beb
DM
108
109 $param = {} if !$param;
110
111 my $username = $param->{username} || '';
112 my $password = $param->{password} || '';
113
114 my $items = [
115 {
116 tag => 'label',
117 text => "Please sign in",
118 },
119 {
120 tag => 'input',
121 type => 'text',
122 class => 'form-control',
123 name => 'username',
124 value => $username,
125 placeholder => "Enter user name",
126 required => 1,
127 autofocus => 1,
128 },
129 {
130 tag => 'input',
131 type => 'password',
132 class => 'form-control',
133 name => 'password',
134 value => $password,
135 placeholder => 'Password',
136 required => 1,
137 },
138 ];
139
140 my $html = '';
141
142 $html .= $doc->alert(text => $errmsg) if ($errmsg);
143
144 $html .= $doc->el(
145 class => 'container',
146 cn => {
147 tag => 'form',
148 role => 'form',
149 method => 'POST',
ca304f91 150 action => $get_portal_login_url->($config),
63307beb
DM
151 cn => [
152 {
153 class => 'form-group',
154 cn => $items,
155 },
156 {
157 tag => 'button',
158 type => 'submit',
159 class => 'btn btn-lg btn-primary btn-block',
160 text => "Sign in",
161 },
162 ],
163 });
164
165 return $html;
166};
167
168PVE::APIServer::Formatter::register_login_formatter($portal_format, sub {
ca304f91 169 my ($path, $auth, $config) = @_;
63307beb 170
ca304f91 171 my $headers = HTTP::Headers->new(Location => $get_portal_login_url->($config));
63307beb
DM
172 return HTTP::Response->new(301, "Moved", $headers);
173});
174
175PVE::APIServer::Formatter::register_formatter($portal_format, sub {
ca304f91 176 my ($res, $data, $param, $path, $auth, $config) = @_;
63307beb
DM
177
178 # fixme: clumsy!
c7154375 179 PVE::APIServer::Formatter::Standard::prepare_response_data($portal_format, $res);
63307beb
DM
180 $data = $res->{data};
181
182 my $html = '';
ca304f91 183 my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
63307beb
DM
184
185 if (!HTTP::Status::is_success($res->{status})) {
186 $html .= $doc->alert(text => "Error $res->{status}: $res->{message}");
187 }
188
dc80cea5 189 my $lnk;
63307beb 190
dc80cea5
DM
191 if (my $info = $res->{info}) {
192 $html .= $doc->el(tag => 'h3', text => 'Description');
193 $html .= $doc->el(tag => 'p', text => $info->{description});
63307beb 194
dc80cea5
DM
195 $lnk = PVE::JSONSchema::method_get_child_link($info);
196 }
63307beb
DM
197
198 if ($lnk && $data && $data->{data} && HTTP::Status::is_success($res->{status})) {
199
200 my $href = $lnk->{href};
201 if ($href =~ m/^\{(\S+)\}$/) {
202
203 my $items = [];
204
205 my $prop = $1;
206 $path =~ s/\/+$//; # remove trailing slash
207
208 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data}}) {
209 next if !ref($elem);
210
211 if (defined(my $value = $elem->{$prop})) {
212 my $tv = to_json($elem, {pretty => 1, allow_nonref => 1, canonical => 1});
213
214 push @$items, {
215 tag => 'a',
216 class => 'list-group-item',
67817350 217 href => "$path/".encode_entities($value),
63307beb
DM
218 cn => [
219 {
220 tag => 'h4',
221 class => 'list-group-item-heading',
222 text => $value,
223 },
224 {
225 tag => 'pre',
226 class => 'list-group-item',
227 text => $tv,
228 },
229 ],
230 };
231 }
232 }
233
234 $html .= $doc->el(class => 'list-group', cn => $items);
235
236 } else {
237
cd64441d 238 my $json = to_json($data, {allow_nonref => 1, pretty => 1, canonical => 1});
63307beb
DM
239 $html .= $doc->el(tag => 'pre', text => $json);
240 }
241
242 } else {
243
cd64441d 244 my $json = to_json($data, {allow_nonref => 1, pretty => 1, canonical => 1});
63307beb
DM
245 $html .= $doc->el(tag => 'pre', text => $json);
246 }
247
248 $html = $doc->el(class => 'container', html => $html);
249
ca304f91 250 my $raw = render_page($doc, $html, $config);
63307beb
DM
251 return ($raw, $portal_ct);
252});
253
c7154375 254PVE::APIServer::Formatter::register_page_formatter(
63307beb
DM
255 'format' => $portal_format,
256 method => 'GET',
257 path => "/access/ticket",
258 code => sub {
ca304f91 259 my ($res, $data, $param, $path, $auth, $config) = @_;
63307beb 260
ca304f91 261 my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
63307beb 262
ca304f91 263 my $html = $login_form->($config, $doc);
63307beb 264
ca304f91 265 my $raw = render_page($doc, $html, $config);
63307beb
DM
266 return ($raw, $portal_ct);
267 });
268
c7154375 269PVE::APIServer::Formatter::register_page_formatter(
63307beb
DM
270 'format' => $portal_format,
271 method => 'POST',
272 path => "/access/ticket",
273 code => sub {
ca304f91 274 my ($res, $data, $param, $path, $auth, $config) = @_;
63307beb
DM
275
276 if (HTTP::Status::is_success($res->{status})) {
277 my $cookie = PVE::APIServer::Formatter::create_auth_cookie(
ca304f91 278 $data->{ticket}, $config->{cookie_name});
63307beb 279
ca304f91 280 my $headers = HTTP::Headers->new(Location => $get_portal_base_url->($config),
63307beb
DM
281 'Set-Cookie' => $cookie);
282 return HTTP::Response->new(301, "Moved", $headers);
283 }
284
285 # Note: HTTP server redirects to 'GET /access/ticket', so below
286 # output is not really visible.
287
ca304f91 288 my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
63307beb 289
ca304f91 290 my $html = $login_form->($config, $doc);
63307beb 291
ca304f91 292 my $raw = render_page($doc, $html, $config);
63307beb
DM
293 return ($raw, $portal_ct);
294 });
295
2961;