package PVE::RESTHandler;
use strict;
-no strict 'refs'; # our autoload requires this
use warnings;
-use PVE::SafeSyslog;
+
+use Clone qw(clone);
+use HTTP::Status qw(:constants :is status_message);
+use Text::Wrap;
+
use PVE::Exception qw(raise raise_param_exc);
use PVE::JSONSchema;
+use PVE::SafeSyslog;
use PVE::Tools;
-use HTTP::Status qw(:constants :is status_message);
-use Text::Wrap;
-use Clone qw(clone);
my $method_registry = {};
my $method_by_name = {};
description => "Do not show column headers (for 'text' format).",
type => 'boolean',
optional => 1,
- default => 1,
+ default => 0,
},
noborder => {
description => "Do not draw borders (for 'text' format).",
type => 'boolean',
optional => 1,
- default => 1,
+ default => 0,
},
quiet => {
description => "Suppress printing results.",
}
}
my $tmp = ref($pd) ? clone($pd) : $pd;
- # NOTE: add typetext property for more complex types, to
- # make the web api viewer code simpler
+ # NOTE: add typetext property for complexer types, to make the web api-viewer code simpler
if (!$no_typetext && !(defined($tmp->{enum}) || defined($tmp->{pattern}))) {
my $typetext = PVE::JSONSchema::schema_get_type_text($tmp);
if ($tmp->{type} && ($tmp->{type} ne $typetext)) {
$data->{$k} = ref($d) ? clone($d) : $d;
}
}
- }
+ }
$res->{info}->{$info->{method}} = $data;
};
}
foreach my $k (keys %$tree) {
if (my $itemclass = ref($tree->{$k})) {
if ($itemclass eq 'CODE') {
- next if $k eq 'completion';
+ next if $k eq 'completion' || $k eq 'proxyto_callback';
}
$res->{$k} = api_dump_remove_refs($tree->{$k});
} else {
$errprefix = "register method ${self}/$info->{path} -";
$info->{method} = 'GET' if !$info->{method};
$method = $info->{method};
+
+ # apply default value
+ $info->{allowtoken} = 1 if !defined($info->{allowtoken});
}
$method_path_lookup->{$self} = {} if !defined($method_path_lookup->{$self});
my $path_lookup = $method_path_lookup->{$self};
die "$errprefix no path" if !defined($info->{path});
-
+
foreach my $comp (split(/\/+/, $info->{path})) {
die "$errprefix path compoment has zero length\n" if $comp eq '';
my ($name, $regex);
- if ($comp =~ m/^\{(\w+)(:(.*))?\}$/) {
+ if ($comp =~ m/^\{([\w-]+)(?::(.*))?\}$/) {
$name = $1;
- $regex = $3 ? $3 : '\S+';
+ $regex = $2 ? $2 : '\S+';
push @$match_re, $regex;
push @$match_name, $name;
} else {
}
if ($regex) {
- $path_lookup->{regex} = {} if !defined($path_lookup->{regex});
+ $path_lookup->{regex} = {} if !defined($path_lookup->{regex});
my $old_name = $path_lookup->{regex}->{match_name};
die "$errprefix found changed regex match name\n"
if defined($old_re) && ($old_re ne $regex);
$path_lookup->{regex}->{match_name} = $name;
$path_lookup->{regex}->{match_re} = $regex;
-
+
die "$errprefix path match error - regex and fixed items\n"
if defined($path_lookup->{folders});
$path_lookup = $path_lookup->{regex};
-
+
} else {
- $path_lookup->{folders}->{$name} = {} if !defined($path_lookup->{folders}->{$name});
+ $path_lookup->{folders}->{$name} = {} if !defined($path_lookup->{folders}->{$name});
die "$errprefix path match error - regex and fixed items\n"
if defined($path_lookup->{regex});
}
}
- die "$errprefix duplicate method definition\n"
+ die "$errprefix duplicate method definition\n"
if defined($path_lookup->{$method});
if ($method eq 'SUBCLASS') {
my ($this) = @_;
# also see "man perldiag"
-
+
my $sub = $AUTOLOAD;
(my $method = $sub) =~ s/.*:://;
my $info = $this->map_method_by_name($method);
- *{$sub} = sub {
- my $self = shift;
- return $self->handle($info, @_);
- };
+ {
+ no strict 'refs'; ## no critic (ProhibitNoStrict)
+ *{$sub} = sub {
+ my $self = shift;
+ return $self->handle($info, @_);
+ };
+ }
goto &$AUTOLOAD;
}
} else {
die "internal error";
}
-
+
return undef if !$path_lookup;
if (my $info = $path_lookup->{SUBCLASS}) {
if (defined($fd)) {
# we only support the empty string '' (match whole URI)
- die "unsupported fragmentDelimiter '$fd'"
+ die "unsupported fragmentDelimiter '$fd'"
if $fd ne '';
$stack = [ join ('/', @$stack) ] if scalar(@$stack) > 1;
return ($handler_class, $method_info);
}
+my sub untaint_recursive : prototype($) {
+ use feature 'current_sub';
+
+ my ($param) = @_;
+
+ my $ref = ref($param);
+ if ($ref eq 'HASH') {
+ $param->{$_} = __SUB__->($param->{$_}) for keys $param->%*;
+ } elsif ($ref eq 'ARRAY') {
+ for (my $i = 0; $i < scalar($param->@*); $i++) {
+ $param->[$i] = __SUB__->($param->[$i]);
+ }
+ } else {
+ if (defined($param)) {
+ my ($newval) = $param =~ /^(.*)$/s;
+ $param = $newval;
+ }
+ }
+
+ return $param;
+};
+
+# convert arrays to strings where we expect a '-list' format and convert scalar
+# values to arrays when we expect an array (because of www-form-urlencoded)
+#
+# only on the top level, since www-form-urlencoded cannot be nested anyway
+#
+# FIXME: change gui/api calls to not rely on this during 8.x, mark the
+# behaviour deprecated with 9.x, and remove it with 10.x
+my $normalize_legacy_param_formats = sub {
+ my ($param, $schema) = @_;
+
+ return $param if !$schema->{properties};
+ return $param if (ref($param) // '') ne 'HASH';
+
+ for my $key (keys $schema->{properties}->%*) {
+ if (my $value = $param->{$key}) {
+ my $type = $schema->{properties}->{$key}->{type} // '';
+ my $format = $schema->{properties}->{$key}->{format} // '';
+ my $ref = ref($value);
+ if ($ref && $ref eq 'ARRAY' && $type eq 'string' && $format =~ m/-list$/) {
+ $param->{$key} = join(',', $value->@*);
+ } elsif (!$ref && $type eq 'array') {
+ $param->{$key} = [$value];
+ }
+ }
+ }
+
+ return $param;
+};
+
sub handle {
- my ($self, $info, $param) = @_;
+ my ($self, $info, $param, $result_verification) = @_;
my $func = $info->{code};
if (!($info->{name} && $func)) {
- raise("Method lookup failed ('$info->{name}')\n",
- code => HTTP_INTERNAL_SERVER_ERROR);
+ raise("Method lookup failed ('$info->{name}')\n", code => HTTP_INTERNAL_SERVER_ERROR);
}
if (my $schema = $info->{parameters}) {
# warn "validate ". Dumper($param}) . "\n" . Dumper($schema);
+ $param = $normalize_legacy_param_formats->($param, $schema);
PVE::JSONSchema::validate($param, $schema);
# untaint data (already validated)
- my $extra = delete $param->{'extra-args'};
- while (my ($key, $val) = each %$param) {
- ($param->{$key}) = $val =~ /^(.*)$/s;
- }
- $param->{'extra-args'} = [map { /^(.*)$/ } @$extra] if $extra;
+ $param = untaint_recursive($param);
}
- my $result = &$func($param);
+ my $result = $func->($param); # the actual API code execution call
- # todo: this is only to be safe - disable?
- if (my $schema = $info->{returns}) {
+ if ($result_verification && (my $schema = $info->{returns})) {
+ # return validation is rather lose-lose, as it can require quite a bit of time and lead to
+ # false-positive errors, any HTTP API handler should avoid enabling it by default.
PVE::JSONSchema::validate($result, $schema, "Result verification failed\n");
}
-
return $result;
}
chomp $wdescr;
$wdescr =~ s/^$/+/mg;
+ $wdescr =~ s/{/\\{/g;
+ $wdescr =~ s/}/\\}/g;
+
$res .= $wdescr . "\n";
if (my $req = $phash->{requires}) {
if ($style eq 'config') {
$display_name = "$name:";
} elsif ($style eq 'arg') {
- $display_name = "-$name";
+ $display_name = "--$name";
} elsif ($style eq 'fixed') {
$display_name = "<$name>";
} else {
die "unknown style '$style'";
}
- my $tmp = sprintf " %-10s %s$defaulttxt\n", $display_name, "$type_text";
+ my $tmp = sprintf " %-10s %s%s\n", $display_name, "$type_text", "$defaulttxt";
my $indend = " ";
$res .= Text::Wrap::wrap('', $indend, ($tmp));
- $res .= "\n",
$res .= Text::Wrap::wrap($indend, $indend, ($descr)) . "\n\n";
if (my $req = $phash->{requires}) {
#
# $info ... method info
# $prefix ... usually something like "$exename $cmd" ('pvesm add')
-# $arg_param ... list of parameters we want to get as ordered arguments
+# $arg_param ... list of parameters we want to get as ordered arguments
# on the command line (or single parameter name for lists)
# $fixed_param ... do not generate and info about those parameters
# $format:
my $schema = $info->{parameters};
my $name = $info->{name};
- my $prop = { %{$schema->{properties}} }; # copy
+ my $prop = {};
+ if ($schema->{properties}) {
+ $prop = { %{$schema->{properties}} }; # copy
+ }
my $has_output_format_option = $formatter_properties->{'output-format'} ? 1 : 0;
my $idx_param = {}; # -vlan\d+ -scsi\d+
my $opts = '';
+
+ my $type_specific_opts = {};
+
foreach my $k (sort keys %$prop) {
next if $arg_hash->{$k};
next if defined($fixed_param->{$k});
my $type_text = $prop->{$k}->{type} || 'string';
+ if ($prop->{$k}->{oneOf}) {
+ $type_text = 'multiple';
+ }
+
my $param_map = {};
if (defined($param_cb)) {
}
}
+ my $is_optional = $prop->{$k}->{optional} // 0;
+
+ if (my $type_property = $prop->{$k}->{'type-property'}) {
+ # save type specific descriptions for later
+ my $type_schema = $prop->{$type_property};
+ if ($prop->{$k}->{oneOf}) {
+ # it's optional if there are less options than types
+ $is_optional = 1 if scalar($type_schema->{enum}->@*) > scalar($prop->{$k}->{oneOf}->@*);
+ for my $alternative ($prop->{$k}->{oneOf}->@*) {
+ # it's optional if at least one variant is optional
+ $is_optional = 1 if $alternative->{optional};
+ for my $type ($alternative->{'instance-types'}->@*) {
+ my $key = "${type_property}=${type}";
+ $type_specific_opts->{$key} //= "";
+ $type_specific_opts->{$key}
+ .= $get_property_description->($base, 'arg', $alternative, $format, $param_map->{$k});
+ }
+ }
+ } elsif (my $types = $prop->{$k}->{'instance-types'}) {
+ # it's optional if not all types has that option
+ $is_optional = 1 if scalar($type_schema->{enum}->@*) > scalar($types->@*);
+ for my $type ($types->@*) {
+ my $key = "${type_property}=${type}";
+ $type_specific_opts->{$key} //= "";
+ $type_specific_opts->{$key}
+ .= $get_property_description->($base, 'arg', $prop->{$k}, $format, $param_map->{$k});
+ }
+ }
+ } elsif ($prop->{$k}->{oneOf}) {
+ my $res = [];
+ for my $alternative ($prop->{$k}->{oneOf}->@*) {
+ # it's optional if at least one variant is optional
+ $is_optional = 1 if $alternative->{optional};
+ push $res->@*, $get_property_description->($base, 'arg', $alternative, $format, $param_map->{$k});
+ }
+ if ($format eq 'asciidoc') {
+ $opts .= join("\n\nor\n\n", $res->@*);
+ } else {
+ $opts .= join(" or\n\n", $res->@*);
+ }
+ } else {
+ $opts .= $get_property_description->($base, 'arg', $prop->{$k}, $format, $param_map->{$k});
+ }
- $opts .= $get_property_description->($base, 'arg', $prop->{$k}, $format, $param_map->{$k});
-
- if (!$prop->{$k}->{optional}) {
+ if (!$is_optional) {
$args .= " " if $args;
$args .= "--$base <$type_text>"
}
- }
+ }
if ($format eq 'asciidoc') {
$out .= "*${prefix}*";
$out .= $opts if $opts;
+ if (scalar(keys $type_specific_opts->%*)) {
+ if ($format eq 'asciidoc') {
+ $out .= "\n\n\n`Conditional options:`\n\n";
+ } else {
+ $out .= " Conditional options:\n\n";
+ }
+ }
+
+ for my $type_opts (sort keys $type_specific_opts->%*) {
+ if ($format eq 'asciidoc') {
+ $out .= "`[$type_opts]` ;;\n\n";
+ } else {
+ $out .= " [$type_opts]\n\n";
+ }
+ $out .= $type_specific_opts->{$type_opts};
+ }
+
return $out;
}
my $raw = '';
$style //= 'config';
-
+
my $idx_param = {}; # -vlan\d+ -scsi\d+
foreach my $k (sort keys %$prop) {
}
}
- $raw .= $get_property_description->($base, $style, $phash, $format);
+ if ($phash->{oneOf}) {
+ for my $alternative ($phash->{oneOf}->@*) {
+ $raw .= $get_property_description->($base, $style, $alternative, $format);
+ }
+ } else {
+ $raw .= $get_property_description->($base, $style, $phash, $format);
+ }
+
next if $style ne 'config';
next if !(ref($prop_fmt) && (ref($prop_fmt) eq 'HASH'));
$raw .= dump_properties($prop_fmt, $format, 'config-sub')
-
+
}
return $raw;
$replace_file_names_with_contents->($param, $param_map);
}
- $res = $self->handle($info, $param);
+ $res = $self->handle($info, $param, 1);
};
if (my $err = $@) {
my $ec = ref($err);
die $err if !$ec || $ec ne "PVE::Exception" || !$err->is_param_exc();
-
+
$err->{usage} = $self->usage_str($name, $prefix, $arg_param, $fixed_param, 'short', $param_cb, $formatter_properties);
die $err;