diff options
Diffstat (limited to 'Bugzilla/WebService/Util.pm')
-rw-r--r-- | Bugzilla/WebService/Util.pm | 474 |
1 files changed, 241 insertions, 233 deletions
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index ce5586911..933d1b5f7 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -29,284 +29,292 @@ use base qw(Exporter); require Test::Taint if ${^TAINT}; our @EXPORT_OK = qw( - extract_flags - filter - filter_wants - taint_data - validate - translate - params_to_objects - fix_credentials + extract_flags + filter + filter_wants + taint_data + validate + translate + params_to_objects + fix_credentials ); sub extract_flags { - my ($flags, $bug, $attachment) = @_; - my (@new_flags, @old_flags); + my ($flags, $bug, $attachment) = @_; + my (@new_flags, @old_flags); - my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types; - my $current_flags = $attachment ? $attachment->flags : $bug->flags; + my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types; + my $current_flags = $attachment ? $attachment->flags : $bug->flags; - # Copy the user provided $flags as we may call extract_flags more than - # once when editing multiple bugs or attachments. - my $flags_copy = dclone($flags); + # Copy the user provided $flags as we may call extract_flags more than + # once when editing multiple bugs or attachments. + my $flags_copy = dclone($flags); - foreach my $flag (@$flags_copy) { - my $id = $flag->{id}; - my $type_id = $flag->{type_id}; + foreach my $flag (@$flags_copy) { + my $id = $flag->{id}; + my $type_id = $flag->{type_id}; - my $new = delete $flag->{new}; - my $name = delete $flag->{name}; + my $new = delete $flag->{new}; + my $name = delete $flag->{name}; - if ($id) { - my $flag_obj = grep($id == $_->id, @$current_flags); - $flag_obj || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::Flag', id => $id }); - } - elsif ($type_id) { - my $type_obj = grep($type_id == $_->id, @$flag_types); - $type_obj || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::FlagType', id => $type_id }); - if (!$new) { - my @flag_matches = grep($type_id == $_->type->id, @$current_flags); - @flag_matches > 1 && ThrowUserError('flag_not_unique', - { value => $type_id }); - if (!@flag_matches) { - delete $flag->{id}; - } - else { - delete $flag->{type_id}; - $flag->{id} = $flag_matches[0]->id; - } - } + if ($id) { + my $flag_obj = grep($id == $_->id, @$current_flags); + $flag_obj + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::Flag', id => $id}); + } + elsif ($type_id) { + my $type_obj = grep($type_id == $_->id, @$flag_types); + $type_obj + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::FlagType', id => $type_id}); + if (!$new) { + my @flag_matches = grep($type_id == $_->type->id, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $type_id}); + if (!@flag_matches) { + delete $flag->{id}; } - elsif ($name) { - my @type_matches = grep($name eq $_->name, @$flag_types); - @type_matches > 1 && ThrowUserError('flag_type_not_unique', - { value => $name }); - @type_matches || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::FlagType', name => $name }); - if ($new) { - delete $flag->{id}; - $flag->{type_id} = $type_matches[0]->id; - } - else { - my @flag_matches = grep($name eq $_->type->name, @$current_flags); - @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name }); - if (@flag_matches) { - $flag->{id} = $flag_matches[0]->id; - } - else { - delete $flag->{id}; - $flag->{type_id} = $type_matches[0]->id; - } - } + else { + delete $flag->{type_id}; + $flag->{id} = $flag_matches[0]->id; } - - if ($flag->{id}) { - push(@old_flags, $flag); + } + } + elsif ($name) { + my @type_matches = grep($name eq $_->name, @$flag_types); + @type_matches > 1 && ThrowUserError('flag_type_not_unique', {value => $name}); + @type_matches + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::FlagType', name => $name}); + if ($new) { + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; + } + else { + my @flag_matches = grep($name eq $_->type->name, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $name}); + if (@flag_matches) { + $flag->{id} = $flag_matches[0]->id; } else { - push(@new_flags, $flag); + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; } + } } - return (\@old_flags, \@new_flags); + if ($flag->{id}) { + push(@old_flags, $flag); + } + else { + push(@new_flags, $flag); + } + } + + return (\@old_flags, \@new_flags); } sub filter($$;$$) { - my ($params, $hash, $types, $prefix) = @_; - my %newhash = %$hash; + my ($params, $hash, $types, $prefix) = @_; + my %newhash = %$hash; - foreach my $key (keys %$hash) { - delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); - } + foreach my $key (keys %$hash) { + delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); + } - return \%newhash; + return \%newhash; } sub filter_wants($$;$$) { - my ($params, $field, $types, $prefix) = @_; - - # Since this is operation is resource intensive, we will cache the results - # This assumes that $params->{*_fields} doesn't change between calls - my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; - $field = "${prefix}.${field}" if $prefix; - - if (exists $cache->{$field}) { - return $cache->{$field}; - } - - # Mimic old behavior if no types provided - my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); - - my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] }; - my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] }; - - my %include_types; - my %exclude_types; - - # Only return default fields if nothing is specified - $include_types{default} = 1 if !%include; - - # Look for any field types requested - foreach my $key (keys %include) { - next if $key !~ /^_(.*)$/; - $include_types{$1} = 1; - delete $include{$key}; - } - foreach my $key (keys %exclude) { - next if $key !~ /^_(.*)$/; - $exclude_types{$1} = 1; - delete $exclude{$key}; - } - - # Explicit inclusion/exclusion - return $cache->{$field} = 0 if $exclude{$field}; - return $cache->{$field} = 1 if $include{$field}; - - # If the user has asked to include all or exclude all - return $cache->{$field} = 0 if $exclude_types{'all'}; - return $cache->{$field} = 1 if $include_types{'all'}; - - # If the user has not asked for any fields specifically or if the user has asked - # for one or more of the field's types (and not excluded them) - foreach my $type (keys %field_types) { - return $cache->{$field} = 0 if $exclude_types{$type}; - return $cache->{$field} = 1 if $include_types{$type}; - } - - my $wants = 0; - if ($prefix) { - # Include the field if the parent is include (and this one is not excluded) - $wants = 1 if $include{$prefix}; - } - else { - # We want to include this if one of the sub keys is included - my $key = $field . '.'; - my $len = length($key); - $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; - } - - return $cache->{$field} = $wants; + my ($params, $field, $types, $prefix) = @_; + + # Since this is operation is resource intensive, we will cache the results + # This assumes that $params->{*_fields} doesn't change between calls + my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; + $field = "${prefix}.${field}" if $prefix; + + if (exists $cache->{$field}) { + return $cache->{$field}; + } + + # Mimic old behavior if no types provided + my %field_types + = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); + + my %include = map { $_ => 1 } @{$params->{'include_fields'} || []}; + my %exclude = map { $_ => 1 } @{$params->{'exclude_fields'} || []}; + + my %include_types; + my %exclude_types; + + # Only return default fields if nothing is specified + $include_types{default} = 1 if !%include; + + # Look for any field types requested + foreach my $key (keys %include) { + next if $key !~ /^_(.*)$/; + $include_types{$1} = 1; + delete $include{$key}; + } + foreach my $key (keys %exclude) { + next if $key !~ /^_(.*)$/; + $exclude_types{$1} = 1; + delete $exclude{$key}; + } + + # Explicit inclusion/exclusion + return $cache->{$field} = 0 if $exclude{$field}; + return $cache->{$field} = 1 if $include{$field}; + + # If the user has asked to include all or exclude all + return $cache->{$field} = 0 if $exclude_types{'all'}; + return $cache->{$field} = 1 if $include_types{'all'}; + + # If the user has not asked for any fields specifically or if the user has asked + # for one or more of the field's types (and not excluded them) + foreach my $type (keys %field_types) { + return $cache->{$field} = 0 if $exclude_types{$type}; + return $cache->{$field} = 1 if $include_types{$type}; + } + + my $wants = 0; + if ($prefix) { + + # Include the field if the parent is include (and this one is not excluded) + $wants = 1 if $include{$prefix}; + } + else { + # We want to include this if one of the sub keys is included + my $key = $field . '.'; + my $len = length($key); + $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; + } + + return $cache->{$field} = $wants; } sub taint_data { - my @params = @_; - return if !@params; - # Though this is a private function, it hasn't changed since 2004 and - # should be safe to use, and prevents us from having to write it ourselves - # or require another module to do it. - if (${^TAINT}) { - Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); - Test::Taint::taint_deeply(\@params); - } + my @params = @_; + return if !@params; + + # Though this is a private function, it hasn't changed since 2004 and + # should be safe to use, and prevents us from having to write it ourselves + # or require another module to do it. + if (${^TAINT}) { + Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); + Test::Taint::taint_deeply(\@params); + } } sub _delete_bad_keys { - foreach my $item (@_) { - next if ref $item ne 'HASH'; - foreach my $key (keys %$item) { - # Making something a hash key always untaints it, in Perl. - # However, we need to validate our argument names in some way. - # We know that all hash keys passed in to the WebService will - # match \w+, so we delete any key that doesn't match that. - if ($key !~ /^[\w\.\-]+$/) { - delete $item->{$key}; - } - } + foreach my $item (@_) { + next if ref $item ne 'HASH'; + foreach my $key (keys %$item) { + + # Making something a hash key always untaints it, in Perl. + # However, we need to validate our argument names in some way. + # We know that all hash keys passed in to the WebService will + # match \w+, so we delete any key that doesn't match that. + if ($key !~ /^[\w\.\-]+$/) { + delete $item->{$key}; + } } - return @_; + } + return @_; } -sub validate { - my ($self, $params, @keys) = @_; - my $cache_key = join('|', (caller(1))[3], sort @keys); - # Type->of() is the same as Type[], used here because it is easier - # to chain with plus_coercions. - state $array_of_nonrefs = ArrayRef->of(Maybe[Value])->plus_coercions( - Maybe[Value], q{ [ $_ ] }, - ); - state $type_cache = {}; - my $params_type = $type_cache->{$cache_key} //= do { - my %fields = map { $_ => Optional[$array_of_nonrefs] } @keys; - Maybe[ Dict[%fields, slurpy Any] ]; - }; - - # If $params is defined but not a reference, then we weren't - # sent any parameters at all, and we're getting @keys where - # $params should be. - return ($self, undef) if (defined $params and !ref $params); - - # If @keys is not empty then we convert any named - # parameters that have scalar values to arrayrefs - # that match. - $params = $params_type->coerce($params); - if (my $type_error = $params_type->validate($params)) { - FATAL("validate() found type error: $type_error"); - ThrowUserError('invalid_params', { type_error => $type_error } ) if $type_error; - } - - return ($self, $params); +sub validate { + my ($self, $params, @keys) = @_; + my $cache_key = join('|', (caller(1))[3], sort @keys); + + # Type->of() is the same as Type[], used here because it is easier + # to chain with plus_coercions. + state $array_of_nonrefs + = ArrayRef->of(Maybe [Value])->plus_coercions(Maybe [Value], q{ [ $_ ] },); + state $type_cache = {}; + my $params_type = $type_cache->{$cache_key} //= do { + my %fields = map { $_ => Optional [$array_of_nonrefs] } @keys; + Maybe [Dict [%fields, slurpy Any]]; + }; + + # If $params is defined but not a reference, then we weren't + # sent any parameters at all, and we're getting @keys where + # $params should be. + return ($self, undef) if (defined $params and !ref $params); + + # If @keys is not empty then we convert any named + # parameters that have scalar values to arrayrefs + # that match. + $params = $params_type->coerce($params); + if (my $type_error = $params_type->validate($params)) { + FATAL("validate() found type error: $type_error"); + ThrowUserError('invalid_params', {type_error => $type_error}) if $type_error; + } + + return ($self, $params); } sub translate { - my ($params, $mapped) = @_; - my %changes; - while (my ($key,$value) = each (%$params)) { - my $new_field = $mapped->{$key} || $key; - $changes{$new_field} = $value; - } - return \%changes; + my ($params, $mapped) = @_; + my %changes; + while (my ($key, $value) = each(%$params)) { + my $new_field = $mapped->{$key} || $key; + $changes{$new_field} = $value; + } + return \%changes; } sub params_to_objects { - my ($params, $class) = @_; - my (@objects, @objects_by_ids); + my ($params, $class) = @_; + my (@objects, @objects_by_ids); - @objects = map { $class->check($_) } - @{ $params->{names} } if $params->{names}; + @objects = map { $class->check($_) } @{$params->{names}} if $params->{names}; - @objects_by_ids = map { $class->check({ id => $_ }) } - @{ $params->{ids} } if $params->{ids}; + @objects_by_ids = map { $class->check({id => $_}) } @{$params->{ids}} + if $params->{ids}; - push(@objects, @objects_by_ids); - my %seen; - @objects = grep { !$seen{$_->id}++ } @objects; - return \@objects; + push(@objects, @objects_by_ids); + my %seen; + @objects = grep { !$seen{$_->id}++ } @objects; + return \@objects; } sub fix_credentials { - my ($params, $cgi) = @_; - - # Allow user to pass in authentication details in X-Headers - # This allows callers to keep credentials out of GET request query-strings - if ($cgi) { - foreach my $field (keys %{ API_AUTH_HEADERS() }) { - next if exists $params->{API_AUTH_HEADERS->{$field}} || ($cgi->http($field) // '') eq ''; - $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field)); - } + my ($params, $cgi) = @_; + + # Allow user to pass in authentication details in X-Headers + # This allows callers to keep credentials out of GET request query-strings + if ($cgi) { + foreach my $field (keys %{API_AUTH_HEADERS()}) { + next + if exists $params->{API_AUTH_HEADERS->{$field}} + || ($cgi->http($field) // '') eq ''; + $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field)); } - - # Allow user to pass in login=foo&password=bar as a convenience - # even if not calling GET /login. We also do not delete them as - # GET /login requires "login" and "password". - if (exists $params->{'login'} && exists $params->{'password'}) { - $params->{'Bugzilla_login'} = delete $params->{'login'}; - $params->{'Bugzilla_password'} = delete $params->{'password'}; - } - # Allow user to pass api_key=12345678 as a convenience which becomes - # "Bugzilla_api_key" which is what the auth code looks for. - if (exists $params->{api_key}) { - $params->{Bugzilla_api_key} = delete $params->{api_key}; - } - # Allow user to pass token=12345678 as a convenience which becomes - # "Bugzilla_token" which is what the auth code looks for. - if (exists $params->{'token'}) { - $params->{'Bugzilla_token'} = delete $params->{'token'}; - } - - # Allow extensions to modify the credential data before login - Bugzilla::Hook::process('webservice_fix_credentials', { params => $params }); + } + + # Allow user to pass in login=foo&password=bar as a convenience + # even if not calling GET /login. We also do not delete them as + # GET /login requires "login" and "password". + if (exists $params->{'login'} && exists $params->{'password'}) { + $params->{'Bugzilla_login'} = delete $params->{'login'}; + $params->{'Bugzilla_password'} = delete $params->{'password'}; + } + + # Allow user to pass api_key=12345678 as a convenience which becomes + # "Bugzilla_api_key" which is what the auth code looks for. + if (exists $params->{api_key}) { + $params->{Bugzilla_api_key} = delete $params->{api_key}; + } + + # Allow user to pass token=12345678 as a convenience which becomes + # "Bugzilla_token" which is what the auth code looks for. + if (exists $params->{'token'}) { + $params->{'Bugzilla_token'} = delete $params->{'token'}; + } + + # Allow extensions to modify the credential data before login + Bugzilla::Hook::process('webservice_fix_credentials', {params => $params}); } __END__ |