mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-08-17 12:32:07 +00:00
add generic formater support
This commit is contained in:
parent
c9c0cd346c
commit
3ed610334e
188
PVE/API2.pm
188
PVE/API2.pm
@ -4,7 +4,12 @@ use strict;
|
|||||||
use warnings;
|
use warnings;
|
||||||
|
|
||||||
use PVE::pvecfg;
|
use PVE::pvecfg;
|
||||||
|
use PVE::REST;
|
||||||
use PVE::RESTHandler;
|
use PVE::RESTHandler;
|
||||||
|
use HTTP::Status;
|
||||||
|
use JSON;
|
||||||
|
use HTML::Entities;
|
||||||
|
use PVE::JSONSchema;
|
||||||
|
|
||||||
use base qw(PVE::RESTHandler);
|
use base qw(PVE::RESTHandler);
|
||||||
|
|
||||||
@ -108,4 +113,187 @@ __PACKAGE__->register_method ({
|
|||||||
|
|
||||||
return $res;
|
return $res;
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
# register result formaters
|
||||||
|
|
||||||
|
my $prepare_response_data = sub {
|
||||||
|
my ($format, $res) = @_;
|
||||||
|
|
||||||
|
my $success = 1;
|
||||||
|
my $new = {
|
||||||
|
data => $res->{data},
|
||||||
|
};
|
||||||
|
if (scalar(keys %{$res->{errors}})) {
|
||||||
|
$success = 0;
|
||||||
|
$new->{errors} = $res->{errors};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($format eq 'extjs' || $format eq 'htmljs') {
|
||||||
|
# HACK: extjs wants 'success' property instead of useful HTTP status codes
|
||||||
|
if (HTTP::Status::is_error($res->{status})) {
|
||||||
|
$success = 0;
|
||||||
|
$new->{message} = $res->{message} || status_message($res->{status});
|
||||||
|
$new->{status} = $res->{status} || 200;
|
||||||
|
$res->{message} = undef;
|
||||||
|
$res->{status} = 200;
|
||||||
|
}
|
||||||
|
$new->{success} = $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success && $res->{total}) {
|
||||||
|
$new->{total} = $res->{total};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success && $res->{changes}) {
|
||||||
|
$new->{changes} = $res->{changes};
|
||||||
|
}
|
||||||
|
|
||||||
|
$res->{data} = $new;
|
||||||
|
};
|
||||||
|
|
||||||
|
PVE::REST::register_formater('json', sub {
|
||||||
|
my ($res, $data, $param, $path, $auth) = @_;
|
||||||
|
|
||||||
|
my $nocomp = 0;
|
||||||
|
|
||||||
|
my $ct = 'application/json;charset=UTF-8';
|
||||||
|
|
||||||
|
&$prepare_response_data('json', $res);
|
||||||
|
|
||||||
|
my $raw = to_json($res->{data}, {utf8 => 1, allow_nonref => 1});
|
||||||
|
|
||||||
|
return ($raw, $ct, $nocomp);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
PVE::REST::register_formater('extjs', sub {
|
||||||
|
my ($res, $data, $param, $path, $auth) = @_;
|
||||||
|
|
||||||
|
my $nocomp = 0;
|
||||||
|
|
||||||
|
my $ct = 'application/json;charset=UTF-8';
|
||||||
|
|
||||||
|
&$prepare_response_data('extjs', $res);
|
||||||
|
|
||||||
|
my $raw = to_json($res->{data}, {utf8 => 1, allow_nonref => 1});
|
||||||
|
|
||||||
|
return ($raw, $ct, $nocomp);
|
||||||
|
});
|
||||||
|
|
||||||
|
PVE::REST::register_formater('htmljs', sub {
|
||||||
|
my ($res, $data, $param, $path, $auth) = @_;
|
||||||
|
|
||||||
|
my $nocomp = 0;
|
||||||
|
|
||||||
|
# we use this for extjs file upload forms
|
||||||
|
|
||||||
|
my $ct = 'text/html;charset=UTF-8';
|
||||||
|
|
||||||
|
&$prepare_response_data('htmljs', $res);
|
||||||
|
|
||||||
|
my $raw = encode_entities(to_json($res->{data}, {allow_nonref => 1}));
|
||||||
|
|
||||||
|
return ($raw, $ct, $nocomp);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
PVE::REST::register_formater('spiceconfig', sub {
|
||||||
|
my ($res, $data, $param, $path, $auth) = @_;
|
||||||
|
|
||||||
|
my $nocomp = 0;
|
||||||
|
|
||||||
|
my $ct = 'application/x-virt-viewer;charset=UTF-8';
|
||||||
|
|
||||||
|
&$prepare_response_data('spiceconfig', $res);
|
||||||
|
|
||||||
|
$data = $res->{data};
|
||||||
|
|
||||||
|
my $raw;
|
||||||
|
|
||||||
|
if ($data && ref($data) && ref($data->{data})) {
|
||||||
|
$raw = "[virt-viewer]\n";
|
||||||
|
while (my ($key, $value) = each %{$data->{data}}) {
|
||||||
|
$raw .= "$key=$value\n" if defined($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($raw, $ct, $nocomp);
|
||||||
|
});
|
||||||
|
|
||||||
|
PVE::REST::register_formater('png', sub {
|
||||||
|
my ($res, $data, $param, $path, $auth) = @_;
|
||||||
|
|
||||||
|
my $nocomp = 1;
|
||||||
|
|
||||||
|
my $ct = 'image/png';
|
||||||
|
|
||||||
|
&$prepare_response_data('png', $res);
|
||||||
|
|
||||||
|
$data = $res->{data};
|
||||||
|
|
||||||
|
# fixme: better to revove that whole png thing ?
|
||||||
|
|
||||||
|
my $filename;
|
||||||
|
my $raw = '';
|
||||||
|
|
||||||
|
if ($data && ref($data) && ref($data->{data}) &&
|
||||||
|
$data->{data}->{filename} && defined($data->{data}->{image})) {
|
||||||
|
$filename = $data->{data}->{filename};
|
||||||
|
$raw = $data->{data}->{image};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($raw, $ct, $nocomp);
|
||||||
|
});
|
||||||
|
|
||||||
|
PVE::REST::register_formater('html', sub {
|
||||||
|
my ($res, $data, $param, $path, $auth) = @_;
|
||||||
|
|
||||||
|
my $nocomp = 0;
|
||||||
|
|
||||||
|
my $ct = 'text/html;charset=UTF-8';
|
||||||
|
|
||||||
|
&$prepare_response_data('html', $res);
|
||||||
|
|
||||||
|
$data = $res->{data};
|
||||||
|
|
||||||
|
my $info = $res->{info};
|
||||||
|
|
||||||
|
my $raw = "<html><body>";
|
||||||
|
if (!HTTP::Status::is_success($res->{status})) {
|
||||||
|
my $msg = $res->{message} || '';
|
||||||
|
$raw .= "<h1>ERROR $res->{status} $msg</h1>";
|
||||||
|
}
|
||||||
|
my $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 $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})) {
|
||||||
|
if ($value ne '') {
|
||||||
|
if (scalar(keys %$elem) > 1) {
|
||||||
|
my $tv = to_json($elem, {allow_nonref => 1, canonical => 1});
|
||||||
|
$raw .= "<a href='$path/$value'>$value</a> <pre>$tv</pre><br>";
|
||||||
|
} else {
|
||||||
|
$raw .= "<a href='$path/$value'>$value</a><br>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$raw .= "<pre>";
|
||||||
|
$raw .= encode_entities(to_json($data, {allow_nonref => 1, pretty => 1}));
|
||||||
|
$raw .= "</pre>";
|
||||||
|
}
|
||||||
|
$raw .= "</body></html>";
|
||||||
|
|
||||||
|
return ($raw, $ct, $nocomp);
|
||||||
|
});
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
@ -49,7 +49,7 @@ my $baseuri = "/api2";
|
|||||||
sub split_abs_uri {
|
sub split_abs_uri {
|
||||||
my ($abs_uri) = @_;
|
my ($abs_uri) = @_;
|
||||||
|
|
||||||
my ($format, $rel_uri) = $abs_uri =~ m/^\Q$baseuri\E\/+(html|text|json|extjs|png|htmljs|spiceconfig)(\/.*)?$/;
|
my ($format, $rel_uri) = $abs_uri =~ m/^\Q$baseuri\E\/+([a-z][a-z0-9]+)(\/.*)?$/;
|
||||||
$rel_uri = '/' if !$rel_uri;
|
$rel_uri = '/' if !$rel_uri;
|
||||||
|
|
||||||
return wantarray ? ($rel_uri, $format) : $rel_uri;
|
return wantarray ? ($rel_uri, $format) : $rel_uri;
|
||||||
@ -445,8 +445,11 @@ sub handle_api2_request {
|
|||||||
my $path = $r->uri->path();
|
my $path = $r->uri->path();
|
||||||
|
|
||||||
my ($rel_uri, $format) = split_abs_uri($path);
|
my ($rel_uri, $format) = split_abs_uri($path);
|
||||||
if (!$format) {
|
|
||||||
$self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri");
|
my $formater = PVE::REST::get_formater($format);
|
||||||
|
|
||||||
|
if (!defined($formater)) {
|
||||||
|
$self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri $rel_uri, $format");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,9 +504,14 @@ sub handle_api2_request {
|
|||||||
$delay = 0 if $delay < 0;
|
$delay = 0 if $delay < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
PVE::REST::prepare_response_data($format, $res);
|
if ($res->{info} && $res->{info}->{formater}) {
|
||||||
my ($raw, $ct, $nocomp) = PVE::REST::format_response_data($format, $res, $path);
|
if (defined(my $func = $res->{info}->{formater}->{$format})) {
|
||||||
|
$formater = $func;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my ($raw, $ct, $nocomp) = &$formater($res, $res->{data}, $path, $auth);
|
||||||
|
|
||||||
my $resp;
|
my $resp;
|
||||||
if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
|
if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
|
||||||
$resp = $raw;
|
$resp = $raw;
|
||||||
@ -954,7 +962,23 @@ sub unshift_read_header {
|
|||||||
$rel_uri, $ticket, $token);
|
$rel_uri, $ticket, $token);
|
||||||
};
|
};
|
||||||
if (my $err = $@) {
|
if (my $err = $@) {
|
||||||
$self->error($reqstate, HTTP_UNAUTHORIZED, $err);
|
# always delay unauthorized calls by 3 seconds
|
||||||
|
my $delay = 3;
|
||||||
|
if (my $formater = PVE::REST::get_login_formater($format)) {
|
||||||
|
my ($raw, $ct, $nocomp) = &$formater($path, $auth);
|
||||||
|
my $resp;
|
||||||
|
if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
|
||||||
|
$resp = $raw;
|
||||||
|
} else {
|
||||||
|
$resp = HTTP::Response->new(HTTP_UNAUTHORIZED, "Login Required");
|
||||||
|
$resp->header("Content-Type" => $ct);
|
||||||
|
$resp->content($raw);
|
||||||
|
}
|
||||||
|
$self->response($reqstate, $resp, undef, $nocomp, 3);
|
||||||
|
} else {
|
||||||
|
my $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, $err);
|
||||||
|
$self->response($reqstate, $resp, undef, 0, $delay);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
200
PVE/REST.pm
200
PVE/REST.pm
@ -52,125 +52,6 @@ sub create_auth_cookie {
|
|||||||
return "${cookie_name}=$encticket; path=/; secure;";
|
return "${cookie_name}=$encticket; path=/; secure;";
|
||||||
}
|
}
|
||||||
|
|
||||||
sub format_response_data {
|
|
||||||
my($format, $res, $uri) = @_;
|
|
||||||
|
|
||||||
my $data = $res->{data};
|
|
||||||
my $info = $res->{info};
|
|
||||||
|
|
||||||
my ($ct, $raw, $nocomp);
|
|
||||||
|
|
||||||
if ($format eq 'json') {
|
|
||||||
$ct = 'application/json;charset=UTF-8';
|
|
||||||
$raw = to_json($data, {utf8 => 1, allow_nonref => 1});
|
|
||||||
} elsif ($format eq 'html') {
|
|
||||||
$ct = 'text/html;charset=UTF-8';
|
|
||||||
$raw = "<html><body>";
|
|
||||||
if (!is_success($res->{status})) {
|
|
||||||
my $msg = $res->{message} || '';
|
|
||||||
$raw .= "<h1>ERROR $res->{status} $msg</h1>";
|
|
||||||
}
|
|
||||||
my $lnk = PVE::JSONSchema::method_get_child_link($info);
|
|
||||||
if ($lnk && $data && $data->{data} && is_success($res->{status})) {
|
|
||||||
|
|
||||||
my $href = $lnk->{href};
|
|
||||||
if ($href =~ m/^\{(\S+)\}$/) {
|
|
||||||
my $prop = $1;
|
|
||||||
$uri =~ 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})) {
|
|
||||||
if ($value ne '') {
|
|
||||||
if (scalar(keys %$elem) > 1) {
|
|
||||||
my $tv = to_json($elem, {allow_nonref => 1, canonical => 1});
|
|
||||||
$raw .= "<a href='$uri/$value'>$value</a> <pre>$tv</pre><br>";
|
|
||||||
} else {
|
|
||||||
$raw .= "<a href='$uri/$value'>$value</a><br>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$raw .= "<pre>";
|
|
||||||
$raw .= encode_entities(to_json($data, {allow_nonref => 1, pretty => 1}));
|
|
||||||
$raw .= "</pre>";
|
|
||||||
}
|
|
||||||
$raw .= "</body></html>";
|
|
||||||
|
|
||||||
} elsif ($format eq 'png') {
|
|
||||||
$ct = 'image/png';
|
|
||||||
$nocomp = 1;
|
|
||||||
# fixme: better to revove that whole png thing ?
|
|
||||||
|
|
||||||
my $filename;
|
|
||||||
$raw = '';
|
|
||||||
|
|
||||||
if ($data && ref($data) && ref($data->{data}) &&
|
|
||||||
$data->{data}->{filename} && defined($data->{data}->{image})) {
|
|
||||||
$filename = $data->{data}->{filename};
|
|
||||||
$raw = $data->{data}->{image};
|
|
||||||
}
|
|
||||||
|
|
||||||
} elsif ($format eq 'extjs') {
|
|
||||||
$ct = 'application/json;charset=UTF-8';
|
|
||||||
$raw = to_json($data, {utf8 => 1, allow_nonref => 1});
|
|
||||||
} elsif ($format eq 'htmljs') {
|
|
||||||
# we use this for extjs file upload forms
|
|
||||||
$ct = 'text/html;charset=UTF-8';
|
|
||||||
$raw = encode_entities(to_json($data, {allow_nonref => 1}));
|
|
||||||
} elsif ($format eq 'spiceconfig') {
|
|
||||||
$ct = 'application/x-virt-viewer;charset=UTF-8';
|
|
||||||
if ($data && ref($data) && ref($data->{data})) {
|
|
||||||
$raw = "[virt-viewer]\n";
|
|
||||||
while (my ($key, $value) = each %{$data->{data}}) {
|
|
||||||
$raw .= "$key=$value\n" if defined($value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$ct = 'text/plain;charset=UTF-8';
|
|
||||||
$raw = to_json($data, {utf8 => 1, allow_nonref => 1, pretty => 1});
|
|
||||||
}
|
|
||||||
|
|
||||||
return wantarray ? ($raw, $ct, $nocomp) : $raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub prepare_response_data {
|
|
||||||
my ($format, $res) = @_;
|
|
||||||
|
|
||||||
my $success = 1;
|
|
||||||
my $new = {
|
|
||||||
data => $res->{data},
|
|
||||||
};
|
|
||||||
if (scalar(keys %{$res->{errors}})) {
|
|
||||||
$success = 0;
|
|
||||||
$new->{errors} = $res->{errors};
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($format eq 'extjs' || $format eq 'htmljs') {
|
|
||||||
# HACK: extjs wants 'success' property instead of useful HTTP status codes
|
|
||||||
if (is_error($res->{status})) {
|
|
||||||
$success = 0;
|
|
||||||
$new->{message} = $res->{message} || status_message($res->{status});
|
|
||||||
$new->{status} = $res->{status} || 200;
|
|
||||||
$res->{message} = undef;
|
|
||||||
$res->{status} = 200;
|
|
||||||
}
|
|
||||||
$new->{success} = $success;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($success && $res->{total}) {
|
|
||||||
$new->{total} = $res->{total};
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($success && $res->{changes}) {
|
|
||||||
$new->{changes} = $res->{changes};
|
|
||||||
}
|
|
||||||
|
|
||||||
$res->{data} = $new;
|
|
||||||
}
|
|
||||||
|
|
||||||
my $exc_to_res = sub {
|
my $exc_to_res = sub {
|
||||||
my ($info, $err, $status) = @_;
|
my ($info, $err, $status) = @_;
|
||||||
|
|
||||||
@ -201,7 +82,7 @@ sub auth_handler {
|
|||||||
|
|
||||||
# explicitly allow some calls without auth
|
# explicitly allow some calls without auth
|
||||||
if (($rel_uri eq '/access/domains' && $method eq 'GET') ||
|
if (($rel_uri eq '/access/domains' && $method eq 'GET') ||
|
||||||
($rel_uri eq '/access/ticket' && $method eq 'POST')) {
|
($rel_uri eq '/access/ticket' && ($method eq 'GET' || $method eq 'POST'))) {
|
||||||
$require_auth = 0;
|
$require_auth = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,4 +199,83 @@ sub rest_handler {
|
|||||||
return $resp;
|
return $resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# generic formater support
|
||||||
|
|
||||||
|
my $formater_hash = {};
|
||||||
|
|
||||||
|
sub register_formater {
|
||||||
|
my ($format, $func) = @_;
|
||||||
|
|
||||||
|
die "formater '$format' already defined" if $formater_hash->{$format};
|
||||||
|
|
||||||
|
$formater_hash->{$format} = {
|
||||||
|
func => $func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_formater {
|
||||||
|
my ($format) = @_;
|
||||||
|
|
||||||
|
return undef if !$format;
|
||||||
|
|
||||||
|
my $info = $formater_hash->{$format};
|
||||||
|
return undef if !$info;
|
||||||
|
|
||||||
|
return $info->{func};
|
||||||
|
}
|
||||||
|
|
||||||
|
my $login_formater_hash = {};
|
||||||
|
|
||||||
|
sub register_login_formater {
|
||||||
|
my ($format, $func) = @_;
|
||||||
|
|
||||||
|
die "login formater '$format' already defined" if $login_formater_hash->{$format};
|
||||||
|
|
||||||
|
$login_formater_hash->{$format} = {
|
||||||
|
func => $func,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_login_formater {
|
||||||
|
my ($format) = @_;
|
||||||
|
|
||||||
|
return undef if !$format;
|
||||||
|
|
||||||
|
my $info = $login_formater_hash->{$format};
|
||||||
|
return undef if !$info;
|
||||||
|
|
||||||
|
return $info->{func};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub register_page_formater {
|
||||||
|
my (%config) = @_;
|
||||||
|
|
||||||
|
my $base_handler_class = $config{base_handler_class} ||
|
||||||
|
die "missing base_handler_class";
|
||||||
|
|
||||||
|
my $format = $config{format} ||
|
||||||
|
die "missing format";
|
||||||
|
|
||||||
|
die "format '$format' is not registered"
|
||||||
|
if !$formater_hash->{$format};
|
||||||
|
|
||||||
|
my $path = $config{path} ||
|
||||||
|
die "missing path";
|
||||||
|
|
||||||
|
my $method = $config{method} ||
|
||||||
|
die "missing method";
|
||||||
|
|
||||||
|
my $code = $config{code} ||
|
||||||
|
die "missing formater code";
|
||||||
|
|
||||||
|
my $uri_param = {};
|
||||||
|
my ($handler, $info) = $base_handler_class->find_handler($method, $path, $uri_param);
|
||||||
|
die "unabe to find handler for '$method: $path'" if !($handler && $info);
|
||||||
|
|
||||||
|
die "duplicate formater for '$method: $path'"
|
||||||
|
if $info->{formater} && $info->{formater}->{$format};
|
||||||
|
|
||||||
|
$info->{formater}->{$format} = $code;
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
Loading…
Reference in New Issue
Block a user