diff options
Diffstat (limited to 'Bugzilla/WebService')
25 files changed, 5209 insertions, 343 deletions
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 006925994..127ea40bb 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -7,22 +7,34 @@ package Bugzilla::WebService::Bug; +use 5.10.1; use strict; -use base qw(Bugzilla::WebService); +use warnings; + +use parent qw(Bugzilla::WebService); use Bugzilla::Comment; +use Bugzilla::Comment::TagWeights; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Field; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(filter filter_wants validate); +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 diff_arrays); +use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural); use Bugzilla::Version; use Bugzilla::Milestone; use Bugzilla::Status; use Bugzilla::Token qw(issue_hash_token); +use Bugzilla::Search; +use Bugzilla::Product; +use Bugzilla::FlagType; +use Bugzilla::Search::Quicksearch; + +use List::Util qw(max); +use List::MoreUtils qw(uniq); +use Storable qw(dclone); ############# # Constants # @@ -32,6 +44,7 @@ use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); use constant DATE_FIELDS => { comments => ['new_since'], + history => ['new_since'], search => ['last_change_time', 'creation_time'], }; @@ -57,28 +70,32 @@ use constant PUBLIC_METHODS => qw( create fields get - get_bugs - get_history history legal_values possible_duplicates render_comment search + search_comment_tags update + update_attachment + update_comment_tags update_see_also update_tags ); -###################################################### -# Add aliases here for old method name compatibility # -###################################################### +use constant ATTACHMENT_MAPPED_SETTERS => { + file_name => 'filename', + summary => 'description', +}; -BEGIN { - # In 3.0, get was called get_bugs - *get_bugs = \&get; - # Before 3.4rc1, "history" was get_history. - *get_history = \&history; -} +use constant ATTACHMENT_MAPPED_RETURNS => { + description => 'summary', + ispatch => 'is_patch', + isprivate => 'is_private', + isobsolete => 'is_obsolete', + filename => 'file_name', + mimetype => 'content_type', +}; ########### # Methods # @@ -312,16 +329,40 @@ sub comments { return { bugs => \%bugs, comments => \%comments }; } +sub render_comment { + my ($self, $params) = @_; + + 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; + + my $tmpl = '[% text FILTER quoteUrls(bug) %]'; + my $html; + my $template = Bugzilla->template; + $template->process( + \$tmpl, + { bug => $bug, text => $params->{text}}, + \$html + ); + + return { html => $html }; +} + # Helper for Bug.comments sub _translate_comment { - my ($self, $comment, $filters) = @_; + my ($self, $comment, $filters, $types, $prefix) = @_; my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef; - return filter $filters, { + + my $comment_hash = { id => $self->type('int', $comment->id), bug_id => $self->type('int', $comment->bug_id), - creator => $self->type('string', $comment->author->login), - author => $self->type('string', $comment->author->login), + creator => $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), @@ -329,18 +370,33 @@ sub _translate_comment { 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'); - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; my $ids = $params->{ids}; defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - my @bugs; - my @faults; + 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}) { @@ -358,10 +414,18 @@ sub get { else { $bug = Bugzilla::Bug->check($bug_id); } - push(@bugs, $self->_bug_to_hash($bug, $params)); + push(@bugs, $bug); + push(@hashes, $self->_bug_to_hash($bug, $params)); } - return { bugs => \@bugs, faults => \@faults }; + # 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); + + return { bugs => \@hashes, faults => \@faults }; } # this is a function that gets bug activity for list of bug ids @@ -385,7 +449,7 @@ sub history { $bug_id = $bug->id; $item{id} = $self->type('int', $bug_id); - my ($activity) = $bug->get_activity; + my ($activity) = $bug->get_activity(undef, $params->{new_since}); my @history; foreach my $changeset (@$activity) { @@ -414,7 +478,7 @@ sub 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 - $item{alias} = $self->type('string', $bug->alias); + $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; push(@return, \%item); } @@ -424,77 +488,110 @@ sub history { sub search { my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; Bugzilla->switch_to_shadow_db(); - if ( defined($params->{offset}) and !defined($params->{limit}) ) { - ThrowCodeError('param_required', + 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 $params->{limit} && $params->{limit} == 0) { - if (!defined $params->{limit} || $params->{limit} > $max_results) { - $params->{limit} = $max_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; } } else { - delete $params->{limit}; - delete $params->{offset}; + delete $match_params->{limit}; + delete $match_params->{offset}; } - $params = Bugzilla::Bug::map_fields($params); - delete $params->{WHERE}; + $match_params = Bugzilla::Bug::map_fields($match_params); - unless (Bugzilla->user->is_timetracker) { - delete $params->{$_} foreach qw(estimated_time remaining_time deadline); - } + 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 $bug_when = delete $params->{delta_ts} ) { - $params->{WHERE}->{'delta_ts >= ?'} = $bug_when; + 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++; } - if (my $when = delete $params->{creation_ts}) { - $params->{WHERE}->{'creation_ts >= ?'} = $when; + + # 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 (my $summary = delete $params->{short_desc}) { - my @strings = ref $summary ? @$summary : ($summary); - my @likes = ("short_desc LIKE ?") x @strings; - my $clause = join(' OR ', @likes); - $params->{WHERE}->{"($clause)"} = [map { "\%$_\%" } @strings]; + if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) { + $match_params->{'keywords_type'} = 'allwords'; } - if (my $whiteboard = delete $params->{status_whiteboard}) { - my @strings = ref $whiteboard ? @$whiteboard : ($whiteboard); - my @likes = ("status_whiteboard LIKE ?") x @strings; - my $clause = join(' OR ', @likes); - $params->{WHERE}->{"($clause)"} = [map { "\%$_\%" } @strings]; + + # 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 longdesc 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 - # and a WHERE parameter was not created earlier, then we throw error - # if system is configured to do so. - if (!$params->{WHERE} - && !grep(!/(limit|offset)/i, keys %$params) + # 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'); } - # We want include_fields and exclude_fields to be passed to - # _bug_to_hash but not to Bugzilla::Bug->match so we copy the - # params and delete those before passing to Bugzilla::Bug->match. - my %match_params = %{ $params }; - delete $match_params{'include_fields'}; - delete $match_params{'exclude_fields'}; + $options{order} = [ split(/\s*,\s*/, delete $match_params->{order}) ] if $match_params->{order}; + $options{params} = $match_params; - my $bugs = Bugzilla::Bug->match(\%match_params); - my $visible = Bugzilla->user->visible_bugs($bugs); - my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible; - return { bugs => \@hashes }; + my $search = new Bugzilla::Search(%options); + my ($data) = $search->data; + + 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; + + return { bugs => \@bugs }; } sub possible_duplicates { - my ($self, $params) = validate(@_, 'product'); + my ($self, $params) = validate(@_, 'products'); my $user = Bugzilla->user; Bugzilla->switch_to_shadow_db(); @@ -504,7 +601,7 @@ sub possible_duplicates { { function => 'Bug.possible_duplicates', param => 'summary' }); my @products; - foreach my $name (@{ $params->{'product'} || [] }) { + foreach my $name (@{ $params->{'products'} || [] }) { my $object = $user->can_enter_product($name, THROW_ERROR); push(@products, $object); } @@ -513,6 +610,7 @@ sub possible_duplicates { { summary => $params->{summary}, products => \@products, limit => $params->{limit} }); my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; + $self->_add_update_tokens($params, $possible_dupes, \@hashes); return { bugs => \@hashes }; } @@ -543,10 +641,25 @@ sub update { # have valid "set_" functions in Bugzilla::Bug, but shouldn't be # called using those field names. delete $values{dependencies}; - delete $values{flags}; + + # For backwards compatibility, treat alias string or array as a set action + if (exists $values{alias}) { + if (not ref $values{alias}) { + $values{alias} = { set => [ $values{alias} ] }; + } + elsif (ref $values{alias} eq 'ARRAY') { + $values{alias} = { set => $values{alias} }; + } + } + + my $flags = delete $values{flags}; foreach my $bug (@bugs) { $bug->set_all(\%values); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($old_flags, $new_flags); + } } my %all_changes; @@ -555,7 +668,7 @@ sub update { $all_changes{$bug->id} = $bug->update(); } $dbh->bz_commit_transaction(); - + foreach my $bug (@bugs) { $bug->send_changes($all_changes{$bug->id}); } @@ -576,7 +689,7 @@ sub update { # 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. - $hash{alias} = $self->type('string', $bug->alias); + $hash{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; my %changes = %{ $all_changes{$bug->id} }; foreach my $field (keys %changes) { @@ -601,10 +714,31 @@ sub update { sub create { my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + Bugzilla->login(LOGIN_REQUIRED); + $params = Bugzilla::Bug::map_fields($params); + + 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(); + my $bug = Bugzilla::Bug->create($params); - Bugzilla::BugMail::Send($bug->bug_id, { changer => $bug->reporter }); + + # 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(); + + $bug->send_changes(); + return { id => $self->type('int', $bug->bug_id) }; } @@ -677,6 +811,8 @@ sub add_attachment { $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, @@ -688,6 +824,13 @@ sub add_attachment { 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, @@ -705,6 +848,119 @@ sub add_attachment { return { ids => \@created_ids }; } +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 $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 }); + } + } + + $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 }); + } + + $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); + + my %hash = ( + id => $self->type('int', $attachment->id), + last_change_time => $self->type('dateTime', $attachment->modification_time), + changes => {}, + ); + + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + + # 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] // '') + }; + } + + push(@result, \%hash); + } + + $dbh->bz_commit_transaction(); + + # 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) = @_; @@ -784,7 +1040,7 @@ sub update_see_also { sub attachments { my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; if (!(defined $params->{ids} or defined $params->{attachment_ids})) @@ -860,6 +1116,73 @@ sub update_tags { return { changes => \%changes }; } +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 $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, + }); + return [ map { $_->tag } @$tags ]; +} + ############################## # Private Helper Subroutines # ############################## @@ -875,12 +1198,12 @@ sub _bug_to_hash { # 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 = ( - alias => $self->type('string', $bug->alias), - creation_time => $self->type('dateTime', $bug->creation_ts), + my %item = %{ filter $params, { + # No need to format $bug->deadline specially, because Bugzilla::Bug + # already does it for us. + deadline => $self->type('string', $bug->deadline), id => $self->type('int', $bug->bug_id), is_confirmed => $self->type('boolean', $bug->everconfirmed), - last_change_time => $self->type('dateTime', $bug->delta_ts), op_sys => $self->type('string', $bug->op_sys), platform => $self->type('string', $bug->rep_platform), priority => $self->type('string', $bug->priority), @@ -892,14 +1215,16 @@ sub _bug_to_hash { url => $self->type('string', $bug->bug_file_loc), version => $self->type('string', $bug->version), whiteboard => $self->type('string', $bug->status_whiteboard), - ); - + } }; - # First we handle any fields that require extra SQL calls. - # We don't do the SQL calls at all if the filter would just - # eliminate them anyway. + # First we handle any fields that require extra work (such as date parsing + # or SQL calls). + if (filter_wants $params, 'alias') { + $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; + } if (filter_wants $params, 'assigned_to') { - $item{'assigned_to'} = $self->type('string', $bug->assigned_to->login); + $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 }; @@ -912,11 +1237,16 @@ sub _bug_to_hash { $item{component} = $self->type('string', $bug->component); } if (filter_wants $params, 'cc') { - my @cc = map { $self->type('string', $_) } @{ $bug->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('string', $bug->reporter->login); + $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 }; @@ -938,12 +1268,18 @@ sub _bug_to_hash { @{ $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('string', $qa_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) } @@ -953,16 +1289,21 @@ sub _bug_to_hash { if (filter_wants $params, 'flags') { $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; } + if (filter_wants $params, 'tags', 'extra') { + $item{'tags'} = $bug->tags; + } # And now custom fields my @custom_fields = Bugzilla->active_custom_fields; foreach my $field (@custom_fields) { my $name = $field->name; - next if !filter_wants $params, $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) { + elsif ($field->type == FIELD_TYPE_DATETIME + || $field->type == FIELD_TYPE_DATE) + { $item{$name} = $self->type('dateTime', $bug->$name); } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { @@ -976,34 +1317,42 @@ sub _bug_to_hash { # Timetracking fields are only sent if the user can see them. if (Bugzilla->user->is_timetracker) { - $item{'estimated_time'} = $self->type('double', $bug->estimated_time); - $item{'remaining_time'} = $self->type('double', $bug->remaining_time); - # 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, '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, 'actual_time') { $item{'actual_time'} = $self->type('double', $bug->actual_time); } } - if (Bugzilla->user->id) { - my $token = issue_hash_token([$bug->id, $bug->delta_ts]); - $item{'update_token'} = $self->type('string', $token); - } - # The "accessible" bits go here because they have long names and it # makes the code look nicer to separate them out. - $item{'is_cc_accessible'} = $self->type('boolean', - $bug->cclist_accessible); - $item{'is_creator_accessible'} = $self->type('boolean', - $bug->reporter_accessible); + 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); + } + + return \%item; +} - return filter $params, \%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; } sub _attachment_to_hash { - my ($self, $attach, $filters) = @_; + my ($self, $attach, $filters, $types, $prefix) = @_; my $item = filter $filters, { creation_time => $self->type('dateTime', $attach->attached), @@ -1012,30 +1361,27 @@ sub _attachment_to_hash { 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 + # creator requires an extra lookup, so we only send them if # the filter wants them. - foreach my $field (qw(creator attacher)) { - if (filter_wants $filters, $field) { - $item->{$field} = $self->type('string', $attach->attacher->login); - } + if (filter_wants $filters, 'creator', $types, $prefix) { + $item->{'creator'} = $self->type('email', $attach->attacher->login); } - if (filter_wants $filters, 'data') { + if (filter_wants $filters, 'data', $types, $prefix) { $item->{'data'} = $self->type('base64', $attach->data); } - if (filter_wants $filters, 'size') { + if (filter_wants $filters, 'size', $types, $prefix) { $item->{'size'} = $self->type('int', $attach->datasize); } - if (filter_wants $filters, 'flags') { + if (filter_wants $filters, 'flags', $types, $prefix) { $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; } @@ -1056,13 +1402,25 @@ sub _flag_to_hash { foreach my $field (qw(setter requestee)) { my $field_id = $field . "_id"; - $item->{$field} = $self->type('string', $flag->$field->login) + $item->{$field} = $self->type('email', $flag->$field->login) if $flag->$field_id; } return $item; } +sub _add_update_tokens { + my ($self, $params, $bugs, $hashes) = @_; + + 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); + } +} + 1; __END__ @@ -1082,6 +1440,10 @@ or get information about bugs that have already been filed. See L<Bugzilla::WebService> for a description of how parameters are passed, and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. +Although the data input and output is the same for JSONRPC, XMLRPC and REST, +the directions for how to access the data via REST is noted in each method +where applicable. + =head1 Utility Functions =head2 fields @@ -1095,11 +1457,26 @@ B<UNSTABLE> Get information about valid bug fields, including the lists of legal values for each field. +=item B<REST> + +You have several options for retreiving information about fields. The first +part is the request method and the rest is the related path needed. + +To get information about all fields: + +GET /rest/field/bug + +To get information related to a single field: + +GET /rest/field/bug/<id_or_name> + +The returned data format is the same as below. + =item B<Params> You can pass either field ids or field names. -B<Note>: If neither C<ids> nor C<names> is specified, then all +B<Note>: If neither C<ids> nor C<names> is specified, then all non-obsolete fields will be returned. In addition to the parameters below, this method also accepts the @@ -1147,6 +1524,12 @@ C<int> The number of the fieldtype. The following values are defined: =item C<7> Bug URLs ("See Also") +=item C<8> Keywords + +=item C<9> Date + +=item C<10> Integer value + =back =item C<is_custom> @@ -1295,10 +1678,11 @@ You specified an invalid field name or id. =item C<is_active> return key for C<values> was added in Bugzilla B<4.4>. -=back +=item REST API call added in Bugzilla B<5.0> =back +=back =head2 legal_values @@ -1310,6 +1694,18 @@ B<DEPRECATED> - Use L</fields> instead. Tells you what values are allowed for a particular field. +=item B<REST> + +To get information on the values for a field based on field name: + +GET /rest/field/bug/<field_name>/values + +To get information based on field name and a specific product: + +GET /rest/field/bug/<field_name>/<product_id>/values + +The returned data format is the same as below. + =item B<Params> =over @@ -1342,6 +1738,14 @@ You specified a field that doesn't exist or isn't a drop-down field. =back +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head1 Bug Information @@ -1360,6 +1764,18 @@ and/or attachment ids. B<Note>: Private attachments will only be returned if you are in the insidergroup or if you are the submitter of the attachment. +=item B<REST> + +To get all current attachments for a bug: + +GET /rest/bug/<bug_id>/attachment + +To get a specific attachment based on attachment ID: + +GET /rest/bug/attachment/<attachment_id> + +The returned data format is the same as below. + =item B<Params> B<Note>: At least one of C<ids> or C<attachment_ids> is required. @@ -1448,10 +1864,6 @@ C<string> The file name of the attachment. C<string> A short string describing the attachment. -Also returned as C<description>, for backwards-compatibility with older -Bugzillas. (However, this backwards-compatibility will go away in Bugzilla -5.0.) - =item C<content_type> C<string> The MIME type of the attachment. @@ -1473,10 +1885,6 @@ C<boolean> True if the attachment is a patch, False otherwise. C<string> The login name of the user that created the attachment. -Also returned as C<attacher>, for backwards-compatibility with older -Bugzillas. (However, this backwards-compatibility will go away in Bugzilla -5.0.) - =item C<flags> An array of hashes containing the information about flags currently set @@ -1557,6 +1965,8 @@ C<summary>. =item The C<flags> array was added in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -1573,6 +1983,18 @@ B<STABLE> This allows you to get data about comments, given a list of bugs and/or comment ids. +=item B<REST> + +To get all comments for a particular bug using the bug ID or alias: + +GET /rest/bug/<id_or_alias>/comment + +To get a specific comment based on the comment ID: + +GET /rest/bug/comment/<comment_id> + +The returned data format is the same as below. + =item B<Params> B<Note>: At least one of C<ids> or C<comment_ids> is required. @@ -1660,10 +2082,6 @@ C<string> The actual text of the comment. C<string> The login name of the comment's author. -Also returned as C<author>, for backwards-compatibility with older -Bugzillas. (However, this backwards-compatibility will go away in Bugzilla -5.0.) - =item time C<dateTime> The time (in Bugzilla's timezone) that the comment was added. @@ -1718,6 +2136,8 @@ C<creator>. =item C<creation_time> was added in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -1733,7 +2153,13 @@ B<STABLE> Gets information about particular bugs in the database. -Note: Can also be called as "get_bugs" for compatibilty with Bugzilla 3.0 API. +=item B<REST> + +To get information about a particular bug using its ID or alias: + +GET /rest/bug/<id_or_alias> + +The returned data format is the same as below. =item B<Params> @@ -1773,6 +2199,9 @@ Two items are returned: An array of hashes that contains information about the bugs with the valid ids. Each hash contains the following items: +These fields are returned by default or by specifying C<_default> +in C<include_fields>. + =over =item C<actual_time> @@ -1784,12 +2213,18 @@ in the return value. =item C<alias> -C<string> The unique alias of this bug. +C<array> of C<string>s The unique aliases of this bug. An empty array will be +returned if this bug has no aliases. =item C<assigned_to> C<string> The login name of the user to whom the bug is assigned. +=item C<assigned_to_detail> + +C<hash> A hash containing detailed user information for the assigned_to. To see the +keys included in the user detail hash, see below. + =item C<blocks> C<array> of C<int>s. The ids of bugs that are "blocked" by this bug. @@ -1799,6 +2234,11 @@ C<array> of C<int>s. The ids of bugs that are "blocked" by this bug. C<array> of C<string>s. The login names of users on the CC list of this bug. +=item C<cc_detail> + +C<array> of hashes containing detailed user information for each of the cc list +members. To see the keys included in the user detail hash, see below. + =item C<classification> C<string> The name of the current classification the bug is in. @@ -1815,14 +2255,16 @@ C<dateTime> When the bug was created. C<string> The login name of the person who filed this bug (the reporter). +=item C<creator_detail> + +C<hash> A hash containing detailed user information for the creator. To see the +keys included in the user detail hash, see below. + =item C<deadline> C<string> The day that this bug is due to be completed, in the format C<YYYY-MM-DD>. -If you are not in the time-tracking group, this field will not be included -in the return value. - =item C<depends_on> C<array> of C<int>s. The ids of bugs that this bug "depends on". @@ -1908,7 +2350,7 @@ C<boolean> True if this bug is open, false if it is closed. =item C<is_creator_accessible> C<boolean> If true, this bug can be accessed by the creator (reporter) -of the bug, even if he or she is not a member of the groups the bug +of the bug, even if they are not a member of the groups the bug is restricted to. =item C<keywords> @@ -1939,6 +2381,11 @@ C<string> The name of the product this bug is in. C<string> The login name of the current QA Contact on the bug. +=item C<qa_contact_detail> + +C<hash> A hash containing detailed user information for the qa_contact. To see the +keys included in the user detail hash, see below. + =item C<remaining_time> C<double> The number of hours of work remaining until work on this bug @@ -2002,7 +2449,11 @@ C<string> The value of the "status whiteboard" field on the bug. Every custom field in this installation will also be included in the return value. Most fields are returned as C<string>s. However, some -field types have different return values: +field types have different return values. + +Normally custom fields are returned by default similar to normal bug +fields or you can specify only custom fields by using C<_custom> in +C<include_fields>. =over @@ -2014,6 +2465,42 @@ field types have different return values: =back +=item I<user detail hashes> + +Each user detail hash contains the following items: + +=over + +=item C<id> + +C<int> The user id for this user. + +=item C<real_name> + +C<string> The 'real' name for this user, if any. + +=item C<name> + +C<string> The user's Bugzilla login. + +=item C<email> + +C<string> The user's email address. Currently this is the same value as the name. + +=back + +=back + +These fields are returned only by specifying "_extra" or the field name in "include_fields". + +=over + +=item C<tags> + +C<array> of C<string>s. Each array item is a tag name. + +Note that tags are personal to the currently logged in user. + =back =item C<faults> B<EXPERIMENTAL> @@ -2065,7 +2552,7 @@ You do not have access to the bug_id you specified. =over -=item C<permissive> argument added to this method's params in Bugzilla B<3.4>. +=item C<permissive> argument added to this method's params in Bugzilla B<3.4>. =item The following properties were added to this method's return values in Bugzilla B<3.4>: @@ -2113,6 +2600,10 @@ and all custom fields. =item The C<actual_time> item was added to the C<bugs> return value in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + +=item In Bugzilla B<5.0>, the following items were added to the bugs return value: C<assigned_to_detail>, C<creator_detail>, C<qa_contact_detail>. + =back =back @@ -2127,6 +2618,14 @@ B<EXPERIMENTAL> Gets the history of changes for particular bugs in the database. +=item B<REST> + +To get the history for a specific bug ID: + +GET /rest/bug/<bug_id>/history + +The returned data format will be the same as below. + =item B<Params> =over @@ -2138,7 +2637,12 @@ An array of numbers and strings. If an element in the array is entirely numeric, it represents a bug_id from the Bugzilla database to fetch. If it contains any non-numeric characters, it is considered to be a bug alias instead, and the data bug -with that alias will be loaded. +with that alias will be loaded. + +item C<new_since> + +C<dateTime> If specified, the method will only return changes I<newer> +than this time. =back @@ -2155,7 +2659,8 @@ C<int> The numeric id of the bug. =item alias -C<string> The alias of this bug. If there is no alias, this will be undef. +C<array> of C<string>s The unique aliases of this bug. An empty array will be +returned if this bug has no aliases. =item history @@ -2218,6 +2723,10 @@ The same as L</get>. consistent with other methods. Since Bugzilla B<4.4>, they now match names used by L<Bug.update|/"update"> for consistency. +=item REST API call added Bugzilla B<5.0>. + +=item Added C<new_since> parameter if Bugzilla B<5.0>. + =back =back @@ -2241,7 +2750,7 @@ narrowed down to specific products. =item C<summary> (string) B<Required> - A string of keywords defining the type of bug you are trying to report. -=item C<product> (array) - One or more product names to narrow the +=item C<products> (array) - One or more product names to narrow the duplicate search to. If omitted, all bugs are searched. =back @@ -2272,6 +2781,9 @@ search for duplicates. =item Added in Bugzilla B<4.0>. +=item The C<product> parameter has been renamed to C<products> in +Bugzilla B<5.0>. + =back =back @@ -2286,6 +2798,14 @@ B<UNSTABLE> Allows you to search for bugs based on particular criteria. +=item <REST> + +To search for bugs: + +GET /bug + +The URL parameters and the returned data format are the same as below. + =item B<Params> Unless otherwise specified in the description of a parameter, bugs are @@ -2306,15 +2826,25 @@ the "Foo" or "Bar" products, you'd pass: product => ['Foo', 'Bar'] Some Bugzillas may treat your arguments case-sensitively, depending -on what database system they are using. Most commonly, though, Bugzilla is -not case-sensitive with the arguments passed (because MySQL is the +on what database system they are using. Most commonly, though, Bugzilla is +not case-sensitive with the arguments passed (because MySQL is the most-common database to use with Bugzilla, and MySQL is not case sensitive). +In addition to the fields listed below, you may also use criteria that +is similar to what is used in the Advanced Search screen of the Bugzilla +UI. This includes fields specified by C<Search by Change History> and +C<Custom Search>. The easiest way to determine what the field names are and what +format Bugzilla expects, is to first construct your query using the +Advanced Search UI, execute it and use the query parameters in they URL +as your key/value pairs for the WebService call. With REST, you can +just reuse the query parameter portion in the REST call itself. + =over =item C<alias> -C<string> The unique alias for this bug. +C<array> of C<string>s The unique aliases of this bug. An empty array will be +returned if this bug has no aliases. =item C<assigned_to> @@ -2403,6 +2933,13 @@ on spaces. So searching for C<foo bar> will match "This is a foo bar" but not "This foo is a bar". C<['foo', 'bar']>, would, however, match the second item. +=item C<tags> + +C<string> Searches for a bug with the specified tag. If you specify an +array, then any bugs that match I<any> of the tags will be returned. + +Note that tags are personal to the currently logged in user. + =item C<target_milestone> C<string> The Target Milestone field of a bug. Note that even if this @@ -2432,6 +2969,10 @@ C<string> Search the "Status Whiteboard" field on bugs for a substring. Works the same as the C<summary> field described above, but searches the Status Whiteboard field. +=item C<quicksearch> + +C<string> Search for bugs using quicksearch syntax. + =back =item B<Returns> @@ -2471,6 +3012,13 @@ in Bugzilla B<4.0>. C<limit> is set equal to zero. Otherwise maximum results returned are limited by system configuration. +=item REST API call added in Bugzilla B<5.0>. + +=item Updated to allow for full search capability similar to the Bugzilla UI +in Bugzilla B<5.0>. + +=item Updated to allow quicksearch capability in Bugzilla B<5.0>. + =back =back @@ -2497,10 +3045,19 @@ The WebService interface may allow you to set things other than those listed here, but realize that anything undocumented is B<UNSTABLE> and will very likely change in the future. +=item B<REST> + +To create a new bug in Bugzilla: + +POST /rest/bug + +The params to include in the POST body as well as the returned data format, +are the same as below. + =item B<Params> Some params must be set, or an error will be thrown. These params are -marked B<Required>. +marked B<Required>. Some parameters can have defaults set in Bugzilla, by the administrator. If these parameters have defaults set, you can omit them. These parameters @@ -2544,7 +3101,7 @@ in by the developer, compared to the developer's other bugs. =item C<severity> (string) B<Defaulted> - How severe the bug is. -=item C<alias> (string) - A brief alias for the bug that can be used +=item C<alias> (array) - A brief alias for the bug that can be used instead of a bug number when accessing this bug. Must be unique in all of this Bugzilla. @@ -2579,6 +3136,32 @@ with L</update>. =item C<target_milestone> (string) - A valid target milestone for this product. +=item C<flags> + +C<array> An array of hashes with flags to add to the bug. To create a flag, +at least the status and the type_id or name must be provided. An optional +requestee can be passed if the flag type is requestable to a specific user. + +=over + +=item C<name> + +C<string> The name of the flag type. + +=item C<type_id> + +C<int> The internal flag type id. + +=item C<status> + +C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag). + +=item C<requestee> + +C<string> The login of the requestee if the flag type is requestable to a specific user. + +=back + =back In addition to the above parameters, if your installation has any custom @@ -2631,6 +3214,28 @@ that would cause a circular dependency between bugs. You tried to restrict the bug to a group which does not exist, or which you cannot use with this product. +=item 129 (Flag Status Invalid) + +The flag status is invalid. + +=item 130 (Flag Modification Denied) + +You tried to request, grant, or deny a flag but only a user with the required +permissions may make the change. + +=item 131 (Flag not Requestable from Specific Person) + +You can't ask a specific person for the flag. + +=item 133 (Flag Type not Unique) + +The flag type specified matches several flag types. You must specify +the type id value to update or add a flag. + +=item 134 (Inactive Flag Type) + +The flag type is inactive and cannot be used to create new flags. + =item 504 (Invalid User) Either the QA Contact, Assignee, or CC lists have some invalid user @@ -2661,6 +3266,8 @@ loop errors had a generic code of C<32000>. =item The ability to file new bugs with a C<resolution> was added in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -2676,6 +3283,16 @@ B<STABLE> This allows you to add an attachment to a bug in Bugzilla. +=item B<REST> + +To create attachment on a current bug: + +POST /rest/bug/<bug_id>/attachment + +The params to include in the POST body, as well as the returned +data format are the same as below. The C<ids> param will be +overridden as it it pulled from the URL path. + =item B<Params> =over @@ -2727,6 +3344,32 @@ to the "insidergroup"), False if the attachment should be public. Defaults to False if not specified. +=item C<flags> + +C<array> An array of hashes with flags to add to the attachment. to create a flag, +at least the status and the type_id or name must be provided. An optional requestee +can be passed if the flag type is requestable to a specific user. + +=over + +=item C<name> + +C<string> The name of the flag type. + +=item C<type_id> + +C<int> The internal flag type id. + +=item C<status> + +C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag). + +=item C<requestee> + +C<string> The login of the requestee if the flag type is requestable to a specific user. + +=back + =back =item B<Returns> @@ -2740,6 +3383,28 @@ This method can throw all the same errors as L</get>, plus: =over +=item 129 (Flag Status Invalid) + +The flag status is invalid. + +=item 130 (Flag Modification Denied) + +You tried to request, grant, or deny a flag but only a user with the required +permissions may make the change. + +=item 131 (Flag not Requestable from Specific Person) + +You can't ask a specific person for the flag. + +=item 133 (Flag Type not Unique) + +The flag type specified matches several flag types. You must specify +the type id value to update or add a flag. + +=item 134 (Inactive Flag Type) + +The flag type is inactive and cannot be used to create new flags. + =item 600 (Attachment Too Large) You tried to attach a file that was larger than Bugzilla will accept. @@ -2773,10 +3438,229 @@ You set the "data" field to an empty string. =item The return value has changed in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + + +=head2 update_attachment + +B<UNSTABLE> + +=over + +=item B<Description> + +This allows you to update attachment metadata in Bugzilla. + +=item B<REST> + +To update attachment metadata on a current attachment: + +PUT /rest/bug/attachment/<attach_id> + +The params to include in the POST body, as well as the returned +data format are the same as below. The C<ids> param will be +overridden as it it pulled from the URL path. + +=item B<Params> + +=over + +=item C<ids> + +B<Required> C<array> An array of integers -- the ids of the attachments you +want to update. + +=item C<file_name> + +C<string> The "file name" that will be displayed +in the UI for this attachment. + +=item C<summary> + +C<string> A short string describing the +attachment. + +=item C<comment> + +C<string> An optional comment to add to the attachment's bug. + +=item C<content_type> + +C<string> The MIME type of the attachment, like +C<text/plain> or C<image/png>. + +=item C<is_patch> + +C<boolean> True if Bugzilla should treat this attachment as a patch. +If you specify this, you do not need to specify a C<content_type>. +The C<content_type> of the attachment will be forced to C<text/plain>. + +=item C<is_private> + +C<boolean> True if the attachment should be private (restricted +to the "insidergroup"), False if the attachment should be public. + +=item C<is_obsolete> + +C<boolean> True if the attachment is obsolete, False otherwise. + +=item C<flags> + +C<array> An array of hashes with changes to the flags. The following values +can be specified. At least the status and one of type_id, id, or name must +be specified. If a type_id or name matches a single currently set flag, +the flag will be updated unless new is specified. + +=over + +=item C<name> + +C<string> The name of the flag that will be created or updated. + +=item C<type_id> + +C<int> The internal flag type id that will be created or updated. You will +need to specify the C<type_id> if more than one flag type of the same name exists. + +=item C<status> + +C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag). + +=item C<requestee> + +C<string> The login of the requestee if the flag type is requestable to a specific user. + +=item C<id> + +C<int> Use id to specify the flag to be updated. You will need to specify the C<id> +if more than one flag is set of the same name. + +=item C<new> + +C<boolean> Set to true if you specifically want a new flag to be created. + +=back + +=item B<Returns> + +A C<hash> with a single field, "attachments". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the attachment that was updated. + +=item C<last_change_time> + +C<dateTime> The exact time that this update was done at, for this attachment. +If no update was done (that is, no fields had their values changed and +no comment was added) then this will instead be the last time the attachment +was updated. + +=item C<changes> + +C<hash> The changes that were actually done on this bug. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C<added> (C<string>) The values that were added to this field. +possibly a comma-and-space-separated list if multiple values were added. + +=item C<removed> (C<string>) The values that were removed from this +field. + =back =back +Here's an example of what a return value might look like: + + { + attachments => [ + { + id => 123, + last_change_time => '2010-01-01T12:34:56', + changes => { + summary => { + removed => 'Sample ptach', + added => 'Sample patch' + }, + is_obsolete => { + removed => '0', + added => '1', + } + }, + } + ] + } + +=item B<Errors> + +This method can throw all the same errors as L</get>, plus: + +=over + +=item 129 (Flag Status Invalid) + +The flag status is invalid. + +=item 130 (Flag Modification Denied) + +You tried to request, grant, or deny a flag but only a user with the required +permissions may make the change. + +=item 131 (Flag not Requestable from Specific Person) + +You can't ask a specific person for the flag. + +=item 132 (Flag not Unique) + +The flag specified has been set multiple times. You must specify the id +value to update the flag. + +=item 133 (Flag Type not Unique) + +The flag type specified matches several flag types. You must specify +the type id value to update or add a flag. + +=item 134 (Inactive Flag Type) + +The flag type is inactive and cannot be used to create new flags. + +=item 601 (Invalid MIME Type) + +You specified a C<content_type> argument that was blank, not a valid +MIME type, or not a MIME type that Bugzilla accepts for attachments. + +=item 603 (File Name Not Specified) + +You did not specify a valid for the C<file_name> argument. + +=item 604 (Summary Required) + +You did not specify a value for the C<summary> argument. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=back =head2 add_comment @@ -2788,6 +3672,15 @@ B<STABLE> This allows you to add a comment to a bug in Bugzilla. +=item B<REST> + +To create a comment on a current bug: + +POST /rest/bug/<bug_id>/comment + +The params to include in the POST body as well as the returned data format, +are the same as below. + =item B<Params> =over @@ -2863,6 +3756,8 @@ purposes if you wish. =item Before Bugzilla B<3.6>, error 54 and error 114 had a generic error code of 32000. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -2879,6 +3774,16 @@ B<UNSTABLE> Allows you to update the fields of a bug. Automatically sends emails out about the changes. +=item B<REST> + +To update the fields of a current bug: + +PUT /rest/bug/<bug_id> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> param will be overridden as it is +pulled from the URL path. + =item B<Params> =over @@ -2897,9 +3802,29 @@ bugs you are updating. =item C<alias> -(string) The alias of the bug. You can only set this if you are modifying -a single bug. If there is more than one bug specified in C<ids>, passing in -a value for C<alias> will cause an error to be thrown. +C<hash> These specify the aliases of a bug that can be used instead of a bug +number when acessing this bug. To set these, you should pass a hash as the +value. The hash may contain the following fields: + +=over + +=item C<add> An array of C<string>s. Aliases to add to this field. + +=item C<remove> An array of C<string>s. Aliases to remove from this field. +If the aliases are not already in the field, they will be ignored. + +=item C<set> An array of C<string>s. An exact set of aliases to set this +field to, overriding the current value. If you specify C<set>, then C<add> +and C<remove> will be ignored. + +=back + +You can only set this if you are modifying a single bug. If there is more +than one bug specified in C<ids>, passing in a value for C<alias> will cause +an error to be thrown. + +For backwards compatibility, you can also specify a single string. This will +be treated as if you specified the set key above. =item C<assigned_to> @@ -2998,6 +3923,43 @@ duplicate bugs. C<double> The total estimate of time required to fix the bug, in hours. This is the I<total> estimate, not the amount of time remaining to fix it. +=item C<flags> + +C<array> An array of hashes with changes to the flags. The following values +can be specified. At least the status and one of type_id, id, or name must +be specified. If a type_id or name matches a single currently set flag, +the flag will be updated unless new is specified. + +=over + +=item C<name> + +C<string> The name of the flag that will be created or updated. + +=item C<type_id> + +C<int> The internal flag type id that will be created or updated. You will +need to specify the C<type_id> if more than one flag type of the same name exists. + +=item C<status> + +C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag). + +=item C<requestee> + +C<string> The login of the requestee if the flag type is requestable to a specific user. + +=item C<id> + +C<int> Use id to specify the flag to be updated. You will need to specify the C<id> +if more than one flag is set of the same name. + +=item C<new> + +C<boolean> Set to true if you specifically want a new flag to be created. + +=back + =item C<groups> C<hash> The groups a bug is in. To modify this field, pass a hash, which @@ -3075,7 +4037,7 @@ C<string> The full login name of the bug's QA Contact. =item C<is_creator_accessible> C<boolean> Whether or not the bug's reporter is allowed to access -the bug, even if he or she isn't in a group that can normally access +the bug, even if they aren't in a group that can normally access the bug. =item C<remaining_time> @@ -3181,7 +4143,8 @@ C<int> The id of the bug that was updated. =item C<alias> -C<string> The alias of the bug that was updated, if this bug has an alias. +C<array> of C<string>s The aliases of the bug that was updated, if this bug +has any alias. =item C<last_change_time> @@ -3215,7 +4178,7 @@ Here's an example of what a return value might look like: bugs => [ { id => 123, - alias => 'foo', + alias => [ 'foo' ], last_change_time => '2010-01-01T12:34:56', changes => { status => { @@ -3315,6 +4278,33 @@ field. You tried to change from one status to another, but the status workflow rules don't allow that change. +=item 129 (Flag Status Invalid) + +The flag status is invalid. + +=item 130 (Flag Modification Denied) + +You tried to request, grant, or deny a flag but only a user with the required +permissions may make the change. + +=item 131 (Flag not Requestable from Specific Person) + +You can't ask a specific person for the flag. + +=item 132 (Flag not Unique) + +The flag specified has been set multiple times. You must specify the id +value to update the flag. + +=item 133 (Flag Type not Unique) + +The flag type specified matches several flag types. You must specify +the type id value to update or add a flag. + +=item 134 (Inactive Flag Type) + +The flag type is inactive and cannot be used to create new flags. + =back =item B<History> @@ -3323,6 +4313,8 @@ rules don't allow that change. =item Added in Bugzilla B<4.0>. +=item REST API call added Bugzilla B<5.0>. + =back =back @@ -3503,3 +4495,176 @@ This method can throw the same errors as L</get>. =back =back + +=head2 search_comment_tags + +B<UNSTABLE> + +=over + +=item B<Description> + +Searches for tags which contain the provided substring. + +=item B<REST> + +To search for comment tags: + +GET /rest/bug/comment/tags/<query> + +=item B<Params> + +=over + +=item C<query> + +B<Required> C<string> Only tags containg this substring will be returned. + +=item C<limit> + +C<int> If provided will return no more than C<limit> tags. Defaults to C<10>. + +=back + +=item B<Returns> + +An C<array of strings> of matching tags. + +=item B<Errors> + +This method can throw all of the errors that L</get> throws, plus: + +=over + +=item 125 (Comment Tagging Disabled) + +Comment tagging support is not available or enabled. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update_comment_tags + +B<UNSTABLE> + +=over + +=item B<Description> + +Adds or removes tags from a comment. + +=item B<REST> + +To update the tags comments attached to a comment: + +PUT /rest/bug/comment/tags + +The params to include in the PUT body as well as the returned data format, +are the same as below. + +=item B<Params> + +=over + +=item C<comment_id> + +B<Required> C<int> The ID of the comment to update. + +=item C<add> + +C<array of strings> The tags to attach to the comment. + +=item C<remove> + +C<array of strings> The tags to detach from the comment. + +=back + +=item B<Returns> + +An C<array of strings> containing the comment's updated tags. + +=item B<Errors> + +This method can throw all of the errors that L</get> throws, plus: + +=over + +=item 125 (Comment Tagging Disabled) + +Comment tagging support is not available or enabled. + +=item 126 (Invalid Comment Tag) + +The comment tag provided was not valid (eg. contains invalid characters). + +=item 127 (Comment Tag Too Short) + +The comment tag provided is shorter than the minimum length. + +=item 128 (Comment Tag Too Long) + +The comment tag provided is longer than the maximum length. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 render_comment + +B<UNSTABLE> + +=over + +=item B<Description> + +Returns the HTML rendering of the provided comment text. + +=item B<Params> + +=over + +=item C<text> + +B<Required> C<strings> Text comment text to render. + +=item C<id> + +C<int> The ID of the bug to render the comment against. + +=back + +=item B<Returns> + +C<html> containing the HTML rendering. + +=item B<Errors> + +This method can throw all of the errors that L</get> throws. + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm new file mode 100644 index 000000000..19a56ff46 --- /dev/null +++ b/Bugzilla/WebService/BugUserLastVisit.pm @@ -0,0 +1,207 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::BugUserLastVisit; + +use 5.10.1; +use strict; +use warnings; + +use parent qw(Bugzilla::WebService); + +use Bugzilla::Bug; +use Bugzilla::Error; +use Bugzilla::WebService::Util qw( validate filter ); +use Bugzilla::Constants; + +use constant PUBLIC_METHODS => qw( + get + update +); + +sub update { + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + $user->login(LOGIN_REQUIRED); + + 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]); + + $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); + + $bug->update_user_last_visit($user, $last_visit_ts); + + push( + @results, + $self->_bug_user_last_visit_to_hash( + $bug, $last_visit_ts, $params + )); + } + $dbh->bz_commit_transaction(); + + 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 + ]; +} + +sub _bug_user_last_visit_to_hash { + 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)); + + return filter($params, \%result); +} + +1; + +__END__ +=head1 NAME + +Bugzilla::WebService::BugUserLastVisit - Find and Store the last time a user +visited a bug. + +=head1 METHODS + +See L<Bugzilla::WebService> for a description of how parameters are passed, +and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. + +Although the data input and output is the same for JSONRPC, XMLRPC and REST, +the directions for how to access the data via REST is noted in each method +where applicable. + +=head2 update + +B<EXPERIMENTAL> + +=over + +=item B<Description> + +Update the last visit time for the specified bug and current user. + +=item B<REST> + +To add a single bug id: + + POST /rest/bug_user_last_visit/<bug-id> + +Tp add one or more bug ids at once: + + POST /rest/bug_user_last_visit + +The returned data format is the same as below. + +=item B<Params> + +=over + +=item C<ids> (array) - One or more bug ids to add. + +=back + +=item B<Returns> + +=over + +=item C<array> - An array of hashes containing the following: + +=over + +=item C<id> - (int) The bug id. + +=item C<last_visit_ts> - (string) The timestamp the user last visited the bug. + +=back + +=back + +=back + +=head2 get + +B<EXPERIMENTAL> + +=over + +=item B<Description> + +Get the last visited timestamp for one or more specified bug ids. + +=item B<REST> + +To return the last visited timestamp for a single bug id: + + GET /rest/bug_user_last_visit/<bug-id> + +=item B<Params> + +=over + +=item C<ids> (integer) - One or more optional bug ids to get. + +=back + +=item B<Returns> + +=over + +=item C<array> - An array of hashes containing the following: + +=over + +=item C<id> - (int) The bug id. + +=item C<last_visit_ts> - (string) The timestamp the user last visited the bug. + +=back + +=back + +=back diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm index a6037e67e..848cffd30 100644 --- a/Bugzilla/WebService/Bugzilla.pm +++ b/Bugzilla/WebService/Bugzilla.pm @@ -7,8 +7,11 @@ package Bugzilla::WebService::Bugzilla; +use 5.10.1; use strict; -use base qw(Bugzilla::WebService); +use warnings; + +use parent qw(Bugzilla::WebService); use Bugzilla::Constants; use Bugzilla::Util qw(datetime_from); use Bugzilla::WebService::Util qw(validate filter_wants); @@ -128,12 +131,12 @@ sub time { sub last_audit_time { my ($self, $params) = validate(@_, 'class'); my $dbh = Bugzilla->dbh; - + my $sql_statement = "SELECT MAX(at_time) FROM audit_log"; my $class_values = $params->{class}; my @class_values_quoted; foreach my $class_value (@$class_values) { - push (@class_values_quoted, $dbh->quote($class_value)) + push (@class_values_quoted, $dbh->quote($class_value)) if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/; } @@ -142,11 +145,11 @@ sub last_audit_time { } my $last_audit_time = $dbh->selectrow_array("$sql_statement"); - + # All Webservices return times in UTC; Use UTC here for backwards compat. # Hardcode values where appropriate $last_audit_time = datetime_from($last_audit_time, 'UTC'); - + return { last_audit_time => $self->type('dateTime', $last_audit_time) }; @@ -154,7 +157,7 @@ sub last_audit_time { sub parameters { my ($self, $args) = @_; - my $user = Bugzilla->login(); + my $user = Bugzilla->login(LOGIN_OPTIONAL); my $params = Bugzilla->params; $args ||= {}; @@ -188,6 +191,10 @@ This provides functions that tell you about Bugzilla in general. See L<Bugzilla::WebService> for a description of how parameters are passed, and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. +Although the data input and output is the same for JSONRPC, XMLRPC and REST, +the directions for how to access the data via REST is noted in each method +where applicable. + =head2 version B<STABLE> @@ -198,6 +205,12 @@ B<STABLE> Returns the current version of Bugzilla. +=item B<REST> + +GET /rest/version + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -207,6 +220,14 @@ string. =item B<Errors> (none) +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head2 extensions @@ -220,6 +241,12 @@ B<EXPERIMENTAL> Gets information about the extensions that are currently installed and enabled in this Bugzilla. +=item B<REST> + +GET /rest/extensions + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -250,6 +277,8 @@ The return value looks something like this: that the extensions define themselves. Before 3.6, the names of the extensions depended on the directory they were in on the Bugzilla server. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -265,6 +294,12 @@ Use L</time> instead. Returns the timezone that Bugzilla expects dates and times in. +=item B<REST> + +GET /rest/timezone + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -279,6 +314,8 @@ string in (+/-)XXXX (RFC 2822) format. =item As of Bugzilla B<3.6>, the timezone returned is always C<+0000> (the UTC timezone). +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -295,6 +332,12 @@ B<STABLE> Gets information about what time the Bugzilla server thinks it is, and what timezone it's running in. +=item B<REST> + +GET /rest/time + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -305,7 +348,7 @@ A struct with the following items: =item C<db_time> -C<dateTime> The current time in UTC, according to the Bugzilla +C<dateTime> The current time in UTC, according to the Bugzilla I<database server>. Note that Bugzilla assumes that the database and the webserver are running @@ -315,7 +358,7 @@ rely on for doing searches and other input to the WebService. =item C<web_time> -C<dateTime> This is the current time in UTC, according to Bugzilla's +C<dateTime> This is the current time in UTC, according to Bugzilla's I<web server>. This might be different by a second from C<db_time> since this comes from @@ -331,7 +374,7 @@ versions of Bugzilla before 3.6.) =item C<tz_name> C<string> The literal string C<UTC>. (Exists only for backwards-compatibility -with versions of Bugzilla before 3.6.) +with versions of Bugzilla before 3.6.) =item C<tz_short_name> @@ -355,6 +398,8 @@ with versions of Bugzilla before 3.6.) were in the UTC timezone, instead of returning information in the server's local timezone. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -369,6 +414,12 @@ B<UNSTABLE> Returns parameter values currently used in this Bugzilla. +=item B<REST> + +GET /rest/parameters + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -426,6 +477,8 @@ never be stable. =item Added in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -440,9 +493,15 @@ B<EXPERIMENTAL> Gets the latest time of the audit_log table. +=item B<REST> + +GET /rest/last_audit_time + +The returned data format is the same as below. + =item B<Params> -You can pass the optional parameter C<class> to get the maximum for only +You can pass the optional parameter C<class> to get the maximum for only the listed classes. =over @@ -467,6 +526,8 @@ at_time from the audit_log. =item Added in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + =back =back diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm index f2c3ec51e..cee597b68 100644 --- a/Bugzilla/WebService/Classification.pm +++ b/Bugzilla/WebService/Classification.pm @@ -7,9 +7,11 @@ package Bugzilla::WebService::Classification; +use 5.10.1; use strict; +use warnings; -use base qw (Bugzilla::WebService); +use parent qw (Bugzilla::WebService); use Bugzilla::Classification; use Bugzilla::Error; @@ -44,39 +46,37 @@ sub get { @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; } - my @classifications = map { filter($params, $self->_classification_to_hash($_)) } @classification_objs; + my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs; return { classifications => \@classifications }; } sub _classification_to_hash { - my ($self, $classification) = @_; + 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); - my %hash = ( + + 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($_) } @$products ], - ); - - return \%hash; + products => [ map { $self->_product_to_hash($_, $params) } @$products ], + }; } sub _product_to_hash { - my ($self, $product) = @_; - my %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), - ); - - return \%hash; + }, undef, 'products'; } 1; @@ -89,7 +89,7 @@ Bugzilla::Webservice::Classification - The Classification API =head1 DESCRIPTION -This part of the Bugzilla API allows you to deal with the available Classifications. +This part of the Bugzilla API allows you to deal with the available Classifications. You will be able to get information about them as well as manipulate them. =head1 METHODS @@ -97,6 +97,10 @@ You will be able to get information about them as well as manipulate them. See L<Bugzilla::WebService> for a description of how parameters are passed, and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. +Although the data input and output is the same for JSONRPC, XMLRPC and REST, +the directions for how to access the data via REST is noted in each method +where applicable. + =head1 Classification Retrieval =head2 get @@ -109,13 +113,21 @@ B<EXPERIMENTAL> Returns a hash containing information about a set of classifications. +=item B<REST> + +To return information on a single classification: + +GET /rest/classification/<classification_id_or_name> + +The returned data format will be the same as below. + =item B<Params> In addition to the parameters below, this method also accepts the standard L<include_fields|Bugzilla::WebService/include_fields> and L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments. -You could get classifications info by supplying their names and/or ids. +You could get classifications info by supplying their names and/or ids. So, this method accepts the following parameters: =over @@ -130,10 +142,10 @@ An array of classification names. =back -=item B<Returns> +=item B<Returns> A hash with the key C<classifications> and an array of hashes as the corresponding value. -Each element of the array represents a classification that the user is authorized to see +Each element of the array represents a classification that the user is authorized to see and has the following keys: =over @@ -193,6 +205,8 @@ Classification is not enabled on this installation. =item Added in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + =back =back diff --git a/Bugzilla/WebService/Component.pm b/Bugzilla/WebService/Component.pm new file mode 100644 index 000000000..4d6723d8b --- /dev/null +++ b/Bugzilla/WebService/Component.pm @@ -0,0 +1,153 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Component; + +use 5.10.1; +use strict; +use warnings; + +use base qw(Bugzilla::WebService); + +use Bugzilla::Component; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Util qw(translate params_to_objects validate); + +use constant PUBLIC_METHODS => qw( + create +); + +use constant MAPPED_FIELDS => { + default_assignee => 'initialowner', + default_qa_contact => 'initialqacontact', + default_cc => 'initial_cc', + is_open => 'isactive', +}; + +sub create { + my ($self, $params) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar @{ $user->get_products_by_permission('editcomponents') } + || ThrowUserError('auth_failure', { group => 'editcomponents', + action => 'edit', + object => 'components' }); + + my $product = $user->check_can_admin_product($params->{product}); + + # Translate the fields + my $values = translate($params, MAPPED_FIELDS); + $values->{product} = $product; + + # Create the component and return the newly created id. + my $component = Bugzilla::Component->create($values); + return { id => $self->type('int', $component->id) }; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Component - The Component API + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to deal with the available product components. +You will be able to get information about them as well as manipulate them. + +=head1 METHODS + +See L<Bugzilla::WebService> for a description of how parameters are passed, +and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. + +=head1 Component Creation and Modification + +=head2 create + +B<EXPERIMENTAL> + +=over + +=item B<Description> + +This allows you to create a new component in Bugzilla. + +=item B<Params> + +Some params must be set, or an error will be thrown. These params are +marked B<Required>. + +=over + +=item C<name> + +B<Required> C<string> The name of the new component. + +=item C<product> + +B<Required> C<string> The name of the product that the component must be +added to. This product must already exist, and the user have the necessary +permissions to edit components for it. + +=item C<description> + +B<Required> C<string> The description of the new component. + +=item C<default_assignee> + +B<Required> C<string> The login name of the default assignee of the component. + +=item C<default_cc> + +C<array> An array of strings with each element representing one login name of the default CC list. + +=item C<default_qa_contact> + +C<string> The login name of the default QA contact for the component. + +=item C<is_open> + +C<boolean> 1 if you want to enable the component for bug creations. 0 otherwise. Default is 1. + +=back + +=item B<Returns> + +A hash with one key: C<id>. This will represent the ID of the newly-added +component. + +=item B<Errors> + +=over + +=item 304 (Authorization Failure) + +You are not authorized to create a new component. + +=item 1200 (Component already exists) + +The name that you specified for the new component already exists in the +specified product. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index f289caef4..0bdd3517e 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -7,14 +7,30 @@ package Bugzilla::WebService::Constants; +use 5.10.1; use strict; -use base qw(Exporter); +use warnings; + +use parent qw(Exporter); our @EXPORT = qw( 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 + ERROR_UNKNOWN_FATAL ERROR_UNKNOWN_TRANSIENT + XMLRPC_CONTENT_TYPE_WHITELIST + REST_CONTENT_TYPE_WHITELIST WS_DISPATCH ); @@ -66,8 +82,9 @@ use constant WS_ERROR_CODE => { illegal_field => 104, freetext_too_long => 104, # Component errors - require_component => 105, - component_name_too_long => 105, + require_component => 105, + component_name_too_long => 105, + product_unknown_component => 105, # Invalid Product no_products => 106, entry_access_denied => 106, @@ -83,7 +100,12 @@ use constant WS_ERROR_CODE => { comment_is_private => 110, comment_id_invalid => 111, comment_too_long => 114, - comment_invalid_isprivate => 117, + 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, @@ -106,14 +128,25 @@ use constant WS_ERROR_CODE => { 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, + invalid_login_or_password => 300, account_disabled => 301, auth_invalid_email => 302, extern_id_conflict => -303, auth_failure => 304, - password_current_too_short => 305, + password_too_short => 305, + password_not_complex => 305, + api_key_not_valid => 306, + api_key_revoked => 306, + auth_invalid_token => 307, # Except, historically, AUTH_NODATA, which is 410. login_required => 410, @@ -157,6 +190,7 @@ use constant WS_ERROR_CODE => { empty_group_description => 802, invalid_regexp => 803, invalid_group_name => 804, + group_cannot_view => 805, # Classification errors are 900-1000 auth_classification_not_enabled => 900, @@ -164,14 +198,73 @@ use constant WS_ERROR_CODE => { # Search errors are 1000-1100 buglist_parameters_required => 1000, + # Flag type errors are 1100-1200 + flag_type_name_invalid => 1101, + flag_type_description_invalid => 1102, + flag_type_cc_list_invalid => 1103, + flag_type_sortkey_invalid => 1104, + flag_type_not_editable => 1105, + + # Component errors are 1200-1300 + component_already_exists => 1200, + component_is_last => 1201, + component_has_bugs => 1202, + # 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, + xmlrpc_illegal_content_type => 32612, + json_rpc_illegal_content_type => 32613, + rest_invalid_resource => 32614, +}; + +# RESTful webservices use the http status code +# to describe whether a call was successful or +# to describe the type of error that occurred. +use constant STATUS_OK => 200; +use constant STATUS_CREATED => 201; +use constant STATUS_ACCEPTED => 202; +use constant STATUS_NO_CONTENT => 204; +use constant STATUS_MULTIPLE_CHOICES => 300; +use constant STATUS_BAD_REQUEST => 400; +use constant STATUS_NOT_AUTHORIZED => 401; +use constant STATUS_NOT_FOUND => 404; +use constant STATUS_GONE => 410; + +# The integer value is the error code above returned by +# the related webvservice call. We choose the appropriate +# 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; }; # These are the fallback defaults for errors not in ERROR_CODE. @@ -185,6 +278,14 @@ use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw( 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 +); + sub WS_DISPATCH { # We "require" here instead of "use" above to avoid a dependency loop. require Bugzilla::Hook; @@ -192,15 +293,28 @@ sub WS_DISPATCH { Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch }); my $dispatch = { - 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', - 'Bug' => 'Bugzilla::WebService::Bug', - 'Classification' => 'Bugzilla::WebService::Classification', - 'Group' => 'Bugzilla::WebService::Group', - 'Product' => 'Bugzilla::WebService::Product', - 'User' => 'Bugzilla::WebService::User', + 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', + 'Bug' => 'Bugzilla::WebService::Bug', + 'Classification' => 'Bugzilla::WebService::Classification', + 'Component' => 'Bugzilla::WebService::Component', + 'FlagType' => 'Bugzilla::WebService::FlagType', + 'Group' => 'Bugzilla::WebService::Group', + 'Product' => 'Bugzilla::WebService::Product', + 'User' => 'Bugzilla::WebService::User', + 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', %hook_dispatch }; return $dispatch; }; 1; + +=head1 B<Methods in need of POD> + +=over + +=item REST_STATUS_CODE_MAP + +=item WS_DISPATCH + +=back diff --git a/Bugzilla/WebService/FlagType.pm b/Bugzilla/WebService/FlagType.pm new file mode 100644 index 000000000..9723d4735 --- /dev/null +++ b/Bugzilla/WebService/FlagType.pm @@ -0,0 +1,834 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::FlagType; + +use 5.10.1; +use strict; +use warnings; + +use parent qw(Bugzilla::WebService); +use Bugzilla::Component; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::FlagType; +use Bugzilla::Product; +use Bugzilla::Util qw(trim); + +use List::MoreUtils qw(uniq); + +use constant PUBLIC_METHODS => qw( + create + get + update +); + +sub get { + 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 create { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + Bugzilla->user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", { group => "editcomponents", + action => "add", + object => "flagtypes" }); + + $params->{name} || ThrowCodeError('param_required', { param => 'name' }); + $params->{description} || ThrowCodeError('param_required', { param => 'description' }); + + my %args = ( + sortkey => 1, + name => undef, + inclusions => ['0:0'], # Default to __ALL__:__ALL__ + cc_list => '', + description => undef, + is_requestable => 'on', + exclusions => [], + is_multiplicable => 'on', + request_group => '', + is_active => 'on', + is_specifically_requestable => 'on', + target_type => 'bug', + grant_group => '', + ); + + foreach my $key (keys %args) { + $args{$key} = $params->{$key} if defined($params->{$key}); + } + + $args{name} = trim($params->{name}); + $args{description} = trim($params->{description}); + + # Is specifically requestable is actually is_requesteeable + if (exists $args{is_specifically_requestable}) { + $args{is_requesteeble} = delete $args{is_specifically_requestable}; + } + + # Default is on for the tickbox flags. + # If the user has set them to 'off' then undefine them so the flags are not ticked + foreach my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) { + if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) { + $args{$arg_name} = undef; + } + } + + # Process group inclusions and exclusions + $args{inclusions} = _process_lists($params->{inclusions}) if defined $params->{inclusions}; + $args{exclusions} = _process_lists($params->{exclusions}) if defined $params->{exclusions}; + + my $flagtype = Bugzilla::FlagType->create(\%args); + + return { id => $self->type('int', $flagtype->id) }; +} + +sub update { + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + Bugzilla->login(LOGIN_REQUIRED); + $user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", { group => "editcomponents", + action => "edit", + object => "flagtypes" }); + + defined($params->{names}) || defined($params->{ids}) + || ThrowCodeError('params_required', + { function => 'FlagType.update', params => ['ids', 'names'] }); + + # Get the list of unique flag type ids we are updating + my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : (); + if (defined $params->{names}) { + push @flag_type_ids, map { $_->id } + @{ Bugzilla::FlagType::match({ name => $params->{names} }) }; + } + @flag_type_ids = uniq @flag_type_ids; + + # We delete names and ids to keep only new values to set. + delete $params->{names}; + delete $params->{ids}; + + # Process group inclusions and exclusions + # We removed them from $params because these are handled differently + my $inclusions = _process_lists(delete $params->{inclusions}) if defined $params->{inclusions}; + my $exclusions = _process_lists(delete $params->{exclusions}) if defined $params->{exclusions}; + + $dbh->bz_start_transaction(); + my %changes = (); + + foreach my $flag_type_id (@flag_type_ids) { + my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id); + + if ($can_fully_edit) { + $flagtype->set_all($params); + } + elsif (scalar keys %$params) { + ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); + } + + # Process the clusions + foreach my $type ('inclusions', 'exclusions') { + my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions; + next if not defined $clusions; + + my @extra_clusions = (); + if (!$user->in_group('editcomponents')) { + my $products = $user->get_products_by_permission('editcomponents'); + # Bring back the products the user cannot edit. + foreach my $item (values %{$flagtype->$type}) { + my ($prod_id, $comp_id) = split(':', $item); + push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products; + } + } + + $flagtype->set_clusions({ + $type => [@$clusions, @extra_clusions], + }); + } + + my $returned_changes = $flagtype->update(); + $changes{$flagtype->id} = { + name => $flagtype->name, + changes => $returned_changes, + }; + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $flag_type_id (keys %changes) { + my %hash = ( + id => $self->type('int', $flag_type_id), + name => $self->type('string', $changes{$flag_type_id}{name}), + changes => {}, + ); + + foreach my $field (keys %{ $changes{$flag_type_id}{changes} }) { + my $change = $changes{$flag_type_id}{changes}{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; + } + + push(@result, \%hash); + } + + return { flagtypes => \@result }; +} + +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; +} + +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 [ '' ]; + } + } + } + return $result; +} + +sub _process_lists { + my $list = shift; + my $user = Bugzilla->user; + + my @products; + if ($user->in_group('editcomponents')) { + @products = Bugzilla::Product->get_all; + } + else { + @products = @{$user->get_products_by_permission('editcomponents')}; + } + + my @component_list; + + foreach my $item (@$list) { + # A hash with products as the key and component names as the values + if(ref($item) eq 'HASH') { + while (my ($product_name, $component_names) = each %$item) { + my $product = Bugzilla::Product->check({name => $product_name}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', { name => $product_name }); + } + my @component_ids; + + foreach my $comp_name (@$component_names) { + my $component = Bugzilla::Component->check({product => $product, name => $comp_name}); + ThrowCodeError('param_invalid', { param => $comp_name}) unless defined $component; + push @component_list, $product->id . ':' . $component->id; + } + } + } + elsif(!ref($item)) { + # These are whole products + my $product = Bugzilla::Product->check({name => $item}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', { name => $item }); + } + push @component_list, $product->id . ':0'; + } + else { + # The user has passed something invalid + ThrowCodeError('param_invalid', { param => $item }); + } + } + + return \@component_list; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::WebService::FlagType - API for creating flags. + +=head1 DESCRIPTION + +This part of the Bugzilla API allows you to create new flags + +=head1 METHODS + +See L<Bugzilla::WebService> for a description of what B<STABLE>, B<UNSTABLE>, +and B<EXPERIMENTAL> mean, and for more description about error codes. + +=head2 Get Flag Types + +=over + +=item C<get> B<UNSTABLE> + +=item B<Description> + +Get information about valid flag types that can be set for bugs and attachments. + +=item B<REST> + +You have several options for retreiving information about flag types. The first +part is the request method and the rest is the related path needed. + +To get information about all flag types for a product: + +GET /rest/flag_type/<product> + +To get information about flag_types for a product and component: + +GET /rest/flag_type/<product>/<component> + +The returned data format is the same as below. + +=item B<Params> + +You must pass a product name and an optional component name. + +=over + +=item C<product> (string) - The name of a valid product. + +=item C<component> (string) - An optional valid component name associated with the product. + +=back + +=item B<Returns> + +A hash containing two keys, C<bug> and C<attachment>. Each key value is an array of hashes, +containing the following keys: + +=over + +=item C<id> + +C<int> An integer id uniquely identifying this flag type. + +=item C<name> + +C<string> The name for the flag type. + +=item C<type> + +C<string> The target of the flag type which is either C<bug> or C<attachment>. + +=item C<description> + +C<string> The description of the flag type. + +=item C<values> + +C<array> An array of string values that the user can set on the flag type. + +=item C<is_requesteeble> + +C<boolean> Users can ask specific other users to set flags of this type. + +=item C<is_multiplicable> + +C<boolean> Multiple flags of this type can be set for the same bug or attachment. + +=back + +=item B<Errors> + +=over + +=item 106 (Product Access Denied) + +Either the product does not exist or you don't have access to it. + +=item 51 (Invalid Component) + +The component provided does not exist in the product. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 Create Flag + +=over + +=item C<create> B<UNSTABLE> + +=item B<Description> + +Creates a new FlagType + +=item B<REST> + +POST /rest/flag_type + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> + +At a minimum the following two arguments must be supplied: + +=over + +=item C<name> (string) - The name of the new Flag Type. + +=item C<description> (string) - A description for the Flag Type object. + +=back + +=item B<Returns> + +C<int> flag_id + +The ID of the new FlagType object is returned. + +=item B<Params> + +=over + +=item name B<required> + +C<string> A short name identifying this type. + +=item description B<required> + +C<string> A comprehensive description of this type. + +=item inclusions B<optional> + +An array of strings or a hash containing product names, and optionally +component names. If you provide a string, the flag type will be shown on +all bugs in that product. If you provide a hash, the key represents the +product name, and the value is the components of the product to be included. + +For example: + + [ 'FooProduct', + { + BarProduct => [ 'C1', 'C3' ], + BazProduct => [ 'C7' ] + } + ] + +This flag will be added to B<All> components of I<FooProduct>, +components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>. + +=item exclusions B<optional> + +An array of strings or hashes containing product names. This uses the same +fromat as inclusions. + +This will exclude the flag from all products and components specified. + +=item sortkey B<optional> + +C<int> A number between 1 and 32767 by which this type will be sorted when +displayed to users in a list; ignore if you don't care what order the types +appear in or if you want them to appear in alphabetical order. + +=item is_active B<optional> + +C<boolean> Flag of this type appear in the UI and can be set. Default is B<true>. + +=item is_requestable B<optional> + +C<boolean> Users can ask for flags of this type to be set. Default is B<true>. + +=item cc_list B<optional> + +C<array> An array of strings. If the flag type is requestable, who should +receive e-mail notification of requests. This is an array of e-mail addresses +which do not need to be Bugzilla logins. + +=item is_specifically_requestable B<optional> + +C<boolean> Users can ask specific other users to set flags of this type as +opposed to just asking the wind. Default is B<true>. + +=item is_multiplicable B<optional> + +C<boolean> Multiple flags of this type can be set on the same bug. Default is B<true>. + +=item grant_group B<optional> + +C<string> The group allowed to grant/deny flags of this type (to allow all +users to grant/deny these flags, select no group). Default is B<no group>. + +=item request_group B<optional> + +C<string> If flags of this type are requestable, the group allowed to request +them (to allow all users to request these flags, select no group). Note that +the request group alone has no effect if the grant group is not defined! +Default is B<no group>. + +=back + +=item B<Errors> + +=over + +=item 51 (Group Does Not Exist) + +The group name you entered does not exist, or you do not have access to it. + +=item 105 (Unknown component) + +The component does not exist for this product. + +=item 106 (Product Access Denied) + +Either the product does not exist or you don't have editcomponents privileges +to it. + +=item 501 (Illegal Email Address) + +One of the e-mail address in the CC list is invalid. An e-mail in the CC +list does NOT need to be a valid Bugzilla user. + +=item 1101 (Flag Type Name invalid) + +You must specify a non-blank name for this flag type. It must +no contain spaces or commas, and must be 50 characters or less. + +=item 1102 (Flag type must have description) + +You must specify a description for this flag type. + +=item 1103 (Flag type CC list is invalid + +The CC list must be 200 characters or less. + +=item 1104 (Flag Type Sort Key Not Valid) + +The sort key is not a valid number. + +=item 1105 (Flag Type Not Editable) + +This flag type is not available for the products you can administer. Therefore +you can not edit attributes of the flag type, other than the inclusion and +exclusion list. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back + +=head2 update + +B<EXPERIMENTAL> + +=over + +=item B<Description> + +This allows you to update a flag type in Bugzilla. + +=item B<REST> + +PUT /rest/flag_type/<product_id_or_name> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> and C<names> params will be overridden as +it is pulled from the URL path. + +=item B<Params> + +B<Note:> The following parameters specify which products you are updating. +You must set one or both of these parameters. + +=over + +=item C<ids> + +C<array> of C<int>s. Numeric ids of the flag types that you wish to update. + +=item C<names> + +C<array> of C<string>s. Names of the flag types that you wish to update. If +many flag types have the same name, this will change ALL of them. + +=back + +B<Note:> The following parameters specify the new values you want to set for +the products you are updating. + +=over + +=item name + +C<string> A short name identifying this type. + +=item description + +C<string> A comprehensive description of this type. + +=item inclusions B<optional> + +An array of strings or a hash containing product names, and optionally +component names. If you provide a string, the flag type will be shown on +all bugs in that product. If you provide a hash, the key represents the +product name, and the value is the components of the product to be included. + +for example + + [ 'FooProduct', + { + BarProduct => [ 'C1', 'C3' ], + BazProduct => [ 'C7' ] + } + ] + +This flag will be added to B<All> components of I<FooProduct>, +components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>. + +=item exclusions B<optional> + +An array of strings or hashes containing product names. +This uses the same fromat as inclusions. + +This will exclude the flag from all products and components specified. + +=item sortkey + +C<int> A number between 1 and 32767 by which this type will be sorted when +displayed to users in a list; ignore if you don't care what order the types +appear in or if you want them to appear in alphabetical order. + +=item is_active + +C<boolean> Flag of this type appear in the UI and can be set. + +=item is_requestable + +C<boolean> Users can ask for flags of this type to be set. + +=item cc_list + +C<array> An array of strings. If the flag type is requestable, who should +receive e-mail notification of requests. This is an array of e-mail addresses +which do not need to be Bugzilla logins. + +=item is_specifically_requestable + +C<boolean> Users can ask specific other users to set flags of this type as +opposed to just asking the wind. + +=item is_multiplicable + +C<boolean> Multiple flags of this type can be set on the same bug. + +=item grant_group + +C<string> The group allowed to grant/deny flags of this type (to allow all +users to grant/deny these flags, select no group). + +=item request_group + +C<string> If flags of this type are requestable, the group allowed to request +them (to allow all users to request these flags, select no group). Note that +the request group alone has no effect if the grant group is not defined! + +=back + +=item B<Returns> + +A C<hash> with a single field "flagtypes". This points to an array of hashes +with the following fields: + +=over + +=item C<id> + +C<int> The id of the product that was updated. + +=item C<name> + +C<string> The name of the product that was updated. + +=item C<changes> + +C<hash> The changes that were actually done on this product. The keys are +the names of the fields that were changed, and the values are a hash +with two keys: + +=over + +=item C<added> + +C<string> The value that this field was changed to. + +=item C<removed> + +C<string> The value that was previously set in this field. + +=back + +Note that booleans will be represented with the strings '1' and '0'. + +Here's an example of what a return value might look like: + + { + products => [ + { + id => 123, + changes => { + name => { + removed => 'FooFlagType', + added => 'BarFlagType' + }, + is_requestable => { + removed => '1', + added => '0', + } + } + } + ] + } + +=back + +=item B<Errors> + +=over + +=item 51 (Group Does Not Exist) + +The group name you entered does not exist, or you do not have access to it. + +=item 105 (Unknown component) + +The component does not exist for this product. + +=item 106 (Product Access Denied) + +Either the product does not exist or you don't have editcomponents privileges +to it. + +=item 501 (Illegal Email Address) + +One of the e-mail address in the CC list is invalid. An e-mail in the CC +list does NOT need to be a valid Bugzilla user. + +=item 1101 (Flag Type Name invalid) + +You must specify a non-blank name for this flag type. It must +no contain spaces or commas, and must be 50 characters or less. + +=item 1102 (Flag type must have description) + +You must specify a description for this flag type. + +=item 1103 (Flag type CC list is invalid + +The CC list must be 200 characters or less. + +=item 1104 (Flag Type Sort Key Not Valid) + +The sort key is not a valid number. + +=item 1105 (Flag Type Not Editable) + +This flag type is not available for the products you can administer. Therefore +you can not edit attributes of the flag type, other than the inclusion and +exclusion list. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=back + +=back diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm index 72c948aa4..468575a35 100644 --- a/Bugzilla/WebService/Group.pm +++ b/Bugzilla/WebService/Group.pm @@ -7,14 +7,18 @@ package Bugzilla::WebService::Group; +use 5.10.1; use strict; -use base qw(Bugzilla::WebService); +use warnings; + +use parent qw(Bugzilla::WebService); use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::WebService::Util qw(validate translate params_to_objects); use constant PUBLIC_METHODS => qw( create + get update ); @@ -97,6 +101,125 @@ sub update { return { groups => \@result }; } +sub get { + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + + Bugzilla->login(LOGIN_REQUIRED); + + # 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'); + } + + Bugzilla->switch_to_shadow_db(); + + my $groups = []; + + if (defined $params->{ids}) { + # Get the groups by id + $groups = Bugzilla::Group->new_from_list($params->{ids}); + } + + 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; + } + 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 }; +} + +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; +} + +sub _get_group_membership { + my ($self, $group, $params) = @_; + my $user = Bugzilla->user; + + my %users_only; + my $dbh = Bugzilla->dbh; + my $editusers = $user->in_group('editusers'); + + 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 (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 + 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; +} + 1; __END__ @@ -116,6 +239,10 @@ get information about them. See L<Bugzilla::WebService> for a description of how parameters are passed, and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. +Although the data input and output is the same for JSONRPC, XMLRPC and REST, +the directions for how to access the data via REST is noted in each method +where applicable. + =head1 Group Creation and Modification =head2 create @@ -128,9 +255,16 @@ B<UNSTABLE> This allows you to create a new group in Bugzilla. -=item B<Params> +=item B<REST> + +POST /rest/group + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> -Some params must be set, or an error will be thrown. These params are +Some params must be set, or an error will be thrown. These params are marked B<Required>. =over @@ -151,7 +285,7 @@ name of the group. C<string> A regular expression. Any user whose Bugzilla username matches this regular expression will automatically be granted membership in this group. -=item C<is_active> +=item C<is_active> C<boolean> C<True> if new group can be used for bugs, C<False> if this is a group that will only contain users and no bugs will be restricted @@ -165,7 +299,7 @@ if they are in this group. =back -=item B<Returns> +=item B<Returns> A hash with one element, C<id>. This is the id of the newly-created group. @@ -191,7 +325,15 @@ You specified an invalid regular expression in the C<user_regexp> field. =back -=back +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back =head2 update @@ -203,6 +345,14 @@ B<UNSTABLE> This allows you to update a group in Bugzilla. +=item B<REST> + +PUT /rest/group/<group_name_or_id> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> param will be overridden as it is pulled +from the URL path. + =item B<Params> At least C<ids> or C<names> must be set, or an error will be thrown. @@ -281,6 +431,176 @@ comma-and-space-separated list if multiple values were removed. The same as L</create>. +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + +=head1 Group Information + +=head2 get + +B<UNSTABLE> + +=over + +=item B<Description> + +Returns information about L<Bugzilla::Group|Groups>. + +=item B<REST> + +To return information about a specific group by C<id> or C<name>: + +GET /rest/group/<group_id_or_name> + +You can also return information about more than one specific group +by using the following in your query string: + +GET /rest/group?ids=1&ids=2&ids=3 or GET /group?names=ProductOne&names=Product2 + +the returned data format is same as below. + +=item B<Params> + +If neither ids or names is passed, and you are in the creategroups or +editusers group, then all groups will be retrieved. Otherwise, only groups +that you have bless privileges for will be returned. + +=over + +=item C<ids> + +C<array> Contain ids of groups to update. + +=item C<names> + +C<array> Contain names of groups to update. + +=item C<membership> + +C<boolean> Set to 1 then a list of members of the passed groups' names and +ids will be returned. + +=back + +=item B<Returns> + +If the user is a member of the "creategroups" group they will receive +information about all groups or groups matching the criteria that they passed. +You have to be in the creategroups group unless you're requesting membership +information. + +If the user is not a member of the "creategroups" group, but they are in the +"editusers" group or have bless privileges to the groups they require +membership information for, the is_active, is_bug_group and user_regexp values +are not supplied. + +The return value will be a hash containing group names as the keys, each group +name will point to a hash that describes the group and has the following items: + +=over + +=item id + +C<int> The unique integer ID that Bugzilla uses to identify this group. +Even if the name of the group changes, this ID will stay the same. + +=item name + +C<string> The name of the group. + +=item description + +C<string> The description of the group. + +=item is_bug_group + +C<int> Whether this groups is to be used for bug reports or is only administrative specific. + +=item user_regexp + +C<string> A regular expression that allows users to be added to this group if their login matches. + +=item is_active + +C<int> Whether this group is currently active or not. + +=item users + +C<array> An array of hashes, each hash contains a user object for one of the +members of this group, only returned if the user sets the C<membership> +parameter to 1, the user hash has the following items: + +=over + +=item id + +C<int> The id of the user. + +=item real_name + +C<string> The actual name of the user. + +=item email + +C<string> The email address of the user. + +=item name + +C<string> The login name of the user. Note that in some situations this is +different than their email. + +=item can_login + +C<boolean> A boolean value to indicate if the user can login into bugzilla. + +=item email_enabled + +C<boolean> A boolean value to indicate if bug-related mail will be sent +to the user or not. + +=item disabled_text + +C<string> A text field that holds the reason for disabling a user from logging +into bugzilla, if empty then the user account is enabled otherwise it is +disabled/closed. + +=back + +=back + +=item B<Errors> + +=over + +=item 51 (Invalid Object) + +A non existing group name was passed to the function, as a result no +group object existed for that invalid name. + +=item 805 (Cannot view groups) + +Logged-in users are not authorized to edit bugzilla groups as they are not +members of the creategroups group in bugzilla, or they are not authorized to +access group member's information as they are not members of the "editusers" +group or can bless the group. + +=back + +=item B<History> + +=over + +=item This function was added in Bugzilla B<5.0>. + +=back + =back =cut diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 1c8d75bb4..f38972bc1 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -7,8 +7,11 @@ package Bugzilla::WebService::Product; +use 5.10.1; use strict; -use base qw(Bugzilla::WebService); +use warnings; + +use parent qw(Bugzilla::WebService); use Bugzilla::Product; use Bugzilla::User; use Bugzilla::Error; @@ -57,64 +60,92 @@ 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}]}; + 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}]}; + 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}]}; + return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; } # Get a list of actual products, based on list of ids or names sub get { - my ($self, $params) = validate(@_, 'ids', 'names'); + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + my $user = Bugzilla->user; - defined $params->{ids} || defined $params->{names} + defined $params->{ids} || defined $params->{names} || defined $params->{type} || ThrowCodeError("params_required", { function => "Product.get", - params => ['ids', 'names'] }); + params => ['ids', 'names', 'type'] }); Bugzilla->switch_to_shadow_db(); - # Only products that are in the users accessible products, - # can be allowed to be returned - my $accessible_products = Bugzilla->user->get_accessible_products; + 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_accessible; + my @requested_products; if (defined $params->{ids}) { # Create a hash with the ids the user wants my %ids = map { $_ => 1 } @{$params->{ids}}; - - # Return the intersection of this, by grepping the ids from - # accessible products. - push(@requested_accessible, - grep { $ids{$_->id} } @$accessible_products); + + # Return the intersection of this, by grepping the ids from $products. + push(@requested_products, + grep { $ids{$_->id} } @$products); } 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 + + # Return the intersection of this, by grepping the names + # from $products, union'ed with products found by ID to # avoid duplicates foreach my $product (grep { $names{lc $_->name} } - @$accessible_products) { + @$products) { next if grep { $_->id == $product->id } - @requested_accessible; - push @requested_accessible, $product; + @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. my @products = map { $self->_product_to_hash($params, $_) } - @requested_accessible; + @requested_products; return { products => \@products }; } @@ -122,7 +153,7 @@ sub create { my ($self, $params) = @_; Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('editcomponents') + Bugzilla->user->in_group('editcomponents') || ThrowUserError("auth_failure", { group => "editcomponents", action => "add", object => "products"}); @@ -158,7 +189,7 @@ sub update { object => "products" }); defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', + || ThrowCodeError('params_required', { function => 'Product.update', params => ['ids', 'names'] }); my $product_objects = params_to_objects($params, 'Bugzilla::Product'); @@ -177,10 +208,10 @@ sub update { my %changes; foreach my $product (@$product_objects) { my $returned_changes = $product->update(); - $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); + $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); } $dbh->bz_commit_transaction(); - + my @result; foreach my $product (@$product_objects) { my %hash = ( @@ -192,7 +223,7 @@ sub update { my $change = $changes{$product->id}->{$field}; $hash{changes}{$field} = { removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) + added => $self->type('string', $change->[1]) }; } @@ -234,7 +265,7 @@ sub _product_to_hash { sub _component_to_hash { my ($self, $component, $params) = @_; - my $field_data = { + my $field_data = filter $params, { id => $self->type('int', $component->id), name => @@ -242,17 +273,17 @@ sub _component_to_hash { description => $self->type('string' , $component->description), default_assigned_to => - $self->type('string' , $component->default_assignee->login), - default_qa_contact => - $self->type('string' , $component->default_qa_contact ? - $component->default_qa_contact->login : ''), + $self->type('email', $component->default_assignee->login), + default_qa_contact => + $self->type('email', $component->default_qa_contact ? + $component->default_qa_contact->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', 'components')) { + if (filter_wants($params, 'flag_types', undef, 'components')) { $field_data->{flag_types} = { bug => [map { @@ -264,12 +295,13 @@ sub _component_to_hash { } @{$component->flag_types->{'attachment'}}], }; } - return filter($params, $field_data, 'components'); + + return $field_data; } sub _flag_type_to_hash { - my ($self, $flag_type) = @_; - return { + my ($self, $flag_type, $params) = @_; + return filter $params, { id => $self->type('int', $flag_type->id), name => @@ -292,12 +324,12 @@ sub _flag_type_to_hash { $self->type('int', $flag_type->grant_group_id), request_group => $self->type('int', $flag_type->request_group_id), - }; + }, undef, 'flag_types'; } sub _version_to_hash { my ($self, $version, $params) = @_; - my $field_data = { + return filter $params, { id => $self->type('int', $version->id), name => @@ -306,13 +338,12 @@ sub _version_to_hash { 0, is_active => $self->type('boolean', $version->is_active), - }; - return filter($params, $field_data, 'versions'); + }, undef, 'versions'; } sub _milestone_to_hash { my ($self, $milestone, $params) = @_; - my $field_data = { + return filter $params, { id => $self->type('int', $milestone->id), name => @@ -321,8 +352,7 @@ sub _milestone_to_hash { $self->type('int', $milestone->sortkey), is_active => $self->type('boolean', $milestone->is_active), - }; - return filter($params, $field_data, 'milestones'); + }, undef, 'milestones'; } 1; @@ -343,6 +373,10 @@ get information about them. See L<Bugzilla::WebService> for a description of how parameters are passed, and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. +Although the data input and output is the same for JSONRPC, XMLRPC and REST, +the directions for how to access the data via REST is noted in each method +where applicable. + =head1 List Products =head2 get_selectable_products @@ -355,15 +389,29 @@ B<EXPERIMENTAL> Returns a list of the ids of the products the user can search on. +=item B<REST> + +GET /rest/product_selectable + +the returned data format is same as below. + =item B<Params> (none) -=item B<Returns> +=item B<Returns> A hash containing one item, C<ids>, that contains an array of product ids. =item B<Errors> (none) +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head2 get_enterable_products @@ -377,6 +425,12 @@ B<EXPERIMENTAL> Returns a list of the ids of the products the user can enter bugs against. +=item B<REST> + +GET /rest/product_enterable + +the returned data format is same as below. + =item B<Params> (none) =item B<Returns> @@ -386,6 +440,14 @@ ids. =item B<Errors> (none) +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head2 get_accessible_products @@ -399,6 +461,12 @@ B<UNSTABLE> Returns a list of the ids of the products the user can search or enter bugs against. +=item B<REST> + +GET /rest/product_accessible + +the returned data format is same as below. + =item B<Params> (none) =item B<Returns> @@ -408,6 +476,14 @@ ids. =item B<Errors> (none) +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head2 get @@ -424,6 +500,24 @@ B<Note>: You must at least specify one of C<ids> or C<names>. B<Note>: Can also be called as "get_products" for compatibilty with Bugzilla 3.0 API. +=item B<REST> + +To return information about a specific groups of products such as +C<accessible>, C<selectable>, or C<enterable>: + +GET /rest/product?type=accessible + +To return information about a specific product by C<id> or C<name>: + +GET /rest/product/<product_id_or_name> + +You can also return information about more than one specific product +by using the following in your query string: + +GET /rest/product?ids=1&ids=2&ids=3 or GET /product?names=ProductOne&names=Product2 + +the returned data format is same as below. + =item B<Params> In addition to the parameters below, this method also accepts the @@ -442,9 +536,15 @@ An array of product ids An array of product names +=item C<type> + +The group of products to return. Valid values are: C<accessible> (default), +C<selectable>, and C<enterable>. C<type> can be a single value or an array +of values if more than one group is needed with duplicates removed. + =back -=item B<Returns> +=item B<Returns> A hash containing one item, C<products>, that is an array of hashes. Each hash describes a product, and has the following items: @@ -524,7 +624,7 @@ components are not enabled for new bugs. =item C<flag_types> -A hash containing the two items C<bug> and C<attachment> that each contains an +A hash containing the two items C<bug> and C<attachment> that each contains an array of hashes, where each hash describes a flagtype, and has the following items: @@ -578,8 +678,8 @@ flagtype. =item C<request_group> -C<int> the group id that is allowed to request the flag if the flag -is of the type requestable. If the item is not included all users +C<int> the group id that is allowed to request the flag if the flag +is of the type requestable. If the item is not included all users are allowed request this flagtype. =back @@ -619,6 +719,8 @@ been removed. =item In Bugzilla B<4.4>, C<flag_types> was added to the fields returned by C<get>. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -635,9 +737,16 @@ B<EXPERIMENTAL> This allows you to create a new product in Bugzilla. -=item B<Params> +=item B<REST> + +POST /rest/product + +The params to include in the POST body as well as the returned data format, +are the same as below. + +=item B<Params> -Some params must be set, or an error will be thrown. These params are +Some params must be set, or an error will be thrown. These params are marked B<Required>. =over @@ -651,11 +760,11 @@ within Bugzilla. B<Required> C<string> A description for this product. Allows some simple HTML. -=item C<version> +=item C<version> B<Required> C<string> The default version for this product. -=item C<has_unconfirmed> +=item C<has_unconfirmed> C<boolean> Allow the UNCONFIRMED status to be set on bugs in this product. Default: true. @@ -664,11 +773,11 @@ Default: true. C<string> The name of the Classification which contains this product. -=item C<default_milestone> +=item C<default_milestone> C<string> The default milestone for this product. Default '---'. -=item C<is_open> +=item C<is_open> C<boolean> True if the product is currently allowing bugs to be entered into it. Default: true. @@ -680,7 +789,7 @@ new product. Default: true. =back -=item B<Returns> +=item B<Returns> A hash with one element, id. This is the id of the newly-filed product. @@ -716,6 +825,14 @@ You must specify a version for this product. =back +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head2 update @@ -728,6 +845,14 @@ B<EXPERIMENTAL> This allows you to update a product in Bugzilla. +=item B<REST> + +PUT /rest/product/<product_id_or_name> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> and C<names> params will be overridden as +it is pulled from the URL path. + =item B<Params> B<Note:> The following parameters specify which products you are updating. @@ -808,7 +933,7 @@ Note that booleans will be represented with the strings '1' and '0'. Here's an example of what a return value might look like: - { + { products => [ { id => 123, @@ -862,6 +987,16 @@ You must define a default milestone. =item Added in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + +=back + =back +=head1 B<Methods in need of POD> + +=over + +=item get_products + =back diff --git a/Bugzilla/WebService/README b/Bugzilla/WebService/README index bbe320979..eb4799cfc 100644 --- a/Bugzilla/WebService/README +++ b/Bugzilla/WebService/README @@ -11,7 +11,7 @@ When XMLRPC::Lite calls a method, $self is the name of the *class* the method is in. For example, if we call Bugzilla.version(), the first argument is Bugzilla::WebService::Bugzilla. So in order to have $self (our first argument) act correctly in XML-RPC, we make all WebService -classes use base qw(Bugzilla::WebService). +classes use parent qw(Bugzilla::WebService). When JSON::RPC calls a method, $self is the JSON-RPC *server object*. In other words, it's an instance of Bugzilla::WebService::Server::JSONRPC. So we have diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm index 15bc4bcca..7950c7a3b 100644 --- a/Bugzilla/WebService/Server.pm +++ b/Bugzilla/WebService/Server.pm @@ -6,12 +6,18 @@ # defined by the Mozilla Public License, v. 2.0. package Bugzilla::WebService::Server; + +use 5.10.1; use strict; +use warnings; use Bugzilla::Error; use Bugzilla::Util qw(datetime_from); use Scalar::Util qw(blessed); +use Digest::MD5 qw(md5_base64); + +use Storable qw(freeze); sub handle_login { my ($self, $class, $method, $full_method) = @_; @@ -23,11 +29,15 @@ sub handle_login { 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 }); @@ -53,4 +63,71 @@ sub datetime_format_outbound { return $time->iso8601(); } +# ETag support +sub bz_etag { + my ($self, $data) = @_; + my $cache = Bugzilla->request_cache; + if (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'}; +} + 1; + +=head1 NAME + +Bugzilla::WebService::Server - Base server class for the WebService API + +=head1 DESCRIPTION + +Bugzilla::WebService::Server is the base class for the individual WebService API +servers such as XMLRPC, JSONRPC, and REST. You never actually create a +Bugzilla::WebService::Server directly, you only make subclasses of it. + +=head1 FUNCTIONS + +=over + +=item C<bz_etag> + +This function is used to store an ETag value that will be used when returning +the data by the different API server modules such as XMLRPC, or REST. The individual +webservice methods can also set the value earlier in the process if needed such as +before a unique update token is added. If a value is not set earlier, an etag will +automatically be created using the returned data except in some cases when an error +has occurred. + +=back + +=head1 SEE ALSO + +L<Bugzilla::WebService::Server::XMLRPC|XMLRPC>, L<Bugzilla::WebService::Server::JSONRPC|JSONRPC>, +and L<Bugzilla::WebService::Server::REST|REST>. + +=head1 B<Methods in need of POD> + +=over + +=item handle_login + +=item datetime_format_outbound + +=item datetime_format_inbound + +=back diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm index 0a0afd400..70b8fd96c 100644 --- a/Bugzilla/WebService/Server/JSONRPC.pm +++ b/Bugzilla/WebService/Server/JSONRPC.pm @@ -7,7 +7,10 @@ package Bugzilla::WebService::Server::JSONRPC; +use 5.10.1; use strict; +use warnings; + use Bugzilla::WebService::Server; BEGIN { our @ISA = qw(Bugzilla::WebService::Server); @@ -24,7 +27,7 @@ BEGIN { use Bugzilla::Error; use Bugzilla::WebService::Constants; use Bugzilla::WebService::Util qw(taint_data fix_credentials); -use Bugzilla::Util qw(correct_urlbase trim disable_utf8); +use Bugzilla::Util; use HTTP::Message; use MIME::Base64 qw(decode_base64 encode_base64); @@ -74,6 +77,7 @@ sub response_header { sub response { my ($self, $response) = @_; + my $cgi = $self->cgi; # Implement JSONP. if (my $callback = $self->_bz_callback) { @@ -95,9 +99,18 @@ sub response { push(@header_args, "-$name", $value); } } - my $cgi = $self->cgi; - print $cgi->header(-status => $response->code, @header_args); - print $response->content; + + # 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); + print $response->content; + } } # The JSON-RPC 1.1 GET specification is not so great--you can't specify @@ -209,6 +222,9 @@ sub type { 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; } @@ -254,7 +270,17 @@ sub _handle { my $self = shift; my ($obj) = @_; $self->{_bz_request_id} = $obj->{id}; - return $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); + } + + return $result; } # Make all error messages returned by JSON::RPC go into the 100000 @@ -577,3 +603,25 @@ the JSON-RPC library that Bugzilla uses, not by Bugzilla. =head1 SEE ALSO L<Bugzilla::WebService> + +=head1 B<Methods in need of POD> + +=over + +=item response + +=item response_header + +=item cgi + +=item retrieve_json_from_get + +=item create_json_coder + +=item type + +=item handle_login + +=item datetime_format_outbound + +=back diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm new file mode 100644 index 000000000..8450a7a28 --- /dev/null +++ b/Bugzilla/WebService/Server/REST.pm @@ -0,0 +1,689 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST; + +use 5.10.1; +use strict; +use warnings; + +use parent qw(Bugzilla::WebService::Server::JSONRPC); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Hook; +use Bugzilla::Util qw(correct_urlbase html_quote); +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Util qw(taint_data fix_credentials); + +# Load resource modules +use Bugzilla::WebService::Server::REST::Resources::Bug; +use Bugzilla::WebService::Server::REST::Resources::Bugzilla; +use Bugzilla::WebService::Server::REST::Resources::Classification; +use Bugzilla::WebService::Server::REST::Resources::Component; +use Bugzilla::WebService::Server::REST::Resources::FlagType; +use Bugzilla::WebService::Server::REST::Resources::Group; +use Bugzilla::WebService::Server::REST::Resources::Product; +use Bugzilla::WebService::Server::REST::Resources::User; +use Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit; + +use List::MoreUtils qw(uniq); +use Scalar::Util qw(blessed reftype); +use MIME::Base64 qw(decode_base64); + +########################### +# Public Method Overrides # +########################### + +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 }); + } + + # 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 }); + + my $params = $self->_retrieve_json_params; + + fix_credentials($params); + + # Fix includes/excludes for each call + rest_include_exclude($params); + + # Set callback name if exists + $self->_bz_callback($params->{'callback'}) if $params->{'callback'}; + + Bugzilla->input_params($params); + + # 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 => correct_urlbase(), + method => $self->bz_method_name, + params => $params + }; + + # Execute the handler + my $result = $self->_handle($obj); + + 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) { + $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} = REST_DOC; + delete $result->{'name'}; # Remove JSONRPCError + } + elsif (exists $json_data->{result}) { + $result = $json_data->{result}; + } + + # The result needs to be a valid JSON data structure + # and not a undefined or scalar value. + if (!ref $result + || blessed($result) + || (ref $result ne 'HASH' && ref $result ne 'ARRAY')) + { + $result = { result => $result }; + } + + Bugzilla::Hook::process('webservice_rest_response', + { rpc => $self, result => \$result, response => $response }); + + # Access Control + $response->header("Access-Control-Allow-Origin", "*"); + $response->header("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with"); + + # 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 = ""; + $template->process("rest.html.tmpl", { result => $result }, \$content) + || ThrowTemplateError($template->error()); + + $response->content_type('text/html'); + } + else { + $content = $self->json->encode($result); + } + + $response->content($content); + + $self->SUPER::response($response); +} + +####################################### +# Bugzilla::WebService Implementation # +####################################### + +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.) We do allow this for GET /login as we need to + # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we + # can also use for Bugzilla_token support. This is OK as it requires + # a login and password to be supplied and will fail if they are not + # valid for the user. + if (!grep($_ eq $self->request->method, ('POST', 'PUT')) + && !($self->bz_class_name eq 'Bugzilla::WebService::User' + && $self->bz_method_name eq 'login')) + { + # 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 $class = $self->bz_class_name; + my $method = $self->bz_method_name; + my $full_method = $class . "." . $method; + + # Bypass JSONRPC::handle_login + Bugzilla::WebService::Server->handle_login($class, $method, $full_method); +} + +############################ +# Private Method Overrides # +############################ + +# 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, @_); + } +} + +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 @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(); + + # 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 }); + + if ($params_is_array) { + $params = [$params]; + } + + return $params; +} + +################### +# Utility Methods # +################### + +sub 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}; +} + +sub 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}; +} + +sub bz_rest_options { + my ($self, $options) = @_; + $self->{_bz_rest_options} = $options if $options; + return $self->{_bz_rest_options}; +} + +sub rest_include_exclude { + my ($params) = @_; + + if ($params->{'include_fields'} && !ref $params->{'include_fields'}) { + $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ]; + } + if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { + $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ]; + } + + return $params; +} + +########################## +# Private Custom Methods # +########################## + +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) ]; + } + } + } + + # 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->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}; + } + + return $params; +} + +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} }) { + eval("require $module") || die $@; + next if !$module->can('rest_resources'); + $resources->{$module} = $module->rest_resources; + } + + Bugzilla::Hook::process('webservice_rest_resources', + { rpc => $self, resources => $resources }); + + # 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; + } + + return $handler_found; +} + +sub _best_content_type { + 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; +} + +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; +} + +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; + } + } + + # 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; + +__END__ + +=head1 NAME + +Bugzilla::WebService::Server::REST - The REST Interface to Bugzilla + +=head1 DESCRIPTION + +This documentation describes things about the Bugzilla WebService that +are specific to REST. For a general overview of the Bugzilla WebServices, +see L<Bugzilla::WebService>. The L<Bugzilla::WebService::Server::REST> +module is a sub-class of L<Bugzilla::WebService::Server::JSONRPC> so any +method documentation not found here can be viewed in it's POD. + +Please note that I<everything> about this REST interface is +B<EXPERIMENTAL>. If you want a fully stable API, please use the +C<Bugzilla::WebService::Server::XMLRPC|XML-RPC> interface. + +=head1 CONNECTING + +The endpoint for the REST interface is the C<rest.cgi> script in +your Bugzilla installation. For example, if your Bugzilla is at +C<bugzilla.yourdomain.com>, to access the API and load a bug, +you would use C<http://bugzilla.yourdomain.com/rest.cgi/bug/35>. + +If using Apache and mod_rewrite is installed and enabled, you can +simplify the endpoint by changing /rest.cgi/ to something like /rest/ +or something similar. So the same example from above would be: +C<http://bugzilla.yourdomain.com/rest/bug/35> which is simpler to remember. + +Add this to your .htaccess file: + + <IfModule mod_rewrite.c> + RewriteEngine On + RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE] + </IfModule> + +=head1 BROWSING + +If the Accept: header of a request is set to text/html (as it is by an +ordinary web browser) then the API will return the JSON data as a HTML +page which the browser can display. In other words, you can play with the +API using just your browser and see results in a human-readable form. +This is a good way to try out the various GET calls, even if you can't use +it for POST or PUT. + +=head1 DATA FORMAT + +The REST API only supports JSON input, and either JSON and JSONP output. +So objects sent and received must be in JSON format. Basically since +the REST API is a sub class of the JSONRPC API, you can refer to +L<JSONRPC|Bugzilla::WebService::Server::JSONRPC> for more information +on data types that are valid for REST. + +On every request, you must set both the "Accept" and "Content-Type" HTTP +headers to the MIME type of the data format you are using to communicate with +the API. Content-Type tells the API how to interpret your request, and Accept +tells it how you want your data back. "Content-Type" must be "application/json". +"Accept" can be either that, or "application/javascript" for JSONP - add a "callback" +parameter to name your callback. + +Parameters may also be passed in as part of the query string for non-GET requests +and will override any matching parameters in the request body. + +=head1 AUTHENTICATION + +Along with viewing data as an anonymous user, you may also see private information +if you have a Bugzilla account by providing your login credentials. + +=over + +=item Login name and password + +Pass in as query parameters of any request: + +login=fred@example.com&password=ilovecheese + +Remember to URL encode any special characters, which are often seen in passwords and to +also enable SSL support. + +=item Login token + +By calling GET /login?login=fred@example.com&password=ilovecheese, you get back +a C<token> value which can then be passed to each subsequent call as +authentication. This is useful for third party clients that cannot use cookies +and do not want to store a user's login and password in the client. You can also +pass in "token" as a convenience. + +=back + +=head1 ERRORS + +When an error occurs over REST, a hash structure is returned with the key C<error> +set to C<true>. + +The error contents look similar to: + + { "error": true, "message": "Some message here", "code": 123 } + +Every error has a "code", as described in L<Bugzilla::WebService/ERRORS>. +Errors with a numeric C<code> higher than 100000 are errors thrown by +the JSON-RPC library that Bugzilla uses, not by Bugzilla. + +=head1 UTILITY FUNCTIONS + +=over + +=item B<handle> + +This method overrides the handle method provided by JSONRPC so that certain +actions related to REST such as determining the proper resource to use, +loading query parameters, etc. can be done before the proper WebService +method is executed. + +=item B<response> + +This method overrides the response method provided by JSONRPC so that +the response content can be altered for REST before being returned to +the client. + +=item B<handle_login> + +This method determines the proper WebService all to make based on class +and method name determined earlier. Then calls L<Bugzilla::WebService::Server::handle_login> +which will attempt to authenticate the client. + +=item B<bz_method_name> + +The WebService method name that matches the path used by the client. + +=item B<bz_class_name> + +The WebService class containing the method that matches the path used by the client. + +=item B<bz_rest_params> + +Each REST resource contains a hash key called C<params> that is a subroutine reference. +This subroutine will return a hash structure based on matched values from the path +information that is formatted properly for the WebService method that will be called. + +=item B<bz_rest_options> + +When a client uses the OPTIONS request method along with a specific path, they are +requesting the list of request methods that are valid for the path. Such as for the +path /bug, the valid request methods are GET (search) and POST (create). So the +client would receive in the response header, C<Access-Control-Allow-Methods: GET, POST>. + +=item B<bz_success_code> + +Each resource can specify a specific SUCCESS CODE if the operation completes successfully. +OTherwise STATUS OK (200) is the default returned. + +=item B<rest_include_exclude> + +Normally the WebService methods required C<include_fields> and C<exclude_fields> to be an +array of field names. REST allows for the values for these to be instead comma delimited +string of field names. This method converts the latter into the former so the WebService +methods will not complain. + +=back + +=head1 SEE ALSO + +L<Bugzilla::WebService> diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm new file mode 100644 index 000000000..3fa8b65cf --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm @@ -0,0 +1,179 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::Bug; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Bug; + +BEGIN { + *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/([^/]+)$}, { + 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/([^/]+)$}, { + 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/([^/]+)/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] }; + } + } + }, + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::Bug - The REST API for creating, +changing, and getting the details of bugs. + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to file a new bug in Bugzilla, +or get information about bugs that have already been filed. + +See L<Bugzilla::WebService::Bug> for more details on how to use this part of +the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm new file mode 100644 index 000000000..8502d6b3b --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm @@ -0,0 +1,52 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit; + +use 5.10.1; +use strict; +use warnings; + +BEGIN { + *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]] }; + }, + }, + }, + ]; +} + +1; +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::BugUserLastVisit - The +BugUserLastVisit REST API + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to lookup and update the last time +a user visited a bug. + +See L<Bugzilla::WebService::BugUserLastVisit> for more details on how to use +this part of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm new file mode 100644 index 000000000..a8f3f9330 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm @@ -0,0 +1,70 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::Bugzilla; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Bugzilla; + +BEGIN { + *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' + } + } + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::WebService::Bugzilla - Global functions for the webservice interface. + +=head1 DESCRIPTION + +This provides functions that tell you about Bugzilla in general. + +See L<Bugzilla::WebService::Bugzilla> for more details on how to use this part +of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm new file mode 100644 index 000000000..3f8d32a03 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm @@ -0,0 +1,50 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::Classification; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Classification; + +BEGIN { + *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] ] }; + } + } + } + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::Classification - The Classification REST API + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to deal with the available Classifications. +You will be able to get information about them as well as manipulate them. + +See L<Bugzilla::WebService::Classification> for more details on how to use this part +of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Component.pm b/Bugzilla/WebService/Server/REST/Resources/Component.pm new file mode 100644 index 000000000..198c09332 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Component.pm @@ -0,0 +1,48 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::Component; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Component; + +use Bugzilla::Error; + +BEGIN { + *Bugzilla::WebService::Component::rest_resources = \&_rest_resources; +}; + +sub _rest_resources { + my $rest_resources = [ + qr{^/component$}, { + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::Component - The Component REST API + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you create Components. + +See L<Bugzilla::WebService::Component> for more details on how to use this +part of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm new file mode 100644 index 000000000..21dad0f73 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm @@ -0,0 +1,72 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::FlagType; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::FlagType; + +use Bugzilla::Error; + +BEGIN { + *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources; +}; + +sub _rest_resources { + my $rest_resources = [ + qr{^/flag_type$}, { + POST => { + method => 'create', + success_code => STATUS_CREATED + } + }, + qr{^/flag_type/([^/]+)/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + return { product => $_[0], + component => $_[1] }; + } + } + }, + qr{^/flag_type/([^/]+)$}, { + GET => { + method => 'get', + params => sub { + return { product => $_[0] }; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + }, + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::FlagType - The Flag Type REST API + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to create and update Flag types. + +See L<Bugzilla::WebService::FlagType> for more details on how to use this +part of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm new file mode 100644 index 000000000..b052e384b --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm @@ -0,0 +1,60 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::Group; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Group; + +BEGIN { + *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/([^/]+)$}, { + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return { $param => [ $_[0] ] }; + } + } + } + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::Group - The REST API for +creating, changing, and getting information about Groups. + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to create Groups and +get information about them. + +See L<Bugzilla::WebService::Group> for more details on how to use this part +of the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm new file mode 100644 index 000000000..607b94b53 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm @@ -0,0 +1,83 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::Product; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Product; + +use Bugzilla::Error; + +BEGIN { + *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; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::Product - The Product REST API + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to list the available Products and +get information about them. + +See L<Bugzilla::WebService::Product> for more details on how to use this part of +the REST API. diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm new file mode 100644 index 000000000..a83109e73 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/User.pm @@ -0,0 +1,81 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::WebService::Server::REST::Resources::User; + +use 5.10.1; +use strict; +use warnings; + +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::User; + +BEGIN { + *Bugzilla::WebService::User::rest_resources = \&_rest_resources; +}; + +sub _rest_resources { + my $rest_resources = [ + qr{^/login$}, { + GET => { + method => 'login' + } + }, + qr{^/logout$}, { + GET => { + method => 'logout' + } + }, + qr{^/valid_login$}, { + GET => { + method => 'valid_login' + } + }, + 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] ] }; + } + } + } + ]; + return $rest_resources; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Server::REST::Resources::User - The User Account REST API + +=head1 DESCRIPTION + +This part of the Bugzilla REST API allows you to get User information as well +as create User Accounts. + +See L<Bugzilla::WebService::User> for more details on how to use this part of +the REST API. diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm index 5f9cb4515..98a0ee405 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -7,7 +7,10 @@ package Bugzilla::WebService::Server::XMLRPC; +use 5.10.1; use strict; +use warnings; + use XMLRPC::Transport::HTTP; use Bugzilla::WebService::Server; if ($ENV{MOD_PERL}) { @@ -18,11 +21,12 @@ if ($ENV{MOD_PERL}) { use Bugzilla::WebService::Constants; use Bugzilla::Error; +use Bugzilla::Util; use List::MoreUtils qw(none); -# Allow WebService methods to call XMLRPC::Lite's type method directly BEGIN { + # Allow WebService methods to call XMLRPC::Lite's type method directly *Bugzilla::WebService::type = sub { my ($self, $type, $value) = @_; if ($type eq 'dateTime') { @@ -31,8 +35,19 @@ BEGIN { $value = Bugzilla::WebService::Server->datetime_format_outbound($value); $value =~ s/-//g; } + 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 { @@ -46,22 +61,38 @@ sub initialize { 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 (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) { + 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]+/, Bugzilla->cgi->header)) { + 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 { @@ -85,8 +116,12 @@ sub handle_login { # This exists to validate input parameters (which XMLRPC::Lite doesn't do) # and also, in some cases, to more-usefully decode them. package Bugzilla::XMLRPC::Deserializer; + +use 5.10.1; use strict; -# We can't use "use base" because XMLRPC::Serializer doesn't return +use warnings; + +# We can't use "use parent" because XMLRPC::Serializer doesn't return # a true value. use XMLRPC::Lite; our @ISA = qw(XMLRPC::Deserializer); @@ -96,6 +131,15 @@ use Bugzilla::WebService::Constants qw(XMLRPC_CONTENT_TYPE_WHITELIST); 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; +} + sub deserialize { my $self = shift; @@ -123,6 +167,7 @@ sub deserialize { fix_credentials($params); Bugzilla->input_params($params); + return $som; } @@ -186,7 +231,11 @@ sub _validation_subs { 1; package Bugzilla::XMLRPC::SOM; + +use 5.10.1; use strict; +use warnings; + use XMLRPC::Lite; our @ISA = qw(XMLRPC::SOM); use Bugzilla::WebService::Util qw(taint_data); @@ -209,9 +258,13 @@ sub paramsin { # This package exists to fix a UTF-8 bug in SOAP::Lite. # See http://rt.cpan.org/Public/Bug/Display.html?id=32952. package Bugzilla::XMLRPC::Serializer; -use Scalar::Util qw(blessed); + +use 5.10.1; use strict; -# We can't use "use base" because XMLRPC::Serializer doesn't return +use warnings; + +use Scalar::Util qw(blessed reftype); +# We can't use "use parent" because XMLRPC::Serializer doesn't return # a true value. use XMLRPC::Lite; our @ISA = qw(XMLRPC::Serializer); @@ -244,8 +297,8 @@ 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); + if ($type eq 'response') { + _strip_undefs($data); } return $self->SUPER::envelope($type, $method, $data); } @@ -256,7 +309,9 @@ sub envelope { # 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"))) { + my $type = reftype($initial) or return; + + if ($type eq "HASH") { while (my ($key, $value) = each(%$initial)) { if ( !defined $value || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) @@ -265,11 +320,11 @@ sub _strip_undefs { delete $initial->{$key}; } else { - $initial->{$key} = _strip_undefs($value); + _strip_undefs($value); } } } - if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) { + elsif ($type eq "ARRAY") { for (my $count = 0; $count < scalar @{$initial}; $count++) { my $value = $initial->[$count]; if ( !defined $value @@ -280,11 +335,10 @@ sub _strip_undefs { $count--; } else { - $initial->[$count] = _strip_undefs($value); + _strip_undefs($value); } } } - return $initial; } sub BEGIN { @@ -386,3 +440,15 @@ perl-SOAP-Lite package in versions 0.68-1 and above. =head1 SEE ALSO L<Bugzilla::WebService> + +=head1 B<Methods in need of POD> + +=over + +=item make_response + +=item initialize + +=item handle_login + +=back diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index 5a7f25036..0ae76d70f 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -7,20 +7,20 @@ package Bugzilla::WebService::User; +use 5.10.1; use strict; -use base qw(Bugzilla::WebService); +use warnings; + +use parent qw(Bugzilla::WebService); -use Bugzilla; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Util qw(trim detaint_natural); -use Bugzilla::WebService::Util qw(filter validate translate params_to_objects); - -use List::Util qw(min); +use Bugzilla::WebService::Util qw(filter filter_wants validate translate params_to_objects); -use List::Util qw(first); +use List::Util qw(first min); # Don't need auth to login use constant LOGIN_EXEMPT => { @@ -39,20 +39,19 @@ use constant PUBLIC_METHODS => qw( logout offer_account_by_email update + valid_login ); use constant MAPPED_FIELDS => { email => 'login', full_name => 'name', login_denied_text => 'disabledtext', - email_enabled => 'disable_mail' }; use constant MAPPED_RETURNS => { login_name => 'email', realname => 'full_name', disabledtext => 'login_denied_text', - disable_mail => 'email_enabled' }; ############## @@ -83,6 +82,17 @@ sub logout { 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); +} + ################# # User Creation # ################# @@ -127,7 +137,7 @@ sub create { # $call = $rpc->call( 'User.get', { ids => [1,2,3], # names => ['testusera@redhat.com', 'testuserb@redhat.com'] }); sub get { - my ($self, $params) = validate(@_, 'names', 'ids'); + my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); Bugzilla->switch_to_shadow_db(); @@ -157,11 +167,11 @@ sub get { } my $in_group = $self->_filter_users_by_group( \@user_objects, $params); - @users = map {filter $params, { + @users = map { filter $params, { id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('string', $_->login), - }} @$in_group; + real_name => $self->type('string', $_->name), + name => $self->type('email', $_->login), + } } @$in_group; return { users => \@users }; } @@ -169,7 +179,7 @@ sub get { 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 + # obj_by_ids are only visible to the user if they can see # the otheruser, for non visible otheruser throw an error foreach my $obj (@$obj_by_ids) { if (Bugzilla->user->can_see_user($obj)){ @@ -207,15 +217,13 @@ sub get { } } - my $in_group = $self->_filter_users_by_group( - \@user_objects, $params); - + my $in_group = $self->_filter_users_by_group(\@user_objects, $params); foreach my $user (@$in_group) { - my $user_info = { + my $user_info = filter $params, { id => $self->type('int', $user->id), real_name => $self->type('string', $user->name), - name => $self->type('string', $user->login), - email => $self->type('string', $user->email), + name => $self->type('email', $user->login), + email => $self->type('email', $user->email), can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), }; @@ -225,18 +233,30 @@ sub get { } if (Bugzilla->user->id == $user->id) { - $user_info->{saved_searches} = [map { $self->_query_to_hash($_) } @{ $user->queries }]; - $user_info->{saved_reports} = [map { $self->_report_to_hash($_) } @{ $user->reports }]; + if (filter_wants($params, 'saved_searches')) { + $user_info->{saved_searches} = [ + map { $self->_query_to_hash($_) } @{ $user->queries } + ]; + } + if (filter_wants($params, 'saved_reports')) { + $user_info->{saved_reports} = [ + map { $self->_report_to_hash($_) } @{ $user->reports } + ]; + } } - 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 (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); + } } - push(@users, filter($params, $user_info)); + push(@users, $user_info); } return { users => \@users }; @@ -296,6 +316,10 @@ sub update { # 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]) @@ -413,11 +437,19 @@ log in/out using an existing account. See L<Bugzilla::WebService> for a description of how parameters are passed, and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. +Although the data input and output is the same for JSONRPC, XMLRPC and REST, +the directions for how to access the data via REST is noted in each method +where applicable. + =head1 Logging In and Out +These method are now deprecated, and will be removed in the release after +Bugzilla 5.0. The correct way of use these REST and RPC calls is noted in +L<Bugzilla::WebService> + =head2 login -B<STABLE> +B<DEPRECATED> =over @@ -431,7 +463,7 @@ etc. This method logs in an user. =over -=item C<login> (string) - The user's login name. +=item C<login> (string) - The user's login name. =item C<password> (string) - The user's password. @@ -444,10 +476,10 @@ which called this method. =item B<Returns> On success, a hash containing two items, C<id>, the numeric id of the -user that was logged in, and a C<token> which can be passed in the parameters -as authentication in other calls. The token can be sent along with any future -requests to the webservice, for the duration of the session, i.e. till -L<User.logout|/logout> is called. +user that was logged in, and a C<token> which can be passed in +the parameters as authentication in other calls. The token can be sent +along with any future requests to the webservice, for the duration of the +session, i.e. till L<User.logout|/logout> is called. =item B<Errors> @@ -465,7 +497,7 @@ specified with the error. =item 305 (New Password Required) The current password is correct, but the user is asked to change -his password. +their password. =item 50 (Param Required) @@ -477,12 +509,14 @@ A login or password parameter was not provided. =over -=item C<remember> was removed in Bugzilla B<4.4> as this method no longer +=item C<remember> was removed in Bugzilla B<5.0> as this method no longer creates a login cookie. -=item C<restrict_login> was added in Bugzilla B<4.4>. +=item C<restrict_login> was added in Bugzilla B<5.0>. + +=item C<token> was added in Bugzilla B<4.4.3>. -=item C<token> was added in Bugzilla B<4.4>. +=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys. =back @@ -490,7 +524,7 @@ creates a login cookie. =head2 logout -B<STABLE> +B<DEPRECATED> =over @@ -506,6 +540,52 @@ Log out the user. Does nothing if there is no user logged in. =back +=head2 valid_login + +B<DEPRECATED> + +=over + +=item B<Description> + +This method will verify whether a client's cookies or current login +token is still valid or have expired. A valid username must be provided +as well that matches. + +=item B<Params> + +=over + +=item C<login> + +The login name that matches the provided cookies or token. + +=item C<token> + +(string) Persistent login token current being used for authentication (optional). +Cookies passed by client will be used before the token if both provided. + +=back + +=item B<Returns> + +Returns true/false depending on if the current cookies or token are valid +for the provided username. + +=item B<Errors> (none) + +=item B<History> + +=over + +=item Added in Bugzilla B<5.0>. + +=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys. + +=back + +=back + =head1 Account Creation and Modification =head2 offer_account_by_email @@ -565,6 +645,13 @@ actually receive an email. This function does not check that. You must be logged in and have the C<editusers> privilege in order to call this function. +=item B<REST> + +POST /rest/user + +The params to include in the POST body as well as the returned data format, +are the same as below. + =item B<Params> =over @@ -608,6 +695,8 @@ password is under three characters.) =item Error 503 (Password Too Long) removed in Bugzilla B<3.6>. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -622,6 +711,14 @@ B<EXPERIMENTAL> Updates user accounts in Bugzilla. +=item B<REST> + +PUT /rest/user/<user_id_or_name> + +The params to include in the PUT body as well as the returned data format, +are the same as below. The C<ids> and C<names> params are overridden as they +are pulled from the URL path. + =item B<Params> =over @@ -659,6 +756,37 @@ C<string> A text field that holds the reason for disabling a user from logging into bugzilla, if empty then the user account is enabled otherwise it is disabled/closed. +=item C<groups> + +C<hash> These specify the groups that this user is directly a member of. +To set these, you should pass a hash as the value. The hash may contain +the following fields: + +=over + +=item C<add> An array of C<int>s or C<string>s. The group ids or group names +that the user should be added to. + +=item C<remove> An array of C<int>s or C<string>s. The group ids or group names +that the user should be removed from. + +=item C<set> An array of C<int>s or C<string>s. An exact set of group ids +and group names that the user should be a member of. NOTE: This does not +remove groups from the user where the person making the change does not +have the bless privilege for. + +If you specify C<set>, then C<add> and C<remove> will be ignored. A group in +both the C<add> and C<remove> list will be added. Specifying a group that the +user making the change does not have bless rights will generate an error. + +=back + +=item C<bless_groups> + +C<hash> - This is the same as groups, but affects what groups a user +has direct membership to bless that group. It takes the same inputs as +groups. + =back =item B<Returns> @@ -708,6 +836,14 @@ Logged-in users are not authorized to edit other users. =back +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head1 User Info @@ -722,6 +858,18 @@ B<STABLE> Gets information about user accounts in Bugzilla. +=item B<REST> + +To get information about a single user: + +GET /rest/user/<user_id_or_name> + +To search for users by name, group using URL params same as below: + +GET /rest/user + +The returned data format is the same as below. + =item B<Params> B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified. @@ -832,7 +980,7 @@ disabled/closed. =item groups C<array> An array of group hashes the user is a member of. If the currently -logged in user is querying his own account or is a member of the 'editusers' +logged in user is querying their own account or is a member of the 'editusers' group, the array will contain all the groups that the user is a member of. Otherwise, the array will only contain groups that the logged in user can bless. Each hash describes the group and contains the following items: @@ -916,7 +1064,7 @@ group ID in the C<group_ids> argument. =item 52 (Invalid Parameter) -The value used must be an integer greater then zero. +The value used must be an integer greater than zero. =item 304 (Authorization Required) @@ -952,6 +1100,8 @@ illegal to pass a group name you don't belong to. =item C<groups>, C<saved_searches>, and C<saved_reports> were added in Bugzilla B<4.4>. +=item REST API call added in Bugzilla B<5.0>. + =back =back diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index c7d63b336..a0a51a8de 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -6,14 +6,25 @@ # defined by the Mozilla Public License, v. 2.0. package Bugzilla::WebService::Util; + +use 5.10.1; use strict; -use base qw(Exporter); +use warnings; + +use Bugzilla::Flag; +use Bugzilla::FlagType; +use Bugzilla::Error; + +use Storable qw(dclone); + +use parent qw(Exporter); # We have to "require", not "use" this, because otherwise it tries to # use features of Test::More during import(). require Test::Taint; our @EXPORT_OK = qw( + extract_flags filter filter_wants taint_data @@ -23,19 +34,93 @@ our @EXPORT_OK = qw( fix_credentials ); -sub filter ($$;$) { - my ($params, $hash, $prefix) = @_; +sub extract_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; + + # 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}; + + 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; + } + } + } + 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; + } + } + } + + 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; foreach my $key (keys %$hash) { - delete $newhash{$key} if !filter_wants($params, $key, $prefix); + delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); } return \%newhash; } -sub filter_wants ($$;$) { - my ($params, $field, $prefix) = @_; +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 @@ -46,28 +131,58 @@ sub filter_wants ($$;$) { 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 $wants = 1; - if (defined $params->{exclude_fields} && $exclude{$field}) { - $wants = 0; + 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}; } - elsif (defined $params->{include_fields} && !$include{$field}) { - if ($prefix) { - # Include the field if the parent is include (and this one is not excluded) - $wants = 0 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 = 0 if ! grep { substr($_, 0, $len) eq $key } keys %include; - } + 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}; } - $cache->{$field} = $wants; - return $wants; + 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 { @@ -87,8 +202,9 @@ sub _delete_bad_keys { # 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+$/) { + # match \w+, contain '.' or '-', so we delete any key that + # doesn't match that. + if ($key !~ /^[\w\.\-]+$/) { delete $item->{$key}; } } @@ -147,17 +263,25 @@ sub params_to_objects { sub fix_credentials { my ($params) = @_; # Allow user to pass in login=foo&password=bar as a convenience - # even if not calling User.login. We also do not delete them as - # User.login requires "login" and "password". + # 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__ @@ -228,3 +352,17 @@ by both "ids" and "names". Returns an arrayref of objects. Allows for certain parameters related to authentication such as Bugzilla_login, Bugzilla_password, and Bugzilla_token to have shorter named equivalents passed in. This function converts the shorter versions to their respective internal names. + +=head2 extract_flags + +Subroutine that takes a list of hashes that are potential flag changes for +both bugs and attachments. Then breaks the list down into two separate lists +based on if the change is to add a new flag or to update an existing flag. + +=head1 B<Methods in need of POD> + +=over + +=item taint_data + +=back |