Commit 23e0c0ba authored by Dietmar Maurer's avatar Dietmar Maurer

add generic formater support

parent 9bbf4e4b
......@@ -4,7 +4,12 @@ use strict;
use warnings;
use PVE::pvecfg;
use PVE::REST;
use PVE::RESTHandler;
use HTTP::Status;
use JSON;
use HTML::Entities;
use PVE::JSONSchema;
use base qw(PVE::RESTHandler);
......@@ -108,4 +113,187 @@ __PACKAGE__->register_method ({
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;
......@@ -49,7 +49,7 @@ my $baseuri = "/api2";
sub split_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;
return wantarray ? ($rel_uri, $format) : $rel_uri;
......@@ -445,8 +445,11 @@ sub handle_api2_request {
my $path = $r->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;
}
......@@ -501,9 +504,14 @@ sub handle_api2_request {
$delay = 0 if $delay < 0;
}
PVE::REST::prepare_response_data($format, $res);
my ($raw, $ct, $nocomp) = PVE::REST::format_response_data($format, $res, $path);
if ($res->{info} && $res->{info}->{formater}) {
if (defined(my $func = $res->{info}->{formater}->{$format})) {
$formater = $func;
}
}
my ($raw, $ct, $nocomp) = &$formater($res, $res->{data}, $path, $auth);
my $resp;
if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
$resp = $raw;
......@@ -954,7 +962,23 @@ sub unshift_read_header {
$rel_uri, $ticket, $token);
};
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;
}
}
......
......@@ -52,125 +52,6 @@ sub create_auth_cookie {
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 ($info, $err, $status) = @_;
......@@ -201,7 +82,7 @@ sub auth_handler {
# explicitly allow some calls without auth
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;
}
......@@ -318,4 +199,83 @@ sub rest_handler {
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;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment