diff options
Diffstat (limited to 'Bugzilla/WebService')
23 files changed, 4138 insertions, 4106 deletions
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 61a95e07d..5b6c31063 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -19,7 +19,8 @@ use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Field; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate); +use Bugzilla::WebService::Util + qw(extract_flags filter filter_wants validate translate); use Bugzilla::Bug; use Bugzilla::BugMail; use Bugzilla::Util qw(trick_taint trim detaint_natural remote_ip); @@ -45,71 +46,68 @@ use Type::Utils; use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); sub DATE_FIELDS { - my $fields = { - comments => ['new_since'], - create => [], - history => ['new_since'], - search => ['last_change_time', 'creation_time'], - update => [] - }; - - # Add date related custom fields - foreach my $field (Bugzilla->active_custom_fields) { - next unless ($field->type == FIELD_TYPE_DATETIME - || $field->type == FIELD_TYPE_DATE); - push(@{ $fields->{create} }, $field->name); - push(@{ $fields->{update} }, $field->name); - } - - return $fields; + my $fields = { + comments => ['new_since'], + create => [], + history => ['new_since'], + search => ['last_change_time', 'creation_time'], + update => [] + }; + + # Add date related custom fields + foreach my $field (Bugzilla->active_custom_fields) { + next + unless ($field->type == FIELD_TYPE_DATETIME + || $field->type == FIELD_TYPE_DATE); + push(@{$fields->{create}}, $field->name); + push(@{$fields->{update}}, $field->name); + } + + return $fields; } -use constant BASE64_FIELDS => { - add_attachment => ['data'], -}; +use constant BASE64_FIELDS => {add_attachment => ['data'],}; use constant READ_ONLY => qw( - attachments - comments - fields - get - history - legal_values - search + attachments + comments + fields + get + history + legal_values + search ); use constant PUBLIC_METHODS => qw( - add_attachment - add_comment - attachments - comments - create - fields - get - history - legal_values - possible_duplicates - render_comment - search - search_comment_tags - update - update_attachment - update_comment_tags - update_see_also + add_attachment + add_comment + attachments + comments + create + fields + get + history + legal_values + possible_duplicates + render_comment + search + search_comment_tags + update + update_attachment + update_comment_tags + update_see_also ); -use constant ATTACHMENT_MAPPED_SETTERS => { - file_name => 'filename', - summary => 'description', -}; +use constant ATTACHMENT_MAPPED_SETTERS => + {file_name => 'filename', summary => 'description',}; use constant ATTACHMENT_MAPPED_RETURNS => { - description => 'summary', - ispatch => 'is_patch', - isprivate => 'is_private', - isobsolete => 'is_obsolete', - filename => 'file_name', - mimetype => 'content_type', + description => 'summary', + ispatch => 'is_patch', + isprivate => 'is_private', + isobsolete => 'is_obsolete', + filename => 'file_name', + mimetype => 'content_type', }; ###################################################### @@ -119,6 +117,7 @@ use constant ATTACHMENT_MAPPED_RETURNS => { BEGIN { # In 3.0, get was called get_bugs *get_bugs = \&get; + # Before 3.4rc1, "history" was get_history. *get_history = \&history; } @@ -128,1216 +127,1240 @@ BEGIN { ########### sub fields { - my ($self, $params) = validate(@_, 'ids', 'names'); + my ($self, $params) = validate(@_, 'ids', 'names'); - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - my @fields; - if (defined $params->{ids}) { - my $ids = $params->{ids}; - foreach my $id (@$ids) { - my $loop_field = Bugzilla::Field->check({ id => $id }); - push(@fields, $loop_field); - } + my @fields; + if (defined $params->{ids}) { + my $ids = $params->{ids}; + foreach my $id (@$ids) { + my $loop_field = Bugzilla::Field->check({id => $id}); + push(@fields, $loop_field); } - - if (defined $params->{names}) { - my $names = $params->{names}; - foreach my $field_name (@$names) { - my $loop_field = Bugzilla::Field->check($field_name); - # Don't push in duplicate fields if we also asked for this field - # in "ids". - if (!grep($_->id == $loop_field->id, @fields)) { - push(@fields, $loop_field); - } - } + } + + if (defined $params->{names}) { + my $names = $params->{names}; + foreach my $field_name (@$names) { + my $loop_field = Bugzilla::Field->check($field_name); + + # Don't push in duplicate fields if we also asked for this field + # in "ids". + if (!grep($_->id == $loop_field->id, @fields)) { + push(@fields, $loop_field); + } } - - if (!defined $params->{ids} and !defined $params->{names}) { - @fields = @{ Bugzilla->fields({ obsolete => 0 }) }; + } + + if (!defined $params->{ids} and !defined $params->{names}) { + @fields = @{Bugzilla->fields({obsolete => 0})}; + } + + my @fields_out; + foreach my $field (@fields) { + my $visibility_field + = $field->visibility_field ? $field->visibility_field->name : undef; + my $vis_values = $field->visibility_values; + my $value_field = $field->value_field ? $field->value_field->name : undef; + + my (@values, $has_values); + if ( ($field->is_select and $field->name ne 'product') + or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) + or $field->name eq 'keywords') + { + $has_values = 1; + @values = @{$self->_legal_field_values({field => $field})}; } - my @fields_out; - foreach my $field (@fields) { - my $visibility_field = $field->visibility_field - ? $field->visibility_field->name : undef; - my $vis_values = $field->visibility_values; - my $value_field = $field->value_field - ? $field->value_field->name : undef; - - my (@values, $has_values); - if ( ($field->is_select and $field->name ne 'product') - or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) - or $field->name eq 'keywords') - { - $has_values = 1; - @values = @{ $self->_legal_field_values({ field => $field }) }; - } - - if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { - $value_field = 'product'; - } + if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { + $value_field = 'product'; + } - my %field_data = ( - id => $self->type('int', $field->id), - type => $self->type('int', $field->type), - is_custom => $self->type('boolean', $field->custom), - name => $self->type('string', $field->name), - display_name => $self->type('string', $field->description), - is_mandatory => $self->type('boolean', $field->is_mandatory), - is_on_bug_entry => $self->type('boolean', $field->enter_bug), - visibility_field => $self->type('string', $visibility_field), - visibility_values => - [ map { $self->type('string', $_->name) } @$vis_values ], - ); - if ($has_values) { - $field_data{value_field} = $self->type('string', $value_field); - $field_data{values} = \@values; - }; - push(@fields_out, filter $params, \%field_data); + my %field_data = ( + id => $self->type('int', $field->id), + type => $self->type('int', $field->type), + is_custom => $self->type('boolean', $field->custom), + name => $self->type('string', $field->name), + display_name => $self->type('string', $field->description), + is_mandatory => $self->type('boolean', $field->is_mandatory), + is_on_bug_entry => $self->type('boolean', $field->enter_bug), + visibility_field => $self->type('string', $visibility_field), + visibility_values => [map { $self->type('string', $_->name) } @$vis_values], + ); + if ($has_values) { + $field_data{value_field} = $self->type('string', $value_field); + $field_data{values} = \@values; } + push(@fields_out, filter $params, \%field_data); + } - return { fields => \@fields_out }; + return {fields => \@fields_out}; } sub _legal_field_values { - my ($self, $params) = @_; - my $field = $params->{field}; - my $field_name = $field->name; - my $user = Bugzilla->user; - - my @result; - if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { - my @list; - if ($field_name eq 'version') { - @list = Bugzilla::Version->get_all; - } - elsif ($field_name eq 'component') { - @list = Bugzilla::Component->get_all; - } - else { - @list = Bugzilla::Milestone->get_all; - } + my ($self, $params) = @_; + my $field = $params->{field}; + my $field_name = $field->name; + my $user = Bugzilla->user; + + my @result; + if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { + my @list; + if ($field_name eq 'version') { + @list = Bugzilla::Version->get_all; + } + elsif ($field_name eq 'component') { + @list = Bugzilla::Component->get_all; + } + else { + @list = Bugzilla::Milestone->get_all; + } - foreach my $value (@list) { - my $sortkey = $field_name eq 'target_milestone' - ? $value->sortkey : 0; - # XXX This is very slow for large numbers of values. - my $product_name = $value->product->name; - if ($user->can_see_product($product_name)) { - push(@result, { - name => $self->type('string', $value->name), - sort_key => $self->type('int', $sortkey), - sortkey => $self->type('int', $sortkey), # deprecated - visibility_values => [$self->type('string', $product_name)], - }); - } - } + foreach my $value (@list) { + my $sortkey = $field_name eq 'target_milestone' ? $value->sortkey : 0; + + # XXX This is very slow for large numbers of values. + my $product_name = $value->product->name; + if ($user->can_see_product($product_name)) { + push( + @result, + { + name => $self->type('string', $value->name), + sort_key => $self->type('int', $sortkey), + sortkey => $self->type('int', $sortkey), # deprecated + visibility_values => [$self->type('string', $product_name)], + } + ); + } } + } + + elsif ($field_name eq 'bug_status') { + my @status_all = Bugzilla::Status->get_all; + foreach my $status (@status_all) { + my @can_change_to; + foreach my $change_to (@{$status->can_change_to}) { + + # There's no need to note that a status can transition + # to itself. + next if $change_to->id == $status->id; + my %change_to_hash = ( + name => $self->type('string', $change_to->name), + comment_required => + $self->type('boolean', $change_to->comment_required_on_change_from($status)), + ); + push(@can_change_to, \%change_to_hash); + } - elsif ($field_name eq 'bug_status') { - my @status_all = Bugzilla::Status->get_all; - foreach my $status (@status_all) { - my @can_change_to; - foreach my $change_to (@{ $status->can_change_to }) { - # There's no need to note that a status can transition - # to itself. - next if $change_to->id == $status->id; - my %change_to_hash = ( - name => $self->type('string', $change_to->name), - comment_required => $self->type('boolean', - $change_to->comment_required_on_change_from($status)), - ); - push(@can_change_to, \%change_to_hash); - } - - push (@result, { - name => $self->type('string', $status->name), - is_open => $self->type('boolean', $status->is_open), - sort_key => $self->type('int', $status->sortkey), - sortkey => $self->type('int', $status->sortkey), # deprecated - can_change_to => \@can_change_to, - visibility_values => [], - }); + push( + @result, + { + name => $self->type('string', $status->name), + is_open => $self->type('boolean', $status->is_open), + sort_key => $self->type('int', $status->sortkey), + sortkey => $self->type('int', $status->sortkey), # deprecated + can_change_to => \@can_change_to, + visibility_values => [], } + ); } - - elsif ($field_name eq 'keywords') { - my @legal_keywords = Bugzilla::Keyword->get_all; - foreach my $value (@legal_keywords) { - next unless $value->is_active; - push (@result, { - name => $self->type('string', $value->name), - description => $self->type('string', $value->description), - }); + } + + elsif ($field_name eq 'keywords') { + my @legal_keywords = Bugzilla::Keyword->get_all; + foreach my $value (@legal_keywords) { + next unless $value->is_active; + push( + @result, + { + name => $self->type('string', $value->name), + description => $self->type('string', $value->description), } + ); } - else { - my @values = Bugzilla::Field::Choice->type($field)->get_all(); - foreach my $value (@values) { - my $vis_val = $value->visibility_value; - push(@result, { - name => $self->type('string', $value->name), - sort_key => $self->type('int' , $value->sortkey), - sortkey => $self->type('int' , $value->sortkey), # deprecated - visibility_values => [ - defined $vis_val ? $self->type('string', $vis_val->name) - : () - ], - }); + } + else { + my @values = Bugzilla::Field::Choice->type($field)->get_all(); + foreach my $value (@values) { + my $vis_val = $value->visibility_value; + push( + @result, + { + name => $self->type('string', $value->name), + sort_key => $self->type('int', $value->sortkey), + sortkey => $self->type('int', $value->sortkey), # deprecated + visibility_values => + [defined $vis_val ? $self->type('string', $vis_val->name) : ()], } + ); } + } - return \@result; + return \@result; } sub comments { - my ($self, $params) = validate(@_, 'ids', 'comment_ids'); + my ($self, $params) = validate(@_, 'ids', 'comment_ids'); - if (!(defined $params->{ids} || defined $params->{comment_ids})) { - ThrowCodeError('params_required', - { function => 'Bug.comments', - params => ['ids', 'comment_ids'] }); - } + if (!(defined $params->{ids} || defined $params->{comment_ids})) { + ThrowCodeError('params_required', + {function => 'Bug.comments', params => ['ids', 'comment_ids']}); + } - my $bug_ids = $params->{ids} || []; - my $comment_ids = $params->{comment_ids} || []; + my $bug_ids = $params->{ids} || []; + my $comment_ids = $params->{comment_ids} || []; - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; - my %bugs; - foreach my $bug_id (@$bug_ids) { - my $bug = Bugzilla::Bug->check($bug_id); - # We want the API to always return comments in the same order. + my %bugs; + foreach my $bug_id (@$bug_ids) { + my $bug = Bugzilla::Bug->check($bug_id); - my $comments = $bug->comments({ order => 'oldest_to_newest', - after => $params->{new_since} }); - my @result; - foreach my $comment (@$comments) { - next if $comment->is_private && !$user->is_insider; - push(@result, $self->_translate_comment($comment, $params)); - } - $bugs{$bug->id}{'comments'} = \@result; - } + # We want the API to always return comments in the same order. - my %comments; - if (scalar @$comment_ids) { - my @ids = map { trim($_) } @$comment_ids; - my $comment_data = Bugzilla::Comment->new_from_list(\@ids); - - # See if we were passed any invalid comment ids. - my %got_ids = map { $_->id => 1 } @$comment_data; - foreach my $comment_id (@ids) { - if (!$got_ids{$comment_id}) { - ThrowUserError('comment_id_invalid', { id => $comment_id }); - } - } + my $comments + = $bug->comments({order => 'oldest_to_newest', after => $params->{new_since} + }); + my @result; + foreach my $comment (@$comments) { + next if $comment->is_private && !$user->is_insider; + push(@result, $self->_translate_comment($comment, $params)); + } + $bugs{$bug->id}{'comments'} = \@result; + } + + my %comments; + if (scalar @$comment_ids) { + my @ids = map { trim($_) } @$comment_ids; + my $comment_data = Bugzilla::Comment->new_from_list(\@ids); + + # See if we were passed any invalid comment ids. + my %got_ids = map { $_->id => 1 } @$comment_data; + foreach my $comment_id (@ids) { + if (!$got_ids{$comment_id}) { + ThrowUserError('comment_id_invalid', {id => $comment_id}); + } + } - # Now make sure that we can see all the associated bugs. - my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; - Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); + # Now make sure that we can see all the associated bugs. + my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; + Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); - foreach my $comment (@$comment_data) { - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment->id }); - } - $comments{$comment->id} = - $self->_translate_comment($comment, $params); - } + foreach my $comment (@$comment_data) { + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment->id}); + } + $comments{$comment->id} = $self->_translate_comment($comment, $params); } + } - return { bugs => \%bugs, comments => \%comments }; + return {bugs => \%bugs, comments => \%comments}; } sub render_comment { - my ($self, $params) = @_; + my ($self, $params) = @_; - unless (defined $params->{text}) { - ThrowCodeError('params_required', - { function => 'Bug.render_comment', - params => ['text'] }); - } + unless (defined $params->{text}) { + ThrowCodeError('params_required', + {function => 'Bug.render_comment', params => ['text']}); + } - Bugzilla->switch_to_shadow_db(); - my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; + Bugzilla->switch_to_shadow_db(); + my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; - my $html = Bugzilla::Template::quoteUrls($params->{text}, $bug); + my $html = Bugzilla::Template::quoteUrls($params->{text}, $bug); - return { html => $html }; + return {html => $html}; } # Helper for Bug.comments sub _translate_comment { - my ($self, $comment, $filters, $types, $prefix) = @_; - my $attach_id = $comment->is_about_attachment ? $comment->extra_data - : undef; - - my $comment_hash = { - id => $self->type('int', $comment->id), - bug_id => $self->type('int', $comment->bug_id), - creator => $self->type('email', $comment->author->login), - author => $self->type('email', $comment->author->login), - time => $self->type('dateTime', $comment->creation_ts), - creation_time => $self->type('dateTime', $comment->creation_ts), - is_private => $self->type('boolean', $comment->is_private), - text => $self->type('string', $comment->body_full), - attachment_id => $self->type('int', $attach_id), - count => $self->type('int', $comment->count), - }; - - # Don't load comment tags unless enabled - if (Bugzilla->params->{'comment_taggers_group'}) { - $comment_hash->{tags} = [ - map { $self->type('string', $_) } - @{ $comment->tags } - ]; - } - - return filter($filters, $comment_hash, $types, $prefix); + my ($self, $comment, $filters, $types, $prefix) = @_; + my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef; + + my $comment_hash = { + id => $self->type('int', $comment->id), + bug_id => $self->type('int', $comment->bug_id), + creator => $self->type('email', $comment->author->login), + author => $self->type('email', $comment->author->login), + time => $self->type('dateTime', $comment->creation_ts), + creation_time => $self->type('dateTime', $comment->creation_ts), + is_private => $self->type('boolean', $comment->is_private), + text => $self->type('string', $comment->body_full), + attachment_id => $self->type('int', $attach_id), + count => $self->type('int', $comment->count), + }; + + # Don't load comment tags unless enabled + if (Bugzilla->params->{'comment_taggers_group'}) { + $comment_hash->{tags} = [map { $self->type('string', $_) } @{$comment->tags}]; + } + + return filter($filters, $comment_hash, $types, $prefix); } sub get { - my ($self, $params) = validate(@_, 'ids'); - - unless (Bugzilla->user->id) { - Bugzilla->check_rate_limit("get_bug", remote_ip()); + my ($self, $params) = validate(@_, 'ids'); + + unless (Bugzilla->user->id) { + Bugzilla->check_rate_limit("get_bug", remote_ip()); + } + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + + my $ids = $params->{ids}; + (defined $ids && scalar @$ids) + || ThrowCodeError('param_required', {param => 'ids'}); + + my (@bugs, @faults, @hashes); + + # Cache permissions for bugs. This highly reduces the number of calls to the DB. + # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. + my @int = grep { $_ =~ /^\d+$/ } @$ids; + Bugzilla->user->visible_bugs(\@int); + + foreach my $bug_id (@$ids) { + my $bug; + if ($params->{permissive}) { + eval { $bug = Bugzilla::Bug->check($bug_id); }; + if ($@) { + push(@faults, + {id => $bug_id, faultString => $@->faultstring, faultCode => $@->faultcode,}); + undef $@; + next; + } } - Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - - my $ids = $params->{ids}; - (defined $ids && scalar @$ids) - || ThrowCodeError('param_required', { param => 'ids' }); - - my (@bugs, @faults, @hashes); - - # Cache permissions for bugs. This highly reduces the number of calls to the DB. - # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. - my @int = grep { $_ =~ /^\d+$/ } @$ids; - Bugzilla->user->visible_bugs(\@int); - - foreach my $bug_id (@$ids) { - my $bug; - if ($params->{permissive}) { - eval { $bug = Bugzilla::Bug->check($bug_id); }; - if ($@) { - push(@faults, {id => $bug_id, - faultString => $@->faultstring, - faultCode => $@->faultcode, - } - ); - undef $@; - next; - } - } - else { - $bug = Bugzilla::Bug->check($bug_id); - } - push(@bugs, $bug); - push(@hashes, $self->_bug_to_hash($bug, $params)); + else { + $bug = Bugzilla::Bug->check($bug_id); } + push(@bugs, $bug); + push(@hashes, $self->_bug_to_hash($bug, $params)); + } - # Set the ETag before inserting the update tokens - # since the tokens will always be unique even if - # the data has not changed. - $self->bz_etag(\@hashes); + # Set the ETag before inserting the update tokens + # since the tokens will always be unique even if + # the data has not changed. + $self->bz_etag(\@hashes); - $self->_add_update_tokens($params, \@bugs, \@hashes); + $self->_add_update_tokens($params, \@bugs, \@hashes); - if (Bugzilla->user->id) { - foreach my $bug (@bugs) { - Bugzilla->log_user_request($bug->id, undef, 'bug-get'); - } + if (Bugzilla->user->id) { + foreach my $bug (@bugs) { + Bugzilla->log_user_request($bug->id, undef, 'bug-get'); } - return { bugs => \@hashes, faults => \@faults }; + } + return {bugs => \@hashes, faults => \@faults}; } # this is a function that gets bug activity for list of bug ids # it can be called as the following: # $call = $rpc->call( 'Bug.history', { ids => [1,2] }); sub history { - my ($self, $params) = validate(@_, 'ids'); - - Bugzilla->switch_to_shadow_db(); - - my $ids = $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - $api_name{'bug_group'} = 'groups'; - - my @return; - foreach my $bug_id (@$ids) { - my %item; - my $bug = Bugzilla::Bug->check($bug_id); - $bug_id = $bug->id; - $item{id} = $self->type('int', $bug_id); - - my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id, undef, $params->{new_since}); - - my @history; - foreach my $changeset (@$activity) { - my %bug_history; - $bug_history{when} = $self->type('dateTime', $changeset->{when}); - $bug_history{who} = $self->type('email', $changeset->{who}); - $bug_history{changes} = []; - foreach my $change (@{ $changeset->{changes} }) { - my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; - my $attach_id = delete $change->{attachid}; - if ($attach_id) { - $change->{attachment_id} = $self->type('int', $attach_id); - } - $change->{removed} = $self->type('string', $change->{removed}); - $change->{added} = $self->type('string', $change->{added}); - $change->{field_name} = $self->type('string', $api_field); - delete $change->{fieldname}; - push (@{$bug_history{changes}}, $change); - } - - push (@history, \%bug_history); - } - - $item{history} = \@history; - - # alias is returned in case users passes a mixture of ids and aliases - # then they get to know which bug activity relates to which value - # they passed - if (Bugzilla->params->{'usebugaliases'}) { - $item{alias} = $self->type('string', $bug->alias); + my ($self, $params) = validate(@_, 'ids'); + + Bugzilla->switch_to_shadow_db(); + + my $ids = $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()}; + $api_name{'bug_group'} = 'groups'; + + my @return; + foreach my $bug_id (@$ids) { + my %item; + my $bug = Bugzilla::Bug->check($bug_id); + $bug_id = $bug->id; + $item{id} = $self->type('int', $bug_id); + + my ($activity) + = Bugzilla::Bug::GetBugActivity($bug_id, undef, $params->{new_since}); + + my @history; + foreach my $changeset (@$activity) { + my %bug_history; + $bug_history{when} = $self->type('dateTime', $changeset->{when}); + $bug_history{who} = $self->type('email', $changeset->{who}); + $bug_history{changes} = []; + foreach my $change (@{$changeset->{changes}}) { + my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; + my $attach_id = delete $change->{attachid}; + if ($attach_id) { + $change->{attachment_id} = $self->type('int', $attach_id); } - else { - # For API reasons, we always want the value to appear, we just - # don't want it to have a value if aliases are turned off. - $item{alias} = undef; - } - - push(@return, \%item); - } - - return { bugs => \@return }; -} - -sub search { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - Bugzilla->switch_to_shadow_db(); - - my $match_params = dclone($params); - delete $match_params->{include_fields}; - delete $match_params->{exclude_fields}; - - # Determine whether this is a quicksearch query - if (exists $match_params->{quicksearch}) { - my $quicksearch = quicksearch($match_params->{'quicksearch'}); - my $cgi = Bugzilla::CGI->new($quicksearch); - $match_params = $cgi->Vars; + $change->{removed} = $self->type('string', $change->{removed}); + $change->{added} = $self->type('string', $change->{added}); + $change->{field_name} = $self->type('string', $api_field); + delete $change->{fieldname}; + push(@{$bug_history{changes}}, $change); + } + + push(@history, \%bug_history); } - if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) { - ThrowCodeError('param_required', - { param => 'limit', function => 'Bug.search()' }); - } + $item{history} = \@history; - my $max_results = Bugzilla->params->{max_search_results}; - unless (defined $match_params->{limit} && $match_params->{limit} == 0) { - if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { - $match_params->{limit} = $max_results; - } + # alias is returned in case users passes a mixture of ids and aliases + # then they get to know which bug activity relates to which value + # they passed + if (Bugzilla->params->{'usebugaliases'}) { + $item{alias} = $self->type('string', $bug->alias); } else { - delete $match_params->{limit}; - delete $match_params->{offset}; + # For API reasons, we always want the value to appear, we just + # don't want it to have a value if aliases are turned off. + $item{alias} = undef; } - $match_params = Bugzilla::Bug::map_fields($match_params); - - my %options = ( fields => ['bug_id'] ); + push(@return, \%item); + } - # Find the highest custom field id - my @field_ids = grep(/^f(\d+)$/, keys %$match_params); - my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + return {bugs => \@return}; +} - # Do special search types for certain fields. - if (my $change_when = delete $match_params->{'delta_ts'}) { - $match_params->{"f${last_field_id}"} = 'delta_ts'; - $match_params->{"o${last_field_id}"} = 'greaterthaneq'; - $match_params->{"v${last_field_id}"} = $change_when; - $last_field_id++; +sub search { + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + Bugzilla->switch_to_shadow_db(); + + my $match_params = dclone($params); + delete $match_params->{include_fields}; + delete $match_params->{exclude_fields}; + + # Determine whether this is a quicksearch query + if (exists $match_params->{quicksearch}) { + my $quicksearch = quicksearch($match_params->{'quicksearch'}); + my $cgi = Bugzilla::CGI->new($quicksearch); + $match_params = $cgi->Vars; + } + + if (defined($match_params->{offset}) and !defined($match_params->{limit})) { + ThrowCodeError('param_required', + {param => 'limit', function => 'Bug.search()'}); + } + + my $max_results = Bugzilla->params->{max_search_results}; + unless (defined $match_params->{limit} && $match_params->{limit} == 0) { + if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { + $match_params->{limit} = $max_results; } - if (my $creation_when = delete $match_params->{'creation_ts'}) { - $match_params->{"f${last_field_id}"} = 'creation_ts'; - $match_params->{"o${last_field_id}"} = 'greaterthaneq'; - $match_params->{"v${last_field_id}"} = $creation_when; - $last_field_id++; + } + else { + delete $match_params->{limit}; + delete $match_params->{offset}; + } + + $match_params = Bugzilla::Bug::map_fields($match_params); + + my %options = (fields => ['bug_id']); + + # Find the highest custom field id + my @field_ids = grep(/^f(\d+)$/, keys %$match_params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + + # Do special search types for certain fields. + if (my $change_when = delete $match_params->{'delta_ts'}) { + $match_params->{"f${last_field_id}"} = 'delta_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $change_when; + $last_field_id++; + } + if (my $creation_when = delete $match_params->{'creation_ts'}) { + $match_params->{"f${last_field_id}"} = 'creation_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $creation_when; + $last_field_id++; + } + + # Some fields require a search type such as short desc, keywords, etc. + foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { + if (defined $match_params->{$param} + && !defined $match_params->{$param . '_type'}) + { + $match_params->{$param . '_type'} = 'allwordssubstr'; } - - # Some fields require a search type such as short desc, keywords, etc. - foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { - if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) { - $match_params->{$param . '_type'} = 'allwordssubstr'; - } + } + if (defined $match_params->{'keywords'} + && !defined $match_params->{'keywords_type'}) + { + $match_params->{'keywords_type'} = 'allwords'; + } + + # Backwards compatibility with old method regarding role search + $match_params->{'reporter'} = delete $match_params->{'creator'} + if $match_params->{'creator'}; + foreach my $role (qw(assigned_to reporter qa_contact commenter cc)) { + next if !exists $match_params->{$role}; + my $value = delete $match_params->{$role}; + $match_params->{"f${last_field_id}"} = $role; + $match_params->{"o${last_field_id}"} = "anywordssubstr"; + $match_params->{"v${last_field_id}"} + = ref $value ? join(" ", @{$value}) : $value; + $last_field_id++; + } + + # If no other parameters have been passed other than limit and offset + # then we throw error if system is configured to do so. + if ( !grep(!/^(limit|offset)$/, keys %$match_params) + && !Bugzilla->params->{search_allow_no_criteria}) + { + ThrowUserError('buglist_parameters_required'); + } + + # Allow the use of order shortcuts similar to web UI + if ($match_params->{order}) { + + # Convert the value of the "order" form field into a list of columns + # by which to sort the results. + my %order_types = ( + "Bug Number" => ["bug_id"], + "Importance" => ["priority", "bug_severity"], + "Assignee" => ["assigned_to", "bug_status", "priority", "bug_id"], + "Last Changed" => + ["changeddate", "bug_status", "priority", "assigned_to", "bug_id"], + ); + if ($order_types{$match_params->{order}}) { + $options{order} = $order_types{$match_params->{order}}; } - if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) { - $match_params->{'keywords_type'} = 'allwords'; + else { + $options{order} = [split(/\s*,\s*/, $match_params->{order})]; } + } - # Backwards compatibility with old method regarding role search - $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'}; - foreach my $role (qw(assigned_to reporter qa_contact commenter cc)) { - next if !exists $match_params->{$role}; - my $value = delete $match_params->{$role}; - $match_params->{"f${last_field_id}"} = $role; - $match_params->{"o${last_field_id}"} = "anywordssubstr"; - $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value; - $last_field_id++; - } + $options{params} = $match_params; - # If no other parameters have been passed other than limit and offset - # then we throw error if system is configured to do so. - if (!grep(!/^(limit|offset)$/, keys %$match_params) - && !Bugzilla->params->{search_allow_no_criteria}) - { - ThrowUserError('buglist_parameters_required'); - } + my $search = new Bugzilla::Search(%options); + my ($data) = $search->data; - # Allow the use of order shortcuts similar to web UI - if ($match_params->{order}) { - # Convert the value of the "order" form field into a list of columns - # by which to sort the results. - my %order_types = ( - "Bug Number" => [ "bug_id" ], - "Importance" => [ "priority", "bug_severity" ], - "Assignee" => [ "assigned_to", "bug_status", "priority", "bug_id" ], - "Last Changed" => [ "changeddate", "bug_status", "priority", - "assigned_to", "bug_id" ], - ); - if ($order_types{$match_params->{order}}) { - $options{order} = $order_types{$match_params->{order}}; - } - else { - $options{order} = [ split(/\s*,\s*/, $match_params->{order}) ]; - } + # BMO if the caller only wants the count, that's all we need to return + if ($params->{count_only}) { + if (Bugzilla->usage_mode == USAGE_MODE_XMLRPC) { + return $data; } - - $options{params} = $match_params; - - my $search = new Bugzilla::Search(%options); - my ($data) = $search->data; - - # BMO if the caller only wants the count, that's all we need to return - if ($params->{count_only}) { - if (Bugzilla->usage_mode == USAGE_MODE_XMLRPC) { - return $data; - } - else { - return { bug_count => $data }; - } + else { + return {bug_count => $data}; } + } - if (!scalar @$data) { - return { bugs => [] }; - } + if (!scalar @$data) { + return {bugs => []}; + } - # Search.pm won't return bugs that the user shouldn't see so no filtering is needed. - my @bug_ids = map { $_->[0] } @$data; - my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) }; - my @bugs = map { $bug_objects{$_} } @bug_ids; - @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; +# Search.pm won't return bugs that the user shouldn't see so no filtering is needed. + my @bug_ids = map { $_->[0] } @$data; + my %bug_objects + = map { $_->id => $_ } @{Bugzilla::Bug->new_from_list(\@bug_ids)}; + my @bugs = map { $bug_objects{$_} } @bug_ids; + @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; - # BzAPI - Bugzilla->request_cache->{bzapi_search_bugs} = [ map { $bug_objects{$_} } @bug_ids ]; + # BzAPI + Bugzilla->request_cache->{bzapi_search_bugs} + = [map { $bug_objects{$_} } @bug_ids]; - return { bugs => \@bugs }; + return {bugs => \@bugs}; } sub possible_duplicates { - my ($self, $params) = validate(@_, 'product'); - my $user = Bugzilla->user; - - Bugzilla->switch_to_shadow_db(); - - state $params_type = Dict [ - id => Optional [Int], - product => Optional [ ArrayRef [Str] ], - limit => Optional [Int], - summary => Optional [Str], - include_fields => Optional [ ArrayRef [Str] ], - Bugzilla_api_token => Optional [Str] - ]; - - ThrowCodeError( 'param_invalid', { function => 'Bug.possible_duplicates', param => 'A param' } ) - if !$params_type->check($params); - - my $summary; - if ($params->{id}) { - my $bug = Bugzilla::Bug->check({ id => $params->{id}, cache => 1 }); - $summary = $bug->short_desc; - } - elsif ($params->{summary}) { - $summary = $params->{summary}; - } - else { - ThrowCodeError('param_required', - { function => 'Bug.possible_duplicates', param => 'id or summary' }); - } - - my @products; - foreach my $name (@{ $params->{'product'} || [] }) { - my $object = $user->can_enter_product($name, THROW_ERROR); - push(@products, $object); - } - - my $possible_dupes = Bugzilla::Bug->possible_duplicates( - { - summary => $summary, - products => \@products, - limit => $params->{limit} - } - ); + my ($self, $params) = validate(@_, 'product'); + my $user = Bugzilla->user; + + Bugzilla->switch_to_shadow_db(); + + state $params_type = Dict [ + id => Optional [Int], + product => Optional [ArrayRef [Str]], + limit => Optional [Int], + summary => Optional [Str], + include_fields => Optional [ArrayRef [Str]], + Bugzilla_api_token => Optional [Str] + ]; + + ThrowCodeError('param_invalid', + {function => 'Bug.possible_duplicates', param => 'A param'}) + if !$params_type->check($params); + + my $summary; + if ($params->{id}) { + my $bug = Bugzilla::Bug->check({id => $params->{id}, cache => 1}); + $summary = $bug->short_desc; + } + elsif ($params->{summary}) { + $summary = $params->{summary}; + } + else { + ThrowCodeError('param_required', + {function => 'Bug.possible_duplicates', param => 'id or summary'}); + } + + my @products; + foreach my $name (@{$params->{'product'} || []}) { + my $object = $user->can_enter_product($name, THROW_ERROR); + push(@products, $object); + } + + my $possible_dupes + = Bugzilla::Bug->possible_duplicates({ + summary => $summary, products => \@products, limit => $params->{limit} + }); - # If a bug id was used, remove the bug with the same id from the list. - if ($params->{id}) { - @$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes; - } + # If a bug id was used, remove the bug with the same id from the list. + if ($params->{id}) { + @$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes; + } - my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; - $self->_add_update_tokens($params, $possible_dupes, \@hashes); - return { bugs => \@hashes }; + my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; + $self->_add_update_tokens($params, $possible_dupes, \@hashes); + return {bugs => \@hashes}; } sub update { - my ($self, $params) = validate(@_, 'ids'); + my ($self, $params) = validate(@_, 'ids'); - # BMO: Don't allow updating of bugs if disabled - if (Bugzilla->params->{disable_bug_updates}) { - ThrowErrorPage('bug/process/updates-disabled.html.tmpl', - 'Bug updates are currently disabled.'); - } + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage( + 'bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.' + ); + } - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; - # We skip certain fields because their set_ methods actually use - # the external names instead of the internal names. - $params = Bugzilla::Bug::map_fields($params, - { summary => 1, platform => 1, severity => 1, url => 1 }); + # We skip certain fields because their set_ methods actually use + # the external names instead of the internal names. + $params = Bugzilla::Bug::map_fields($params, + {summary => 1, platform => 1, severity => 1, url => 1}); - my $ids = delete $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); - my @bugs = map { Bugzilla::Bug->check($_) } @$ids; + my @bugs = map { Bugzilla::Bug->check($_) } @$ids; - my %values = %$params; - $values{other_bugs} = \@bugs; + my %values = %$params; + $values{other_bugs} = \@bugs; - if (exists $values{comment} and exists $values{comment}{comment}) { - $values{comment}{body} = delete $values{comment}{comment}; - } + if (exists $values{comment} and exists $values{comment}{comment}) { + $values{comment}{body} = delete $values{comment}{comment}; + } - # Prevent bugs that could be triggered by specifying fields that - # have valid "set_" functions in Bugzilla::Bug, but shouldn't be - # called using those field names. - delete $values{dependencies}; + # Prevent bugs that could be triggered by specifying fields that + # have valid "set_" functions in Bugzilla::Bug, but shouldn't be + # called using those field names. + delete $values{dependencies}; - my $flags = delete $values{flags}; + my $flags = delete $values{flags}; - foreach my $bug (@bugs) { - if (!$user->can_edit_product($bug->product_obj->id) ) { - ThrowUserError("product_edit_denied", - { product => $bug->product }); - } - - $bug->set_all(\%values); - if ($flags) { - my ($old_flags, $new_flags) = extract_flags($flags, $bug); - $bug->set_flags($old_flags, $new_flags); - } + foreach my $bug (@bugs) { + if (!$user->can_edit_product($bug->product_obj->id)) { + ThrowUserError("product_edit_denied", {product => $bug->product}); } - my %all_changes; - $dbh->bz_start_transaction(); - foreach my $bug (@bugs) { - $all_changes{$bug->id} = $bug->update(); + $bug->set_all(\%values); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($old_flags, $new_flags); } - $dbh->bz_commit_transaction(); + } + + my %all_changes; + $dbh->bz_start_transaction(); + foreach my $bug (@bugs) { + $all_changes{$bug->id} = $bug->update(); + } + $dbh->bz_commit_transaction(); + + foreach my $bug (@bugs) { + $bug->send_changes($all_changes{$bug->id}); + } + + my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()}; + + # This doesn't normally belong in FIELD_MAP, but we do want to translate + # "bug_group" back into "groups". + $api_name{'bug_group'} = 'groups'; + + my @result; + foreach my $bug (@bugs) { + my %hash = ( + id => $self->type('int', $bug->id), + last_change_time => $self->type('dateTime', $bug->delta_ts), + changes => {}, + ); - foreach my $bug (@bugs) { - $bug->send_changes($all_changes{$bug->id}); + # alias is returned in case users pass a mixture of ids and aliases, + # so that they can know which set of changes relates to which value + # they passed. + if (Bugzilla->params->{'usebugaliases'}) { + $hash{alias} = $self->type('string', $bug->alias); + } + else { + # For API reasons, we always want the alias field to appear, we + # just don't want it to have a value if aliases are turned off. + $hash{alias} = $self->type('string', ''); } - my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - # This doesn't normally belong in FIELD_MAP, but we do want to translate - # "bug_group" back into "groups". - $api_name{'bug_group'} = 'groups'; - - my @result; - foreach my $bug (@bugs) { - my %hash = ( - id => $self->type('int', $bug->id), - last_change_time => $self->type('dateTime', $bug->delta_ts), - changes => {}, - ); - - # alias is returned in case users pass a mixture of ids and aliases, - # so that they can know which set of changes relates to which value - # they passed. - if (Bugzilla->params->{'usebugaliases'}) { - $hash{alias} = $self->type('string', $bug->alias); - } - else { - # For API reasons, we always want the alias field to appear, we - # just don't want it to have a value if aliases are turned off. - $hash{alias} = $self->type('string', ''); - } - - my %changes = %{ $all_changes{$bug->id} }; - foreach my $field (keys %changes) { - my $change = $changes{$field}; - my $api_field = $api_name{$field} || $field; - # We normalize undef to an empty string, so that the API - # stays consistent for things like Deadline that can become - # empty. - $change->[0] = '' if !defined $change->[0]; - $change->[1] = '' if !defined $change->[1]; - $hash{changes}->{$api_field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - - push(@result, \%hash); + my %changes = %{$all_changes{$bug->id}}; + foreach my $field (keys %changes) { + my $change = $changes{$field}; + my $api_field = $api_name{$field} || $field; + + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; + $hash{changes}->{$api_field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - return { bugs => \@result }; + push(@result, \%hash); + } + + return {bugs => \@result}; } sub create { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - # BMO: Don't allow updating of bugs if disabled - if (Bugzilla->params->{disable_bug_updates}) { - ThrowErrorPage('bug/process/updates-disabled.html.tmpl', - 'Bug updates are currently disabled.'); - } + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage( + 'bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.' + ); + } - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); - # Some fields cannot be sent to Bugzilla::Bug->create - foreach my $key (qw(login password token)) { - delete $params->{$key}; - } + # Some fields cannot be sent to Bugzilla::Bug->create + foreach my $key (qw(login password token)) { + delete $params->{$key}; + } - $params = Bugzilla::Bug::map_fields($params); + $params = Bugzilla::Bug::map_fields($params); - my $flags = delete $params->{flags}; + my $flags = delete $params->{flags}; - # We start a nested transaction in case flag setting fails - # we want the bug creation to roll back as well. - $dbh->bz_start_transaction(); + # We start a nested transaction in case flag setting fails + # we want the bug creation to roll back as well. + $dbh->bz_start_transaction(); - my $bug = Bugzilla::Bug->create($params); + my $bug = Bugzilla::Bug->create($params); - # Set bug flags - if ($flags) { - my ($flags, $new_flags) = extract_flags($flags, $bug); - $bug->set_flags($flags, $new_flags); - $bug->update($bug->creation_ts); - } + # Set bug flags + if ($flags) { + my ($flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($flags, $new_flags); + $bug->update($bug->creation_ts); + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - $bug->send_changes(); + $bug->send_changes(); - return { id => $self->type('int', $bug->bug_id) }; + return {id => $self->type('int', $bug->bug_id)}; } sub legal_values { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - defined $params->{field} - or ThrowCodeError('param_required', { param => 'field' }); + defined $params->{field} + or ThrowCodeError('param_required', {param => 'field'}); - my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} - || $params->{field}; + my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field}; - my @global_selects = - @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) }; + my @global_selects = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})}; - my $values; - if (grep($_->name eq $field, @global_selects)) { - # The field is a valid one. - trick_taint($field); - $values = get_legal_field_values($field); - } - elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { - my $id = $params->{product_id}; - defined $id || ThrowCodeError('param_required', - { function => 'Bug.legal_values', param => 'product_id' }); - grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) - || ThrowUserError('product_access_denied', { id => $id }); - - my $product = new Bugzilla::Product($id); - my @objects; - if ($field eq 'version') { - @objects = @{$product->versions}; - } - elsif ($field eq 'target_milestone') { - @objects = @{$product->milestones}; - } - elsif ($field eq 'component') { - @objects = @{$product->components}; - } + my $values; + if (grep($_->name eq $field, @global_selects)) { - $values = [map { $_->name } @objects]; + # The field is a valid one. + trick_taint($field); + $values = get_legal_field_values($field); + } + elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { + my $id = $params->{product_id}; + defined $id + || ThrowCodeError('param_required', + {function => 'Bug.legal_values', param => 'product_id'}); + grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) + || ThrowUserError('product_access_denied', {id => $id}); + + my $product = new Bugzilla::Product($id); + my @objects; + if ($field eq 'version') { + @objects = @{$product->versions}; } - else { - ThrowCodeError('invalid_field_name', { field => $params->{field} }); + elsif ($field eq 'target_milestone') { + @objects = @{$product->milestones}; } - - my @result; - foreach my $val (@$values) { - push(@result, $self->type('string', $val)); + elsif ($field eq 'component') { + @objects = @{$product->components}; } - return { values => \@result }; + $values = [map { $_->name } @objects]; + } + else { + ThrowCodeError('invalid_field_name', {field => $params->{field}}); + } + + my @result; + foreach my $val (@$values) { + push(@result, $self->type('string', $val)); + } + + return {values => \@result}; } sub add_attachment { - my ($self, $params) = validate(@_, 'ids'); - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'ids'); + my $dbh = Bugzilla->dbh; + + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage( + 'bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.' + ); + } - # BMO: Don't allow updating of bugs if disabled - if (Bugzilla->params->{disable_bug_updates}) { - ThrowErrorPage('bug/process/updates-disabled.html.tmpl', - 'Bug updates are currently disabled.'); - } + Bugzilla->login(LOGIN_REQUIRED); + defined $params->{ids} || ThrowCodeError('param_required', {param => 'ids'}); + defined $params->{data} || ThrowCodeError('param_required', {param => 'data'}); - Bugzilla->login(LOGIN_REQUIRED); - defined $params->{ids} - || ThrowCodeError('param_required', { param => 'ids' }); - defined $params->{data} - || ThrowCodeError('param_required', { param => 'data' }); + my @bugs = map { Bugzilla::Bug->check($_) } @{$params->{ids}}; + foreach my $bug (@bugs) { + Bugzilla->user->can_edit_product($bug->product_id) + || ThrowUserError("product_edit_denied", {product => $bug->product}); + } + + my @created; + $dbh->bz_start_transaction(); + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my $flags = delete $params->{flags}; + + foreach my $bug (@bugs) { + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $params->{data}, + description => $params->{summary}, + filename => $params->{file_name}, + mimetype => $params->{content_type}, + ispatch => $params->{is_patch}, + isprivate => $params->{is_private}, + }); - my @bugs = map { Bugzilla::Bug->check($_) } @{ $params->{ids} }; - foreach my $bug (@bugs) { - Bugzilla->user->can_edit_product($bug->product_id) - || ThrowUserError("product_edit_denied", {product => $bug->product}); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment); + $attachment->set_flags($old_flags, $new_flags); } - my @created; - $dbh->bz_start_transaction(); - my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - my $flags = delete $params->{flags}; - - foreach my $bug (@bugs) { - my $attachment = Bugzilla::Attachment->create({ - bug => $bug, - creation_ts => $timestamp, - data => $params->{data}, - description => $params->{summary}, - filename => $params->{file_name}, - mimetype => $params->{content_type}, - ispatch => $params->{is_patch}, - isprivate => $params->{is_private}, - }); - - if ($flags) { - my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment); - $attachment->set_flags($old_flags, $new_flags); - } - - $attachment->update($timestamp); - my $comment = $params->{comment} || ''; - $attachment->bug->add_comment($comment, - { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - push(@created, $attachment); - } - $_->bug->update($timestamp) foreach @created; - $dbh->bz_commit_transaction(); + $attachment->update($timestamp); + my $comment = $params->{comment} || ''; + $attachment->bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_CREATED, + extra_data => $attachment->id + } + ); + push(@created, $attachment); + } + $_->bug->update($timestamp) foreach @created; + $dbh->bz_commit_transaction(); - $_->send_changes() foreach @bugs; + $_->send_changes() foreach @bugs; - my %attachments = map { $_->id => $self->_attachment_to_hash($_, $params) } - @created; + my %attachments + = map { $_->id => $self->_attachment_to_hash($_, $params) } @created; - return { attachments => \%attachments }; + return {attachments => \%attachments}; } sub update_attachment { - my ($self, $params) = validate(@_, 'ids'); - - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; - - my $ids = delete $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - # Some fields cannot be sent to set_all - foreach my $key (qw(login password token)) { - delete $params->{$key}; - } - - $params = translate($params, ATTACHMENT_MAPPED_SETTERS); - - # Get all the attachments, after verifying that they exist and are editable - my @attachments = (); - my %bugs = (); - foreach my $id (@$ids) { - my $attachment = Bugzilla::Attachment->new($id) - || ThrowUserError("invalid_attach_id", { attach_id => $id }); - my $bug = $attachment->bug; - $attachment->_check_bug; - - push @attachments, $attachment; - $bugs{$bug->id} = $bug; + my ($self, $params) = validate(@_, 'ids'); + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + # Some fields cannot be sent to set_all + foreach my $key (qw(login password token)) { + delete $params->{$key}; + } + + $params = translate($params, ATTACHMENT_MAPPED_SETTERS); + + # Get all the attachments, after verifying that they exist and are editable + my @attachments = (); + my %bugs = (); + foreach my $id (@$ids) { + my $attachment = Bugzilla::Attachment->new($id) + || ThrowUserError("invalid_attach_id", {attach_id => $id}); + my $bug = $attachment->bug; + $attachment->_check_bug; + + push @attachments, $attachment; + $bugs{$bug->id} = $bug; + } + + my $flags = delete $params->{flags}; + my $comment = delete $params->{comment}; + + # Update the values + foreach my $attachment (@attachments) { + my ($update_flags, $new_flags) + = $flags ? extract_flags($flags, $attachment->bug, $attachment) : ([], []); + if ($attachment->validate_can_edit) { + $attachment->set_all($params); + $attachment->set_flags($update_flags, $new_flags) if $flags; } - - my $flags = delete $params->{flags}; - my $comment = delete $params->{comment}; - - # Update the values - foreach my $attachment (@attachments) { - my ($update_flags, $new_flags) = $flags - ? extract_flags($flags, $attachment->bug, $attachment) - : ([], []); - if ($attachment->validate_can_edit) { - $attachment->set_all($params); - $attachment->set_flags($update_flags, $new_flags) if $flags; - } - elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) { - # Requestees can set flags targetted to them, even if they cannot - # edit the attachment. Flag setters can edit their own flags too. - my %flag_list = map { $_->{id} => $_ } @$update_flags; - my $flag_objs = Bugzilla::Flag->new_from_list([ keys %flag_list ]); - my @editable_flags; - foreach my $flag_obj (@$flag_objs) { - if ($flag_obj->setter_id == $user->id - || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) - { - push(@editable_flags, $flag_list{$flag_obj->id}); - } - } - if (!scalar @editable_flags) { - ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id }); - } - $attachment->set_flags(\@editable_flags, []); - } - else { - ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id }); + elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) { + + # Requestees can set flags targetted to them, even if they cannot + # edit the attachment. Flag setters can edit their own flags too. + my %flag_list = map { $_->{id} => $_ } @$update_flags; + my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]); + my @editable_flags; + foreach my $flag_obj (@$flag_objs) { + if ($flag_obj->setter_id == $user->id + || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) + { + push(@editable_flags, $flag_list{$flag_obj->id}); } + } + if (!scalar @editable_flags) { + ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id}); + } + $attachment->set_flags(\@editable_flags, []); } + else { + ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id}); + } + } - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Do the actual update and get information to return to user - my @result; - foreach my $attachment (@attachments) { - my $changes = $attachment->update(); - - if ($comment = trim($comment)) { - $attachment->bug->add_comment($comment, - { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_UPDATED, - extra_data => $attachment->id }); - } + # Do the actual update and get information to return to user + my @result; + foreach my $attachment (@attachments) { + my $changes = $attachment->update(); - $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); + if ($comment = trim($comment)) { + $attachment->bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_UPDATED, + extra_data => $attachment->id + } + ); + } - my %hash = ( - id => $self->type('int', $attachment->id), - last_change_time => $self->type('dateTime', $attachment->modification_time), - changes => {}, - ); + $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; + my %hash = ( + id => $self->type('int', $attachment->id), + last_change_time => $self->type('dateTime', $attachment->modification_time), + changes => {}, + ); - # We normalize undef to an empty string, so that the API - # stays consistent for things like Deadline that can become - # empty. - $hash{changes}->{$field} = { - removed => $self->type('string', $change->[0] // ''), - added => $self->type('string', $change->[1] // '') - }; - } + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; - push(@result, \%hash); + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $hash{changes}->{$field} = { + removed => $self->type('string', $change->[0] // ''), + added => $self->type('string', $change->[1] // '') + }; } - $dbh->bz_commit_transaction(); + push(@result, \%hash); + } - # Email users about the change - foreach my $bug (values %bugs) { - $bug->update(); - $bug->send_changes(); - } + $dbh->bz_commit_transaction(); - # Return the information to the user - return { attachments => \@result }; + # Email users about the change + foreach my $bug (values %bugs) { + $bug->update(); + $bug->send_changes(); + } + + # Return the information to the user + return {attachments => \@result}; } sub add_comment { - my ($self, $params) = @_; + my ($self, $params) = @_; - # BMO: Don't allow updating of bugs if disabled - if (Bugzilla->params->{disable_bug_updates}) { - ThrowErrorPage('bug/process/updates-disabled.html.tmpl', - 'Bug updates are currently disabled.'); - } + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage( + 'bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.' + ); + } - #The user must login in order add a comment - Bugzilla->login(LOGIN_REQUIRED); + #The user must login in order add a comment + Bugzilla->login(LOGIN_REQUIRED); - # Check parameters - defined $params->{id} - || ThrowCodeError('param_required', { param => 'id' }); - my $comment = $params->{comment}; - (defined $comment && trim($comment) ne '') - || ThrowCodeError('param_required', { param => 'comment' }); + # Check parameters + defined $params->{id} || ThrowCodeError('param_required', {param => 'id'}); + my $comment = $params->{comment}; + (defined $comment && trim($comment) ne '') + || ThrowCodeError('param_required', {param => 'comment'}); - my $bug = Bugzilla::Bug->check($params->{id}); + my $bug = Bugzilla::Bug->check($params->{id}); - Bugzilla->user->can_edit_product($bug->product_id) - || ThrowUserError("product_edit_denied", {product => $bug->product}); + Bugzilla->user->can_edit_product($bug->product_id) + || ThrowUserError("product_edit_denied", {product => $bug->product}); - # Backwards-compatibility for versions before 3.6 - if (defined $params->{private}) { - $params->{is_private} = delete $params->{private}; - } - # Append comment - $bug->add_comment($comment, { isprivate => $params->{is_private}, - work_time => $params->{work_time} }); + # Backwards-compatibility for versions before 3.6 + if (defined $params->{private}) { + $params->{is_private} = delete $params->{private}; + } - # Add comment tags - $bug->set_all({ comment_tags => $params->{comment_tags} }) - if defined $params->{comment_tags}; + # Append comment + $bug->add_comment($comment, + {isprivate => $params->{is_private}, work_time => $params->{work_time}}); - # Capture the call to bug->update (which creates the new comment) in - # a transaction so we're sure to get the correct comment_id. + # Add comment tags + $bug->set_all({comment_tags => $params->{comment_tags}}) + if defined $params->{comment_tags}; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + # Capture the call to bug->update (which creates the new comment) in + # a transaction so we're sure to get the correct comment_id. - $bug->update(); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); - my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id'); + $bug->update(); - $dbh->bz_commit_transaction(); + my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id'); - # Send mail. - Bugzilla::BugMail::Send($bug->bug_id, { changer => Bugzilla->user }); + $dbh->bz_commit_transaction(); - return { id => $self->type('int', $new_comment_id) }; + # Send mail. + Bugzilla::BugMail::Send($bug->bug_id, {changer => Bugzilla->user}); + + return {id => $self->type('int', $new_comment_id)}; } sub update_see_also { - my ($self, $params) = @_; + my ($self, $params) = @_; - # BMO: Don't allow updating of bugs if disabled - if (Bugzilla->params->{disable_bug_updates}) { - ThrowErrorPage('bug/process/updates-disabled.html.tmpl', - 'Bug updates are currently disabled.'); + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage( + 'bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.' + ); + } + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Check parameters + $params->{ids} || ThrowCodeError('param_required', {param => 'id'}); + my ($add, $remove) = @$params{qw(add remove)}; + ($add || $remove) + or ThrowCodeError('params_required', {params => ['add', 'remove']}); + + my @bugs; + foreach my $id (@{$params->{ids}}) { + my $bug = Bugzilla::Bug->check($id); + $user->can_edit_product($bug->product_id) + || ThrowUserError("product_edit_denied", {product => $bug->product}); + push(@bugs, $bug); + if ($remove) { + $bug->remove_see_also($_) foreach @$remove; } - - my $user = Bugzilla->login(LOGIN_REQUIRED); - - # Check parameters - $params->{ids} - || ThrowCodeError('param_required', { param => 'id' }); - my ($add, $remove) = @$params{qw(add remove)}; - ($add || $remove) - or ThrowCodeError('params_required', { params => ['add', 'remove'] }); - - my @bugs; - foreach my $id (@{ $params->{ids} }) { - my $bug = Bugzilla::Bug->check($id); - $user->can_edit_product($bug->product_id) - || ThrowUserError("product_edit_denied", - { product => $bug->product }); - push(@bugs, $bug); - if ($remove) { - $bug->remove_see_also($_) foreach @$remove; - } - if ($add) { - $bug->add_see_also($_) foreach @$add; - } + if ($add) { + $bug->add_see_also($_) foreach @$add; } - - my %changes; - foreach my $bug (@bugs) { - my $change = $bug->update(); - if (my $see_also = $change->{see_also}) { - $changes{$bug->id}->{see_also} = { - removed => [split(', ', $see_also->[0])], - added => [split(', ', $see_also->[1])], - }; - } - else { - # We still want a changes entry, for API consistency. - $changes{$bug->id}->{see_also} = { added => [], removed => [] }; - } - - Bugzilla::BugMail::Send($bug->id, { changer => $user }); + } + + my %changes; + foreach my $bug (@bugs) { + my $change = $bug->update(); + if (my $see_also = $change->{see_also}) { + $changes{$bug->id}->{see_also} = { + removed => [split(', ', $see_also->[0])], + added => [split(', ', $see_also->[1])], + }; } + else { + # We still want a changes entry, for API consistency. + $changes{$bug->id}->{see_also} = {added => [], removed => []}; + } + + Bugzilla::BugMail::Send($bug->id, {changer => $user}); + } - return { changes => \%changes }; + return {changes => \%changes}; } sub attachments { - my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); + my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); - Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - if (!(defined $params->{ids} - or defined $params->{attachment_ids})) - { - ThrowCodeError('param_required', - { function => 'Bug.attachments', - params => ['ids', 'attachment_ids'] }); - } + if (!(defined $params->{ids} or defined $params->{attachment_ids})) { + ThrowCodeError('param_required', + {function => 'Bug.attachments', params => ['ids', 'attachment_ids']}); + } - my $ids = $params->{ids} || []; - my $attach_ids = $params->{attachment_ids} || []; + my $ids = $params->{ids} || []; + my $attach_ids = $params->{attachment_ids} || []; - my %bugs; - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check($bug_id); - $bugs{$bug->id} = []; - foreach my $attach (@{$bug->attachments}) { - push @{$bugs{$bug->id}}, - $self->_attachment_to_hash($attach, $params); - } + my %bugs; + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check($bug_id); + $bugs{$bug->id} = []; + foreach my $attach (@{$bug->attachments}) { + push @{$bugs{$bug->id}}, $self->_attachment_to_hash($attach, $params); } - - my %attachments; - my @log_attachments; - foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) { - Bugzilla::Bug->check($attach->bug_id); - if ($attach->isprivate && !Bugzilla->user->is_insider) { - ThrowUserError('auth_failure', {action => 'access', - object => 'attachment', - attach_id => $attach->id}); - } - push @log_attachments, $attach; - - $attachments{$attach->id} = - $self->_attachment_to_hash($attach, $params); + } + + my %attachments; + my @log_attachments; + foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) { + Bugzilla::Bug->check($attach->bug_id); + if ($attach->isprivate && !Bugzilla->user->is_insider) { + ThrowUserError('auth_failure', + {action => 'access', object => 'attachment', attach_id => $attach->id}); } + push @log_attachments, $attach; - if (Bugzilla->user->id) { - foreach my $attachment (@log_attachments) { - Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get"); - } + $attachments{$attach->id} = $self->_attachment_to_hash($attach, $params); + } + + if (Bugzilla->user->id) { + foreach my $attachment (@log_attachments) { + Bugzilla->log_user_request($attachment->bug_id, $attachment->id, + "attachment-get"); } + } - return { bugs => \%bugs, attachments => \%attachments }; + return {bugs => \%bugs, attachments => \%attachments}; } sub flag_types { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - defined $params->{product} - || ThrowCodeError('param_required', - { function => 'Bug.flag_types', - param => 'product' }); - - my $product = delete $params->{product}; - my $component = delete $params->{component}; - - $product = Bugzilla::Product->check({ name => $product, cache => 1 }); - $component = Bugzilla::Component->check( - { name => $component, product => $product, cache => 1 }) if $component; - - my $flag_params = { product_id => $product->id }; - $flag_params->{component_id} = $component->id if $component; - my $matched_flag_types = Bugzilla::FlagType::match($flag_params); - - my $flag_types = { bug => [], attachment => [] }; - foreach my $flag_type (@$matched_flag_types) { - push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'bug'; - push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'attachment'; - } - - return $flag_types; + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + defined $params->{product} + || ThrowCodeError('param_required', + {function => 'Bug.flag_types', param => 'product'}); + + my $product = delete $params->{product}; + my $component = delete $params->{component}; + + $product = Bugzilla::Product->check({name => $product, cache => 1}); + $component + = Bugzilla::Component->check( + {name => $component, product => $product, cache => 1}) + if $component; + + my $flag_params = {product_id => $product->id}; + $flag_params->{component_id} = $component->id if $component; + my $matched_flag_types = Bugzilla::FlagType::match($flag_params); + + my $flag_types = {bug => [], attachment => []}; + foreach my $flag_type (@$matched_flag_types) { + push(@{$flag_types->{bug}}, $self->_flagtype_to_hash($flag_type, $product)) + if $flag_type->target_type eq 'bug'; + push( + @{$flag_types->{attachment}}, + $self->_flagtype_to_hash($flag_type, $product) + ) if $flag_type->target_type eq 'attachment'; + } + + return $flag_types; } sub update_comment_tags { - my ($self, $params) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->params->{'comment_taggers_group'} - || ThrowUserError("comment_tag_disabled"); - $user->can_tag_comments - || ThrowUserError("auth_failure", - { group => Bugzilla->params->{'comment_taggers_group'}, - action => "update", - object => "comment_tags" }); - - my $comment_id = $params->{comment_id} - // ThrowCodeError('param_required', - { function => 'Bug.update_comment_tags', - param => 'comment_id' }); - - my $comment = Bugzilla::Comment->new($comment_id) - || return []; - $comment->bug->check_is_visible(); - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment_id }); - } + my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - foreach my $tag (@{ $params->{add} || [] }) { - $comment->add_tag($tag) if defined $tag; - } - foreach my $tag (@{ $params->{remove} || [] }) { - $comment->remove_tag($tag) if defined $tag; + my $user = Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + $user->can_tag_comments || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{'comment_taggers_group'}, + action => "update", + object => "comment_tags" } - $comment->update(); - $dbh->bz_commit_transaction(); - - return $comment->tags; + ); + + my $comment_id = $params->{comment_id} // ThrowCodeError('param_required', + {function => 'Bug.update_comment_tags', param => 'comment_id'}); + + my $comment = Bugzilla::Comment->new($comment_id) || return []; + $comment->bug->check_is_visible(); + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment_id}); + } + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + foreach my $tag (@{$params->{add} || []}) { + $comment->add_tag($tag) if defined $tag; + } + foreach my $tag (@{$params->{remove} || []}) { + $comment->remove_tag($tag) if defined $tag; + } + $comment->update(); + $dbh->bz_commit_transaction(); + + return $comment->tags; } sub search_comment_tags { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->params->{'comment_taggers_group'} - || ThrowUserError("comment_tag_disabled"); - Bugzilla->user->can_tag_comments - || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'}, - action => "search", - object => "comment_tags"}); - - my $query = $params->{query}; - $query - // ThrowCodeError('param_required', { param => 'query' }); - my $limit = $params->{limit} || 7; - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', { param => 'limit', - function => 'Bug.search_comment_tags' }); - - - my $tags = Bugzilla::Comment::TagWeights->match({ - WHERE => { - 'tag LIKE ?' => "\%$query\%", - }, - LIMIT => $limit, + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + Bugzilla->user->can_tag_comments || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{'comment_taggers_group'}, + action => "search", + object => "comment_tags" + } + ); + + my $query = $params->{query}; + $query // ThrowCodeError('param_required', {param => 'query'}); + my $limit = $params->{limit} || 7; + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {param => 'limit', function => 'Bug.search_comment_tags'}); + + + my $tags + = Bugzilla::Comment::TagWeights->match({ + WHERE => {'tag LIKE ?' => "\%$query\%",}, LIMIT => $limit, }); - return [ map { $_->tag } @$tags ]; + return [map { $_->tag } @$tags]; } ############################## @@ -1350,304 +1373,320 @@ sub search_comment_tags { # return them directly. sub _bug_to_hash { - my ($self, $bug, $params) = @_; - - # All the basic bug attributes are here, in alphabetical order. - # A bug attribute is "basic" if it doesn't require an additional - # database call to get the info. - my %item = %{ filter $params, { - alias => $self->type('string', $bug->alias), - id => $self->type('int', $bug->bug_id), - is_confirmed => $self->type('boolean', $bug->everconfirmed), - op_sys => $self->type('string', $bug->op_sys), - platform => $self->type('string', $bug->rep_platform), - priority => $self->type('string', $bug->priority), - resolution => $self->type('string', $bug->resolution), - severity => $self->type('string', $bug->bug_severity), - status => $self->type('string', $bug->bug_status), - summary => $self->type('string', $bug->short_desc), - target_milestone => $self->type('string', $bug->target_milestone), - url => $self->type('string', $bug->bug_file_loc), - version => $self->type('string', $bug->version), - whiteboard => $self->type('string', $bug->status_whiteboard), - } }; - - state $voting_enabled //= $bug->can('votes') ? 1 : 0; - if ($voting_enabled && filter_wants $params, 'votes') { - $item{votes} = $self->type('int', $bug->votes); - } + my ($self, $bug, $params) = @_; - # First we handle any fields that require extra work (such as date parsing - # or SQL calls). - if (filter_wants $params, 'assigned_to') { - $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login); - $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); - } - if (filter_wants $params, 'blocks') { - my @blocks = map { $self->type('int', $_) } @{ $bug->blocked }; - $item{'blocks'} = \@blocks; - } - if (filter_wants $params, 'classification') { - $item{classification} = $self->type('string', $bug->classification); - } - if (filter_wants $params, 'component') { - $item{component} = $self->type('string', $bug->component); - } - if (filter_wants $params, 'cc') { - my @cc = map { $self->type('email', $_) } @{ $bug->cc || [] }; - $item{'cc'} = \@cc; - $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ]; - } - if (filter_wants $params, 'creation_time') { - $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts); - } - if (filter_wants $params, 'creator') { - $item{'creator'} = $self->type('email', $bug->reporter->login); - $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); - } - if (filter_wants $params, 'depends_on') { - my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson }; - $item{'depends_on'} = \@depends_on; - } - if (filter_wants $params, 'dupe_of') { - $item{'dupe_of'} = $self->type('int', $bug->dup_id); - } - if (filter_wants $params, 'duplicates') { - $item{'duplicates'} = [ map { $self->type('int', $_->id) } @{ $bug->duplicates } ]; - } - if (filter_wants $params, 'groups') { - my @groups = map { $self->type('string', $_->name) } - @{ $bug->groups_in }; - $item{'groups'} = \@groups; - } - if (filter_wants $params, 'is_open') { - $item{'is_open'} = $self->type('boolean', $bug->status->is_open); - } - if (filter_wants $params, 'keywords') { - my @keywords = map { $self->type('string', $_->name) } - @{ $bug->keyword_objects }; - $item{'keywords'} = \@keywords; - } - if (filter_wants $params, 'last_change_time') { - $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts); + # All the basic bug attributes are here, in alphabetical order. + # A bug attribute is "basic" if it doesn't require an additional + # database call to get the info. + my %item = %{filter $params, + { + alias => $self->type('string', $bug->alias), + id => $self->type('int', $bug->bug_id), + is_confirmed => $self->type('boolean', $bug->everconfirmed), + op_sys => $self->type('string', $bug->op_sys), + platform => $self->type('string', $bug->rep_platform), + priority => $self->type('string', $bug->priority), + resolution => $self->type('string', $bug->resolution), + severity => $self->type('string', $bug->bug_severity), + status => $self->type('string', $bug->bug_status), + summary => $self->type('string', $bug->short_desc), + target_milestone => $self->type('string', $bug->target_milestone), + url => $self->type('string', $bug->bug_file_loc), + version => $self->type('string', $bug->version), + whiteboard => $self->type('string', $bug->status_whiteboard), } - if (filter_wants $params, 'product') { - $item{product} = $self->type('string', $bug->product); + }; + + state $voting_enabled //= $bug->can('votes') ? 1 : 0; + if ($voting_enabled && filter_wants $params, 'votes') { + $item{votes} = $self->type('int', $bug->votes); + } + + # First we handle any fields that require extra work (such as date parsing + # or SQL calls). + if (filter_wants $params, 'assigned_to') { + $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login); + $item{'assigned_to_detail'} + = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); + } + if (filter_wants $params, 'blocks') { + my @blocks = map { $self->type('int', $_) } @{$bug->blocked}; + $item{'blocks'} = \@blocks; + } + if (filter_wants $params, 'classification') { + $item{classification} = $self->type('string', $bug->classification); + } + if (filter_wants $params, 'component') { + $item{component} = $self->type('string', $bug->component); + } + if (filter_wants $params, 'cc') { + my @cc = map { $self->type('email', $_) } @{$bug->cc || []}; + $item{'cc'} = \@cc; + $item{'cc_detail'} + = [map { $self->_user_to_hash($_, $params, undef, 'cc') } @{$bug->cc_users}]; + } + if (filter_wants $params, 'creation_time') { + $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts); + } + if (filter_wants $params, 'creator') { + $item{'creator'} = $self->type('email', $bug->reporter->login); + $item{'creator_detail'} + = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); + } + if (filter_wants $params, 'depends_on') { + my @depends_on = map { $self->type('int', $_) } @{$bug->dependson}; + $item{'depends_on'} = \@depends_on; + } + if (filter_wants $params, 'dupe_of') { + $item{'dupe_of'} = $self->type('int', $bug->dup_id); + } + if (filter_wants $params, 'duplicates') { + $item{'duplicates'} = [map { $self->type('int', $_->id) } @{$bug->duplicates}]; + } + if (filter_wants $params, 'groups') { + my @groups = map { $self->type('string', $_->name) } @{$bug->groups_in}; + $item{'groups'} = \@groups; + } + if (filter_wants $params, 'is_open') { + $item{'is_open'} = $self->type('boolean', $bug->status->is_open); + } + if (filter_wants $params, 'keywords') { + my @keywords = map { $self->type('string', $_->name) } @{$bug->keyword_objects}; + $item{'keywords'} = \@keywords; + } + if (filter_wants $params, 'last_change_time') { + $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts); + } + if (filter_wants $params, 'product') { + $item{product} = $self->type('string', $bug->product); + } + if (filter_wants $params, 'qa_contact') { + my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; + $item{'qa_contact'} = $self->type('email', $qa_login); + if ($bug->qa_contact) { + $item{'qa_contact_detail'} + = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); } - if (filter_wants $params, 'qa_contact') { - my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; - $item{'qa_contact'} = $self->type('email', $qa_login); - if ($bug->qa_contact) { - $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); - } + } + if (filter_wants $params, 'see_also') { + my @see_also = map { $self->type('string', $_->name) } @{$bug->see_also}; + $item{'see_also'} = \@see_also; + } + if (filter_wants $params, 'flags') { + $item{'flags'} = [map { $self->_flag_to_hash($_) } @{$bug->flags}]; + } + + # And now custom fields + my @custom_fields = Bugzilla->active_custom_fields({ + product => $bug->product_obj, + component => $bug->component_obj, + bug_id => $bug->id + }); + foreach my $field (@custom_fields) { + my $name = $field->name; + next if !filter_wants($params, $name, ['default', 'custom']); + if ($field->type == FIELD_TYPE_BUG_ID) { + $item{$name} = $self->type('int', $bug->$name); } - if (filter_wants $params, 'see_also') { - my @see_also = map { $self->type('string', $_->name) } - @{ $bug->see_also }; - $item{'see_also'} = \@see_also; + elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { + my $value = $bug->$name; + $item{$name} = defined($value) ? $self->type('dateTime', $value) : undef; } - if (filter_wants $params, 'flags') { - $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + my @values = map { $self->type('string', $_) } @{$bug->$name}; + $item{$name} = \@values; } - - # And now custom fields - my @custom_fields = Bugzilla->active_custom_fields({ - product => $bug->product_obj, component => $bug->component_obj, bug_id => $bug->id }); - foreach my $field (@custom_fields) { - my $name = $field->name; - next if !filter_wants($params, $name, ['default', 'custom']); - if ($field->type == FIELD_TYPE_BUG_ID) { - $item{$name} = $self->type('int', $bug->$name); - } - elsif ($field->type == FIELD_TYPE_DATETIME - || $field->type == FIELD_TYPE_DATE) - { - my $value = $bug->$name; - $item{$name} = defined($value) ? $self->type('dateTime', $value) : undef; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - my @values = map { $self->type('string', $_) } @{ $bug->$name }; - $item{$name} = \@values; - } - else { - $item{$name} = $self->type('string', $bug->$name); - } - } - - # Timetracking fields are only sent if the user can see them. - if (Bugzilla->user->is_timetracker) { - if (filter_wants $params, 'estimated_time') { - $item{'estimated_time'} = $self->type('double', $bug->estimated_time); - } - if (filter_wants $params, 'remaining_time') { - $item{'remaining_time'} = $self->type('double', $bug->remaining_time); - } - if (filter_wants $params, 'deadline') { - # No need to format $bug->deadline specially, because Bugzilla::Bug - # already does it for us. - $item{'deadline'} = $self->type('string', $bug->deadline); - } - if (filter_wants $params, 'actual_time') { - $item{'actual_time'} = $self->type('double', $bug->actual_time); - } + else { + $item{$name} = $self->type('string', $bug->$name); } + } - # The "accessible" bits go here because they have long names and it - # makes the code look nicer to separate them out. - if (filter_wants $params, 'is_cc_accessible') { - $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); + # Timetracking fields are only sent if the user can see them. + if (Bugzilla->user->is_timetracker) { + if (filter_wants $params, 'estimated_time') { + $item{'estimated_time'} = $self->type('double', $bug->estimated_time); } - if (filter_wants $params, 'is_creator_accessible') { - $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible); + if (filter_wants $params, 'remaining_time') { + $item{'remaining_time'} = $self->type('double', $bug->remaining_time); } + if (filter_wants $params, 'deadline') { - # BMO - support for special mentors field - if (filter_wants $params, 'mentors') { - $item{'mentors'} - = [ map { $self->type('email', $_->login) } @{ $bug->mentors || [] } ]; - $item{'mentors_detail'} - = [ map { $self->_user_to_hash($_, $params, undef, 'mentors') } @{ $bug->mentors } ]; + # No need to format $bug->deadline specially, because Bugzilla::Bug + # already does it for us. + $item{'deadline'} = $self->type('string', $bug->deadline); } - - if (filter_wants $params, 'comment_count') { - $item{'comment_count'} = $self->type('int', $bug->comment_count); + if (filter_wants $params, 'actual_time') { + $item{'actual_time'} = $self->type('double', $bug->actual_time); } - - return \%item; + } + + # The "accessible" bits go here because they have long names and it + # makes the code look nicer to separate them out. + if (filter_wants $params, 'is_cc_accessible') { + $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); + } + if (filter_wants $params, 'is_creator_accessible') { + $item{'is_creator_accessible'} + = $self->type('boolean', $bug->reporter_accessible); + } + + # BMO - support for special mentors field + if (filter_wants $params, 'mentors') { + $item{'mentors'} + = [map { $self->type('email', $_->login) } @{$bug->mentors || []}]; + $item{'mentors_detail'} + = [map { $self->_user_to_hash($_, $params, undef, 'mentors') } + @{$bug->mentors}]; + } + + if (filter_wants $params, 'comment_count') { + $item{'comment_count'} = $self->type('int', $bug->comment_count); + } + + return \%item; } sub _user_to_hash { - my ($self, $user, $filters, $types, $prefix) = @_; - my $item = filter $filters, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), - email => $self->type('email', $user->email), - }, $types, $prefix; - return $item; + my ($self, $user, $filters, $types, $prefix) = @_; + my $item = filter $filters, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + email => $self->type('email', $user->email), + }, + $types, $prefix; + return $item; } sub _attachment_to_hash { - my ($self, $attach, $filters, $types, $prefix) = @_; - - my $item = filter $filters, { - creation_time => $self->type('dateTime', $attach->attached), - last_change_time => $self->type('dateTime', $attach->modification_time), - id => $self->type('int', $attach->id), - bug_id => $self->type('int', $attach->bug_id), - file_name => $self->type('string', $attach->filename), - summary => $self->type('string', $attach->description), - description => $self->type('string', $attach->description), - content_type => $self->type('string', $attach->contenttype), - is_private => $self->type('int', $attach->isprivate), - is_obsolete => $self->type('int', $attach->isobsolete), - is_patch => $self->type('int', $attach->ispatch), - }, $types, $prefix; - - # creator/attacher require an extra lookup, so we only send them if - # the filter wants them. - foreach my $field (qw(creator attacher)) { - if (filter_wants $filters, $field, $types, $prefix) { - $item->{$field} = $self->type('email', $attach->attacher->login); - } - } + my ($self, $attach, $filters, $types, $prefix) = @_; - if (filter_wants $filters, 'data', $types, $prefix) { - $item->{'data'} = $self->type('base64', $attach->data); + my $item = filter $filters, + { + creation_time => $self->type('dateTime', $attach->attached), + last_change_time => $self->type('dateTime', $attach->modification_time), + id => $self->type('int', $attach->id), + bug_id => $self->type('int', $attach->bug_id), + file_name => $self->type('string', $attach->filename), + summary => $self->type('string', $attach->description), + description => $self->type('string', $attach->description), + content_type => $self->type('string', $attach->contenttype), + is_private => $self->type('int', $attach->isprivate), + is_obsolete => $self->type('int', $attach->isobsolete), + is_patch => $self->type('int', $attach->ispatch), + }, + $types, $prefix; + + # creator/attacher require an extra lookup, so we only send them if + # the filter wants them. + foreach my $field (qw(creator attacher)) { + if (filter_wants $filters, $field, $types, $prefix) { + $item->{$field} = $self->type('email', $attach->attacher->login); } + } - if (filter_wants $filters, 'size', $types, $prefix) { - $item->{'size'} = $self->type('int', $attach->datasize); - } + if (filter_wants $filters, 'data', $types, $prefix) { + $item->{'data'} = $self->type('base64', $attach->data); + } - if (filter_wants $filters, 'flags', $types, $prefix) { - $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; - } + if (filter_wants $filters, 'size', $types, $prefix) { + $item->{'size'} = $self->type('int', $attach->datasize); + } + + if (filter_wants $filters, 'flags', $types, $prefix) { + $item->{'flags'} = [map { $self->_flag_to_hash($_) } @{$attach->flags}]; + } - return $item; + return $item; } sub _flag_to_hash { - my ($self, $flag) = @_; - - my $item = { - id => $self->type('int', $flag->id), - name => $self->type('string', $flag->name), - type_id => $self->type('int', $flag->type_id), - creation_date => $self->type('dateTime', $flag->creation_date), - modification_date => $self->type('dateTime', $flag->modification_date), - status => $self->type('string', $flag->status) - }; - - foreach my $field (qw(setter requestee)) { - my $field_id = $field . "_id"; - $item->{$field} = $self->type('email', $flag->$field->login) - if $flag->$field_id; - } - - return $item; + my ($self, $flag) = @_; + + my $item = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $item->{$field} = $self->type('email', $flag->$field->login) + if $flag->$field_id; + } + + return $item; } sub _flagtype_to_hash { - my ($self, $flagtype, $product) = @_; - my $user = Bugzilla->user; - - my @values = ('X'); - push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); - push(@values, '+', '-') if $user->can_set_flag($flagtype); - - my $item = { - id => $self->type('int' , $flagtype->id), - name => $self->type('string' , $flagtype->name), - description => $self->type('string' , $flagtype->description), - type => $self->type('string' , $flagtype->target_type), - values => \@values, - is_active => $self->type('boolean', $flagtype->is_active), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) - }; - - if ($product) { - my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); - my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); - # if we have both inclusions and exclusions, the exclusions are redundant - $exclusions = [] if @$inclusions && @$exclusions; - # no need to return anything if there's just "any component" - $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; - $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; - } - - return $item; + my ($self, $flagtype, $product) = @_; + my $user = Bugzilla->user; + + my @values = ('X'); + push(@values, '?') + if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); + push(@values, '+', '-') if $user->can_set_flag($flagtype); + + my $item = { + id => $self->type('int', $flagtype->id), + name => $self->type('string', $flagtype->name), + description => $self->type('string', $flagtype->description), + type => $self->type('string', $flagtype->target_type), + values => \@values, + is_active => $self->type('boolean', $flagtype->is_active), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) + }; + + if ($product) { + my $inclusions + = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); + my $exclusions + = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); + + # if we have both inclusions and exclusions, the exclusions are redundant + $exclusions = [] if @$inclusions && @$exclusions; + + # no need to return anything if there's just "any component" + $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; + $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; + } + + return $item; } sub _flagtype_clusions_to_hash { - my ($self, $clusions, $product_id) = @_; - my $result = []; - foreach my $key (keys %$clusions) { - my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); - if ($prod_id == 0 || $prod_id == $product_id) { - if ($comp_id) { - my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 }); - push @$result, $component->name; - } - else { - return [ '' ]; - } - } + my ($self, $clusions, $product_id) = @_; + my $result = []; + foreach my $key (keys %$clusions) { + my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); + if ($prod_id == 0 || $prod_id == $product_id) { + if ($comp_id) { + my $component = Bugzilla::Component->new({id => $comp_id, cache => 1}); + push @$result, $component->name; + } + else { + return ['']; + } } - return $result; + } + return $result; } sub _add_update_tokens { - my ($self, $params, $bugs, $hashes) = @_; + my ($self, $params, $bugs, $hashes) = @_; - return if !Bugzilla->user->id; - return if !filter_wants($params, 'update_token'); + return if !Bugzilla->user->id; + return if !filter_wants($params, 'update_token'); - for(my $i = 0; $i < @$bugs; $i++) { - my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); - $hashes->[$i]->{'update_token'} = $self->type('string', $token); - } + for (my $i = 0; $i < @$bugs; $i++) { + my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); + $hashes->[$i]->{'update_token'} = $self->type('string', $token); + } } 1; diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm index 5e4c0d2ba..9b4261bc3 100644 --- a/Bugzilla/WebService/BugUserLastVisit.pm +++ b/Bugzilla/WebService/BugUserLastVisit.pm @@ -19,84 +19,84 @@ use Bugzilla::WebService::Util qw( validate filter ); use Bugzilla::Constants; use constant PUBLIC_METHODS => qw( - get - update + get + update ); sub update { - my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; - $user->login(LOGIN_REQUIRED); + $user->login(LOGIN_REQUIRED); - my $ids = $params->{ids} // []; - ThrowCodeError('param_required', { param => 'ids' }) unless @$ids; + my $ids = $params->{ids} // []; + ThrowCodeError('param_required', {param => 'ids'}) unless @$ids; - # Cache permissions for bugs. This highly reduces the number of calls to the - # DB. visible_bugs() is only able to handle bug IDs, so we have to skip - # aliases. - $user->visible_bugs([grep /^[0-9]+$/, @$ids]); + # Cache permissions for bugs. This highly reduces the number of calls to the + # DB. visible_bugs() is only able to handle bug IDs, so we have to skip + # aliases. + $user->visible_bugs([grep /^[0-9]+$/, @$ids]); - $dbh->bz_start_transaction(); - my @results; - my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }); + $dbh->bz_start_transaction(); + my @results; + my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1}); - ThrowUserError('user_not_involved', { bug_id => $bug->id }) - unless $user->is_involved_in_bug($bug); + ThrowUserError('user_not_involved', {bug_id => $bug->id}) + unless $user->is_involved_in_bug($bug); - $bug->update_user_last_visit($user, $last_visit_ts); + $bug->update_user_last_visit($user, $last_visit_ts); - push( - @results, - $self->_bug_user_last_visit_to_hash( - $bug_id, $last_visit_ts, $params - )); - } - $dbh->bz_commit_transaction(); + push(@results, + $self->_bug_user_last_visit_to_hash($bug_id, $last_visit_ts, $params)); + } + $dbh->bz_commit_transaction(); - return \@results; + return \@results; } sub get { - my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->user; - my $ids = $params->{ids}; - - $user->login(LOGIN_REQUIRED); - - if ($ids) { - # Cache permissions for bugs. This highly reduces the number of calls to - # the DB. visible_bugs() is only able to handle bug IDs, so we have to - # skip aliases. - $user->visible_bugs([grep /^[0-9]+$/, @$ids]); - } - - my @last_visits = @{ $user->last_visited }; - - if ($ids) { - # remove bugs that we are not interested in if ids is passed in. - my %id_set = map { ($_ => 1) } @$ids; - @last_visits = grep { $id_set{ $_->bug_id } } @last_visits; - } - - return [ - map { - $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, - $params) - } @last_visits - ]; + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $ids = $params->{ids}; + + $user->login(LOGIN_REQUIRED); + + if ($ids) { + + # Cache permissions for bugs. This highly reduces the number of calls to + # the DB. visible_bugs() is only able to handle bug IDs, so we have to + # skip aliases. + $user->visible_bugs([grep /^[0-9]+$/, @$ids]); + } + + my @last_visits = @{$user->last_visited}; + + if ($ids) { + + # remove bugs that we are not interested in if ids is passed in. + my %id_set = map { ($_ => 1) } @$ids; + @last_visits = grep { $id_set{$_->bug_id} } @last_visits; + } + + return [ + map { + $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params) + } @last_visits + ]; } sub _bug_user_last_visit_to_hash { - my ($self, $bug_id, $last_visit_ts, $params) = @_; + my ($self, $bug_id, $last_visit_ts, $params) = @_; - my %result = (id => $self->type('int', $bug_id), - last_visit_ts => $self->type('dateTime', $last_visit_ts)); + my %result = ( + id => $self->type('int', $bug_id), + last_visit_ts => $self->type('dateTime', $last_visit_ts) + ); - return filter($params, \%result); + return filter($params, \%result); } 1; diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm index 8e95028cb..f8f745a55 100644 --- a/Bugzilla/WebService/Bugzilla.pm +++ b/Bugzilla/WebService/Bugzilla.pm @@ -21,77 +21,76 @@ use Try::Tiny; use DateTime; # Basic info that is needed before logins -use constant LOGIN_EXEMPT => { - timezone => 1, - version => 1, -}; +use constant LOGIN_EXEMPT => {timezone => 1, version => 1,}; use constant READ_ONLY => qw( - extensions - timezone - time - version - jobqueue_status + extensions + timezone + time + version + jobqueue_status ); use constant PUBLIC_METHODS => qw( - extensions - time - timezone - version - jobqueue_status + extensions + time + timezone + version + jobqueue_status ); sub version { - my $self = shift; - return { version => $self->type('string', BUGZILLA_VERSION) }; + my $self = shift; + return {version => $self->type('string', BUGZILLA_VERSION)}; } sub extensions { - my $self = shift; - - my %retval; - foreach my $extension (@{ Bugzilla->extensions }) { - my $version = $extension->VERSION || 0; - my $name = $extension->NAME; - $retval{$name}->{version} = $self->type('string', $version); - } - return { extensions => \%retval }; + my $self = shift; + + my %retval; + foreach my $extension (@{Bugzilla->extensions}) { + my $version = $extension->VERSION || 0; + my $name = $extension->NAME; + $retval{$name}->{version} = $self->type('string', $version); + } + return {extensions => \%retval}; } sub timezone { - my $self = shift; - # All Webservices return times in UTC; Use UTC here for backwards compat. - return { timezone => $self->type('string', "+0000") }; + my $self = shift; + + # All Webservices return times in UTC; Use UTC here for backwards compat. + return {timezone => $self->type('string', "+0000")}; } sub time { - my ($self) = @_; - # All Webservices return times in UTC; Use UTC here for backwards compat. - # Hardcode values where appropriate - my $dbh = Bugzilla->dbh; - - my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $db_time = datetime_from($db_time, 'UTC'); - my $now_utc = DateTime->now(); - - return { - db_time => $self->type('dateTime', $db_time), - web_time => $self->type('dateTime', $now_utc), - web_time_utc => $self->type('dateTime', $now_utc), - tz_name => $self->type('string', 'UTC'), - tz_offset => $self->type('string', '+0000'), - tz_short_name => $self->type('string', 'UTC'), - }; + my ($self) = @_; + + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + my $dbh = Bugzilla->dbh; + + my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $db_time = datetime_from($db_time, 'UTC'); + my $now_utc = DateTime->now(); + + return { + db_time => $self->type('dateTime', $db_time), + web_time => $self->type('dateTime', $now_utc), + web_time_utc => $self->type('dateTime', $now_utc), + tz_name => $self->type('string', 'UTC'), + tz_offset => $self->type('string', '+0000'), + tz_short_name => $self->type('string', 'UTC'), + }; } sub jobqueue_status { - my ( $self, $params ) = @_; + my ($self, $params) = @_; - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; - my $query = q{ + my $dbh = Bugzilla->dbh; + my $query = q{ SELECT COUNT(*) AS total, COALESCE( @@ -105,17 +104,18 @@ sub jobqueue_status { ON f.funcid = j.funcid; }; - my $status; - try { - $status = $dbh->selectrow_hashref($query); - $status->{errors} = 0 + $status->{errors}; - $status->{total} = 0 + $status->{total}; - } catch { - ERROR($_); - ThrowCodeError('jobqueue_status_error'); - }; - - return $status; + my $status; + try { + $status = $dbh->selectrow_hashref($query); + $status->{errors} = 0 + $status->{errors}; + $status->{total} = 0 + $status->{total}; + } + catch { + ERROR($_); + ThrowCodeError('jobqueue_status_error'); + }; + + return $status; } 1; diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm index 32139ff3f..35e67ba08 100644 --- a/Bugzilla/WebService/Classification.pm +++ b/Bugzilla/WebService/Classification.pm @@ -18,65 +18,76 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(filter validate params_to_objects); use constant READ_ONLY => qw( - get + get ); use constant PUBLIC_METHODS => qw( - get + get ); sub get { - my ($self, $params) = validate(@_, 'names', 'ids'); + my ($self, $params) = validate(@_, 'names', 'ids'); - defined $params->{names} || defined $params->{ids} - || ThrowCodeError('params_required', { function => 'Classification.get', - params => ['names', 'ids'] }); + defined $params->{names} + || defined $params->{ids} + || ThrowCodeError('params_required', + {function => 'Classification.get', params => ['names', 'ids']}); - my $user = Bugzilla->user; + my $user = Bugzilla->user; - Bugzilla->params->{'useclassification'} - || $user->in_group('editclassifications') - || ThrowUserError('auth_classification_not_enabled'); + Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications') + || ThrowUserError('auth_classification_not_enabled'); - Bugzilla->switch_to_shadow_db; + Bugzilla->switch_to_shadow_db; - my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') }; - unless ($user->in_group('editclassifications')) { - my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications}; - @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; - } + my @classification_objs + = @{params_to_objects($params, 'Bugzilla::Classification')}; + unless ($user->in_group('editclassifications')) { + my %selectable_class + = map { $_->id => 1 } @{$user->get_selectable_classifications}; + @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; + } - my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs; + my @classifications + = map { $self->_classification_to_hash($_, $params) } @classification_objs; - return { classifications => \@classifications }; + return {classifications => \@classifications}; } sub _classification_to_hash { - my ($self, $classification, $params) = @_; - - my $user = Bugzilla->user; - return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications')); - - my $products = $user->in_group('editclassifications') ? - $classification->products : $user->get_selectable_products($classification->id); - - return filter $params, { - id => $self->type('int', $classification->id), - name => $self->type('string', $classification->name), - description => $self->type('string', $classification->description), - sort_key => $self->type('int', $classification->sortkey), - products => [ map { $self->_product_to_hash($_, $params) } @$products ], + my ($self, $classification, $params) = @_; + + my $user = Bugzilla->user; + return + unless (Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications')); + + my $products + = $user->in_group('editclassifications') + ? $classification->products + : $user->get_selectable_products($classification->id); + + return filter $params, + { + id => $self->type('int', $classification->id), + name => $self->type('string', $classification->name), + description => $self->type('string', $classification->description), + sort_key => $self->type('int', $classification->sortkey), + products => [map { $self->_product_to_hash($_, $params) } @$products], }; } sub _product_to_hash { - my ($self, $product, $params) = @_; - - return filter $params, { - id => $self->type('int', $product->id), - name => $self->type('string', $product->name), - description => $self->type('string', $product->description), - }, undef, 'products'; + my ($self, $product, $params) = @_; + + return filter $params, + { + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + }, + undef, 'products'; } 1; diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 71435c13a..71fcccd6e 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -14,27 +14,27 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - WS_ERROR_CODE + WS_ERROR_CODE - STATUS_OK - STATUS_CREATED - STATUS_ACCEPTED - STATUS_NO_CONTENT - STATUS_MULTIPLE_CHOICES - STATUS_BAD_REQUEST - STATUS_NOT_FOUND - STATUS_GONE - REST_STATUS_CODE_MAP + STATUS_OK + STATUS_CREATED + STATUS_ACCEPTED + STATUS_NO_CONTENT + STATUS_MULTIPLE_CHOICES + STATUS_BAD_REQUEST + STATUS_NOT_FOUND + STATUS_GONE + REST_STATUS_CODE_MAP - ERROR_UNKNOWN_FATAL - ERROR_UNKNOWN_TRANSIENT + ERROR_UNKNOWN_FATAL + ERROR_UNKNOWN_TRANSIENT - XMLRPC_CONTENT_TYPE_WHITELIST - REST_CONTENT_TYPE_WHITELIST + XMLRPC_CONTENT_TYPE_WHITELIST + REST_CONTENT_TYPE_WHITELIST - WS_DISPATCH + WS_DISPATCH - API_AUTH_HEADERS + API_AUTH_HEADERS ); # This maps the error names in global/*-error.html.tmpl to numbers. @@ -56,161 +56,184 @@ our @EXPORT = qw( # comment that it was retired. Also, if an error changes its name, you'll # have to fix it here. use constant WS_ERROR_CODE => { - # Generic errors (Bugzilla::Object and others) are 50-99. - object_not_specified => 50, - reassign_to_empty => 50, - param_required => 50, - params_required => 50, - undefined_field => 50, - object_does_not_exist => 51, - param_must_be_numeric => 52, - number_not_numeric => 52, - param_invalid => 53, - number_too_large => 54, - number_too_small => 55, - illegal_date => 56, - # Bug errors usually occupy the 100-200 range. - improper_bug_id_field_value => 100, - bug_id_does_not_exist => 101, - bug_access_denied => 102, - bug_access_query => 102, - # These all mean "invalid alias" - alias_too_long => 103, - alias_in_use => 103, - alias_is_numeric => 103, - alias_has_comma_or_space => 103, - multiple_alias_not_allowed => 103, - # Misc. bug field errors - illegal_field => 104, - freetext_too_long => 104, - # Component errors - require_component => 105, - component_name_too_long => 105, - # Invalid Product - no_products => 106, - entry_access_denied => 106, - product_access_denied => 106, - product_disabled => 106, - # Invalid Summary - require_summary => 107, - # Invalid field name - invalid_field_name => 108, - # Not authorized to edit the bug - product_edit_denied => 109, - # Comment-related errors - comment_is_private => 110, - comment_id_invalid => 111, - comment_too_long => 114, - comment_invalid_isprivate => 117, - # Comment tagging - comment_tag_disabled => 125, - comment_tag_invalid => 126, - comment_tag_too_long => 127, - comment_tag_too_short => 128, - # See Also errors - bug_url_invalid => 112, - bug_url_too_long => 112, - # Insidergroup Errors - user_not_insider => 113, - # Note: 114 is above in the Comment-related section. - # Bug update errors - illegal_change => 115, - # Dependency errors - dependency_loop_single => 116, - dependency_loop_multi => 116, - # Note: 117 is above in the Comment-related section. - # Dup errors - dupe_loop_detected => 118, - dupe_id_required => 119, - # Bug-related group errors - group_invalid_removal => 120, - group_restriction_not_allowed => 120, - # Status/Resolution errors - missing_resolution => 121, - resolution_not_allowed => 122, - illegal_bug_status_transition => 123, - # Flag errors - flag_status_invalid => 129, - flag_update_denied => 130, - flag_type_requestee_disabled => 131, - flag_not_unique => 132, - flag_type_not_unique => 133, - flag_type_inactive => 134, - - # Authentication errors are usually 300-400. - invalid_username_or_password => 300, - account_disabled => 301, - auth_invalid_email => 302, - extern_id_conflict => -303, - auth_failure => 304, - password_insecure => 305, - api_key_not_valid => 306, - api_key_revoked => 306, - auth_invalid_token => 307, - invalid_cookies_or_token => 307, - - # Except, historically, AUTH_NODATA, which is 410. - login_required => 410, - - # User errors are 500-600. - account_exists => 500, - illegal_email_address => 501, - auth_cant_create_account => 501, - account_creation_disabled => 501, - account_creation_restricted => 501, - # Error 502 password_too_short no longer exists. - # Error 503 password_too_long no longer exists. - invalid_username => 504, - # This is from strict_isolation, but it also basically means - # "invalid user." - invalid_user_group => 504, - user_access_by_id_denied => 505, - user_access_by_match_denied => 505, - - # Attachment errors are 600-700. - file_too_large => 600, - invalid_content_type => 601, - # Error 602 attachment_illegal_url no longer exists. - file_not_specified => 603, - missing_attachment_description => 604, - # Error 605 attachment_url_disabled no longer exists. - zero_length_file => 606, - - # Product erros are 700-800 - product_blank_name => 700, - product_name_too_long => 701, - product_name_already_in_use => 702, - product_name_diff_in_case => 702, - product_must_have_description => 703, - product_must_have_version => 704, - product_must_define_defaultmilestone => 705, - - # Group errors are 800-900 - empty_group_name => 800, - group_exists => 801, - empty_group_description => 802, - invalid_regexp => 803, - invalid_group_name => 804, - group_cannot_view => 805, - - # Search errors are 1000-1100 - buglist_parameters_required => 1000, - - # BugUserLastVisited errors - user_not_involved => 1300, - - # Job queue errors 1400-1500 - jobqueue_status_error => 1400, - - # Errors thrown by the WebService itself. The ones that are negative - # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php - xmlrpc_invalid_value => -32600, - unknown_method => -32601, - json_rpc_post_only => 32610, - json_rpc_invalid_callback => 32611, - xmlrpc_illegal_content_type => 32612, - json_rpc_illegal_content_type => 32613, - rest_invalid_resource => 32614, + + # Generic errors (Bugzilla::Object and others) are 50-99. + object_not_specified => 50, + reassign_to_empty => 50, + param_required => 50, + params_required => 50, + undefined_field => 50, + object_does_not_exist => 51, + param_must_be_numeric => 52, + number_not_numeric => 52, + param_invalid => 53, + number_too_large => 54, + number_too_small => 55, + illegal_date => 56, + + # Bug errors usually occupy the 100-200 range. + improper_bug_id_field_value => 100, + bug_id_does_not_exist => 101, + bug_access_denied => 102, + bug_access_query => 102, + + # These all mean "invalid alias" + alias_too_long => 103, + alias_in_use => 103, + alias_is_numeric => 103, + alias_has_comma_or_space => 103, + multiple_alias_not_allowed => 103, + + # Misc. bug field errors + illegal_field => 104, + freetext_too_long => 104, + + # Component errors + require_component => 105, + component_name_too_long => 105, + + # Invalid Product + no_products => 106, + entry_access_denied => 106, + product_access_denied => 106, + product_disabled => 106, + + # Invalid Summary + require_summary => 107, + + # Invalid field name + invalid_field_name => 108, + + # Not authorized to edit the bug + product_edit_denied => 109, + + # Comment-related errors + comment_is_private => 110, + comment_id_invalid => 111, + comment_too_long => 114, + comment_invalid_isprivate => 117, + + # Comment tagging + comment_tag_disabled => 125, + comment_tag_invalid => 126, + comment_tag_too_long => 127, + comment_tag_too_short => 128, + + # See Also errors + bug_url_invalid => 112, + bug_url_too_long => 112, + + # Insidergroup Errors + user_not_insider => 113, + + # Note: 114 is above in the Comment-related section. + # Bug update errors + illegal_change => 115, + + # Dependency errors + dependency_loop_single => 116, + dependency_loop_multi => 116, + + # Note: 117 is above in the Comment-related section. + # Dup errors + dupe_loop_detected => 118, + dupe_id_required => 119, + + # Bug-related group errors + group_invalid_removal => 120, + group_restriction_not_allowed => 120, + + # Status/Resolution errors + missing_resolution => 121, + resolution_not_allowed => 122, + illegal_bug_status_transition => 123, + + # Flag errors + flag_status_invalid => 129, + flag_update_denied => 130, + flag_type_requestee_disabled => 131, + flag_not_unique => 132, + flag_type_not_unique => 133, + flag_type_inactive => 134, + + # Authentication errors are usually 300-400. + invalid_username_or_password => 300, + account_disabled => 301, + auth_invalid_email => 302, + extern_id_conflict => -303, + auth_failure => 304, + password_insecure => 305, + api_key_not_valid => 306, + api_key_revoked => 306, + auth_invalid_token => 307, + invalid_cookies_or_token => 307, + + # Except, historically, AUTH_NODATA, which is 410. + login_required => 410, + + # User errors are 500-600. + account_exists => 500, + illegal_email_address => 501, + auth_cant_create_account => 501, + account_creation_disabled => 501, + account_creation_restricted => 501, + + # Error 502 password_too_short no longer exists. + # Error 503 password_too_long no longer exists. + invalid_username => 504, + + # This is from strict_isolation, but it also basically means + # "invalid user." + invalid_user_group => 504, + user_access_by_id_denied => 505, + user_access_by_match_denied => 505, + + # Attachment errors are 600-700. + file_too_large => 600, + invalid_content_type => 601, + + # Error 602 attachment_illegal_url no longer exists. + file_not_specified => 603, + missing_attachment_description => 604, + + # Error 605 attachment_url_disabled no longer exists. + zero_length_file => 606, + + # Product erros are 700-800 + product_blank_name => 700, + product_name_too_long => 701, + product_name_already_in_use => 702, + product_name_diff_in_case => 702, + product_must_have_description => 703, + product_must_have_version => 704, + product_must_define_defaultmilestone => 705, + + # Group errors are 800-900 + empty_group_name => 800, + group_exists => 801, + empty_group_description => 802, + invalid_regexp => 803, + invalid_group_name => 804, + group_cannot_view => 805, + + # Search errors are 1000-1100 + buglist_parameters_required => 1000, + + # BugUserLastVisited errors + user_not_involved => 1300, + + # Job queue errors 1400-1500 + jobqueue_status_error => 1400, + + # Errors thrown by the WebService itself. The ones that are negative + # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + xmlrpc_invalid_value => -32600, + unknown_method => -32601, + json_rpc_post_only => 32610, + json_rpc_invalid_callback => 32611, + xmlrpc_illegal_content_type => 32612, + json_rpc_illegal_content_type => 32613, + rest_invalid_resource => 32614, }; # RESTful webservices use the http status code @@ -231,81 +254,82 @@ use constant STATUS_GONE => 410; # http status code based on the error code or use the # default STATUS_BAD_REQUEST. sub REST_STATUS_CODE_MAP { - my $status_code_map = { - 51 => STATUS_NOT_FOUND, - 101 => STATUS_NOT_FOUND, - 102 => STATUS_NOT_AUTHORIZED, - 106 => STATUS_NOT_AUTHORIZED, - 109 => STATUS_NOT_AUTHORIZED, - 110 => STATUS_NOT_AUTHORIZED, - 113 => STATUS_NOT_AUTHORIZED, - 115 => STATUS_NOT_AUTHORIZED, - 120 => STATUS_NOT_AUTHORIZED, - 300 => STATUS_NOT_AUTHORIZED, - 301 => STATUS_NOT_AUTHORIZED, - 302 => STATUS_NOT_AUTHORIZED, - 303 => STATUS_NOT_AUTHORIZED, - 304 => STATUS_NOT_AUTHORIZED, - 410 => STATUS_NOT_AUTHORIZED, - 504 => STATUS_NOT_AUTHORIZED, - 505 => STATUS_NOT_AUTHORIZED, - 32614 => STATUS_NOT_FOUND, - _default => STATUS_BAD_REQUEST - }; - - Bugzilla::Hook::process('webservice_status_code_map', - { status_code_map => $status_code_map }); - - return $status_code_map; -}; + my $status_code_map = { + 51 => STATUS_NOT_FOUND, + 101 => STATUS_NOT_FOUND, + 102 => STATUS_NOT_AUTHORIZED, + 106 => STATUS_NOT_AUTHORIZED, + 109 => STATUS_NOT_AUTHORIZED, + 110 => STATUS_NOT_AUTHORIZED, + 113 => STATUS_NOT_AUTHORIZED, + 115 => STATUS_NOT_AUTHORIZED, + 120 => STATUS_NOT_AUTHORIZED, + 300 => STATUS_NOT_AUTHORIZED, + 301 => STATUS_NOT_AUTHORIZED, + 302 => STATUS_NOT_AUTHORIZED, + 303 => STATUS_NOT_AUTHORIZED, + 304 => STATUS_NOT_AUTHORIZED, + 410 => STATUS_NOT_AUTHORIZED, + 504 => STATUS_NOT_AUTHORIZED, + 505 => STATUS_NOT_AUTHORIZED, + 32614 => STATUS_NOT_FOUND, + _default => STATUS_BAD_REQUEST + }; + + Bugzilla::Hook::process('webservice_status_code_map', + {status_code_map => $status_code_map}); + + return $status_code_map; +} # These are the fallback defaults for errors not in ERROR_CODE. use constant ERROR_UNKNOWN_FATAL => -32000; use constant ERROR_UNKNOWN_TRANSIENT => 32000; -use constant ERROR_GENERAL => 999; +use constant ERROR_GENERAL => 999; use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw( - text/xml - application/xml + text/xml + application/xml ); # The first content type specified is used as the default. use constant REST_CONTENT_TYPE_WHITELIST => qw( - application/json - application/javascript - text/javascript - text/html + application/json + application/javascript + text/javascript + text/html ); sub WS_DISPATCH { - # We "require" here instead of "use" above to avoid a dependency loop. - require Bugzilla::Hook; - my %hook_dispatch; - Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch }); - - my $dispatch = { - 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', - 'Bug' => 'Bugzilla::WebService::Bug', - 'Classification' => 'Bugzilla::WebService::Classification', - 'User' => 'Bugzilla::WebService::User', - 'Product' => 'Bugzilla::WebService::Product', - 'Group' => 'Bugzilla::WebService::Group', - 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', - 'Elastic' => 'Bugzilla::WebService::Elastic', - %hook_dispatch - }; - return $dispatch; -}; + + # We "require" here instead of "use" above to avoid a dependency loop. + require Bugzilla::Hook; + my %hook_dispatch; + Bugzilla::Hook::process('webservice', {dispatch => \%hook_dispatch}); + + my $dispatch = { + 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', + 'Bug' => 'Bugzilla::WebService::Bug', + 'Classification' => 'Bugzilla::WebService::Classification', + 'User' => 'Bugzilla::WebService::User', + 'Product' => 'Bugzilla::WebService::Product', + 'Group' => 'Bugzilla::WebService::Group', + 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', + 'Elastic' => 'Bugzilla::WebService::Elastic', + %hook_dispatch + }; + return $dispatch; +} # Custom HTTP headers that can be used for API authentication rather than # passing as URL parameters. This is useful if you do not want sensitive # information to show up in webserver log files. use constant API_AUTH_HEADERS => { - X_BUGZILLA_LOGIN => 'Bugzilla_login', - X_BUGZILLA_PASSWORD => 'Bugzilla_password', - X_BUGZILLA_API_KEY => 'Bugzilla_api_key', - X_BUGZILLA_TOKEN => 'Bugzilla_token', + X_BUGZILLA_LOGIN => 'Bugzilla_login', + X_BUGZILLA_PASSWORD => 'Bugzilla_password', + X_BUGZILLA_API_KEY => 'Bugzilla_api_key', + X_BUGZILLA_TOKEN => 'Bugzilla_token', }; 1; diff --git a/Bugzilla/WebService/Elastic.pm b/Bugzilla/WebService/Elastic.pm index 3a33a1dba..373f6db58 100644 --- a/Bugzilla/WebService/Elastic.pm +++ b/Bugzilla/WebService/Elastic.pm @@ -30,30 +30,28 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(validate); use Bugzilla::Util qw(trim detaint_natural trick_taint); -use constant READ_ONLY => qw( suggest_users ); +use constant READ_ONLY => qw( suggest_users ); use constant PUBLIC_METHODS => qw( suggest_users ); sub suggest_users { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - ThrowCodeError('params_required', { function => 'Elastic.suggest_users', params => ['match'] }) - unless defined $params->{match}; + ThrowCodeError('params_required', + {function => 'Elastic.suggest_users', params => ['match']}) + unless defined $params->{match}; - ThrowUserError('user_access_by_match_denied') - unless Bugzilla->user->id; + ThrowUserError('user_access_by_match_denied') unless Bugzilla->user->id; - trick_taint($params->{match}); - my $results = Bugzilla->elastic->suggest_users($params->{match} . ""); - my @users = map { - { - real_name => $self->type(string => $_->{real_name}), - name => $self->type(email => $_->{name}), - } - } @$results; + trick_taint($params->{match}); + my $results = Bugzilla->elastic->suggest_users($params->{match} . ""); + my @users = map { { + real_name => $self->type(string => $_->{real_name}), + name => $self->type(email => $_->{name}), + } } @$results; - return { users => \@users }; + return {users => \@users}; } -1;
\ No newline at end of file +1; diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm index b13003e08..4467883a4 100644 --- a/Bugzilla/WebService/Group.pm +++ b/Bugzilla/WebService/Group.pm @@ -17,207 +17,210 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(validate translate params_to_objects); use constant PUBLIC_METHODS => qw( - create - get - update + create + get + update ); -use constant MAPPED_RETURNS => { - userregexp => 'user_regexp', - isactive => 'is_active' -}; +use constant MAPPED_RETURNS => + {userregexp => 'user_regexp', isactive => 'is_active'}; sub create { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('creategroups') - || ThrowUserError("auth_failure", { group => "creategroups", - action => "add", - object => "group"}); - # Create group - my $group = Bugzilla::Group->create({ - name => $params->{name}, - description => $params->{description}, - userregexp => $params->{user_regexp}, - isactive => $params->{is_active}, - isbuggroup => 1, - icon_url => $params->{icon_url} - }); - return { id => $self->type('int', $group->id) }; + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", + {group => "creategroups", action => "add", object => "group"}); + + # Create group + my $group = Bugzilla::Group->create({ + name => $params->{name}, + description => $params->{description}, + userregexp => $params->{user_regexp}, + isactive => $params->{is_active}, + isbuggroup => 1, + icon_url => $params->{icon_url} + }); + return {id => $self->type('int', $group->id)}; } sub update { - my ($self, $params) = @_; - - my $dbh = Bugzilla->dbh; + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", + {group => "creategroups", action => "edit", object => "group"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Group.update', params => ['ids', 'names']}); + + my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + + my %values = %$params; + + # We delete names and ids to keep only new values to set. + delete $values{names}; + delete $values{ids}; + + $dbh->bz_start_transaction(); + foreach my $group (@$group_objects) { + $group->set_all(\%values); + } + + my %changes; + foreach my $group (@$group_objects) { + my $returned_changes = $group->update(); + $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $group (@$group_objects) { + my %hash = (id => $group->id, changes => {},); + foreach my $field (keys %{$changes{$group->id}}) { + my $change = $changes{$group->id}->{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; + } + push(@result, \%hash); + } - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('creategroups') - || ThrowUserError("auth_failure", { group => "creategroups", - action => "edit", - object => "group" }); + return {groups => \@result}; +} - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Group.update', params => ['ids', 'names'] }); +sub get { + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); - my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + Bugzilla->login(LOGIN_REQUIRED); - my %values = %$params; + # Reject access if there is no sense in continuing. + my $user = Bugzilla->user; + my $all_groups + = $user->in_group('editusers') || $user->in_group('creategroups'); + if (!$all_groups && !$user->can_bless) { + ThrowUserError('group_cannot_view'); + } - # We delete names and ids to keep only new values to set. - delete $values{names}; - delete $values{ids}; + Bugzilla->switch_to_shadow_db(); - $dbh->bz_start_transaction(); - foreach my $group (@$group_objects) { - $group->set_all(\%values); - } + my $groups = []; - my %changes; - foreach my $group (@$group_objects) { - my $returned_changes = $group->update(); - $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $group (@$group_objects) { - my %hash = ( - id => $group->id, - changes => {}, - ); - foreach my $field (keys %{ $changes{$group->id} }) { - my $change = $changes{$group->id}->{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - push(@result, \%hash); - } - - return { groups => \@result }; -} - -sub get { - my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + if (defined $params->{ids}) { - Bugzilla->login(LOGIN_REQUIRED); + # Get the groups by id + $groups = Bugzilla::Group->new_from_list($params->{ids}); + } - # Reject access if there is no sense in continuing. - my $user = Bugzilla->user; - my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups'); - if (!$all_groups && !$user->can_bless) { - ThrowUserError('group_cannot_view'); - } + if (defined $params->{names}) { - Bugzilla->switch_to_shadow_db(); + # Get the groups by name. Check will throw an error if a bad name is given + foreach my $name (@{$params->{names}}) { - my $groups = []; + # Skip if we got this from params->{id} + next if grep { $_->name eq $name } @$groups; - if (defined $params->{ids}) { - # Get the groups by id - $groups = Bugzilla::Group->new_from_list($params->{ids}); + push @$groups, Bugzilla::Group->check({name => $name}); } + } - if (defined $params->{names}) { - # Get the groups by name. Check will throw an error if a bad name is given - foreach my $name (@{$params->{names}}) { - # Skip if we got this from params->{id} - next if grep { $_->name eq $name } @$groups; - - push @$groups, Bugzilla::Group->check({ name => $name }); - } + if (!defined $params->{ids} && !defined $params->{names}) { + if ($all_groups) { + @$groups = Bugzilla::Group->get_all; } - - if (!defined $params->{ids} && !defined $params->{names}) { - if ($all_groups) { - @$groups = Bugzilla::Group->get_all; - } - else { - # Get only groups the user has bless groups too - $groups = $user->bless_groups; - } + else { + # Get only groups the user has bless groups too + $groups = $user->bless_groups; } + } - # Now create a result entry for each. - my @groups = map { $self->_group_to_hash($params, $_) } @$groups; - return { groups => \@groups }; + # Now create a result entry for each. + my @groups = map { $self->_group_to_hash($params, $_) } @$groups; + return {groups => \@groups}; } sub _group_to_hash { - my ($self, $params, $group) = @_; - my $user = Bugzilla->user; - - my $field_data = { - id => $self->type('int', $group->id), - name => $self->type('string', $group->name), - description => $self->type('string', $group->description), - }; - - if ($user->in_group('creategroups')) { - $field_data->{is_active} = $self->type('boolean', $group->is_active); - $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group); - $field_data->{user_regexp} = $self->type('string', $group->user_regexp); - } - - if ($params->{membership}) { - $field_data->{membership} = $self->_get_group_membership($group, $params); - } - return $field_data; + my ($self, $params, $group) = @_; + my $user = Bugzilla->user; + + my $field_data = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + + if ($user->in_group('creategroups')) { + $field_data->{is_active} = $self->type('boolean', $group->is_active); + $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group); + $field_data->{user_regexp} = $self->type('string', $group->user_regexp); + } + + if ($params->{membership}) { + $field_data->{membership} = $self->_get_group_membership($group, $params); + } + return $field_data; } sub _get_group_membership { - my ($self, $group, $params) = @_; - my $user = Bugzilla->user; + my ($self, $group, $params) = @_; + my $user = Bugzilla->user; - my %users_only; - my $dbh = Bugzilla->dbh; - my $editusers = $user->in_group('editusers'); + my %users_only; + my $dbh = Bugzilla->dbh; + my $editusers = $user->in_group('editusers'); - my $query = 'SELECT userid FROM profiles'; - my $visibleGroups; + my $query = 'SELECT userid FROM profiles'; + my $visibleGroups; - if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - # Show only users in visible groups. - $visibleGroups = $user->visible_groups_inherited; + if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - if (scalar @$visibleGroups) { - $query .= qq{, user_group_map AS ugm + # Show only users in visible groups. + $visibleGroups = $user->visible_groups_inherited; + + if (scalar @$visibleGroups) { + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 AND } . $dbh->sql_in('ugm.group_id', $visibleGroups); - } - } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) { - $visibleGroups = 1; - $query .= qq{, user_group_map AS ugm + } + } + elsif ($editusers + || $user->can_bless($group->id) + || $user->in_group('creategroups')) + { + $visibleGroups = 1; + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 }; - } - if (!$visibleGroups) { - ThrowUserError('group_not_visible', { group => $group }); - } - - my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); - $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); - - my $userids = $dbh->selectcol_arrayref($query); - my $user_objects = Bugzilla::User->new_from_list($userids); - my @users = - map {{ - id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('string', $_->login), - email => $self->type('string', $_->email), - can_login => $self->type('boolean', $_->is_enabled), - email_enabled => $self->type('boolean', $_->email_enabled), - login_denied_text => $self->type('string', $_->disabledtext), - }} @$user_objects; - - return \@users; + } + if (!$visibleGroups) { + ThrowUserError('group_not_visible', {group => $group}); + } + + my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); + $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); + + my $userids = $dbh->selectcol_arrayref($query); + my $user_objects = Bugzilla::User->new_from_list($userids); + my @users = map { { + id => $self->type('int', $_->id), + real_name => $self->type('string', $_->name), + name => $self->type('string', $_->login), + email => $self->type('string', $_->email), + can_login => $self->type('boolean', $_->is_enabled), + email_enabled => $self->type('boolean', $_->email_enabled), + login_denied_text => $self->type('string', $_->disabledtext), + } } @$user_objects; + + return \@users; } 1; diff --git a/Bugzilla/WebService/JSON.pm b/Bugzilla/WebService/JSON.pm index 5c28b20f4..f670d1fd9 100644 --- a/Bugzilla/WebService/JSON.pm +++ b/Bugzilla/WebService/JSON.pm @@ -39,7 +39,7 @@ sub decode { } } -sub _build_json { JSON::MaybeXS->new } +sub _build_json { JSON::MaybeXS->new } # delegation all the json options to the real json encoder. { diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index cdd8a0a92..0726c371d 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -20,24 +20,22 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Util qw(validate filter filter_wants); use constant READ_ONLY => qw( - get - get_accessible_products - get_enterable_products - get_selectable_products + get + get_accessible_products + get_enterable_products + get_selectable_products ); use constant PUBLIC_METHODS => qw( - create - get - get_accessible_products - get_enterable_products - get_selectable_products + create + get + get_accessible_products + get_enterable_products + get_selectable_products ); -use constant FIELD_MAP => { - has_unconfirmed => 'allows_unconfirmed', - is_open => 'isactive', -}; +use constant FIELD_MAP => + {has_unconfirmed => 'allows_unconfirmed', is_open => 'isactive',}; ################################################## # Add aliases here for method name compatibility # @@ -47,256 +45,240 @@ BEGIN { *get_products = \&get } # Get the ids of the products the user can search sub get_selectable_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_selectable_products}]}; } # Get the ids of the products the user can enter bugs against sub get_enterable_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_enterable_products}]}; } # Get the union of the products the user can search and enter bugs against. sub get_accessible_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_accessible_products}]}; } # Get a list of actual products, based on list of ids or names our %FLAG_CACHE; + sub get { - my ($self, $params) = validate(@_, 'ids', 'names', 'type'); - my $user = Bugzilla->user; - - Bugzilla->request_cache->{bz_etag_disable} = 1; - - defined $params->{ids} || defined $params->{names} || defined $params->{type} - || ThrowCodeError("params_required", { function => "Product.get", - params => ['ids', 'names', 'type'] }); - - Bugzilla->switch_to_shadow_db(); - - my $products = []; - if (defined $params->{type}) { - my %product_hash; - foreach my $type (@{ $params->{type} }) { - my $result = []; - if ($type eq 'accessible') { - $result = $user->get_accessible_products(); - } - elsif ($type eq 'enterable') { - $result = $user->get_enterable_products(); - } - elsif ($type eq 'selectable') { - $result = $user->get_selectable_products(); - } - else { - ThrowUserError('get_products_invalid_type', - { type => $type }); - } - map { $product_hash{$_->id} = $_ } @$result; - } - $products = [ values %product_hash ]; - } - else { - $products = $user->get_accessible_products; + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + my $user = Bugzilla->user; + + Bugzilla->request_cache->{bz_etag_disable} = 1; + + defined $params->{ids} + || defined $params->{names} + || defined $params->{type} + || ThrowCodeError("params_required", + {function => "Product.get", params => ['ids', 'names', 'type']}); + + Bugzilla->switch_to_shadow_db(); + + my $products = []; + if (defined $params->{type}) { + my %product_hash; + foreach my $type (@{$params->{type}}) { + my $result = []; + if ($type eq 'accessible') { + $result = $user->get_accessible_products(); + } + elsif ($type eq 'enterable') { + $result = $user->get_enterable_products(); + } + elsif ($type eq 'selectable') { + $result = $user->get_selectable_products(); + } + else { + ThrowUserError('get_products_invalid_type', {type => $type}); + } + map { $product_hash{$_->id} = $_ } @$result; } + $products = [values %product_hash]; + } + else { + $products = $user->get_accessible_products; + } - my @requested_products; + my @requested_products; - if (defined $params->{ids}) { - # Create a hash with the ids the user wants - my %ids = map { $_ => 1 } @{$params->{ids}}; + if (defined $params->{ids}) { - # Return the intersection of this, by grepping the ids from - # accessible products. - push(@requested_products, - grep { $ids{$_->id} } @$products); - } + # Create a hash with the ids the user wants + my %ids = map { $_ => 1 } @{$params->{ids}}; - if (defined $params->{names}) { - # Create a hash with the names the user wants - my %names = map { lc($_) => 1 } @{$params->{names}}; - - # Return the intersection of this, by grepping the names from - # accessible products, union'ed with products found by ID to - # avoid duplicates - foreach my $product (grep { $names{lc $_->name} } - @$products) { - next if grep { $_->id == $product->id } - @requested_products; - push @requested_products, $product; - } - } + # Return the intersection of this, by grepping the ids from + # accessible products. + push(@requested_products, grep { $ids{$_->id} } @$products); + } - # If we just requested a specific type of products without - # specifying ids or names, then return the entire list. - if (!defined $params->{ids} && !defined $params->{names}) { - @requested_products = @$products; - } + if (defined $params->{names}) { + + # Create a hash with the names the user wants + my %names = map { lc($_) => 1 } @{$params->{names}}; - # Now create a result entry for each. - local %FLAG_CACHE = (); - my @products = map { $self->_product_to_hash($params, $_) } - @requested_products; - return { products => \@products }; + # Return the intersection of this, by grepping the names from + # accessible products, union'ed with products found by ID to + # avoid duplicates + foreach my $product (grep { $names{lc $_->name} } @$products) { + next if grep { $_->id == $product->id } @requested_products; + push @requested_products, $product; + } + } + + # If we just requested a specific type of products without + # specifying ids or names, then return the entire list. + if (!defined $params->{ids} && !defined $params->{names}) { + @requested_products = @$products; + } + + # Now create a result entry for each. + local %FLAG_CACHE = (); + my @products = map { $self->_product_to_hash($params, $_) } @requested_products; + return {products => \@products}; } sub create { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('editcomponents') - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "add", - object => "products"}); - # Create product - my $args = { - name => $params->{name}, - description => $params->{description}, - version => $params->{version}, - defaultmilestone => $params->{default_milestone}, - # create_series has no default value. - create_series => defined $params->{create_series} ? - $params->{create_series} : 1 - }; - foreach my $field (qw(has_unconfirmed is_open classification)) { - if (defined $params->{$field}) { - my $name = FIELD_MAP->{$field} || $field; - $args->{$name} = $params->{$field}; - } + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "products"}); + + # Create product + my $args = { + name => $params->{name}, + description => $params->{description}, + version => $params->{version}, + defaultmilestone => $params->{default_milestone}, + + # create_series has no default value. + create_series => defined $params->{create_series} + ? $params->{create_series} + : 1 + }; + foreach my $field (qw(has_unconfirmed is_open classification)) { + if (defined $params->{$field}) { + my $name = FIELD_MAP->{$field} || $field; + $args->{$name} = $params->{$field}; } - my $product = Bugzilla::Product->create($args); - return { id => $self->type('int', $product->id) }; + } + my $product = Bugzilla::Product->create($args); + return {id => $self->type('int', $product->id)}; } sub _product_to_hash { - my ($self, $params, $product) = @_; - - my $field_data = { - id => $self->type('int', $product->id), - name => $self->type('string', $product->name), - description => $self->type('string', $product->description), - is_active => $self->type('boolean', $product->is_active), - default_milestone => $self->type('string', $product->default_milestone), - has_unconfirmed => $self->type('boolean', $product->allows_unconfirmed), - classification => $self->type('string', $product->classification->name), - }; - if (filter_wants($params, 'components')) { - $field_data->{components} = [map { - $self->_component_to_hash($_, $params) - } @{$product->components}]; - } - if (filter_wants($params, 'versions')) { - $field_data->{versions} = [map { - $self->_version_to_hash($_, $params) - } @{$product->versions}]; - } - if (filter_wants($params, 'milestones')) { - $field_data->{milestones} = [map { - $self->_milestone_to_hash($_, $params) - } @{$product->milestones}]; - } - # BMO - add default hw/os - $field_data->{default_platform} = $self->type('string', $product->default_platform); - $field_data->{default_op_sys} = $self->type('string', $product->default_op_sys); - # BMO - add default security group - $field_data->{default_security_group} = $self->type('string', $product->default_security_group); - return filter($params, $field_data); + my ($self, $params, $product) = @_; + + my $field_data = { + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + is_active => $self->type('boolean', $product->is_active), + default_milestone => $self->type('string', $product->default_milestone), + has_unconfirmed => $self->type('boolean', $product->allows_unconfirmed), + classification => $self->type('string', $product->classification->name), + }; + if (filter_wants($params, 'components')) { + $field_data->{components} + = [map { $self->_component_to_hash($_, $params) } @{$product->components}]; + } + if (filter_wants($params, 'versions')) { + $field_data->{versions} + = [map { $self->_version_to_hash($_, $params) } @{$product->versions}]; + } + if (filter_wants($params, 'milestones')) { + $field_data->{milestones} + = [map { $self->_milestone_to_hash($_, $params) } @{$product->milestones}]; + } + + # BMO - add default hw/os + $field_data->{default_platform} + = $self->type('string', $product->default_platform); + $field_data->{default_op_sys} = $self->type('string', $product->default_op_sys); + + # BMO - add default security group + $field_data->{default_security_group} + = $self->type('string', $product->default_security_group); + return filter($params, $field_data); } sub _component_to_hash { - my ($self, $component, $params) = @_; - my $field_data = filter $params, { - id => - $self->type('int', $component->id), - name => - $self->type('string', $component->name), - description => - $self->type('string', $component->description), - default_assigned_to => - $self->type('email', $component->default_assignee->login), - default_qa_contact => - $self->type('email', $component->default_qa_contact->login), - triage_owner => - $self->type('email', $component->triage_owner->login), - sort_key => # sort_key is returned to match Bug.fields - 0, - is_active => - $self->type('boolean', $component->is_active), - }, undef, 'components'; - - if (filter_wants($params, 'flag_types', undef, 'components')) { - $field_data->{flag_types} = { - bug => - [map { - $FLAG_CACHE{ $_->id } //= $self->_flag_type_to_hash($_) - } @{$component->flag_types->{'bug'}}], - attachment => - [map { - $FLAG_CACHE{ $_->id } //= $self->_flag_type_to_hash($_) - } @{$component->flag_types->{'attachment'}}], - }; - } + my ($self, $component, $params) = @_; + my $field_data = filter $params, { + id => $self->type('int', $component->id), + name => $self->type('string', $component->name), + description => $self->type('string', $component->description), + default_assigned_to => + $self->type('email', $component->default_assignee->login), + default_qa_contact => + $self->type('email', $component->default_qa_contact->login), + triage_owner => $self->type('email', $component->triage_owner->login), + sort_key => # sort_key is returned to match Bug.fields + 0, + is_active => $self->type('boolean', $component->is_active), + }, + undef, 'components'; + + if (filter_wants($params, 'flag_types', undef, 'components')) { + $field_data->{flag_types} = { + bug => [ + map { $FLAG_CACHE{$_->id} //= $self->_flag_type_to_hash($_) } + @{$component->flag_types->{'bug'}} + ], + attachment => [ + map { $FLAG_CACHE{$_->id} //= $self->_flag_type_to_hash($_) } + @{$component->flag_types->{'attachment'}} + ], + }; + } - return $field_data; + return $field_data; } sub _flag_type_to_hash { - my ($self, $flag_type) = @_; - return { - id => - $self->type('int', $flag_type->id), - name => - $self->type('string', $flag_type->name), - description => - $self->type('string', $flag_type->description), - cc_list => - $self->type('string', $flag_type->cc_list), - sort_key => - $self->type('int', $flag_type->sortkey), - is_active => - $self->type('boolean', $flag_type->is_active), - is_requestable => - $self->type('boolean', $flag_type->is_requestable), - is_requesteeble => - $self->type('boolean', $flag_type->is_requesteeble), - is_multiplicable => - $self->type('boolean', $flag_type->is_multiplicable), - grant_group => - $self->type('int', $flag_type->grant_group_id), - request_group => - $self->type('int', $flag_type->request_group_id), - }; + my ($self, $flag_type) = @_; + return { + id => $self->type('int', $flag_type->id), + name => $self->type('string', $flag_type->name), + description => $self->type('string', $flag_type->description), + cc_list => $self->type('string', $flag_type->cc_list), + sort_key => $self->type('int', $flag_type->sortkey), + is_active => $self->type('boolean', $flag_type->is_active), + is_requestable => $self->type('boolean', $flag_type->is_requestable), + is_requesteeble => $self->type('boolean', $flag_type->is_requesteeble), + is_multiplicable => $self->type('boolean', $flag_type->is_multiplicable), + grant_group => $self->type('int', $flag_type->grant_group_id), + request_group => $self->type('int', $flag_type->request_group_id), + }; } sub _version_to_hash { - my ($self, $version, $params) = @_; - return filter $params, { - id => - $self->type('int', $version->id), - name => - $self->type('string', $version->name), - sort_key => # sort_key is returened to match Bug.fields - 0, - is_active => - $self->type('boolean', $version->is_active), - }, undef, 'versions'; + my ($self, $version, $params) = @_; + return filter $params, { + id => $self->type('int', $version->id), + name => $self->type('string', $version->name), + sort_key => # sort_key is returened to match Bug.fields + 0, + is_active => $self->type('boolean', $version->is_active), + }, + undef, 'versions'; } sub _milestone_to_hash { - my ($self, $milestone, $params) = @_; - return filter $params, { - id => - $self->type('int', $milestone->id), - name => - $self->type('string', $milestone->name), - sort_key => - $self->type('int', $milestone->sortkey), - is_active => - $self->type('boolean', $milestone->is_active), - }, undef, 'milestones'; + my ($self, $milestone, $params) = @_; + return filter $params, + { + id => $self->type('int', $milestone->id), + name => $self->type('string', $milestone->name), + sort_key => $self->type('int', $milestone->sortkey), + is_active => $self->type('boolean', $milestone->is_active), + }, + undef, 'milestones'; } 1; diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm index c4bd3e605..2aed48e22 100644 --- a/Bugzilla/WebService/Server.pm +++ b/Bugzilla/WebService/Server.pm @@ -22,88 +22,93 @@ use Module::Runtime qw(require_module); use Try::Tiny; sub handle_login { - my ($self, $class, $method, $full_method) = @_; - # Throw error if the supplied class does not exist or the method is private - ThrowCodeError('unknown_method', {method => $full_method}) if (!$class or $method =~ /^_/); - - # We never want to create a new session unless the user is calling the - # login method. Setting dont_persist_session makes - # Bugzilla::Auth::_handle_login_result() skip calling persist_login(). - if ($full_method ne 'User.login') { - Bugzilla->request_cache->{dont_persist_session} = 1; - } - - try { - require_module($class); - } - catch { - ThrowCodeError('unknown_method', {method => $full_method}); - FATAL($_); - }; - return if ($class->login_exempt($method) - and !defined Bugzilla->input_params->{Bugzilla_login}); - Bugzilla->login(); - - Bugzilla::Hook::process( - 'webservice_before_call', - { 'method' => $method, full_method => $full_method }); + my ($self, $class, $method, $full_method) = @_; + + # Throw error if the supplied class does not exist or the method is private + ThrowCodeError('unknown_method', {method => $full_method}) + if (!$class or $method =~ /^_/); + + # We never want to create a new session unless the user is calling the + # login method. Setting dont_persist_session makes + # Bugzilla::Auth::_handle_login_result() skip calling persist_login(). + if ($full_method ne 'User.login') { + Bugzilla->request_cache->{dont_persist_session} = 1; + } + + try { + require_module($class); + } + catch { + ThrowCodeError('unknown_method', {method => $full_method}); + FATAL($_); + }; + return + if ($class->login_exempt($method) + and !defined Bugzilla->input_params->{Bugzilla_login}); + Bugzilla->login(); + + Bugzilla::Hook::process('webservice_before_call', + {'method' => $method, full_method => $full_method}); } sub datetime_format_inbound { - my ($self, $time) = @_; - - my $converted = datetime_from($time, Bugzilla->local_timezone); - if (!defined $converted) { - ThrowUserError('illegal_date', { date => $time }); - } - $time = $converted->ymd() . ' ' . $converted->hms(); - return $time + my ($self, $time) = @_; + + my $converted = datetime_from($time, Bugzilla->local_timezone); + if (!defined $converted) { + ThrowUserError('illegal_date', {date => $time}); + } + $time = $converted->ymd() . ' ' . $converted->hms(); + return $time; } sub datetime_format_outbound { - my ($self, $date) = @_; - - return undef if (!defined $date or $date eq ''); - - my $time = $date; - if (blessed($date)) { - # We expect this to mean we were sent a datetime object - $time->set_time_zone('UTC'); - } else { - # We always send our time in UTC, for consistency. - # passed in value is likely a string, create a datetime object - $time = datetime_from($date, 'UTC'); - } - return $time->iso8601(); + my ($self, $date) = @_; + + return undef if (!defined $date or $date eq ''); + + my $time = $date; + if (blessed($date)) { + + # We expect this to mean we were sent a datetime object + $time->set_time_zone('UTC'); + } + else { + # We always send our time in UTC, for consistency. + # passed in value is likely a string, create a datetime object + $time = datetime_from($date, 'UTC'); + } + return $time->iso8601(); } # ETag support sub bz_etag { - my ($self, $data) = @_; - my $cache = Bugzilla->request_cache; - - if (Bugzilla->request_cache->{bz_etag_disable}) { - return undef; + my ($self, $data) = @_; + my $cache = Bugzilla->request_cache; + + if (Bugzilla->request_cache->{bz_etag_disable}) { + return undef; + } + elsif (defined $data) { + + # Serialize the data if passed a reference + local $Storable::canonical = 1; + $data = freeze($data) if ref $data; + + # Wide characters cause md5_base64() to die. + utf8::encode($data) if utf8::is_utf8($data); + + # Append content_type to the end of the data + # string as we want the etag to be unique to + # the content_type. We do not need this for + # XMLRPC as text/xml is always returned. + if (blessed($self) && $self->can('content_type')) { + $data .= $self->content_type if $self->content_type; } - elsif (defined $data) { - # Serialize the data if passed a reference - local $Storable::canonical = 1; - $data = freeze($data) if ref $data; - - # Wide characters cause md5_base64() to die. - utf8::encode($data) if utf8::is_utf8($data); - - # Append content_type to the end of the data - # string as we want the etag to be unique to - # the content_type. We do not need this for - # XMLRPC as text/xml is always returned. - if (blessed($self) && $self->can('content_type')) { - $data .= $self->content_type if $self->content_type; - } - - $cache->{'bz_etag'} = md5_base64($data); - } - return $cache->{'bz_etag'}; + + $cache->{'bz_etag'} = md5_base64($data); + } + return $cache->{'bz_etag'}; } 1; diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm index 12a3143cc..ef00737ad 100644 --- a/Bugzilla/WebService/Server/JSONRPC.pm +++ b/Bugzilla/WebService/Server/JSONRPC.pm @@ -12,16 +12,17 @@ use strict; use warnings; use Bugzilla::WebService::Server; -BEGIN { - our @ISA = qw(Bugzilla::WebService::Server); - if (eval { require JSON::RPC::Server::CGI }) { - unshift(@ISA, 'JSON::RPC::Server::CGI'); - } - else { - require JSON::RPC::Legacy::Server::CGI; - unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); - } +BEGIN { + our @ISA = qw(Bugzilla::WebService::Server); + + if (eval { require JSON::RPC::Server::CGI }) { + unshift(@ISA, 'JSON::RPC::Server::CGI'); + } + else { + require JSON::RPC::Legacy::Server::CGI; + unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); + } } use Bugzilla::Error; @@ -40,88 +41,92 @@ use Bugzilla::WebService::JSON; ##################################### sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - Bugzilla->_json_server($self); - $self->dispatch(WS_DISPATCH); - $self->return_die_message(1); - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + Bugzilla->_json_server($self); + $self->dispatch(WS_DISPATCH); + $self->return_die_message(1); + return $self; } sub create_json_coder { - my $self = shift; - my $json = Bugzilla::WebService::JSON->new; - $json->allow_blessed(1); - $json->convert_blessed(1); - $json->allow_nonref(1); - # This may seem a little backwards, but what this really means is - # "don't convert our utf8 into byte strings, just leave it as a - # utf8 string." - $json->utf8(0) if Bugzilla->params->{'utf8'}; - return $json; + my $self = shift; + my $json = Bugzilla::WebService::JSON->new; + $json->allow_blessed(1); + $json->convert_blessed(1); + $json->allow_nonref(1); + + # This may seem a little backwards, but what this really means is + # "don't convert our utf8 into byte strings, just leave it as a + # utf8 string." + $json->utf8(0) if Bugzilla->params->{'utf8'}; + return $json; } # Override the JSON::RPC method to return our CGI object instead of theirs. sub cgi { return Bugzilla->cgi; } sub response_header { - my $self = shift; - # The HTTP body needs to be bytes (not a utf8 string) for recent - # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this - # properly. $_[1] is the HTTP body content we're going to be sending. - if (utf8::is_utf8($_[1])) { - utf8::encode($_[1]); - # Since we're going to just be sending raw bytes, we need to - # set STDOUT to not expect utf8. - disable_utf8(); - } - return $self->SUPER::response_header(@_); + my $self = shift; + + # The HTTP body needs to be bytes (not a utf8 string) for recent + # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this + # properly. $_[1] is the HTTP body content we're going to be sending. + if (utf8::is_utf8($_[1])) { + utf8::encode($_[1]); + + # Since we're going to just be sending raw bytes, we need to + # set STDOUT to not expect utf8. + disable_utf8(); + } + return $self->SUPER::response_header(@_); } sub response { - my ($self, $response) = @_; - my $cgi = $self->cgi; - - # Implement JSONP. - if (my $callback = $self->_bz_callback) { - my $content = $response->content; - if (blessed $content) { - $content = $content->encode; - } - # Prepend the JSONP response with /**/ in order to protect - # against possible encoding attacks (e.g., affecting Flash). - $response->content("/**/$callback($content)"); - } - - # Use $cgi->header properly instead of just printing text directly. - # This fixes various problems, including sending Bugzilla's cookies - # properly. - my $headers = $response->headers; - my @header_args; - foreach my $name ($headers->header_field_names) { - my @values = $headers->header($name); - $name =~ s/-/_/g; - foreach my $value (@values) { - push(@header_args, "-$name", $value); - } - } - - # ETag support - my $etag = $self->bz_etag; - if ($etag && $cgi->check_etag($etag)) { - push(@header_args, "-ETag", $etag); - print $cgi->header(-status => '304 Not Modified', @header_args); - } - else { - push(@header_args, "-ETag", $etag) if $etag; - print $cgi->header(-status => $response->code, @header_args); - my $content = $response->content; - if (blessed $content) { - $content = $content->encode; - utf8::encode($content); - } - print $content; - } + my ($self, $response) = @_; + my $cgi = $self->cgi; + + # Implement JSONP. + if (my $callback = $self->_bz_callback) { + my $content = $response->content; + if (blessed $content) { + $content = $content->encode; + } + + # Prepend the JSONP response with /**/ in order to protect + # against possible encoding attacks (e.g., affecting Flash). + $response->content("/**/$callback($content)"); + } + + # Use $cgi->header properly instead of just printing text directly. + # This fixes various problems, including sending Bugzilla's cookies + # properly. + my $headers = $response->headers; + my @header_args; + foreach my $name ($headers->header_field_names) { + my @values = $headers->header($name); + $name =~ s/-/_/g; + foreach my $value (@values) { + push(@header_args, "-$name", $value); + } + } + + # ETag support + my $etag = $self->bz_etag; + if ($etag && $cgi->check_etag($etag)) { + push(@header_args, "-ETag", $etag); + print $cgi->header(-status => '304 Not Modified', @header_args); + } + else { + push(@header_args, "-ETag", $etag) if $etag; + print $cgi->header(-status => $response->code, @header_args); + my $content = $response->content; + if (blessed $content) { + $content = $content->encode; + utf8::encode($content); + } + print $content; + } } # The JSON-RPC 1.1 GET specification is not so great--you can't specify @@ -133,70 +138,69 @@ sub response { # Base64 encoded, because that is ridiculous and obnoxious for JavaScript # clients. sub retrieve_json_from_get { - my $self = shift; - my $cgi = $self->cgi; - - my %input; - - # Both version and id must be set before any errors are thrown. - if ($cgi->param('version')) { - $self->version(scalar $cgi->param('version')); - $input{version} = $cgi->param('version'); - } - else { - $self->version('1.0'); - } - - # The JSON-RPC 2.0 spec says that any request that omits an id doesn't - # want a response. However, in an HTTP GET situation, it's stupid to - # expect all clients to specify some id parameter just to get a response, - # so we don't require it. - my $id; - if (defined $cgi->param('id')) { - $id = $cgi->param('id'); - } - # However, JSON::RPC does require that an id exist in most cases, in - # order to throw proper errors. We use the installation's urlbase as - # the id, in this case. - else { - $id = Bugzilla->localconfig->{urlbase}; - } - # Setting _bz_request_id here is required in case we throw errors early, - # before _handle. - $self->{_bz_request_id} = $input{id} = $id; - - # _bz_callback can throw an error, so we have to set it here, after we're - # ready to throw errors. - $self->_bz_callback(scalar $cgi->param('callback')); - - if (!$cgi->param('method')) { - ThrowUserError('json_rpc_get_method_required'); - } - $input{method} = $cgi->param('method'); - - my $params; - if (defined $cgi->param('params')) { - local $@; - $params = eval { - $self->json->decode(scalar $cgi->param('params')) - }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', - { params => scalar $cgi->param('params'), - err_msg => $@ }); - } - } - elsif (!$self->version or $self->version ne '1.1') { - $params = []; - } - else { - $params = {}; - } - - $input{params} = $params; - - my $json = $self->json->encode(\%input); - return $json; + my $self = shift; + my $cgi = $self->cgi; + + my %input; + + # Both version and id must be set before any errors are thrown. + if ($cgi->param('version')) { + $self->version(scalar $cgi->param('version')); + $input{version} = $cgi->param('version'); + } + else { + $self->version('1.0'); + } + + # The JSON-RPC 2.0 spec says that any request that omits an id doesn't + # want a response. However, in an HTTP GET situation, it's stupid to + # expect all clients to specify some id parameter just to get a response, + # so we don't require it. + my $id; + if (defined $cgi->param('id')) { + $id = $cgi->param('id'); + } + + # However, JSON::RPC does require that an id exist in most cases, in + # order to throw proper errors. We use the installation's urlbase as + # the id, in this case. + else { + $id = Bugzilla->localconfig->{urlbase}; + } + + # Setting _bz_request_id here is required in case we throw errors early, + # before _handle. + $self->{_bz_request_id} = $input{id} = $id; + + # _bz_callback can throw an error, so we have to set it here, after we're + # ready to throw errors. + $self->_bz_callback(scalar $cgi->param('callback')); + + if (!$cgi->param('method')) { + ThrowUserError('json_rpc_get_method_required'); + } + $input{method} = $cgi->param('method'); + + my $params; + if (defined $cgi->param('params')) { + local $@; + $params = eval { $self->json->decode(scalar $cgi->param('params')) }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', + {params => scalar $cgi->param('params'), err_msg => $@}); + } + } + elsif (!$self->version or $self->version ne '1.1') { + $params = []; + } + else { + $params = {}; + } + + $input{params} = $params; + + my $json = $self->json->encode(\%input); + return $json; } ####################################### @@ -204,72 +208,76 @@ sub retrieve_json_from_get { ####################################### sub type { - my ($self, $type, $value) = @_; - - # This is the only type that does something special with undef. - if ($type eq 'boolean') { - return $value ? JSON::true : JSON::false; - } - - return JSON::null if !defined $value; - - my $retval = $value; - - if ($type eq 'int') { - $retval = int($value); - } - if ($type eq 'double') { - $retval = 0.0 + $value; - } - elsif ($type eq 'string') { - # Forces string context, so that JSON will make it a string. - $retval = "$value"; - } - elsif ($type eq 'dateTime') { - # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T - $retval = $self->datetime_format_outbound($value); - } - elsif ($type eq 'base64') { - utf8::encode($value) if utf8::is_utf8($value); - $retval = encode_base64($value, ''); - } - elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { - $retval = email_filter($value); - } - - return $retval; + my ($self, $type, $value) = @_; + + # This is the only type that does something special with undef. + if ($type eq 'boolean') { + return $value ? JSON::true : JSON::false; + } + + return JSON::null if !defined $value; + + my $retval = $value; + + if ($type eq 'int') { + $retval = int($value); + } + if ($type eq 'double') { + $retval = 0.0 + $value; + } + elsif ($type eq 'string') { + + # Forces string context, so that JSON will make it a string. + $retval = "$value"; + } + elsif ($type eq 'dateTime') { + + # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T + $retval = $self->datetime_format_outbound($value); + } + elsif ($type eq 'base64') { + utf8::encode($value) if utf8::is_utf8($value); + $retval = encode_base64($value, ''); + } + elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { + $retval = email_filter($value); + } + + return $retval; } sub datetime_format_outbound { - my $self = shift; - # YUI expects ISO8601 in UTC time; including TZ specifier - return $self->SUPER::datetime_format_outbound(@_) . 'Z'; + my $self = shift; + + # YUI expects ISO8601 in UTC time; including TZ specifier + return $self->SUPER::datetime_format_outbound(@_) . 'Z'; } sub handle_login { - my $self = shift; - - # If we're being called using GET, we don't allow cookie-based or Env - # login, because GET requests can be done cross-domain, and we don't - # want private data showing up on another site unless the user - # explicitly gives that site their username and password. (This is - # particularly important for JSONP, which would allow a remote site - # to use private data without the user's knowledge, unless we had this - # protection in place.) - if ($self->request->method ne 'POST') { - # XXX There's no particularly good way for us to get a parameter - # to Bugzilla->login at this point, so we pass this information - # around using request_cache, which is a bit of a hack. The - # implementation of it is in Bugzilla::Auth::Login::Stack. - Bugzilla->request_cache->{auth_no_automatic_login} = 1; - } - - my $path = $self->path_info; - my $class = $self->{dispatch_path}->{$path}; - my $full_method = $self->_bz_method_name; - $full_method =~ /^\S+\.(\S+)/; - my $method = $1; - $self->SUPER::handle_login($class, $method, $full_method); + my $self = shift; + + # If we're being called using GET, we don't allow cookie-based or Env + # login, because GET requests can be done cross-domain, and we don't + # want private data showing up on another site unless the user + # explicitly gives that site their username and password. (This is + # particularly important for JSONP, which would allow a remote site + # to use private data without the user's knowledge, unless we had this + # protection in place.) + if ($self->request->method ne 'POST') { + + # XXX There's no particularly good way for us to get a parameter + # to Bugzilla->login at this point, so we pass this information + # around using request_cache, which is a bit of a hack. The + # implementation of it is in Bugzilla::Auth::Login::Stack. + Bugzilla->request_cache->{auth_no_automatic_login} = 1; + } + + my $path = $self->path_info; + my $class = $self->{dispatch_path}->{$path}; + my $full_method = $self->_bz_method_name; + $full_method =~ /^\S+\.(\S+)/; + my $method = $1; + $self->SUPER::handle_login($class, $method, $full_method); } ###################################### @@ -278,165 +286,165 @@ sub handle_login { # Store the ID of the current call, because Bugzilla::Error will need it. sub _handle { - my $self = shift; - my ($obj) = @_; - $self->{_bz_request_id} = $obj->{id}; + my $self = shift; + my ($obj) = @_; + $self->{_bz_request_id} = $obj->{id}; - my $result = $self->SUPER::_handle(@_); + my $result = $self->SUPER::_handle(@_); - # Set the ETag if not already set in the webservice methods. - my $etag = $self->bz_etag; - if (!$etag && ref $result) { - my $data = $self->json->decode($result)->{'result'}; - $self->bz_etag($data); - } + # Set the ETag if not already set in the webservice methods. + my $etag = $self->bz_etag; + if (!$etag && ref $result) { + my $data = $self->json->decode($result)->{'result'}; + $self->bz_etag($data); + } - return $result; + return $result; } # Make all error messages returned by JSON::RPC go into the 100000 # range, and bring down all our errors into the normal range. sub _error { - my ($self, $id, $code) = (shift, shift, shift); - # All JSON::RPC errors are less than 1000. - if ($code < 1000) { - $code += 100000; - } - # Bugzilla::Error adds 100,000 to all *our* errors, so - # we know they came from us. - elsif ($code > 100000) { - $code -= 100000; - } - - # We can't just set $_[1] because it's not always settable, - # in JSON::RPC::Server. - unshift(@_, $id, $code); - my $json = $self->SUPER::_error(@_); - - # We want to always send the JSON-RPC 1.1 error format, although - # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter. - if (!$self->version or $self->version ne '1.1') { - my $object = $self->json->decode($json); - my $message = $object->{error}; - # Just assure that future versions of JSON::RPC don't change the - # JSON-RPC 1.0 error format. - if (!ref $message) { - $object->{error} = { - code => $code, - message => $message, - }; - $json = $self->json->encode($object); - } - } - return $json; + my ($self, $id, $code) = (shift, shift, shift); + + # All JSON::RPC errors are less than 1000. + if ($code < 1000) { + $code += 100000; + } + + # Bugzilla::Error adds 100,000 to all *our* errors, so + # we know they came from us. + elsif ($code > 100000) { + $code -= 100000; + } + + # We can't just set $_[1] because it's not always settable, + # in JSON::RPC::Server. + unshift(@_, $id, $code); + my $json = $self->SUPER::_error(@_); + + # We want to always send the JSON-RPC 1.1 error format, although + # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter. + if (!$self->version or $self->version ne '1.1') { + my $object = $self->json->decode($json); + my $message = $object->{error}; + + # Just assure that future versions of JSON::RPC don't change the + # JSON-RPC 1.0 error format. + if (!ref $message) { + $object->{error} = {code => $code, message => $message,}; + $json = $self->json->encode($object); + } + } + return $json; } # This handles dispatching our calls to the appropriate class based on # the name of the method. sub _find_procedure { - my $self = shift; + my $self = shift; - my $method = shift; - $self->{_bz_method_name} = $method; + my $method = shift; + $self->{_bz_method_name} = $method; - # This tricks SUPER::_find_procedure into finding the right class. - $method =~ /^(\S+)\.(\S+)$/; - $self->path_info($1); - unshift(@_, $2); + # This tricks SUPER::_find_procedure into finding the right class. + $method =~ /^(\S+)\.(\S+)$/; + $self->path_info($1); + unshift(@_, $2); - return $self->SUPER::_find_procedure(@_); + return $self->SUPER::_find_procedure(@_); } # This is a hacky way to do something right before methods are called. # This is the last thing that JSON::RPC::Server::_handle calls right before # the method is actually called. sub _argument_type_check { - my $self = shift; - my $params = $self->SUPER::_argument_type_check(@_); - - # JSON-RPC 1.0 requires all parameters to be passed as an array, so - # we just pull out the first item and assume it's an object. - my $params_is_array; - if (ref $params eq 'ARRAY') { - $params = $params->[0]; - $params_is_array = 1; - } - - taint_data($params); - - # Now, convert dateTime fields on input. - $self->_bz_method_name =~ /^(\S+)\.(\S+)$/; - my ($class, $method) = ($1, $2); - my $pkg = $self->{dispatch_path}->{$class}; - my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] }; - foreach my $field (@date_fields) { - if (defined $params->{$field}) { - my $value = $params->{$field}; - if (ref $value eq 'ARRAY') { - $params->{$field} = - [ map { $self->datetime_format_inbound($_) } @$value ]; - } - else { - $params->{$field} = $self->datetime_format_inbound($value); - } - } - } - my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] }; - foreach my $field (@base64_fields) { - if (defined $params->{$field}) { - $params->{$field} = decode_base64($params->{$field}); - } - } - - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params, $self->cgi); - - Bugzilla->input_params($params); - - if ($self->request->method eq 'POST') { - # CSRF is possible via XMLHttpRequest when the Content-Type header - # is not application/json (for example: text/plain or - # application/x-www-form-urlencoded). - # application/json is the single official MIME type, per RFC 4627. - my $content_type = $self->cgi->content_type; - # The charset can be appended to the content type, so we use a regexp. - if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { - ThrowUserError('json_rpc_illegal_content_type', - { content_type => $content_type }); - } - } - else { - # When being called using GET, we don't allow calling - # methods that can change data. This protects us against cross-site - # request forgeries. - if (!grep($_ eq $method, $pkg->READ_ONLY)) { - ThrowUserError('json_rpc_post_only', - { method => $self->_bz_method_name }); - } - } - - # Only allowed methods to be used from our whitelist - if (none { $_ eq $method} $pkg->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $self->_bz_method_name }); - } - - # This is the best time to do login checks. - $self->handle_login(); - - # Bugzilla::WebService packages call internal methods like - # $self->_some_private_method. So we have to inherit from - # that class as well as this Server class. - my $new_class = ref($self) . '::' . $pkg; - my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; - eval "package $new_class;$isa_string;"; - bless $self, $new_class; - - if ($params_is_array) { - $params = [$params]; - } - - return $params; + my $self = shift; + my $params = $self->SUPER::_argument_type_check(@_); + + # JSON-RPC 1.0 requires all parameters to be passed as an array, so + # we just pull out the first item and assume it's an object. + my $params_is_array; + if (ref $params eq 'ARRAY') { + $params = $params->[0]; + $params_is_array = 1; + } + + taint_data($params); + + # Now, convert dateTime fields on input. + $self->_bz_method_name =~ /^(\S+)\.(\S+)$/; + my ($class, $method) = ($1, $2); + my $pkg = $self->{dispatch_path}->{$class}; + my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []}; + foreach my $field (@date_fields) { + if (defined $params->{$field}) { + my $value = $params->{$field}; + if (ref $value eq 'ARRAY') { + $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value]; + } + else { + $params->{$field} = $self->datetime_format_inbound($value); + } + } + } + my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []}; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); + } + } + + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params, $self->cgi); + + Bugzilla->input_params($params); + + if ($self->request->method eq 'POST') { + + # CSRF is possible via XMLHttpRequest when the Content-Type header + # is not application/json (for example: text/plain or + # application/x-www-form-urlencoded). + # application/json is the single official MIME type, per RFC 4627. + my $content_type = $self->cgi->content_type; + + # The charset can be appended to the content type, so we use a regexp. + if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { + ThrowUserError('json_rpc_illegal_content_type', + {content_type => $content_type}); + } + } + else { + # When being called using GET, we don't allow calling + # methods that can change data. This protects us against cross-site + # request forgeries. + if (!grep($_ eq $method, $pkg->READ_ONLY)) { + ThrowUserError('json_rpc_post_only', {method => $self->_bz_method_name}); + } + } + + # Only allowed methods to be used from our whitelist + if (none { $_ eq $method } $pkg->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $self->_bz_method_name}); + } + + # This is the best time to do login checks. + $self->handle_login(); + + # Bugzilla::WebService packages call internal methods like + # $self->_some_private_method. So we have to inherit from + # that class as well as this Server class. + my $new_class = ref($self) . '::' . $pkg; + my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; + eval "package $new_class;$isa_string;"; + bless $self, $new_class; + + if ($params_is_array) { + $params = [$params]; + } + + return $params; } ########################## @@ -445,22 +453,24 @@ sub _argument_type_check { # _bz_method_name is stored by _find_procedure for later use. sub _bz_method_name { - return $_[0]->{_bz_method_name}; + return $_[0]->{_bz_method_name}; } sub _bz_callback { - my ($self, $value) = @_; - if (defined $value) { - $value = trim($value); - # We don't use \w because we don't want to allow Unicode here. - if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { - ThrowUserError('json_rpc_invalid_callback', { callback => $value }); - } - $self->{_bz_callback} = $value; - # JSONP needs to be parsed by a JS parser, not by a JSON parser. - $self->content_type('text/javascript'); + my ($self, $value) = @_; + if (defined $value) { + $value = trim($value); + + # We don't use \w because we don't want to allow Unicode here. + if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { + ThrowUserError('json_rpc_invalid_callback', {callback => $value}); } - return $self->{_bz_callback}; + $self->{_bz_callback} = $value; + + # JSONP needs to be parsed by a JS parser, not by a JSON parser. + $self->content_type('text/javascript'); + } + return $self->{_bz_callback}; } 1; diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm index 5d8367410..781960c68 100644 --- a/Bugzilla/WebService/Server/REST.pm +++ b/Bugzilla/WebService/Server/REST.pm @@ -41,146 +41,146 @@ use Module::Runtime qw(require_module); ########################### sub handle { - my ($self) = @_; - - # Determine how the data should be represented. We do this early so - # errors will also be returned with the proper content type. - # If no accept header was sent or the content types specified were not - # matched, we default to the first type in the whitelist. - $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST())); - - # Using current path information, decide which class/method to - # use to serve the request. Throw error if no resource was found - # unless we were looking for OPTIONS - if (!$self->_find_resource($self->cgi->path_info)) { - if ($self->request->method eq 'OPTIONS' - && $self->bz_rest_options) - { - my $response = $self->response_header(STATUS_OK, ""); - my $options_string = join(', ', @{ $self->bz_rest_options }); - $response->header('Allow' => $options_string, - 'Access-Control-Allow-Methods' => $options_string); - return $self->response($response); - } - - ThrowUserError("rest_invalid_resource", - { path => $self->cgi->path_info, - method => $self->request->method }); + my ($self) = @_; + + # Determine how the data should be represented. We do this early so + # errors will also be returned with the proper content type. + # If no accept header was sent or the content types specified were not + # matched, we default to the first type in the whitelist. + $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST())); + + # Using current path information, decide which class/method to + # use to serve the request. Throw error if no resource was found + # unless we were looking for OPTIONS + if (!$self->_find_resource($self->cgi->path_info)) { + if ($self->request->method eq 'OPTIONS' && $self->bz_rest_options) { + my $response = $self->response_header(STATUS_OK, ""); + my $options_string = join(', ', @{$self->bz_rest_options}); + $response->header( + 'Allow' => $options_string, + 'Access-Control-Allow-Methods' => $options_string + ); + return $self->response($response); } - # Dispatch to the proper module - my $class = $self->bz_class_name; - my ($path) = $class =~ /::([^:]+)$/; - $self->path_info($path); - delete $self->{dispatch_path}; - $self->dispatch({ $path => $class }); + ThrowUserError("rest_invalid_resource", + {path => $self->cgi->path_info, method => $self->request->method}); + } - my $params = $self->_retrieve_json_params; + # Dispatch to the proper module + my $class = $self->bz_class_name; + my ($path) = $class =~ /::([^:]+)$/; + $self->path_info($path); + delete $self->{dispatch_path}; + $self->dispatch({$path => $class}); - fix_credentials($params, $self->cgi); + my $params = $self->_retrieve_json_params; - # Fix includes/excludes for each call - rest_include_exclude($params); + fix_credentials($params, $self->cgi); - # Set callback name if content-type is 'application/javascript' - if ($params->{'callback'} - || $self->content_type eq 'application/javascript') - { - $self->_bz_callback($params->{'callback'} || 'callback'); - } + # Fix includes/excludes for each call + rest_include_exclude($params); - Bugzilla->input_params($params); + # Set callback name if content-type is 'application/javascript' + if ($params->{'callback'} || $self->content_type eq 'application/javascript') { + $self->_bz_callback($params->{'callback'} || 'callback'); + } - # Set the JSON version to 1.1 and the id to the current urlbase - # also set up the correct handler method - my $obj = { - version => '1.1', - id => Bugzilla->localconfig->{urlbase}, - method => $self->bz_method_name, - params => $params - }; + Bugzilla->input_params($params); - # Execute the handler - my $result = $self->_handle($obj); + # Set the JSON version to 1.1 and the id to the current urlbase + # also set up the correct handler method + my $obj = { + version => '1.1', + id => Bugzilla->localconfig->{urlbase}, + method => $self->bz_method_name, + params => $params + }; - if (!$self->error_response_header) { - return $self->response( - $self->response_header($self->bz_success_code || STATUS_OK, $result)); - } + # Execute the handler + my $result = $self->_handle($obj); - $self->response($self->error_response_header); + if (!$self->error_response_header) { + return $self->response( + $self->response_header($self->bz_success_code || STATUS_OK, $result)); + } + + $self->response($self->error_response_header); } sub response { - my ($self, $response) = @_; - - # If we have thrown an error, the 'error' key will exist - # otherwise we use 'result'. JSONRPC returns other data - # along with the result/error such as version and id which - # we will strip off for REST calls. - my $content = $response->content; - - my $json_data = {}; - if ($content) { - # Content is in bytes at this point and needs to be converted - # back to utf8 string. - enable_utf8(); - utf8::decode($content) if !utf8::is_utf8($content); - $json_data = $self->json->decode($content); - } - - my $result = {}; - if (exists $json_data->{error}) { - $result = $json_data->{error}; - $result->{error} = $self->type('boolean', 1); - - $result->{documentation} = Bugzilla->params->{docs_urlbase} . "api/"; - delete $result->{'name'}; # Remove JSONRPCError - } - elsif (exists $json_data->{result}) { - $result = $json_data->{result}; - } - - Bugzilla::Hook::process('webservice_rest_response', - { rpc => $self, result => \$result, response => $response }); - - # Access Control - my @allowed_headers = qw(accept content-type origin user-agent x-requested-with); - foreach my $header (keys %{ API_AUTH_HEADERS() }) { - # We want to lowercase and replace _ with - - my $translated_header = $header; - $translated_header =~ tr/A-Z_/a-z\-/; - push(@allowed_headers, $translated_header); - } - $response->header("Access-Control-Allow-Origin", "*"); - $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers)); - - # ETag support - my $etag = $self->bz_etag; - $self->bz_etag($result) if !$etag; - - # If accessing through web browser, then display in readable format - if ($self->content_type eq 'text/html') { - $result = $self->json->pretty->canonical->allow_nonref->encode($result); - - my $template = Bugzilla->template; - $content = ""; - $result->encode if blessed $result; - $template->process("rest.html.tmpl", { result => $result }, \$content) - || ThrowTemplateError($template->error()); - - $response->content_type('text/html'); - } - else { - $content = $self->json->encode($result); - } - - utf8::encode($content) if utf8::is_utf8($content); - disable_utf8(); - - $response->content($content); - - $self->SUPER::response($response); + my ($self, $response) = @_; + + # If we have thrown an error, the 'error' key will exist + # otherwise we use 'result'. JSONRPC returns other data + # along with the result/error such as version and id which + # we will strip off for REST calls. + my $content = $response->content; + + my $json_data = {}; + if ($content) { + + # Content is in bytes at this point and needs to be converted + # back to utf8 string. + enable_utf8(); + utf8::decode($content) if !utf8::is_utf8($content); + $json_data = $self->json->decode($content); + } + + my $result = {}; + if (exists $json_data->{error}) { + $result = $json_data->{error}; + $result->{error} = $self->type('boolean', 1); + + $result->{documentation} = Bugzilla->params->{docs_urlbase} . "api/"; + delete $result->{'name'}; # Remove JSONRPCError + } + elsif (exists $json_data->{result}) { + $result = $json_data->{result}; + } + + Bugzilla::Hook::process('webservice_rest_response', + {rpc => $self, result => \$result, response => $response}); + + # Access Control + my @allowed_headers + = qw(accept content-type origin user-agent x-requested-with); + foreach my $header (keys %{API_AUTH_HEADERS()}) { + + # We want to lowercase and replace _ with - + my $translated_header = $header; + $translated_header =~ tr/A-Z_/a-z\-/; + push(@allowed_headers, $translated_header); + } + $response->header("Access-Control-Allow-Origin", "*"); + $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers)); + + # ETag support + my $etag = $self->bz_etag; + $self->bz_etag($result) if !$etag; + + # If accessing through web browser, then display in readable format + if ($self->content_type eq 'text/html') { + $result = $self->json->pretty->canonical->allow_nonref->encode($result); + + my $template = Bugzilla->template; + $content = ""; + $result->encode if blessed $result; + $template->process("rest.html.tmpl", {result => $result}, \$content) + || ThrowTemplateError($template->error()); + + $response->content_type('text/html'); + } + else { + $content = $self->json->encode($result); + } + + utf8::encode($content) if utf8::is_utf8($content); + disable_utf8(); + + $response->content($content); + + $self->SUPER::response($response); } ####################################### @@ -188,21 +188,21 @@ sub response { ####################################### sub handle_login { - my $self = shift; - my $class = $self->bz_class_name; - my $method = $self->bz_method_name; - my $full_method = $class . "." . $method; - $full_method =~ s/^Bugzilla::WebService:://; - - # We never want to create a new session unless the user is calling the - # login method. Setting dont_persist_session makes - # Bugzilla::Auth::_handle_login_result() skip calling persist_login(). - if ($full_method ne 'User.login') { - Bugzilla->request_cache->{dont_persist_session} = 1; - } - - # Bypass JSONRPC::handle_login - Bugzilla::WebService::Server->handle_login($class, $method, $full_method); + my $self = shift; + my $class = $self->bz_class_name; + my $method = $self->bz_method_name; + my $full_method = $class . "." . $method; + $full_method =~ s/^Bugzilla::WebService:://; + + # We never want to create a new session unless the user is calling the + # login method. Setting dont_persist_session makes + # Bugzilla::Auth::_handle_login_result() skip calling persist_login(). + if ($full_method ne 'User.login') { + Bugzilla->request_cache->{dont_persist_session} = 1; + } + + # Bypass JSONRPC::handle_login + Bugzilla::WebService::Server->handle_login($class, $method, $full_method); } ############################ @@ -212,79 +212,78 @@ sub handle_login { # We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure # as it determines the method name differently. sub _find_procedure { - my $self = shift; - if ($self->isa('JSON::RPC::Server::CGI')) { - return JSON::RPC::Server::_find_procedure($self, @_); - } - else { - return JSON::RPC::Legacy::Server::_find_procedure($self, @_); - } + my $self = shift; + if ($self->isa('JSON::RPC::Server::CGI')) { + return JSON::RPC::Server::_find_procedure($self, @_); + } + else { + return JSON::RPC::Legacy::Server::_find_procedure($self, @_); + } } sub _argument_type_check { - my $self = shift; - my $params; - - if ($self->isa('JSON::RPC::Server::CGI')) { - $params = JSON::RPC::Server::_argument_type_check($self, @_); - } - else { - $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_); - } - - # JSON-RPC 1.0 requires all parameters to be passed as an array, so - # we just pull out the first item and assume it's an object. - my $params_is_array; - if (ref $params eq 'ARRAY') { - $params = $params->[0]; - $params_is_array = 1; - } - - taint_data($params); - - # Now, convert dateTime fields on input. - my $method = $self->bz_method_name; - my $pkg = $self->{dispatch_path}->{$self->path_info}; - my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] }; - foreach my $field (@date_fields) { - if (defined $params->{$field}) { - my $value = $params->{$field}; - if (ref $value eq 'ARRAY') { - $params->{$field} = - [ map { $self->datetime_format_inbound($_) } @$value ]; - } - else { - $params->{$field} = $self->datetime_format_inbound($value); - } - } + my $self = shift; + my $params; + + if ($self->isa('JSON::RPC::Server::CGI')) { + $params = JSON::RPC::Server::_argument_type_check($self, @_); + } + else { + $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_); + } + + # JSON-RPC 1.0 requires all parameters to be passed as an array, so + # we just pull out the first item and assume it's an object. + my $params_is_array; + if (ref $params eq 'ARRAY') { + $params = $params->[0]; + $params_is_array = 1; + } + + taint_data($params); + + # Now, convert dateTime fields on input. + my $method = $self->bz_method_name; + my $pkg = $self->{dispatch_path}->{$self->path_info}; + my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []}; + foreach my $field (@date_fields) { + if (defined $params->{$field}) { + my $value = $params->{$field}; + if (ref $value eq 'ARRAY') { + $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value]; + } + else { + $params->{$field} = $self->datetime_format_inbound($value); + } } - my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] }; - foreach my $field (@base64_fields) { - if (defined $params->{$field}) { - $params->{$field} = decode_base64($params->{$field}); - } + } + my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []}; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); } + } - # This is the best time to do login checks. - $self->handle_login(); + # This is the best time to do login checks. + $self->handle_login(); - # Bugzilla::WebService packages call internal methods like - # $self->_some_private_method. So we have to inherit from - # that class as well as this Server class. - my $new_class = ref($self) . '::' . $pkg; - my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; - eval "package $new_class;$isa_string;"; - bless $self, $new_class; + # Bugzilla::WebService packages call internal methods like + # $self->_some_private_method. So we have to inherit from + # that class as well as this Server class. + my $new_class = ref($self) . '::' . $pkg; + my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; + eval "package $new_class;$isa_string;"; + bless $self, $new_class; - # Allow extensions to modify the params post login - Bugzilla::Hook::process('webservice_rest_request', - { rpc => $self, params => $params }); + # Allow extensions to modify the params post login + Bugzilla::Hook::process('webservice_rest_request', + {rpc => $self, params => $params}); - if ($params_is_array) { - $params = [$params]; - } + if ($params_is_array) { + $params = [$params]; + } - return $params; + return $params; } ################### @@ -292,46 +291,46 @@ sub _argument_type_check { ################### sub bz_method_name { - my ($self, $method) = @_; - $self->{_bz_method_name} = $method if $method; - return $self->{_bz_method_name}; + my ($self, $method) = @_; + $self->{_bz_method_name} = $method if $method; + return $self->{_bz_method_name}; } sub bz_class_name { - my ($self, $class) = @_; - $self->{_bz_class_name} = $class if $class; - return $self->{_bz_class_name}; + my ($self, $class) = @_; + $self->{_bz_class_name} = $class if $class; + return $self->{_bz_class_name}; } sub bz_success_code { - my ($self, $value) = @_; - $self->{_bz_success_code} = $value if $value; - return $self->{_bz_success_code}; + my ($self, $value) = @_; + $self->{_bz_success_code} = $value if $value; + return $self->{_bz_success_code}; } sub bz_rest_params { - my ($self, $params) = @_; - $self->{_bz_rest_params} = $params if $params; - return $self->{_bz_rest_params}; + my ($self, $params) = @_; + $self->{_bz_rest_params} = $params if $params; + return $self->{_bz_rest_params}; } sub bz_rest_options { - my ($self, $options) = @_; - $self->{_bz_rest_options} = $options if $options; - return [ sort { $a cmp $b } @{ $self->{_bz_rest_options} } ]; + my ($self, $options) = @_; + $self->{_bz_rest_options} = $options if $options; + return [sort { $a cmp $b } @{$self->{_bz_rest_options}}]; } sub rest_include_exclude { - my ($params) = @_; + my ($params) = @_; - if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) { - $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ]; - } - if (exists $params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { - $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ]; - } + if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) { + $params->{'include_fields'} = [split(/[\s+,]/, $params->{'include_fields'})]; + } + if (exists $params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { + $params->{'exclude_fields'} = [split(/[\s+,]/, $params->{'exclude_fields'})]; + } - return $params; + return $params; } ########################## @@ -339,187 +338,195 @@ sub rest_include_exclude { ########################## sub _retrieve_json_params { - my $self = shift; - - # Make a copy of the current input_params rather than edit directly - my $params = {}; - %{$params} = %{ Bugzilla->input_params }; - - # First add any parameters we were able to pull out of the path - # based on the resource regexp and combine with the normal URL - # parameters. - if (my $rest_params = $self->bz_rest_params) { - foreach my $param (keys %$rest_params) { - # If the param does not already exist or if the - # rest param is a single value, add it to the - # global params. - if (!exists $params->{$param} || !ref $rest_params->{$param}) { - $params->{$param} = $rest_params->{$param}; - } - # If rest_param is a list then add any extra values to the list - elsif (ref $rest_params->{$param}) { - my @extra_values = ref $params->{$param} - ? @{ $params->{$param} } - : ($params->{$param}); - $params->{$param} - = [ uniq (@{ $rest_params->{$param} }, @extra_values) ]; - } - } + my $self = shift; + + # Make a copy of the current input_params rather than edit directly + my $params = {}; + %{$params} = %{Bugzilla->input_params}; + + # First add any parameters we were able to pull out of the path + # based on the resource regexp and combine with the normal URL + # parameters. + if (my $rest_params = $self->bz_rest_params) { + foreach my $param (keys %$rest_params) { + + # If the param does not already exist or if the + # rest param is a single value, add it to the + # global params. + if (!exists $params->{$param} || !ref $rest_params->{$param}) { + $params->{$param} = $rest_params->{$param}; + } + + # If rest_param is a list then add any extra values to the list + elsif (ref $rest_params->{$param}) { + my @extra_values + = ref $params->{$param} ? @{$params->{$param}} : ($params->{$param}); + $params->{$param} = [uniq(@{$rest_params->{$param}}, @extra_values)]; + } + } + } + + # Any parameters passed in in the body of a non-GET request will override + # any parameters pull from the url path. Otherwise non-unique keys are + # combined. + if ($self->request->method ne 'GET') { + my $extra_params = {}; + + # We do this manually because CGI.pm doesn't understand JSON strings. + my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; + if ($json) { + eval { $extra_params = $self->json->utf8(0)->decode($json); }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', {err_msg => $@}); + } } - # Any parameters passed in in the body of a non-GET request will override - # any parameters pull from the url path. Otherwise non-unique keys are - # combined. - if ($self->request->method ne 'GET') { - my $extra_params = {}; - # We do this manually because CGI.pm doesn't understand JSON strings. - my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; - if ($json) { - eval { $extra_params = $self->json->utf8(0)->decode($json); }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', { err_msg => $@ }); - } - } - - # Allow parameters in the query string if request was non-GET. - # Note: parameters in query string body override any matching - # parameters in the request body. - foreach my $param ($self->cgi->url_param()) { - $extra_params->{$param} = $self->cgi->url_param($param); - } - - %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params}; + # Allow parameters in the query string if request was non-GET. + # Note: parameters in query string body override any matching + # parameters in the request body. + foreach my $param ($self->cgi->url_param()) { + $extra_params->{$param} = $self->cgi->url_param($param); } - return $params; + %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params}; + } + + return $params; } sub preload { - require_module($_) for values %{ WS_DISPATCH() }; + require_module($_) for values %{WS_DISPATCH()}; } sub _find_resource { - my ($self, $path) = @_; - - # Load in the WebService module from the dispatch map and then call - # $module->rest_resources to get the resources array ref. - my $resources = {}; - foreach my $module (values %{ $self->{dispatch_path} }) { - next if !$module->can('rest_resources'); - $resources->{$module} = $module->rest_resources; - } - - Bugzilla::Hook::process('webservice_rest_resources', - { rpc => $self, resources => $resources }) if Bugzilla::request_cache->{bzapi}; - - # Use the resources hash from each module loaded earlier to determine - # which handler to use based on a regex match of the CGI path. - # Also any matches found in the regex will be passed in later to the - # handler for possible use. - my $request_method = $self->request->method; - - my (@matches, $handler_found, $handler_method, $handler_class); - foreach my $class (keys %{ $resources }) { - # The resource data for each module needs to be - # an array ref with an even number of elements - # to work correctly. - next if (ref $resources->{$class} ne 'ARRAY' - || scalar @{ $resources->{$class} } % 2 != 0); - - while (my $regex = shift @{ $resources->{$class} }) { - my $options_data = shift @{ $resources->{$class} }; - next if ref $options_data ne 'HASH'; - - if (@matches = ($path =~ $regex)) { - # If a specific path is accompanied by a OPTIONS request - # method, the user is asking for a list of possible request - # methods for a specific path. - $self->bz_rest_options([ keys %{ $options_data } ]); - - if ($options_data->{$request_method}) { - my $resource_data = $options_data->{$request_method}; - $self->bz_class_name($class); - - # The method key/value can be a simple scalar method name - # or a anonymous subroutine so we execute it here. - my $method = ref $resource_data->{method} eq 'CODE' - ? $resource_data->{method}->($self) - : $resource_data->{method}; - $self->bz_method_name($method); - - # Pull out any parameters parsed from the URL path - # and store them for use by the method. - if ($resource_data->{params}) { - $self->bz_rest_params($resource_data->{params}->(@matches)); - } - - # If a special success code is needed for this particular - # method, then store it for later when generating response. - if ($resource_data->{success_code}) { - $self->bz_success_code($resource_data->{success_code}); - } - $handler_found = 1; - } - } - last if $handler_found; + my ($self, $path) = @_; + + # Load in the WebService module from the dispatch map and then call + # $module->rest_resources to get the resources array ref. + my $resources = {}; + foreach my $module (values %{$self->{dispatch_path}}) { + next if !$module->can('rest_resources'); + $resources->{$module} = $module->rest_resources; + } + + Bugzilla::Hook::process('webservice_rest_resources', + {rpc => $self, resources => $resources}) + if Bugzilla::request_cache->{bzapi}; + + # Use the resources hash from each module loaded earlier to determine + # which handler to use based on a regex match of the CGI path. + # Also any matches found in the regex will be passed in later to the + # handler for possible use. + my $request_method = $self->request->method; + + my (@matches, $handler_found, $handler_method, $handler_class); + foreach my $class (keys %{$resources}) { + + # The resource data for each module needs to be + # an array ref with an even number of elements + # to work correctly. + next + if (ref $resources->{$class} ne 'ARRAY' + || scalar @{$resources->{$class}} % 2 != 0); + + while (my $regex = shift @{$resources->{$class}}) { + my $options_data = shift @{$resources->{$class}}; + next if ref $options_data ne 'HASH'; + + if (@matches = ($path =~ $regex)) { + + # If a specific path is accompanied by a OPTIONS request + # method, the user is asking for a list of possible request + # methods for a specific path. + $self->bz_rest_options([keys %{$options_data}]); + + if ($options_data->{$request_method}) { + my $resource_data = $options_data->{$request_method}; + $self->bz_class_name($class); + + # The method key/value can be a simple scalar method name + # or a anonymous subroutine so we execute it here. + my $method + = ref $resource_data->{method} eq 'CODE' + ? $resource_data->{method}->($self) + : $resource_data->{method}; + $self->bz_method_name($method); + + # Pull out any parameters parsed from the URL path + # and store them for use by the method. + if ($resource_data->{params}) { + $self->bz_rest_params($resource_data->{params}->(@matches)); + } + + # If a special success code is needed for this particular + # method, then store it for later when generating response. + if ($resource_data->{success_code}) { + $self->bz_success_code($resource_data->{success_code}); + } + $handler_found = 1; } - last if $handler_found; + } + last if $handler_found; } + last if $handler_found; + } - return $handler_found; + return $handler_found; } sub _best_content_type { - my ($self, @types) = @_; - return ($self->_simple_content_negotiation(@types))[0] || '*/*'; + my ($self, @types) = @_; + return ($self->_simple_content_negotiation(@types))[0] || '*/*'; } sub _simple_content_negotiation { - my ($self, @types) = @_; - my @accept_types = $self->_get_content_prefs(); - # Return the types as-is if no accept header sent, since sorting will be a no-op. - if (!@accept_types) { - return @types; - } - my $score = sub { $self->_score_type(shift, @accept_types) }; - return sort {$score->($b) <=> $score->($a)} @types; + my ($self, @types) = @_; + my @accept_types = $self->_get_content_prefs(); + + # Return the types as-is if no accept header sent, since sorting will be a no-op. + if (!@accept_types) { + return @types; + } + my $score = sub { $self->_score_type(shift, @accept_types) }; + return sort { $score->($b) <=> $score->($a) } @types; } sub _score_type { - my ($self, $type, @accept_types) = @_; - my $score = scalar(@accept_types); - for my $accept_type (@accept_types) { - return $score if $type eq $accept_type; - $score--; - } - return 0; + my ($self, $type, @accept_types) = @_; + my $score = scalar(@accept_types); + for my $accept_type (@accept_types) { + return $score if $type eq $accept_type; + $score--; + } + return 0; } sub _get_content_prefs { - my $self = shift; - my $default_weight = 1; - my @prefs; - - # Parse the Accept header, and save type name, score, and position. - my @accept_types = split /,/, $self->cgi->http('accept') || ''; - my $order = 0; - for my $accept_type (@accept_types) { - my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/); - my ($name) = ($accept_type =~ m#(\S+/[^;]+)#); - next unless $name; - push @prefs, { name => $name, order => $order++}; - if (defined $weight) { - $prefs[-1]->{score} = $weight; - } else { - $prefs[-1]->{score} = $default_weight; - $default_weight -= 0.001; - } + my $self = shift; + my $default_weight = 1; + my @prefs; + + # Parse the Accept header, and save type name, score, and position. + my @accept_types = split /,/, $self->cgi->http('accept') || ''; + my $order = 0; + for my $accept_type (@accept_types) { + my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/); + my ($name) = ($accept_type =~ m#(\S+/[^;]+)#); + next unless $name; + push @prefs, {name => $name, order => $order++}; + if (defined $weight) { + $prefs[-1]->{score} = $weight; + } + else { + $prefs[-1]->{score} = $default_weight; + $default_weight -= 0.001; } + } - # Sort the types by score, subscore by order, and pull out just the name - @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} || - $a->{order} <=> $b->{order}} @prefs; - return @prefs; + # Sort the types by score, subscore by order, and pull out just the name + @prefs = map { $_->{name} } + sort { $b->{score} <=> $a->{score} || $a->{order} <=> $b->{order} } @prefs; + return @prefs; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm index 26aec011c..34580368d 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm @@ -15,177 +15,172 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Bug; BEGIN { - *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/bug$}, { - GET => { - method => 'search', - }, - POST => { - method => 'create', - status_code => STATUS_CREATED - } - }, - qr{^/bug/$}, { - GET => { - method => 'get' - } - }, - qr{^/bug/possible_duplicates$}, { - GET => { - method => 'possible_duplicates' - } - }, - qr{^/bug/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - return { ids => [ $_[0] ] }; - } - } - }, - qr{^/bug/([^/]+)/comment$}, { - GET => { - method => 'comments', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - POST => { - method => 'add_comment', - params => sub { - return { id => $_[0] }; - }, - success_code => STATUS_CREATED - } - }, - qr{^/bug/comment/(\d+)$}, { - GET => { - method => 'comments', - params => sub { - return { comment_ids => [ $_[0] ] }; - } - } - }, - qr{^/bug/comment/tags/([^/]+)$}, { - GET => { - method => 'search_comment_tags', - params => sub { - return { query => $_[0] }; - }, - }, - }, - qr{^/bug/comment/([^/]+)/tags$}, { - PUT => { - method => 'update_comment_tags', - params => sub { - return { comment_id => $_[0] }; - }, - }, - }, - qr{^/bug/comment/render$}, { - POST => { - method => 'render_comment', - }, - }, - qr{^/bug/([^/]+)/history$}, { - GET => { - method => 'history', - params => sub { - return { ids => [ $_[0] ] }; - }, - } - }, - qr{^/bug/([^/]+)/attachment$}, { - GET => { - method => 'attachments', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - POST => { - method => 'add_attachment', - params => sub { - return { ids => [ $_[0] ] }; - }, - success_code => STATUS_CREATED - } - }, - qr{^/bug/attachment/([^/]+)$}, { - GET => { - method => 'attachments', - params => sub { - return { attachment_ids => [ $_[0] ] }; - } - }, - PUT => { - method => 'update_attachment', - params => sub { - return { ids => [ $_[0] ] }; - } - } - }, - qr{^/field/bug$}, { - GET => { - method => 'fields', - } - }, - qr{^/field/bug/([^/]+)$}, { - GET => { - method => 'fields', - params => sub { - my $value = $_[0]; - my $param = 'names'; - $param = 'ids' if $value =~ /^\d+$/; - return { $param => [ $_[0] ] }; - } - } - }, - qr{^/field/bug/([^/]+)/values$}, { - GET => { - method => 'legal_values', - params => sub { - return { field => $_[0] }; - } - } - }, - qr{^/field/bug/([^/]+)/([^/]+)/values$}, { - GET => { - method => 'legal_values', - params => sub { - return { field => $_[0], - product_id => $_[1] }; - } - } - }, - qr{^/flag_types/([^/]+)/([^/]+)$}, { - GET => { - method => 'flag_types', - params => sub { - return { product => $_[0], - component => $_[1] }; - } - } - }, - qr{^/flag_types/([^/]+)$}, { - GET => { - method => 'flag_types', - params => sub { - return { product => $_[0] }; - } - } + my $rest_resources = [ + qr{^/bug$}, + { + GET => {method => 'search',}, + POST => {method => 'create', status_code => STATUS_CREATED} + }, + qr{^/bug/$}, + {GET => {method => 'get'}}, + qr{^/bug/possible_duplicates$}, + {GET => {method => 'possible_duplicates'}}, + qr{^/bug/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + return {ids => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + return {ids => [$_[0]]}; + } + } + }, + qr{^/bug/([^/]+)/comment$}, + { + GET => { + method => 'comments', + params => sub { + return {ids => [$_[0]]}; + } + }, + POST => { + method => 'add_comment', + params => sub { + return {id => $_[0]}; + }, + success_code => STATUS_CREATED + } + }, + qr{^/bug/comment/(\d+)$}, + { + GET => { + method => 'comments', + params => sub { + return {comment_ids => [$_[0]]}; + } + } + }, + qr{^/bug/comment/tags/([^/]+)$}, + { + GET => { + method => 'search_comment_tags', + params => sub { + return {query => $_[0]}; + }, + }, + }, + qr{^/bug/comment/([^/]+)/tags$}, + { + PUT => { + method => 'update_comment_tags', + params => sub { + return {comment_id => $_[0]}; + }, + }, + }, + qr{^/bug/comment/render$}, + {POST => {method => 'render_comment',},}, + qr{^/bug/([^/]+)/history$}, + { + GET => { + method => 'history', + params => sub { + return {ids => [$_[0]]}; + }, + } + }, + qr{^/bug/([^/]+)/attachment$}, + { + GET => { + method => 'attachments', + params => sub { + return {ids => [$_[0]]}; + } + }, + POST => { + method => 'add_attachment', + params => sub { + return {ids => [$_[0]]}; + }, + success_code => STATUS_CREATED + } + }, + qr{^/bug/attachment/([^/]+)$}, + { + GET => { + method => 'attachments', + params => sub { + return {attachment_ids => [$_[0]]}; + } + }, + PUT => { + method => 'update_attachment', + params => sub { + return {ids => [$_[0]]}; + } + } + }, + qr{^/field/bug$}, + {GET => {method => 'fields',}}, + qr{^/field/bug/([^/]+)$}, + { + GET => { + method => 'fields', + params => sub { + my $value = $_[0]; + my $param = 'names'; + $param = 'ids' if $value =~ /^\d+$/; + return {$param => [$_[0]]}; + } + } + }, + qr{^/field/bug/([^/]+)/values$}, + { + GET => { + method => 'legal_values', + params => sub { + return {field => $_[0]}; + } + } + }, + qr{^/field/bug/([^/]+)/([^/]+)/values$}, + { + GET => { + method => 'legal_values', + params => sub { + return {field => $_[0], product_id => $_[1]}; + } + } + }, + qr{^/flag_types/([^/]+)/([^/]+)$}, + { + GET => { + method => 'flag_types', + params => sub { + return {product => $_[0], component => $_[1]}; + } + } + }, + qr{^/flag_types/([^/]+)$}, + { + GET => { + method => 'flag_types', + params => sub { + return {product => $_[0]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm index 12290e84e..72aa0d40f 100644 --- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm +++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm @@ -12,36 +12,32 @@ use strict; use warnings; BEGIN { - *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; + *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; } sub _rest_resources { - return [ - # bug-id - qr{^/bug_user_last_visit/(\d+)$}, { - GET => { - method => 'get', - params => sub { - return { ids => $_[0] }; - }, - }, - POST => { - method => 'update', - params => sub { - return { ids => $_[0] }; - }, - }, + return [ + # bug-id + qr{^/bug_user_last_visit/(\d+)$}, + { + GET => { + method => 'get', + params => sub { + return {ids => $_[0]}; }, - # no bug-id - qr{^/bug_user_last_visit$}, { - GET => { - method => 'get', - }, - POST => { - method => 'update', - }, + }, + POST => { + method => 'update', + params => sub { + return {ids => $_[0]}; }, - ]; + }, + }, + + # no bug-id + qr{^/bug_user_last_visit$}, + {GET => {method => 'get',}, POST => {method => 'update',},}, + ]; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm index 646355cd3..28872f698 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm @@ -15,48 +15,20 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Bugzilla; BEGIN { - *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/version$}, { - GET => { - method => 'version' - } - }, - qr{^/extensions$}, { - GET => { - method => 'extensions' - } - }, - qr{^/timezone$}, { - GET => { - method => 'timezone' - } - }, - qr{^/time$}, { - GET => { - method => 'time' - } - }, - qr{^/last_audit_time$}, { - GET => { - method => 'last_audit_time' - } - }, - qr{^/parameters$}, { - GET => { - method => 'parameters' - } - }, - qr{^/jobqueue_status$}, { - GET => { - method => 'jobqueue_status' - } - } - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/version$}, {GET => {method => 'version'}}, + qr{^/extensions$}, {GET => {method => 'extensions'}}, + qr{^/timezone$}, {GET => {method => 'timezone'}}, + qr{^/time$}, {GET => {method => 'time'}}, + qr{^/last_audit_time$}, {GET => {method => 'last_audit_time'}}, + qr{^/parameters$}, {GET => {method => 'parameters'}}, + qr{^/jobqueue_status$}, {GET => {method => 'jobqueue_status'}} + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm index f20278f55..88ba028ba 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm @@ -15,22 +15,23 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Classification; BEGIN { - *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/classification/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/classification/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Elastic.pm b/Bugzilla/WebService/Server/REST/Resources/Elastic.pm index 2f7c1eaa4..367dd9134 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Elastic.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Elastic.pm @@ -15,16 +15,13 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Elastic; BEGIN { - *Bugzilla::WebService::Elastic::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Elastic::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/elastic/suggest_users$}, { - GET => { method => 'suggest_users' }, - }, - ]; - return $rest_resources; + my $rest_resources + = [qr{^/elastic/suggest_users$}, {GET => {method => 'suggest_users'},},]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm index 6e3d934eb..b6a1b9b34 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Group.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm @@ -15,38 +15,35 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Group; BEGIN { - *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/group$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/group/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/group$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/group/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm index 9ca6e3074..3222642c8 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Product.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm @@ -17,53 +17,41 @@ use Bugzilla::WebService::Product; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::Product::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Product::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/product_accessible$}, { - GET => { - method => 'get_accessible_products' - } - }, - qr{^/product_enterable$}, { - GET => { - method => 'get_enterable_products' - } - }, - qr{^/product_selectable$}, { - GET => { - method => 'get_selectable_products' - } - }, - qr{^/product$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/product/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } - }, - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/product_accessible$}, + {GET => {method => 'get_accessible_products'}}, + qr{^/product_enterable$}, + {GET => {method => 'get_enterable_products'}}, + qr{^/product_selectable$}, + {GET => {method => 'get_selectable_products'}}, + qr{^/product$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/product/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + } + }, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm index 6185237fb..ab5f78bde 100644 --- a/Bugzilla/WebService/Server/REST/Resources/User.pm +++ b/Bugzilla/WebService/Server/REST/Resources/User.pm @@ -15,71 +15,54 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::User; BEGIN { - *Bugzilla::WebService::User::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::User::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/user/suggest$}, { - GET => { - method => 'suggest', - }, - }, - qr{^/valid_login$}, { - GET => { - method => 'valid_login' - } - }, - qr{^/login$}, { - GET => { - method => 'login' - } - }, - qr{^/logout$}, { - GET => { - method => 'logout' - } - }, - qr{^/user$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/user/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } - }, - qr{^/user/mfa/([^/]+)/enroll$}, { - GET => { - method => 'mfa_enroll', - params => sub { - return { provider => $_[0] }; - } - }, - }, - qr{^/whoami$}, { - GET => { - method => 'whoami' - } + my $rest_resources = [ + qr{^/user/suggest$}, + {GET => {method => 'suggest',},}, + qr{^/valid_login$}, + {GET => {method => 'valid_login'}}, + qr{^/login$}, + {GET => {method => 'login'}}, + qr{^/logout$}, + {GET => {method => 'logout'}}, + qr{^/user$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/user/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + } + }, + qr{^/user/mfa/([^/]+)/enroll$}, + { + GET => { + method => 'mfa_enroll', + params => sub { + return {provider => $_[0]}; } - ]; - return $rest_resources; + }, + }, + qr{^/whoami$}, + {GET => {method => 'whoami'}} + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm index 5ad50e91c..ca2d119de 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -23,99 +23,102 @@ use Bugzilla::Util; use List::MoreUtils qw(none); BEGIN { - # Allow WebService methods to call XMLRPC::Lite's type method directly - *Bugzilla::WebService::type = sub { - my ($self, $type, $value) = @_; - if ($type eq 'dateTime') { - # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. - # Our "base" implementation is in Bugzilla::WebService::Server. - if (defined $value) { - $value = Bugzilla::WebService::Server->datetime_format_outbound($value); - $value =~ s/-//g; - } - else { - my ($pkg, $file, $line) = caller; - my $class = ref $self; - ERROR("$class->type($type, undef) called from $pkg ($file line $line)"); - } - } - elsif ($type eq 'email') { - $type = 'string'; - if (Bugzilla->params->{'webservice_email_filter'}) { - $value = email_filter($value); - } - } - return XMLRPC::Data->type($type)->value($value); - }; - - # Add support for ETags into XMLRPC WebServices - *Bugzilla::WebService::bz_etag = sub { - return Bugzilla::WebService::Server->bz_etag($_[1]); - }; + # Allow WebService methods to call XMLRPC::Lite's type method directly + *Bugzilla::WebService::type = sub { + my ($self, $type, $value) = @_; + if ($type eq 'dateTime') { + + # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. + # Our "base" implementation is in Bugzilla::WebService::Server. + if (defined $value) { + $value = Bugzilla::WebService::Server->datetime_format_outbound($value); + $value =~ s/-//g; + } + else { + my ($pkg, $file, $line) = caller; + my $class = ref $self; + ERROR("$class->type($type, undef) called from $pkg ($file line $line)"); + } + } + elsif ($type eq 'email') { + $type = 'string'; + if (Bugzilla->params->{'webservice_email_filter'}) { + $value = email_filter($value); + } + } + return XMLRPC::Data->type($type)->value($value); + }; + + # Add support for ETags into XMLRPC WebServices + *Bugzilla::WebService::bz_etag = sub { + return Bugzilla::WebService::Server->bz_etag($_[1]); + }; } sub initialize { - my $self = shift; - my %retval = $self->SUPER::initialize(@_); - $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; - $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; - $retval{'dispatch_with'} = WS_DISPATCH; - return %retval; + my $self = shift; + my %retval = $self->SUPER::initialize(@_); + $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; + $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; + $retval{'dispatch_with'} = WS_DISPATCH; + return %retval; } sub make_response { - my $self = shift; - my $cgi = Bugzilla->cgi; - - $self->SUPER::make_response(@_); - - # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around - # its cookies in Bugzilla::CGI, so we need to copy them over. - foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { - $self->response->headers->push_header('Set-Cookie', $cookie); - } - - # Copy across security related headers from Bugzilla::CGI - foreach my $header (split(/[\r\n]+/, $cgi->header)) { - my ($name, $value) = $header =~ /^([^:]+): (.*)/; - if (!$self->response->headers->header($name)) { - $self->response->headers->header($name => $value); - } - } - - # ETag support - my $etag = $self->bz_etag; - if (!$etag) { - my $data = $self->response->as_string; - $etag = $self->bz_etag($data); - } - - if ($etag && $cgi->check_etag($etag)) { - $self->response->headers->push_header('ETag', $etag); - $self->response->headers->push_header('status', '304 Not Modified'); - } - elsif ($etag) { - $self->response->headers->push_header('ETag', $etag); + my $self = shift; + my $cgi = Bugzilla->cgi; + + $self->SUPER::make_response(@_); + + # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around + # its cookies in Bugzilla::CGI, so we need to copy them over. + foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { + $self->response->headers->push_header('Set-Cookie', $cookie); + } + + # Copy across security related headers from Bugzilla::CGI + foreach my $header (split(/[\r\n]+/, $cgi->header)) { + my ($name, $value) = $header =~ /^([^:]+): (.*)/; + if (!$self->response->headers->header($name)) { + $self->response->headers->header($name => $value); } + } + + # ETag support + my $etag = $self->bz_etag; + if (!$etag) { + my $data = $self->response->as_string; + $etag = $self->bz_etag($data); + } + + if ($etag && $cgi->check_etag($etag)) { + $self->response->headers->push_header('ETag', $etag); + $self->response->headers->push_header('status', '304 Not Modified'); + } + elsif ($etag) { + $self->response->headers->push_header('ETag', $etag); + } } sub handle_login { - my ($self, $classes, $action, $uri, $method) = @_; - my $class = $classes->{$uri}; - if (!$class) { - ThrowCodeError('unknown_method', { method => $method eq 'methodName' ? '' : '.' . $method }); - } - my $full_method = $uri . "." . $method; - # Only allowed methods to be used from the module's whitelist - my $file = $class; - $file =~ s{::}{/}g; - $file .= ".pm"; - require $file; - if (none { $_ eq $method } $class->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $full_method }); - } - $self->SUPER::handle_login($class, $method, $full_method); - return; + my ($self, $classes, $action, $uri, $method) = @_; + my $class = $classes->{$uri}; + if (!$class) { + ThrowCodeError('unknown_method', + {method => $method eq 'methodName' ? '' : '.' . $method}); + } + my $full_method = $uri . "." . $method; + + # Only allowed methods to be used from the module's whitelist + my $file = $class; + $file =~ s{::}{/}g; + $file .= ".pm"; + require $file; + if (none { $_ eq $method } $class->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $full_method}); + } + $self->SUPER::handle_login($class, $method, $full_method); + return; } 1; @@ -124,6 +127,7 @@ sub handle_login { # and also, in some cases, to more-usefully decode them. package Bugzilla::XMLRPC::Deserializer; use strict; + # We can't use "use base" because XMLRPC::Serializer doesn't return # a true value. use XMLRPC::Lite; @@ -135,100 +139,111 @@ use Bugzilla::WebService::Util qw(fix_credentials); use Scalar::Util qw(tainted); sub new { - my $self = shift->SUPER::new(@_); - # Initialise XML::Parser to not expand references to entities, to prevent DoS - require XML::Parser; - my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } ); - $self->{_parser}->parser($parser, $parser); - return $self; + my $self = shift->SUPER::new(@_); + + # Initialise XML::Parser to not expand references to entities, to prevent DoS + require XML::Parser; + my $parser = XML::Parser->new( + NoExpand => 1, + Handlers => { + Default => sub { } + } + ); + $self->{_parser}->parser($parser, $parser); + return $self; } sub deserialize { - my $self = shift; - - # Only allow certain content types to protect against CSRF attacks - my $content_type = lc($ENV{'CONTENT_TYPE'}); - # Remove charset, etc, if provided - $content_type =~ s/^([^;]+);.*/$1/; - if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { - ThrowUserError('xmlrpc_illegal_content_type', - { content_type => $ENV{'CONTENT_TYPE'} }); - } + my $self = shift; - my ($xml) = @_; - my $som = $self->SUPER::deserialize(@_); - if (tainted($xml)) { - $som->{_bz_do_taint} = 1; - } - bless $som, 'Bugzilla::XMLRPC::SOM'; - my $params = $som->paramsin; - # This allows positional parameters for Testopia. - $params = {} if ref $params ne 'HASH'; + # Only allow certain content types to protect against CSRF attacks + my $content_type = lc($ENV{'CONTENT_TYPE'}); + + # Remove charset, etc, if provided + $content_type =~ s/^([^;]+);.*/$1/; + if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { + ThrowUserError('xmlrpc_illegal_content_type', + {content_type => $ENV{'CONTENT_TYPE'}}); + } + + my ($xml) = @_; + my $som = $self->SUPER::deserialize(@_); + if (tainted($xml)) { + $som->{_bz_do_taint} = 1; + } + bless $som, 'Bugzilla::XMLRPC::SOM'; + my $params = $som->paramsin; + + # This allows positional parameters for Testopia. + $params = {} if ref $params ne 'HASH'; - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params); + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); - Bugzilla->input_params($params); + Bugzilla->input_params($params); - return $som; + return $som; } # Some method arguments need to be converted in some way, when they are input. sub decode_value { - my $self = shift; - my ($type) = @{ $_[0] }; - my $value = $self->SUPER::decode_value(@_); - - # We only validate/convert certain types here. - return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; - - # Though the XML-RPC standard doesn't allow an empty <int>, - # <double>,or <dateTime.iso8601>, we do, and we just say - # "that's undef". - if (grep($type eq $_, qw(int double dateTime))) { - return undef if $value eq ''; - } - - my $validator = $self->_validation_subs->{$type}; - if (!$validator->($value)) { - ThrowUserError('xmlrpc_invalid_value', - { type => $type, value => $value }); - } - - # We convert dateTimes to a DB-friendly date format. - if ($type eq 'dateTime.iso8601') { - if ($value !~ /T.*[\-+Z]/i) { - # The caller did not specify a timezone, so we assume UTC. - # pass 'Z' specifier to datetime_from to force it - $value = $value . 'Z'; - } - $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + my $self = shift; + my ($type) = @{$_[0]}; + my $value = $self->SUPER::decode_value(@_); + + # We only validate/convert certain types here. + return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; + + # Though the XML-RPC standard doesn't allow an empty <int>, + # <double>,or <dateTime.iso8601>, we do, and we just say + # "that's undef". + if (grep($type eq $_, qw(int double dateTime))) { + return undef if $value eq ''; + } + + my $validator = $self->_validation_subs->{$type}; + if (!$validator->($value)) { + ThrowUserError('xmlrpc_invalid_value', {type => $type, value => $value}); + } + + # We convert dateTimes to a DB-friendly date format. + if ($type eq 'dateTime.iso8601') { + if ($value !~ /T.*[\-+Z]/i) { + + # The caller did not specify a timezone, so we assume UTC. + # pass 'Z' specifier to datetime_from to force it + $value = $value . 'Z'; } + $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + } - return $value; + return $value; } sub _validation_subs { - my $self = shift; - return $self->{_validation_subs} if $self->{_validation_subs}; - # The only place that XMLRPC::Lite stores any sort of validation - # regex is in XMLRPC::Serializer. We want to re-use those regexes here. - my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; - - # $lookup is a hash whose values are arrayrefs, and whose keys are the - # names of types. The second item of each arrayref is a subroutine - # that will do our validation for us. - my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); - # Add a boolean validator - $validators{'boolean'} = sub {$_[0] =~ /^[01]$/}; - # Some types have multiple names, or have a different name in - # XMLRPC::Serializer than their standard XML-RPC name. - $validators{'dateTime.iso8601'} = $validators{'dateTime'}; - $validators{'i4'} = $validators{'int'}; - - $self->{_validation_subs} = \%validators; - return \%validators; + my $self = shift; + return $self->{_validation_subs} if $self->{_validation_subs}; + + # The only place that XMLRPC::Lite stores any sort of validation + # regex is in XMLRPC::Serializer. We want to re-use those regexes here. + my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; + + # $lookup is a hash whose values are arrayrefs, and whose keys are the + # names of types. The second item of each arrayref is a subroutine + # that will do our validation for us. + my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); + + # Add a boolean validator + $validators{'boolean'} = sub { $_[0] =~ /^[01]$/ }; + + # Some types have multiple names, or have a different name in + # XMLRPC::Serializer than their standard XML-RPC name. + $validators{'dateTime.iso8601'} = $validators{'dateTime'}; + $validators{'i4'} = $validators{'int'}; + + $self->{_validation_subs} = \%validators; + return \%validators; } 1; @@ -240,16 +255,16 @@ our @ISA = qw(XMLRPC::SOM); use Bugzilla::WebService::Util qw(taint_data); sub paramsin { - my $self = shift; - if (!$self->{bz_params_in}) { - my @params = $self->SUPER::paramsin(@_); - if ($self->{_bz_do_taint}) { - taint_data(@params); - } - $self->{bz_params_in} = \@params; + my $self = shift; + if (!$self->{bz_params_in}) { + my @params = $self->SUPER::paramsin(@_); + if ($self->{_bz_do_taint}) { + taint_data(@params); } - my $params = $self->{bz_params_in}; - return wantarray ? @$params : $params->[0]; + $self->{bz_params_in} = \@params; + } + my $params = $self->{bz_params_in}; + return wantarray ? @$params : $params->[0]; } 1; @@ -259,43 +274,46 @@ sub paramsin { package Bugzilla::XMLRPC::Serializer; use Scalar::Util qw(blessed); use strict; + # We can't use "use base" because XMLRPC::Serializer doesn't return # a true value. use XMLRPC::Lite; our @ISA = qw(XMLRPC::Serializer); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - # This fixes UTF-8. - $self->{'_typelookup'}->{'base64'} = - [10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/}, - 'as_base64']; - # This makes arrays work right even though we're a subclass. - # (See http://rt.cpan.org//Ticket/Display.html?id=34514) - $self->{'_encodingStyle'} = ''; - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + + # This fixes UTF-8. + $self->{'_typelookup'}->{'base64'} = [ + 10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/ }, + 'as_base64' + ]; + + # This makes arrays work right even though we're a subclass. + # (See http://rt.cpan.org//Ticket/Display.html?id=34514) + $self->{'_encodingStyle'} = ''; + return $self; } # Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension. sub encode_object { - my $self = shift; - my @encoded = $self->SUPER::encode_object(@_); + my $self = shift; + my @encoded = $self->SUPER::encode_object(@_); - return $encoded[0]->[0] eq 'nil' - ? ['value', {}, [@encoded]] - : @encoded; + return $encoded[0]->[0] eq 'nil' ? ['value', {}, [@encoded]] : @encoded; } # Removes undefined values so they do not produce invalid XMLRPC. sub envelope { - my $self = shift; - my ($type, $method, $data) = @_; - # If the type isn't a successful response we don't want to change the values. - if ($type eq 'response'){ - $data = _strip_undefs($data); - } - return $self->SUPER::envelope($type, $method, $data); + my $self = shift; + my ($type, $method, $data) = @_; + + # If the type isn't a successful response we don't want to change the values. + if ($type eq 'response') { + $data = _strip_undefs($data); + } + return $self->SUPER::envelope($type, $method, $data); } # In an XMLRPC response we have to handle hashes of arrays, hashes, scalars, @@ -303,57 +321,57 @@ sub envelope { # The whole XMLRPC::Data object must be removed if its value key is undefined # so it cannot be recursed like the other hash type objects. sub _strip_undefs { - my ($initial) = @_; - if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) { - while (my ($key, $value) = each(%$initial)) { - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the hash. - delete $initial->{$key}; - } - else { - $initial->{$key} = _strip_undefs($value); - } - } + my ($initial) = @_; + if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) { + while (my ($key, $value) = each(%$initial)) { + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the hash. + delete $initial->{$key}; + } + else { + $initial->{$key} = _strip_undefs($value); + } } - if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) { - for (my $count = 0; $count < scalar @{$initial}; $count++) { - my $value = $initial->[$count]; - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the array. - splice(@$initial, $count, 1); - $count--; - } - else { - $initial->[$count] = _strip_undefs($value); - } - } + } + if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) { + for (my $count = 0; $count < scalar @{$initial}; $count++) { + my $value = $initial->[$count]; + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the array. + splice(@$initial, $count, 1); + $count--; + } + else { + $initial->[$count] = _strip_undefs($value); + } } - return $initial; + } + return $initial; } sub BEGIN { - no strict 'refs'; - for my $type (qw(double i4 int dateTime)) { - my $method = 'as_' . $type; - *$method = sub { - my ($self, $value) = @_; - if (!defined($value)) { - return as_nil(); - } - else { - my $super_method = "SUPER::$method"; - return $self->$super_method($value); - } - } - } + no strict 'refs'; + for my $type (qw(double i4 int dateTime)) { + my $method = 'as_' . $type; + *$method = sub { + my ($self, $value) = @_; + if (!defined($value)) { + return as_nil(); + } + else { + my $super_method = "SUPER::$method"; + return $self->$super_method($value); + } + } + } } sub as_nil { - return ['nil', {}]; + return ['nil', {}]; } 1; diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index c569cf9d8..1e5127f8a 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -20,44 +20,38 @@ use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Util qw(trim detaint_natural); use Bugzilla::WebService::Util qw(filter filter_wants validate - translate params_to_objects); + translate params_to_objects); use Bugzilla::Hook; use List::Util qw(first); use Taint::Util qw(untaint); # Don't need auth to login -use constant LOGIN_EXEMPT => { - login => 1, - offer_account_by_email => 1, -}; +use constant LOGIN_EXEMPT => {login => 1, offer_account_by_email => 1,}; use constant READ_ONLY => qw( - get - suggest + get + suggest ); use constant PUBLIC_METHODS => qw( - create - get - login - logout - offer_account_by_email - update - valid_login - whoami + create + get + login + logout + offer_account_by_email + update + valid_login + whoami ); -use constant MAPPED_FIELDS => { - email => 'login', - full_name => 'name', - login_denied_text => 'disabledtext', -}; +use constant MAPPED_FIELDS => + {email => 'login', full_name => 'name', login_denied_text => 'disabledtext',}; use constant MAPPED_RETURNS => { - login_name => 'email', - realname => 'full_name', - disabledtext => 'login_denied_text', + login_name => 'email', + realname => 'full_name', + disabledtext => 'login_denied_text', }; ############## @@ -65,38 +59,38 @@ use constant MAPPED_RETURNS => { ############## sub login { - my ($self, $params) = @_; + my ($self, $params) = @_; - # Check to see if we are already logged in - my $user = Bugzilla->user; - if ($user->id) { - return $self->_login_to_hash($user); - } + # Check to see if we are already logged in + my $user = Bugzilla->user; + if ($user->id) { + return $self->_login_to_hash($user); + } - # Username and password params are required - foreach my $param ("login", "password") { - (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) - || ThrowCodeError('param_required', { param => $param }); - } + # Username and password params are required + foreach my $param ("login", "password") { + (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) + || ThrowCodeError('param_required', {param => $param}); + } - $user = Bugzilla->login(); - return $self->_login_to_hash($user); + $user = Bugzilla->login(); + return $self->_login_to_hash($user); } sub logout { - my $self = shift; - Bugzilla->logout; + my $self = shift; + Bugzilla->logout; } sub valid_login { - my ($self, $params) = @_; - defined $params->{login} - || ThrowCodeError('param_required', { param => 'login' }); - Bugzilla->login(); - if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { - return $self->type('boolean', 1); - } - return $self->type('boolean', 0); + my ($self, $params) = @_; + defined $params->{login} + || ThrowCodeError('param_required', {param => 'login'}); + Bugzilla->login(); + if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { + return $self->type('boolean', 1); + } + return $self->type('boolean', 0); } ################# @@ -104,102 +98,100 @@ sub valid_login { ################# sub offer_account_by_email { - my $self = shift; - my ($params) = @_; - my $email = trim($params->{email}) - || ThrowCodeError('param_required', { param => 'email' }); - - Bugzilla->user->check_account_creation_enabled; - Bugzilla->user->check_and_send_account_creation_confirmation($email); - return undef; + my $self = shift; + my ($params) = @_; + my $email = trim($params->{email}) + || ThrowCodeError('param_required', {param => 'email'}); + + Bugzilla->user->check_account_creation_enabled; + Bugzilla->user->check_and_send_account_creation_confirmation($email); + return undef; } sub create { - my $self = shift; - my ($params) = @_; - - Bugzilla->user->in_group('editusers') - || ThrowUserError("auth_failure", { group => "editusers", - action => "add", - object => "users"}); - - my $email = trim($params->{email}) - || ThrowCodeError('param_required', { param => 'email' }); - my $realname = trim($params->{full_name}); - my $password = trim($params->{password}) || '*'; - - my $user = Bugzilla::User->create({ - login_name => $email, - realname => $realname, - cryptpassword => $password - }); - - return { id => $self->type('int', $user->id) }; -} + my $self = shift; + my ($params) = @_; -sub suggest { - my ($self, $params) = @_; + Bugzilla->user->in_group('editusers') + || ThrowUserError("auth_failure", + {group => "editusers", action => "add", object => "users"}); - Bugzilla->switch_to_shadow_db(); + my $email = trim($params->{email}) + || ThrowCodeError('param_required', {param => 'email'}); + my $realname = trim($params->{full_name}); + my $password = trim($params->{password}) || '*'; - ThrowCodeError('params_required', { function => 'User.suggest', params => ['match'] }) - unless defined $params->{match}; - - ThrowUserError('user_access_by_match_denied') - unless Bugzilla->user->id; - - untaint($params->{match}); - my $s = $params->{match}; - trim($s); - return { users => [] } if length($s) < 3; + my $user + = Bugzilla::User->create({ + login_name => $email, realname => $realname, cryptpassword => $password + }); - my $dbh = Bugzilla->dbh; - my @select = ('userid AS id', 'realname AS real_name', 'login_name AS name'); - my $order = 'last_seen_date DESC'; - my $where; - state $have_mysql = $dbh->isa('Bugzilla::DB::Mysql'); + return {id => $self->type('int', $user->id)}; +} - if ($s =~ /^[:@](.+)$/s) { - $where = $dbh->sql_prefix_match(nickname => $1); +sub suggest { + my ($self, $params) = @_; + + Bugzilla->switch_to_shadow_db(); + + ThrowCodeError('params_required', + {function => 'User.suggest', params => ['match']}) + unless defined $params->{match}; + + ThrowUserError('user_access_by_match_denied') unless Bugzilla->user->id; + + untaint($params->{match}); + my $s = $params->{match}; + trim($s); + return {users => []} if length($s) < 3; + + my $dbh = Bugzilla->dbh; + my @select = ('userid AS id', 'realname AS real_name', 'login_name AS name'); + my $order = 'last_seen_date DESC'; + my $where; + state $have_mysql = $dbh->isa('Bugzilla::DB::Mysql'); + + if ($s =~ /^[:@](.+)$/s) { + $where = $dbh->sql_prefix_match(nickname => $1); + } + elsif ($s =~ /@/) { + $where = $dbh->sql_prefix_match(login_name => $s); + } + else { + if ($have_mysql && ($s =~ /[[:space:]]/ || $s =~ /[^[:ascii:]]/)) { + my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s); + push @select, "$match AS relevance"; + $order = 'relevance DESC'; + $where = $match; } - elsif ($s =~ /@/) { - $where = $dbh->sql_prefix_match(login_name => $s); + elsif ($have_mysql && $s =~ /^[[:upper:]]/) { + my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s); + $where = join ' OR ', $match, $dbh->sql_prefix_match(nickname => $s), + $dbh->sql_prefix_match(login_name => $s); } else { - if ($have_mysql && ( $s =~ /[[:space:]]/ || $s =~ /[^[:ascii:]]/ ) ) { - my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s); - push @select, "$match AS relevance"; - $order = 'relevance DESC'; - $where = $match; - } - elsif ($have_mysql && $s =~ /^[[:upper:]]/) { - my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s); - $where = join ' OR ', - $match, - $dbh->sql_prefix_match( nickname => $s ), - $dbh->sql_prefix_match( login_name => $s ); - } - else { - $where = join ' OR ', $dbh->sql_prefix_match( nickname => $s ), $dbh->sql_prefix_match( login_name => $s ); - } + $where = join ' OR ', $dbh->sql_prefix_match(nickname => $s), + $dbh->sql_prefix_match(login_name => $s); } - $where = "($where) AND is_enabled = 1"; + } + $where = "($where) AND is_enabled = 1"; - my $sql = 'SELECT ' . join(', ', @select) . " FROM profiles WHERE $where ORDER BY $order LIMIT 25"; - my $results = $dbh->selectall_arrayref($sql, { Slice => {} }); + my $sql + = 'SELECT ' + . join(', ', @select) + . " FROM profiles WHERE $where ORDER BY $order LIMIT 25"; + my $results = $dbh->selectall_arrayref($sql, {Slice => {}}); - my @users = map { - { - id => $self->type(int => $_->{id}), - real_name => $self->type(string => $_->{real_name}), - name => $self->type(email => $_->{name}), - } - } @$results; + my @users = map { { + id => $self->type(int => $_->{id}), + real_name => $self->type(string => $_->{real_name}), + name => $self->type(email => $_->{name}), + } } @$results; - Bugzilla::Hook::process('webservice_user_suggest', - { webservice => $self, params => $params, users => \@users }); + Bugzilla::Hook::process('webservice_user_suggest', + {webservice => $self, params => $params, users => \@users}); - return { users => \@users }; + return {users => \@users}; } # function to return user information by passing either user ids or @@ -207,125 +199,132 @@ sub suggest { # $call = $rpc->call( 'User.get', { ids => [1,2,3], # names => ['testusera@redhat.com', 'testuserb@redhat.com'] }); sub get { - my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); + my ($self, $params) + = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - defined($params->{names}) || defined($params->{ids}) - || defined($params->{match}) - || ThrowCodeError('params_required', - { function => 'User.get', params => ['ids', 'names', 'match'] }); + defined($params->{names}) + || defined($params->{ids}) + || defined($params->{match}) + || ThrowCodeError('params_required', + {function => 'User.get', params => ['ids', 'names', 'match']}); - my @user_objects; - @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} } - if $params->{names}; + my @user_objects; + @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}} + if $params->{names}; - # start filtering to remove duplicate user ids - my %unique_users = map { $_->id => $_ } @user_objects; - @user_objects = values %unique_users; + # start filtering to remove duplicate user ids + my %unique_users = map { $_->id => $_ } @user_objects; + @user_objects = values %unique_users; - my @users; + my @users; - # If the user is not logged in: Return an error if they passed any user ids. - # Otherwise, return a limited amount of information based on login names. - if (!Bugzilla->user->id){ - if ($params->{ids}){ - ThrowUserError("user_access_by_id_denied"); - } - if ($params->{match}) { - ThrowUserError('user_access_by_match_denied'); - } - my $in_group = $self->_filter_users_by_group( - \@user_objects, $params); - @users = map { filter $params, { - id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('email', $_->login), - } } @$in_group; - - return { users => \@users }; + # If the user is not logged in: Return an error if they passed any user ids. + # Otherwise, return a limited amount of information based on login names. + if (!Bugzilla->user->id) { + if ($params->{ids}) { + ThrowUserError("user_access_by_id_denied"); } - - my $obj_by_ids; - $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; - - # obj_by_ids are only visible to the user if he can see - # the otheruser, for non visible otheruser throw an error - foreach my $obj (@$obj_by_ids) { - if (Bugzilla->user->can_see_user($obj)){ - if (!$unique_users{$obj->id}) { - push (@user_objects, $obj); - $unique_users{$obj->id} = $obj; - } + if ($params->{match}) { + ThrowUserError('user_access_by_match_denied'); + } + my $in_group = $self->_filter_users_by_group(\@user_objects, $params); + @users = map { + filter $params, + { + id => $self->type('int', $_->id), + real_name => $self->type('string', $_->name), + name => $self->type('email', $_->login), } - else { - ThrowUserError('auth_failure', {reason => "not_visible", - action => "access", - object => "user", - userid => $obj->id}); + } @$in_group; + + return {users => \@users}; + } + + my $obj_by_ids; + $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; + + # obj_by_ids are only visible to the user if he can see + # the otheruser, for non visible otheruser throw an error + foreach my $obj (@$obj_by_ids) { + if (Bugzilla->user->can_see_user($obj)) { + if (!$unique_users{$obj->id}) { + push(@user_objects, $obj); + $unique_users{$obj->id} = $obj; + } + } + else { + ThrowUserError( + 'auth_failure', + { + reason => "not_visible", + action => "access", + object => "user", + userid => $obj->id } + ); } - - # User Matching - my $limit; - if ($params->{limit}) { - detaint_natural($params->{limit}) - || ThrowCodeError('param_must_be_numeric', - { function => 'User.match', param => 'limit' }); - $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; + } + + # User Matching + my $limit; + if ($params->{limit}) { + detaint_natural($params->{limit}) + || ThrowCodeError('param_must_be_numeric', + {function => 'User.match', param => 'limit'}); + $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; + } + my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; + foreach my $match_string (@{$params->{'match'} || []}) { + my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); + foreach my $user (@$matched) { + if (!$unique_users{$user->id}) { + push(@user_objects, $user); + $unique_users{$user->id} = $user; + } } - my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; - foreach my $match_string (@{ $params->{'match'} || [] }) { - my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); - foreach my $user (@$matched) { - if (!$unique_users{$user->id}) { - push(@user_objects, $user); - $unique_users{$user->id} = $user; - } - } + } + + my $in_group = $self->_filter_users_by_group(\@user_objects, $params); + foreach my $user (@$in_group) { + my $user_info = filter $params, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + email => $self->type('email', $user->email), + can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), + }; + + if (Bugzilla->user->in_group('editusers')) { + $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled); + $user_info->{login_denied_text} = $self->type('string', $user->disabledtext); } - my $in_group = $self->_filter_users_by_group(\@user_objects, $params); - foreach my $user (@$in_group) { - my $user_info = filter $params, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), - email => $self->type('email', $user->email), - can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), - }; - - if (Bugzilla->user->in_group('editusers')) { - $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled); - $user_info->{login_denied_text} = $self->type('string', $user->disabledtext); - } - - if (Bugzilla->user->id == $user->id) { - if (filter_wants($params, 'saved_searches')) { - $user_info->{saved_searches} = [ - map { $self->_query_to_hash($_) } @{ $user->queries } - ]; - } - } - - if (filter_wants($params, 'groups')) { - if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { - $user_info->{groups} = [ - map { $self->_group_to_hash($_) } @{ $user->groups } - ]; - } - else { - $user_info->{groups} = $self->_filter_bless_groups($user->groups); - } - } + if (Bugzilla->user->id == $user->id) { + if (filter_wants($params, 'saved_searches')) { + $user_info->{saved_searches} + = [map { $self->_query_to_hash($_) } @{$user->queries}]; + } + } - push(@users, $user_info); + if (filter_wants($params, 'groups')) { + if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { + $user_info->{groups} = [map { $self->_group_to_hash($_) } @{$user->groups}]; + } + else { + $user_info->{groups} = $self->_filter_bless_groups($user->groups); + } } - Bugzilla::Hook::process('webservice_user_get', - { webservice => $self, params => $params, users => \@users }); + push(@users, $user_info); + } - return { users => \@users }; + Bugzilla::Hook::process('webservice_user_get', + {webservice => $self, params => $params, users => \@users}); + + return {users => \@users}; } ############### @@ -333,145 +332,144 @@ sub get { ############### sub update { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - # Reject access if there is no sense in continuing. - $user->in_group('editusers') - || ThrowUserError("auth_failure", {group => "editusers", - action => "edit", - object => "users"}); + # Reject access if there is no sense in continuing. + $user->in_group('editusers') + || ThrowUserError("auth_failure", + {group => "editusers", action => "edit", object => "users"}); - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'User.update', params => ['ids', 'names'] }); + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'User.update', params => ['ids', 'names']}); - my $user_objects = params_to_objects($params, 'Bugzilla::User'); + my $user_objects = params_to_objects($params, 'Bugzilla::User'); - my $values = translate($params, MAPPED_FIELDS); + my $values = translate($params, MAPPED_FIELDS); - # We delete names and ids to keep only new values to set. - delete $values->{names}; - delete $values->{ids}; + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; - $dbh->bz_start_transaction(); - foreach my $user (@$user_objects){ - $user->set_all($values); - } + $dbh->bz_start_transaction(); + foreach my $user (@$user_objects) { + $user->set_all($values); + } - my %changes; - foreach my $user (@$user_objects){ - my $returned_changes = $user->update(); - $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $user (@$user_objects) { - my %hash = ( - id => $user->id, - changes => {}, - ); - - foreach my $field (keys %{ $changes{$user->id} }) { - my $change = $changes{$user->id}->{$field}; - # We normalize undef to an empty string, so that the API - # stays consistent for things that can become empty. - $change->[0] = '' if !defined $change->[0]; - $change->[1] = '' if !defined $change->[1]; - # We also flatten arrays (used by groups and blessed_groups) - $change->[0] = join(',', @{$change->[0]}) if ref $change->[0]; - $change->[1] = join(',', @{$change->[1]}) if ref $change->[1]; - - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } + my %changes; + foreach my $user (@$user_objects) { + my $returned_changes = $user->update(); + $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $user (@$user_objects) { + my %hash = (id => $user->id, changes => {},); + + foreach my $field (keys %{$changes{$user->id}}) { + my $change = $changes{$user->id}->{$field}; + + # We normalize undef to an empty string, so that the API + # stays consistent for things that can become empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; - push(@result, \%hash); + # We also flatten arrays (used by groups and blessed_groups) + $change->[0] = join(',', @{$change->[0]}) if ref $change->[0]; + $change->[1] = join(',', @{$change->[1]}) if ref $change->[1]; + + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - return { users => \@result }; + push(@result, \%hash); + } + + return {users => \@result}; } sub _filter_users_by_group { - my ($self, $users, $params) = @_; - my ($group_ids, $group_names) = @$params{qw(group_ids groups)}; + my ($self, $users, $params) = @_; + my ($group_ids, $group_names) = @$params{qw(group_ids groups)}; - # If no groups are specified, we return all users. - return $users if (!$group_ids and !$group_names); + # If no groups are specified, we return all users. + return $users if (!$group_ids and !$group_names); - my $user = Bugzilla->user; + my $user = Bugzilla->user; - my @groups = map { Bugzilla::Group->check({ id => $_ }) } - @{ $group_ids || [] }; + my @groups = map { Bugzilla::Group->check({id => $_}) } @{$group_ids || []}; - if ($group_names) { - foreach my $name (@$group_names) { - my $group = Bugzilla::Group->check({ name => $name, _error => 'invalid_group_name' }); - $user->in_group($group) || ThrowUserError('invalid_group_name', { name => $name }); - push(@groups, $group); - } + if ($group_names) { + foreach my $name (@$group_names) { + my $group + = Bugzilla::Group->check({name => $name, _error => 'invalid_group_name'}); + $user->in_group($group) + || ThrowUserError('invalid_group_name', {name => $name}); + push(@groups, $group); } + } - my @in_group = grep { $self->_user_in_any_group($_, \@groups) } - @$users; - return \@in_group; + my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users; + return \@in_group; } sub _user_in_any_group { - my ($self, $user, $groups) = @_; - foreach my $group (@$groups) { - return 1 if $user->in_group($group); - } - return 0; + my ($self, $user, $groups) = @_; + foreach my $group (@$groups) { + return 1 if $user->in_group($group); + } + return 0; } sub _filter_bless_groups { - my ($self, $groups) = @_; - my $user = Bugzilla->user; + my ($self, $groups) = @_; + my $user = Bugzilla->user; - my @filtered_groups; - foreach my $group (@$groups) { - next unless ($user->in_group('editusers') || $user->can_bless($group->id)); - push(@filtered_groups, $self->_group_to_hash($group)); - } + my @filtered_groups; + foreach my $group (@$groups) { + next unless ($user->in_group('editusers') || $user->can_bless($group->id)); + push(@filtered_groups, $self->_group_to_hash($group)); + } - return \@filtered_groups; + return \@filtered_groups; } sub _group_to_hash { - my ($self, $group) = @_; - my $item = { - id => $self->type('int', $group->id), - name => $self->type('string', $group->name), - description => $self->type('string', $group->description), - }; - return $item; + my ($self, $group) = @_; + my $item = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + return $item; } sub _query_to_hash { - my ($self, $query) = @_; - my $item = { - id => $self->type('int', $query->id), - name => $self->type('string', $query->name), - url => $self->type('string', $query->url), - }; - - return $item; + my ($self, $query) = @_; + my $item = { + id => $self->type('int', $query->id), + name => $self->type('string', $query->name), + url => $self->type('string', $query->url), + }; + + return $item; } sub _login_to_hash { - my ($self, $user) = @_; - my $item = { id => $self->type('int', $user->id) }; - if (my $login_token = $user->{_login_token}) { - $item->{'token'} = $user->id . "-" . $login_token; - } - return $item; + my ($self, $user) = @_; + my $item = {id => $self->type('int', $user->id)}; + if (my $login_token = $user->{_login_token}) { + $item->{'token'} = $user->id . "-" . $login_token; + } + return $item; } # @@ -479,27 +477,27 @@ sub _login_to_hash { # sub mfa_enroll { - my ($self, $params) = @_; - my $provider_name = lc($params->{provider}); + my ($self, $params) = @_; + my $provider_name = lc($params->{provider}); - my $user = Bugzilla->login(LOGIN_REQUIRED); - $user->set_mfa($provider_name); - my $provider = $user->mfa_provider // die "Unknown MFA provider\n"; - return $provider->enroll_api(); + my $user = Bugzilla->login(LOGIN_REQUIRED); + $user->set_mfa($provider_name); + my $provider = $user->mfa_provider // die "Unknown MFA provider\n"; + return $provider->enroll_api(); } sub whoami { - my ( $self, $params ) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - return filter( - $params, - { - id => $self->type( 'int', $user->id ), - real_name => $self->type( 'string', $user->name ), - name => $self->type( 'email', $user->login ), - mfa_status => $self->type( 'boolean', !!$user->mfa ), - } - ); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + return filter( + $params, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + mfa_status => $self->type('boolean', !!$user->mfa), + } + ); } 1; 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__ |