]> git.proxmox.com Git - pve-http-server.git/blob - src/PVE/APIServer/Formatter/HTML.pm
743d0ad8165a16612c04cf27c3e7d95efb2590f5
[pve-http-server.git] / src / PVE / APIServer / Formatter / HTML.pm
1 package PVE::APIServer::Formatter::HTML;
2
3 use strict;
4 use warnings;
5
6 use PVE::APIServer::Formatter;
7 use HTTP::Status;
8 use JSON;
9 use HTML::Entities;
10 use PVE::JSONSchema;
11 use PVE::APIServer::Formatter::Bootstrap;
12 use PVE::APIServer::Formatter::Standard;
13
14 my $portal_format = 'html';
15 my $portal_ct = 'text/html;charset=UTF-8';
16
17 my $get_portal_base_url = sub {
18 my ($config) = @_;
19 return "$config->{base_uri}/$portal_format";
20 };
21
22 my $get_portal_login_url = sub {
23 my ($config) = @_;
24 return "$config->{base_uri}/$portal_format/access/ticket";
25 };
26
27 sub render_page {
28 my ($doc, $html, $config) = @_;
29
30 my $items = [];
31
32 push @$items, {
33 tag => 'li',
34 cn => {
35 tag => 'a',
36 href => $get_portal_login_url->($config),
37 onClick => "PVE.delete_auth_cookie();",
38 text => "Logout",
39 }};
40
41 my $base_url = $get_portal_base_url->($config);
42
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",
65 href => $base_url,
66 text => $config->{title},
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
87 my $href = $base_url;
88 push @$items, { tag => 'li', cn => {
89 tag => 'a',
90 href => $href,
91 text => 'Home'}};
92
93 foreach my $comp (@pcomp) {
94 $href .= "/$comp";
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
106 my $login_form = sub {
107 my ($config, $doc, $param, $errmsg) = @_;
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',
150 action => $get_portal_login_url->($config),
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
168 PVE::APIServer::Formatter::register_login_formatter($portal_format, sub {
169 my ($path, $auth, $config) = @_;
170
171 my $headers = HTTP::Headers->new(Location => $get_portal_login_url->($config));
172 return HTTP::Response->new(301, "Moved", $headers);
173 });
174
175 PVE::APIServer::Formatter::register_formatter($portal_format, sub {
176 my ($res, $data, $param, $path, $auth, $config) = @_;
177
178 # fixme: clumsy!
179 PVE::APIServer::Formatter::Standard::prepare_response_data($portal_format, $res);
180 $data = $res->{data};
181
182 my $html = '';
183 my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
184
185 if (!HTTP::Status::is_success($res->{status})) {
186 $html .= $doc->alert(text => "Error $res->{status}: $res->{message}");
187 }
188
189 my $lnk;
190
191 if (my $info = $res->{info}) {
192 $html .= $doc->el(tag => 'h3', text => 'Description');
193 $html .= $doc->el(tag => 'p', text => $info->{description});
194
195 $lnk = PVE::JSONSchema::method_get_child_link($info);
196 }
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',
217 href => "$path/$value",
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
238 my $json = to_json($data, {allow_nonref => 1, pretty => 1, canonical => 1});
239 $html .= $doc->el(tag => 'pre', text => $json);
240 }
241
242 } else {
243
244 my $json = to_json($data, {allow_nonref => 1, pretty => 1, canonical => 1});
245 $html .= $doc->el(tag => 'pre', text => $json);
246 }
247
248 $html = $doc->el(class => 'container', html => $html);
249
250 my $raw = render_page($doc, $html, $config);
251 return ($raw, $portal_ct);
252 });
253
254 PVE::APIServer::Formatter::register_page_formatter(
255 'format' => $portal_format,
256 method => 'GET',
257 path => "/access/ticket",
258 code => sub {
259 my ($res, $data, $param, $path, $auth, $config) = @_;
260
261 my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
262
263 my $html = $login_form->($config, $doc);
264
265 my $raw = render_page($doc, $html, $config);
266 return ($raw, $portal_ct);
267 });
268
269 PVE::APIServer::Formatter::register_page_formatter(
270 'format' => $portal_format,
271 method => 'POST',
272 path => "/access/ticket",
273 code => sub {
274 my ($res, $data, $param, $path, $auth, $config) = @_;
275
276 if (HTTP::Status::is_success($res->{status})) {
277 my $cookie = PVE::APIServer::Formatter::create_auth_cookie(
278 $data->{ticket}, $config->{cookie_name});
279
280 my $headers = HTTP::Headers->new(Location => $get_portal_base_url->($config),
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
288 my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
289
290 my $html = $login_form->($config, $doc);
291
292 my $raw = render_page($doc, $html, $config);
293 return ($raw, $portal_ct);
294 });
295
296 1;