From 10988be74f22fcba096fcb76af43c2d534095d8b Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Fri, 23 Oct 2015 12:40:36 +0300 Subject: [PATCH] Merge WebServices from Bugzilla 5.0.1 --- Bugzilla/Flag.pm | 1 + Bugzilla/Product.pm | 4 + Bugzilla/WebService/Bug.pm | 2029 ++++++++++++++++++++----- Bugzilla/WebService/Classification.pm | 212 +++ Bugzilla/WebService/Component.pm | 153 ++ Bugzilla/WebService/FlagType.pm | 833 ++++++++++ Bugzilla/WebService/Group.pm | 605 ++++++++ Bugzilla/WebService/Product.pm | 905 ++++++++++- Bugzilla/WebService/User.pm | 719 +++++++-- Bugzilla/WebService/Util.pm | 291 +++- js/global.js | 2 +- 11 files changed, 5182 insertions(+), 572 deletions(-) create mode 100644 Bugzilla/WebService/Classification.pm create mode 100644 Bugzilla/WebService/Component.pm create mode 100644 Bugzilla/WebService/FlagType.pm create mode 100644 Bugzilla/WebService/Group.pm diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm index 4041725f0..5b49d890f 100644 --- a/Bugzilla/Flag.pm +++ b/Bugzilla/Flag.pm @@ -150,6 +150,7 @@ sub status { return $_[0]->{status}; } sub setter_id { return $_[0]->{setter_id}; } sub requestee_id { return $_[0]->{requestee_id}; } sub creation_date { return $_[0]->{creation_date}; } +sub modification_date { return $_[0]->{modification_date}; } ############################### #### Methods #### diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm index 635896cbf..b092303f0 100644 --- a/Bugzilla/Product.pm +++ b/Bugzilla/Product.pm @@ -1007,7 +1007,11 @@ sub flag_types sub allows_unconfirmed { return $_[0]->{allows_unconfirmed}; } sub description { return $_[0]->{description}; } +sub isactive { return $_[0]->{isactive}; } sub is_active { return $_[0]->{isactive}; } +sub votesperuser { return $_[0]->{votesperuser}; } +sub maxvotesperbug { return $_[0]->{maxvotesperbug}; } +sub votestoconfirm { return $_[0]->{votestoconfirm}; } sub votes_per_user { return $_[0]->{votesperuser}; } sub max_votes_per_bug { return $_[0]->{maxvotesperbug}; } sub votes_to_confirm { return $_[0]->{votestoconfirm}; } diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index e05650c19..4d5ba0de8 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -1,27 +1,15 @@ -# -*- Mode: perl; indent-tabs-mode: nil -*- +# 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/. # -# The contents of this file are subject to the Mozilla Public -# License Version 1.1 (the "License"); you may not use this file -# except in compliance with the License. You may obtain a copy of -# the License at http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS -# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or -# implied. See the License for the specific language governing -# rights and limitations under the License. -# -# The Original Code is the Bugzilla Bug Tracking System. -# -# Contributor(s): Marc Schumann -# Max Kanat-Alexander -# Mads Bondo Dydensborg -# Tsahi Asher -# Noura Elhawary -# Frank Becker +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. package Bugzilla::WebService::Bug; use strict; +use warnings; + use base qw(Bugzilla::WebService); use Bugzilla::Comment; @@ -29,14 +17,22 @@ 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); +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 # @@ -46,8 +42,8 @@ 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'], - update => ['deadline'], }; use constant BASE64_FIELDS => { @@ -64,16 +60,40 @@ use constant READ_ONLY => qw( search ); -###################################################### -# Add aliases here for old method name compatibility # -###################################################### +use constant PUBLIC_METHODS => qw( + add_attachment + add_comment + attachments + comments + create + fields + get + history + legal_values + possible_duplicates + render_comment + search + search_comment_tags + update + update_attachment + update_comment_tags + update_see_also + update_tags +); -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_SETTERS => { + file_name => 'filename', + summary => 'description', +}; + +use constant ATTACHMENT_MAPPED_RETURNS => { + description => 'summary', + ispatch => 'is_patch', + isprivate => 'is_private', + isobsolete => 'is_obsolete', + filename => 'file_name', + mimetype => 'content_type', +}; ########### # Methods # @@ -82,11 +102,13 @@ BEGIN { sub fields { my ($self, $params) = validate(@_, 'ids', 'names'); + Bugzilla->switch_to_shadow_db(); + my @fields; if (defined $params->{ids}) { my $ids = $params->{ids}; foreach my $id (@$ids) { - my $loop_field = Bugzilla->get_field($id, THROW_ERROR); + my $loop_field = Bugzilla::Field->check({ id => $id }); push(@fields, $loop_field); } } @@ -94,7 +116,7 @@ sub fields { if (defined $params->{names}) { my $names = $params->{names}; foreach my $field_name (@$names) { - my $loop_field = Bugzilla->get_field($field_name, THROW_ERROR); + my $loop_field = Bugzilla::Field->check($field_name); # Don't push in duplicate fields if we also asked for this field # in "ids". if (!grep($_->id == $loop_field->id, @fields)) { @@ -104,39 +126,42 @@ sub fields { } if (!defined $params->{ids} and !defined $params->{names}) { - @fields = Bugzilla->get_fields({ obsolete => 0 }); + @fields = @{ Bugzilla->fields({ obsolete => 0 }) }; } my @fields_out; foreach my $field (@fields) { - my $visibility_field = $field->visibility_field + my $visibility_field = $field->visibility_field ? $field->visibility_field->name : undef; my $vis_values = $field->visibility_values; + $vis_values = $field->visibility_field ? $field->visibility_field->value_type->new_from_list($vis_values) : []; my $value_field = $field->value_field ? $field->value_field->name : undef; my (@values, $has_values); if ( ($field->is_select and $field->name ne 'product') - or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) + or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) + or $field->name eq 'keywords') { $has_values = 1; @values = @{ $self->_legal_field_values({ field => $field }) }; - } + } if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { $value_field = 'product'; } my %field_data = ( - id => $self->type('int', $field->id), - type => $self->type('int', $field->type), - is_custom => $self->type('boolean', $field->custom), - name => $self->type('string', $field->name), - display_name => $self->type('string', $field->description), - is_mandatory => $self->type('boolean', $field->is_mandatory), - is_on_bug_entry => $self->type('boolean', 1), - visibility_field => $self->type('string', $visibility_field), - visibility_values => [ map { $self->type('string', $_->name) } $field->visibility_field->value_type->new_from_list($vis_values) ], + id => $self->type('int', $field->id), + type => $self->type('int', $field->type), + is_custom => $self->type('boolean', $field->custom), + name => $self->type('string', $field->name), + display_name => $self->type('string', $field->description), + is_mandatory => $self->type('boolean', $field->is_mandatory), + is_on_bug_entry => $self->type('boolean', 1), + visibility_field => $self->type('string', $visibility_field), + visibility_values => + [ map { $self->type('string', $_->name) } @$vis_values ], ); if ($has_values) { $field_data{value_field} = $self->type('string', $value_field); @@ -175,9 +200,11 @@ sub _legal_field_values { my $product_name = $value->product->name; if ($user->can_see_product($product_name)) { push(@result, { - name => $self->type('string', $value->name), - sortkey => $self->type('int', $sortkey), + name => $self->type('string', $value->name), + sort_key => $self->type('int', $sortkey), + sortkey => $self->type('int', $sortkey), # deprecated visibility_values => [$self->type('string', $product_name)], + is_active => $self->type('boolean', $value->is_active), }); } } @@ -185,6 +212,11 @@ sub _legal_field_values { elsif ($field_name eq 'bug_status') { my @status_all = Bugzilla::Status->get_all; + my $initial_status = bless({ id => 0, name => '', is_open => 1, sortkey => 0, + can_change_to => Bugzilla::Status->can_change_to }, + 'Bugzilla::Status'); + unshift(@status_all, $initial_status); + foreach my $status (@status_all) { my @can_change_to; foreach my $change_to (@{ $status->can_change_to }) { @@ -200,23 +232,37 @@ sub _legal_field_values { } push (@result, { - name => $self->type('string', $status->name), - is_open => $self->type('boolean', $status->is_open), - sortkey => $self->type('int', $status->sortkey), + name => $self->type('string', $status->name), + is_open => $self->type('boolean', $status->is_open), + sort_key => $self->type('int', $status->sortkey), + sortkey => $self->type('int', $status->sortkey), # deprecated can_change_to => \@can_change_to, visibility_values => [], }); } } + elsif ($field_name eq 'keywords') { + my @legal_keywords = Bugzilla::Keyword->get_all; + foreach my $value (@legal_keywords) { + push (@result, { + name => $self->type('string', $value->name), + description => $self->type('string', $value->description), + }); + } + } else { my @values = $field->value_type->get_all(); foreach my $value (@values) { - my $vis_values = $value->visibility_values; + my $vis_val = $value->visibility_value; push(@result, { - name => $self->type('string', $value->name), - sortkey => $self->type('int' , $value->sortkey), - visibility_values => [ map { $self->type('string', $_->name) } @{ $vis_values || [] } ], + name => $self->type('string', $value->name), + sort_key => $self->type('int' , $value->sortkey), + sortkey => $self->type('int' , $value->sortkey), # deprecated + visibility_values => [ + defined $vis_val ? $self->type('string', $vis_val->name) + : () + ], }); } } @@ -236,7 +282,7 @@ sub comments { my $bug_ids = $params->{ids} || []; my $comment_ids = $params->{comment_ids} || []; - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->switch_to_shadow_db(); my $user = Bugzilla->user; my %bugs; @@ -283,32 +329,67 @@ 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), text => $self->type('string', $comment->body_full), rawtext => $self->type('string', $comment->body), attachment_id => $self->type('int', $attach_id), + count => $self->type('int', $comment->count), }; + + return filter($filters, $comment_hash, $types, $prefix); } sub get { my ($self, $params) = validate(@_, 'ids'); + 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}) { @@ -326,10 +407,13 @@ 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 }; + $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 @@ -338,18 +422,22 @@ sub get { sub history { my ($self, $params) = validate(@_, 'ids'); + Bugzilla->switch_to_shadow_db(); + my $ids = $params->{ids}; defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - my @return; + my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; + $api_name{'bug_group'} = 'groups'; + my @return; foreach my $bug_id (@$ids) { my %item; my $bug = Bugzilla::Bug->check($bug_id); $bug_id = $bug->id; $item{id} = $self->type('int', $bug_id); - my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id); + my ($activity) = $bug->get_activity(undef, $params->{new_since}); my @history; foreach my $changeset (@$activity) { @@ -358,14 +446,15 @@ sub history { $bug_history{who} = $self->type('string', $changeset->{who}); $bug_history{changes} = []; foreach my $change (@{ $changeset->{changes} }) { + my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; my $attach_id = delete $change->{attachid}; if ($attach_id) { $change->{attachment_id} = $self->type('int', $attach_id); } $change->{removed} = $self->type('string', $change->{removed}); $change->{added} = $self->type('string', $change->{added}); - $change->{field_name} = $self->type('string', - delete $change->{fieldname}); + $change->{field_name} = $self->type('string', $api_field); + delete $change->{fieldname}; push (@{$bug_history{changes}}, $change); } @@ -375,16 +464,9 @@ sub history { $item{history} = \@history; # alias is returned in case users passes a mixture of ids and aliases - # then they get to know which bug activity relates to which value + # then they get to know which bug activity relates to which value # they passed - if (!Bugzilla->get_field('alias')->obsolete) { - $item{alias} = $self->type('string', $bug->alias); - } - else { - # For API reasons, we always want the value to appear, we just - # don't want it to have a value if aliases are turned off. - $item{alias} = undef; - } + $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; push(@return, \%item); } @@ -392,58 +474,122 @@ sub history { return { bugs => \@return }; } -# FIXME replace by normal "Search" sub search { my ($self, $params) = @_; - - if ( defined($params->{offset}) and !defined($params->{limit}) ) { - ThrowCodeError('param_required', + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + Bugzilla->switch_to_shadow_db(); + + my $match_params = dclone($params); + delete $match_params->{include_fields}; + delete $match_params->{exclude_fields}; + + # Determine whether this is a quicksearch query + if (exists $match_params->{quicksearch}) { + my $quicksearch = quicksearch($match_params->{'quicksearch'}); + my $cgi = Bugzilla::CGI->new($quicksearch); + $match_params = $cgi->Vars; + } + + if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) { + ThrowCodeError('param_required', { param => 'limit', function => 'Bug.search()' }); } - $params = Bugzilla::Bug::map_fields($params); - delete $params->{WHERE}; - - unless (Bugzilla->user->is_timetracker) { - delete $params->{$_} foreach qw(estimated_time remaining_time deadline); + my $max_results = Bugzilla->params->{max_search_results}; + unless (defined $match_params->{limit} && $match_params->{limit} == 0) { + if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { + $match_params->{limit} = $max_results; + } } + else { + delete $match_params->{limit}; + delete $match_params->{offset}; + } + + $match_params = Bugzilla::Bug::map_fields($match_params); + + my %options = ( fields => ['bug_id'] ); + + # Find the highest custom field id + my @field_ids = grep(/^f(\d+)$/, keys %$match_params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; # Do special search types for certain fields. - if ( my $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 $when = delete $params->{creation_ts}) { - $params->{WHERE}->{'creation_ts >= ?'} = $when; - } - 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 (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]; + 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++; } - my $bugs = Bugzilla::Bug->match($params); - my $visible = Bugzilla->user->visible_bugs($bugs); - my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible; - return { bugs => \@hashes }; + # Some fields require a search type such as short desc, keywords, etc. + foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { + if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) { + $match_params->{$param . '_type'} = 'allwordssubstr'; + } + } + if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) { + $match_params->{'keywords_type'} = 'allwords'; + } + + # Backwards compatibility with old method regarding role search + $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'}; + foreach my $role (qw(assigned_to reporter qa_contact 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 + # 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'); + } + + $options{order} = [ split(/\s*,\s*/, delete $match_params->{order}) ] if $match_params->{order}; + $options{params} = $match_params; + + 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(); + # Undo the array-ification that validate() does, for "summary". $params->{summary} || ThrowCodeError('param_required', { 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); } @@ -452,6 +598,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 }; } @@ -461,12 +608,15 @@ sub update { my $user = Bugzilla->login(LOGIN_REQUIRED); my $dbh = Bugzilla->dbh; - $params = Bugzilla::Bug::map_fields($params, { summary => 1 }); + # We skip certain fields because their set_ methods actually use + # the external names instead of the internal names. + $params = Bugzilla::Bug::map_fields($params, + { summary => 1, platform => 1, severity => 1, url => 1 }); my $ids = delete $params->{ids}; defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - my @bugs = map { Bugzilla::Bug->check($_) } @$ids; + my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids; my %values = %$params; $values{other_bugs} = \@bugs; @@ -479,15 +629,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) { - if (!$user->can_edit_product($bug->product_obj->id) ) { - ThrowUserError("product_edit_denied", - { product => $bug->product }); - } - $bug->set_all(\%values); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($old_flags, $new_flags); + } } my %all_changes; @@ -517,19 +677,17 @@ 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. - if (Bugzilla->get_field('alias')->enabled) { - $hash{alias} = $self->type('string', $bug->alias); - } - else { - # For API reasons, we always want the alias field to appear, we - # just don't want it to have a value if aliases are turned off. - $hash{alias} = $self->type('string', ''); - } + $hash{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; my %changes = %{ $all_changes{$bug->id} }; foreach my $field (keys %changes) { my $change = $changes{$field}; my $api_field = $api_name{$field} || $field; + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; $hash{changes}->{$api_field} = { removed => $self->type('string', $change->[0]), added => $self->type('string', $change->[1]) @@ -544,23 +702,50 @@ 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->new; $bug->set_all($params); $bug->add_comment($params->{comment}); $bug->update; - 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) }; } sub legal_values { my ($self, $params) = @_; + + Bugzilla->switch_to_shadow_db(); + + defined $params->{field} + or ThrowCodeError('param_required', { param => 'field' }); + my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field}; my @global_selects = grep { $_->name ne 'product' && $_->name ne 'classification' && $_->name ne 'component' } - Bugzilla->get_fields({ is_select => 1 }); + @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) }; my $values; if (grep($_->name eq $field, @global_selects)) { @@ -612,17 +797,18 @@ sub add_attachment { defined $params->{data} || ThrowCodeError('param_required', { param => 'data' }); - my @bugs = map { Bugzilla::Bug->check($_) } @{ $params->{ids} }; - foreach my $bug (@bugs) { - Bugzilla->user->can_edit_product($bug->product_id) - || ThrowUserError("product_edit_denied", {product => $bug->product}); - } + my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{ $params->{ids} }; my @created; $dbh->bz_start_transaction(); + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my $flags = delete $params->{flags}; + foreach my $bug (@bugs) { my $attachment = Bugzilla::Attachment->create({ bug => $bug, + creation_ts => $timestamp, data => $params->{data}, description => $params->{summary}, filename => $params->{file_name}, @@ -630,6 +816,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, @@ -637,23 +830,135 @@ sub add_attachment { extra_data => $attachment->id }); push(@created, $attachment); } - $_->bug->update($_->attached) foreach @created; + $_->bug->update($timestamp) foreach @created; $dbh->bz_commit_transaction(); $_->send_changes() foreach @bugs; - my %attachments = map { $_->id => $self->_attachment_to_hash($_, $params) } - @created; + my @created_ids = map { $_->id } @created; - return { attachments => \%attachments }; + 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) = @_; - - #The user must login in order add a comment - Bugzilla->login(LOGIN_REQUIRED); - + + # The user must login in order add a comment + my $user = Bugzilla->login(LOGIN_REQUIRED); + # Check parameters defined $params->{id} || ThrowCodeError('param_required', { param => 'id' }); @@ -661,10 +966,8 @@ sub add_comment { (defined $comment && trim($comment) ne '') || ThrowCodeError('param_required', { param => 'comment' }); - my $bug = Bugzilla::Bug->check($params->{id}); + my $bug = Bugzilla::Bug->check_for_edit($params->{id}); - Bugzilla->user->can_edit_bug($bug, THROW_ERROR); - # Backwards-compatibility for versions before 3.6 if (defined $params->{private}) { $params->{is_private} = delete $params->{private}; @@ -672,22 +975,13 @@ sub add_comment { # Append comment $bug->add_comment($comment, { isprivate => $params->{is_private}, work_time => $params->{work_time} }); - - # Capture the call to bug->update (which creates the new comment) in - # a transaction so we're sure to get the correct comment_id. - - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - $bug->update(); - - my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id'); - - $dbh->bz_commit_transaction(); - + + my $new_comment_id = $bug->{added_comments}[0]->id; + # Send mail. - Bugzilla::BugMail::Send($bug->bug_id, { changer => Bugzilla->user }); - + Bugzilla::BugMail::Send($bug->bug_id, { changer => $user }); + return { id => $self->type('int', $new_comment_id) }; } @@ -705,8 +999,7 @@ sub update_see_also { my @bugs; foreach my $id (@{ $params->{ids} }) { - my $bug = Bugzilla::Bug->check($id); - $user->can_edit_bug($bug, THROW_ERROR); + my $bug = Bugzilla::Bug->check_for_edit($id); push(@bugs, $bug); if ($remove) { $bug->remove_see_also($_) foreach @$remove; @@ -739,6 +1032,8 @@ sub update_see_also { sub attachments { my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + if (!(defined $params->{ids} or defined $params->{attachment_ids})) { @@ -775,6 +1070,21 @@ sub attachments { return { bugs => \%bugs, attachments => \%attachments }; } +sub update_tags { + my ($self, $params) = @_; + return { changes => {} }; +} + +sub update_comment_tags { + my ($self, $params) = @_; + return []; +} + +sub search_comment_tags { + my ($self, $params) = @_; + return []; +} + ############################## # Private Helper Subroutines # ############################## @@ -791,16 +1101,13 @@ sub _bug_to_hash { # A bug attribute is "basic" if it doesn't require an additional # database call to get the info. # FIXME remove hardcode, use global "fielddefs" metadata - my %item = ( - alias => $self->type('string', $bug->alias), - classification => $self->type('string', $bug->classification), - component => $self->type('string', $bug->component), - 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), priority => $self->type('string', $bug->priority && $bug->priority_obj->name), - product => $self->type('string', $bug->product), resolution => $self->type('string', $bug->resolution && $bug->resolution_obj->name), severity => $self->type('string', $bug->bug_severity && $bug->bug_severity_obj->name), status => $self->type('string', $bug->bug_status && $bug->bug_status_obj->name), @@ -809,7 +1116,7 @@ sub _bug_to_hash { url => $self->type('string', $bug->bug_file_loc), version => $self->type('string', $bug->version && $bug->version_obj->name), whiteboard => $self->type('string', $bug->status_whiteboard), - ); + } }; if (Bugzilla->get_field('op_sys')->enabled && filter_wants $params, 'op_sys') { @@ -821,22 +1128,37 @@ sub _bug_to_hash { $item{platform} = $self->type('string', $bug->rep_platform && $bug->rep_platform_obj->name); } - # 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 (Bugzilla->get_field('alias')->enabled && + filter_wants $params, 'alias') { + $item{alias} = [ $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 }; $item{'blocks'} = \@blocks; } + if (filter_wants $params, 'classification') { + $item{classification} = $self->type('string', $bug->classification); + } + if (filter_wants $params, 'component') { + $item{component} = $self->type('string', $bug->component); + } if (filter_wants $params, 'cc') { - my @cc = map { $self->type('string', $_->login) } @{ $bug->cc_users || [] }; + 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 }; @@ -858,25 +1180,39 @@ sub _bug_to_hash { @{ $bug->keywords_obj }; $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 (Bugzilla->get_field('qa_contact')->enabled && 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', $_) } @{ $bug->see_also }; + my @see_also = map { $self->type('string', $_->name) } + @{ $bug->see_also }; $item{'see_also'} = \@see_also; } + if (filter_wants $params, 'flags') { + $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; + } # And now custom fields my @custom_fields = Bugzilla->active_custom_fields; 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) # FIXME: || $field->type == FIELD_TYPE_DATE + { $item{$name} = $self->type('dateTime', $bug->$name); } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { @@ -893,33 +1229,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 (Bugzilla->user->id) { - my $token = issue_hash_token([$bug->id, $bug->delta_ts]); - $item{'update_token'} = $self->type('string', $token); + 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); + } } # 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 filter $params, \%item; + return \%item; +} + +sub _user_to_hash { + my ($self, $user, $filters, $types, $prefix) = @_; + my $item = filter $filters, { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->realname), + name => $self->type('email', $user->login), + email => $self->type('email', $user->email), + }, $types, $prefix; + return $item; } sub _attachment_to_hash { - my ($self, $attach, $filters) = @_; - - # Skipping attachment flags for now. - delete $attach->{flags}; + my ($self, $attach, $filters, $types, $prefix) = @_; my $item = filter $filters, { creation_time => $self->type('dateTime', $attach->attached), @@ -928,28 +1273,66 @@ 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', $types, $prefix) { + $item->{'size'} = $self->type('int', $attach->datasize); + } + + if (filter_wants $filters, 'flags', $types, $prefix) { + $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; + } + return $item; } +sub _flag_to_hash { + my ($self, $flag) = @_; + + my $item = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $item->{$field} = $self->type('email', $flag->$field->login) + if $flag->$field_id; + } + + return $item; +} + +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__ @@ -969,11 +1352,13 @@ or get information about bugs that have already been filed. See L for a description of how parameters are passed, and what B, B, and B mean. -=head2 Utility Functions +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. -=over +=head1 Utility Functions -=item C +=head2 fields B @@ -984,11 +1369,26 @@ B Get information about valid bug fields, including the lists of legal values for each field. +=item B + +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/ + +The returned data format is the same as below. + =item B You can pass either field ids or field names. -B: If neither C nor C is specified, then all +B: If neither C nor C is specified, then all non-obsolete fields will be returned. In addition to the parameters below, this method also accepts the @@ -1012,7 +1412,7 @@ containing the following keys: =item C -C An integer id uniquely idenfifying this field in this installation only. +C An integer id uniquely identifying this field in this installation only. =item C @@ -1036,6 +1436,12 @@ C 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 @@ -1087,7 +1493,7 @@ values of the field are shown in the user interface. Can be null. This is an array of hashes, representing the legal values for select-type (drop-down and multiple-selection) fields. This is also -populated for the C, C, and C +populated for the C, C, C, and C fields, but not for the C field (you must use L for that. @@ -1104,11 +1510,15 @@ Each hash has the following keys: C The actual value--this is what you would specify for this field in L, etc. -=item C +=item C C Values, when displayed in a list, are sorted first by this integer and then secondly by their name. +=item C + +B - Use C instead. + =item C If C is defined for this field, then this value is only shown @@ -1116,6 +1526,17 @@ if the C is set to one of the values listed in this array. Note that for per-product fields, C is set to C<'product'> and C will reflect which product(s) this value appears in. +=item C + +C This value is defined only for certain product specific fields +such as version, target_milestone or component. When true, the value is active, +otherwise the value is not active. + +=item C + +C The description of the value. This item is only included for the +C field. + =item C C For C values, determines whether this status @@ -1163,15 +1584,19 @@ You specified an invalid field name or id. =item Added in Bugzilla B<3.6>. -=item There is no C return value yet in Bugzilla4Intranet. -The C return value was added in Bugzilla B<4.0>. +=item The C return value was added in Bugzilla B<4.0>. + +=item C was renamed to C in Bugzilla B<4.2>. + +=item C return key for C was added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0> =back =back - -=item C +=head2 legal_values B - Use L instead. @@ -1181,6 +1606,18 @@ B - Use L instead. Tells you what values are allowed for a particular field. +=item B + +To get information on the values for a field based on field name: + +GET /rest/field/bug//values + +To get information based on field name and a specific product: + +GET /rest/field/bug///values + +The returned data format is the same as below. + =item B =over @@ -1213,17 +1650,19 @@ You specified a field that doesn't exist or isn't a drop-down field. =back -=back - - -=back - -=head2 Bug Information +=item B =over +=item REST API call added in Bugzilla B<5.0>. -=item C +=back + +=back + +=head1 Bug Information + +=head2 attachments B @@ -1237,6 +1676,18 @@ and/or attachment ids. B: Private attachments will only be returned if you are in the insidergroup or if you are the submitter of the attachment. +=item B + +To get all current attachments for a bug: + +GET /rest/bug//attachment + +To get a specific attachment based on attachment ID: + +GET /rest/bug/attachment/ + +The returned data format is the same as below. + =item B B: At least one of C or C is required. @@ -1263,19 +1714,14 @@ value looks like this: { bugs => { - 1345 => { - attachments => [ - { (attachment) }, - { (attachment) } - ] - }, - 9874 => { - attachments => [ - { (attachment) }, - { (attachment) } - ] - - }, + 1345 => [ + { (attachment) }, + { (attachment) } + ], + 9874 => [ + { (attachment) }, + { (attachment) } + ], }, attachments => { @@ -1286,9 +1732,8 @@ value looks like this: The attachments of any bugs that you specified in the C argument in input are returned in C on output. C is a hash that has integer -bug IDs for keys and contains a single key, C. That key points -to an arrayref that contains attachments as a hash. (Fields for attachments -are described below.) +bug IDs for keys and the values are arrayrefs that contain hashes as attachments. +(Fields for attachments are described below.) For any attachments that you specified directly in C, they are returned in C on output. This is a hash where the attachment @@ -1303,6 +1748,10 @@ diagram above) are: C The raw data of the attachment, encoded as Base64. +=item C + +C The length (in bytes) of the attachment. + =item C C The time the attachment was created. @@ -1327,10 +1776,6 @@ C The file name of the attachment. C A short string describing the attachment. -Also returned as C, for backwards-compatibility with older -Bugzillas. (However, this backwards-compatibility will go away in Bugzilla -5.0.) - =item C C The MIME type of the attachment. @@ -1344,13 +1789,6 @@ group called the "insidergroup"), False otherwise. C True if the attachment is obsolete, False otherwise. -=item C - -C True if the attachment is a URL instead of actual data, -False otherwise. Note that such attachments only happen when the -Bugzilla installation has at some point had the C -parameter enabled. - =item C C True if the attachment is a patch, False otherwise. @@ -1359,9 +1797,47 @@ C True if the attachment is a patch, False otherwise. C The login name of the user that created the attachment. -Also returned as C, for backwards-compatibility with older -Bugzillas. (However, this backwards-compatibility will go away in Bugzilla -5.0.) +=item C + +An array of hashes containing the information about flags currently set +for each attachment. Each flag hash contains the following items: + +=over + +=item C + +C The id of the flag. + +=item C + +C The name of the flag. + +=item C + +C The type id of the flag. + +=item C + +C The timestamp when this flag was originally created. + +=item C + +C The timestamp when the flag was last modified. + +=item C + +C The current status of the flag. + +=item C + +C The login name of the user who created or last modified the flag. + +=item C + +C The login name of the user this flag has been requested to be granted or denied. +Note, this field is only returned if a requestee is set. + +=back =back @@ -1394,12 +1870,21 @@ C. =item The C return value was added in Bugzilla B<4.0>. +=item In Bugzilla B<4.2>, the C return value was removed +(this attribute no longer exists for attachments). + +=item The C return value was added in Bugzilla B<4.4>. + +=item The C array was added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + =back =back -=item C +=head2 comments B @@ -1410,6 +1895,18 @@ B This allows you to get data about comments, given a list of bugs and/or comment ids. +=item B + +To get all comments for a particular bug using the bug ID or alias: + +GET /rest/bug//comment + +To get a specific comment based on the comment ID: + +GET /rest/bug/comment/ + +The returned data format is the same as below. + =item B B: At least one of C or C is required. @@ -1484,6 +1981,11 @@ C The ID of the bug that this comment is on. C If the comment was made on an attachment, this will be the ID of that attachment. Otherwise it will be null. +=item count + +C The number of the comment local to the bug. The Description is 0, +comments start with 1. + =item text C The actual text of the comment. @@ -1492,14 +1994,17 @@ C The actual text of the comment. C The login name of the comment's author. -Also returned as C, for backwards-compatibility with older -Bugzillas. (However, this backwards-compatibility will go away in Bugzilla -5.0.) - =item time C The time (in Bugzilla's timezone) that the comment was added. +=item creation_time + +C This is exactly same as the C