]>
Commit | Line | Data |
---|---|---|
63307beb DM |
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 | ||
ca304f91 DM |
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 | }; | |
63307beb DM |
26 | |
27 | sub 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 | ||
106 | my $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 | ||
168 | PVE::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 | ||
175 | PVE::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 | 254 | PVE::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 | 269 | PVE::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 | ||
296 | 1; |