mirror of
https://git.proxmox.com/git/pve-http-server
synced 2025-05-03 01:41:43 +00:00
297 lines
6.8 KiB
Perl
297 lines
6.8 KiB
Perl
package PVE::APIServer::Formatter::HTML;
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use PVE::APIServer::Formatter;
|
|
use HTTP::Status;
|
|
use JSON;
|
|
use HTML::Entities;
|
|
use PVE::JSONSchema;
|
|
use PVE::APIServer::Formatter::Bootstrap;
|
|
use PVE::APIServer::Formatter::Standard;
|
|
|
|
my $portal_format = 'html';
|
|
my $portal_ct = 'text/html;charset=UTF-8';
|
|
|
|
my $get_portal_base_url = sub {
|
|
my ($config) = @_;
|
|
return "$config->{base_uri}/$portal_format";
|
|
};
|
|
|
|
my $get_portal_login_url = sub {
|
|
my ($config) = @_;
|
|
return "$config->{base_uri}/$portal_format/access/ticket";
|
|
};
|
|
|
|
sub render_page {
|
|
my ($doc, $html, $config) = @_;
|
|
|
|
my $items = [];
|
|
|
|
push @$items, {
|
|
tag => 'li',
|
|
cn => {
|
|
tag => 'a',
|
|
href => $get_portal_login_url->($config),
|
|
onClick => "PVE.delete_auth_cookie();",
|
|
text => "Logout",
|
|
}};
|
|
|
|
my $base_url = $get_portal_base_url->($config);
|
|
|
|
my $nav = $doc->el(
|
|
class => "navbar navbar-inverse navbar-fixed-top",
|
|
role => "navigation", cn => {
|
|
class => "container", cn => [
|
|
{
|
|
class => "navbar-header", cn => [
|
|
{
|
|
tag => 'button',
|
|
type => 'button',
|
|
class => "navbar-toggle",
|
|
'data-toggle' => "collapse",
|
|
'data-target' => ".navbar-collapse",
|
|
cn => [
|
|
{ tag => 'span', class => 'sr-only', text => "Toggle navigation" },
|
|
{ tag => 'span', class => 'icon-bar' },
|
|
{ tag => 'span', class => 'icon-bar' },
|
|
{ tag => 'span', class => 'icon-bar' },
|
|
],
|
|
},
|
|
{
|
|
tag => 'a',
|
|
class => "navbar-brand",
|
|
href => $base_url,
|
|
text => $config->{title},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
class => "collapse navbar-collapse",
|
|
cn => {
|
|
tag => 'ul',
|
|
class => "nav navbar-nav",
|
|
cn => $items,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
$items = [];
|
|
my @pcomp = split('/', $doc->{url});
|
|
shift @pcomp; # empty
|
|
shift @pcomp; # api2
|
|
shift @pcomp; # $format
|
|
|
|
my $href = $base_url;
|
|
push @$items, { tag => 'li', cn => {
|
|
tag => 'a',
|
|
href => $href,
|
|
text => 'Home'}};
|
|
|
|
foreach my $comp (@pcomp) {
|
|
$href .= "/$comp";
|
|
push @$items, { tag => 'li', cn => {
|
|
tag => 'a',
|
|
href => $href,
|
|
text => $comp}};
|
|
}
|
|
|
|
my $breadcrumbs = $doc->el(tag => 'ol', class => 'breadcrumb container', cn => $items);
|
|
|
|
return $doc->body($nav . $breadcrumbs . $html);
|
|
}
|
|
|
|
my $login_form = sub {
|
|
my ($config, $doc, $param, $errmsg) = @_;
|
|
|
|
$param = {} if !$param;
|
|
|
|
my $username = $param->{username} || '';
|
|
my $password = $param->{password} || '';
|
|
|
|
my $items = [
|
|
{
|
|
tag => 'label',
|
|
text => "Please sign in",
|
|
},
|
|
{
|
|
tag => 'input',
|
|
type => 'text',
|
|
class => 'form-control',
|
|
name => 'username',
|
|
value => $username,
|
|
placeholder => "Enter user name",
|
|
required => 1,
|
|
autofocus => 1,
|
|
},
|
|
{
|
|
tag => 'input',
|
|
type => 'password',
|
|
class => 'form-control',
|
|
name => 'password',
|
|
value => $password,
|
|
placeholder => 'Password',
|
|
required => 1,
|
|
},
|
|
];
|
|
|
|
my $html = '';
|
|
|
|
$html .= $doc->alert(text => $errmsg) if ($errmsg);
|
|
|
|
$html .= $doc->el(
|
|
class => 'container',
|
|
cn => {
|
|
tag => 'form',
|
|
role => 'form',
|
|
method => 'POST',
|
|
action => $get_portal_login_url->($config),
|
|
cn => [
|
|
{
|
|
class => 'form-group',
|
|
cn => $items,
|
|
},
|
|
{
|
|
tag => 'button',
|
|
type => 'submit',
|
|
class => 'btn btn-lg btn-primary btn-block',
|
|
text => "Sign in",
|
|
},
|
|
],
|
|
});
|
|
|
|
return $html;
|
|
};
|
|
|
|
PVE::APIServer::Formatter::register_login_formatter($portal_format, sub {
|
|
my ($path, $auth, $config) = @_;
|
|
|
|
my $headers = HTTP::Headers->new(Location => $get_portal_login_url->($config));
|
|
return HTTP::Response->new(301, "Moved", $headers);
|
|
});
|
|
|
|
PVE::APIServer::Formatter::register_formatter($portal_format, sub {
|
|
my ($res, $data, $param, $path, $auth, $config) = @_;
|
|
|
|
# fixme: clumsy!
|
|
PVE::APIServer::Formatter::Standard::prepare_response_data($portal_format, $res);
|
|
$data = $res->{data};
|
|
|
|
my $html = '';
|
|
my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
|
|
|
|
if (!HTTP::Status::is_success($res->{status})) {
|
|
$html .= $doc->alert(text => "Error $res->{status}: $res->{message}");
|
|
}
|
|
|
|
my $lnk;
|
|
|
|
if (my $info = $res->{info}) {
|
|
$html .= $doc->el(tag => 'h3', text => 'Description');
|
|
$html .= $doc->el(tag => 'p', text => $info->{description});
|
|
|
|
$lnk = PVE::JSONSchema::method_get_child_link($info);
|
|
}
|
|
|
|
if ($lnk && $data && $data->{data} && HTTP::Status::is_success($res->{status})) {
|
|
|
|
my $href = $lnk->{href};
|
|
if ($href =~ m/^\{(\S+)\}$/) {
|
|
|
|
my $items = [];
|
|
|
|
my $prop = $1;
|
|
$path =~ s/\/+$//; # remove trailing slash
|
|
|
|
foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data}}) {
|
|
next if !ref($elem);
|
|
|
|
if (defined(my $value = $elem->{$prop})) {
|
|
my $tv = to_json($elem, {pretty => 1, allow_nonref => 1, canonical => 1});
|
|
|
|
push @$items, {
|
|
tag => 'a',
|
|
class => 'list-group-item',
|
|
href => "$path/$value",
|
|
cn => [
|
|
{
|
|
tag => 'h4',
|
|
class => 'list-group-item-heading',
|
|
text => $value,
|
|
},
|
|
{
|
|
tag => 'pre',
|
|
class => 'list-group-item',
|
|
text => $tv,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
$html .= $doc->el(class => 'list-group', cn => $items);
|
|
|
|
} else {
|
|
|
|
my $json = to_json($data, {allow_nonref => 1, pretty => 1, canonical => 1});
|
|
$html .= $doc->el(tag => 'pre', text => $json);
|
|
}
|
|
|
|
} else {
|
|
|
|
my $json = to_json($data, {allow_nonref => 1, pretty => 1, canonical => 1});
|
|
$html .= $doc->el(tag => 'pre', text => $json);
|
|
}
|
|
|
|
$html = $doc->el(class => 'container', html => $html);
|
|
|
|
my $raw = render_page($doc, $html, $config);
|
|
return ($raw, $portal_ct);
|
|
});
|
|
|
|
PVE::APIServer::Formatter::register_page_formatter(
|
|
'format' => $portal_format,
|
|
method => 'GET',
|
|
path => "/access/ticket",
|
|
code => sub {
|
|
my ($res, $data, $param, $path, $auth, $config) = @_;
|
|
|
|
my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
|
|
|
|
my $html = $login_form->($config, $doc);
|
|
|
|
my $raw = render_page($doc, $html, $config);
|
|
return ($raw, $portal_ct);
|
|
});
|
|
|
|
PVE::APIServer::Formatter::register_page_formatter(
|
|
'format' => $portal_format,
|
|
method => 'POST',
|
|
path => "/access/ticket",
|
|
code => sub {
|
|
my ($res, $data, $param, $path, $auth, $config) = @_;
|
|
|
|
if (HTTP::Status::is_success($res->{status})) {
|
|
my $cookie = PVE::APIServer::Formatter::create_auth_cookie(
|
|
$data->{ticket}, $config->{cookie_name});
|
|
|
|
my $headers = HTTP::Headers->new(Location => $get_portal_base_url->($config),
|
|
'Set-Cookie' => $cookie);
|
|
return HTTP::Response->new(301, "Moved", $headers);
|
|
}
|
|
|
|
# Note: HTTP server redirects to 'GET /access/ticket', so below
|
|
# output is not really visible.
|
|
|
|
my $doc = PVE::APIServer::Formatter::Bootstrap->new($res, $path, $auth, $config);
|
|
|
|
my $html = $login_form->($config, $doc);
|
|
|
|
my $raw = render_page($doc, $html, $config);
|
|
return ($raw, $portal_ct);
|
|
});
|
|
|
|
1;
|