diff options
Diffstat (limited to 'Bugzilla/WebService')
-rw-r--r-- | Bugzilla/WebService/Bug.pm | 541 | ||||
-rw-r--r-- | Bugzilla/WebService/Bugzilla.pm | 48 | ||||
-rw-r--r-- | Bugzilla/WebService/Classification.pm | 210 | ||||
-rw-r--r-- | Bugzilla/WebService/Constants.pm | 74 | ||||
-rw-r--r-- | Bugzilla/WebService/Group.pm | 29 | ||||
-rw-r--r-- | Bugzilla/WebService/Product.pm | 210 | ||||
-rw-r--r-- | Bugzilla/WebService/Server.pm | 72 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/JSONRPC.pm | 40 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST.pm | 638 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Bug.pm | 152 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm | 69 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Classification.pm | 49 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Group.pm | 56 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/Product.pm | 82 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/REST/Resources/User.pm | 75 | ||||
-rw-r--r-- | Bugzilla/WebService/Server/XMLRPC.pm | 34 | ||||
-rw-r--r-- | Bugzilla/WebService/User.pm | 187 | ||||
-rw-r--r-- | Bugzilla/WebService/Util.pm | 104 |
18 files changed, 2511 insertions, 159 deletions
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index 9ccd84cc2..e3ebf8dc8 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -38,6 +38,10 @@ use Bugzilla::Version; use Bugzilla::Milestone; use Bugzilla::Status; use Bugzilla::Token qw(issue_hash_token); +use Bugzilla::Search; + +use List::Util qw(max); +use List::MoreUtils qw(uniq); ############# # Constants # @@ -82,6 +86,8 @@ BEGIN { sub fields { my ($self, $params) = validate(@_, 'ids', 'names'); + Bugzilla->switch_to_shadow_db(); + my @fields; if (defined $params->{ids}) { my $ids = $params->{ids}; @@ -117,11 +123,12 @@ sub fields { 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'; @@ -211,6 +218,15 @@ sub _legal_field_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 = Bugzilla::Field::Choice->type($field)->get_all(); foreach my $value (@values) { @@ -242,7 +258,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; @@ -297,9 +313,10 @@ sub _translate_comment { return filter $filters, { 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), + author => $self->type('email', $comment->author->login), time => $self->type('dateTime', $comment->creation_ts), + creation_time => $self->type('dateTime', $comment->creation_ts), is_private => $self->type('boolean', $comment->is_private), text => $self->type('string', $comment->body_full), attachment_id => $self->type('int', $attach_id), @@ -309,11 +326,20 @@ sub _translate_comment { sub get { my ($self, $params) = validate(@_, 'ids'); + Bugzilla->switch_to_shadow_db(); + my $ids = $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + (defined $ids && scalar @$ids) + || ThrowCodeError('param_required', { param => 'ids' }); my @bugs; my @faults; + + # 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}) { @@ -334,6 +360,18 @@ sub get { push(@bugs, $self->_bug_to_hash($bug, $params)); } + # Set the ETag before inserting the update tokens + # since the tokens will always be unique even if + # the data has not changed. + $self->bz_etag(\@bugs); + + if (Bugzilla->user->id) { + foreach my $bug (@bugs) { + my $token = issue_hash_token([$bug->{'id'}, $bug->{'last_change_time'}]); + $bug->{'update_token'} = $self->type('string', $token); + } + } + return { bugs => \@bugs, faults => \@faults }; } @@ -343,34 +381,39 @@ 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) = Bugzilla::Bug::GetBugActivity($bug_id, undef, $params->{start_time}); my @history; foreach my $changeset (@$activity) { my %bug_history; $bug_history{when} = $self->type('dateTime', $changeset->{when}); - $bug_history{who} = $self->type('string', $changeset->{who}); + $bug_history{who} = $self->type('email', $changeset->{who}); $bug_history{changes} = []; foreach my $change (@{ $changeset->{changes} }) { + my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; my $attach_id = delete $change->{attachid}; if ($attach_id) { $change->{attachment_id} = $self->type('int', $attach_id); } $change->{removed} = $self->type('string', $change->{removed}); $change->{added} = $self->type('string', $change->{added}); - $change->{field_name} = $self->type('string', - delete $change->{fieldname}); + $change->{field_name} = $self->type('string', $api_field); + delete $change->{fieldname}; push (@{$bug_history{changes}}, $change); } @@ -399,9 +442,13 @@ 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', + ThrowCodeError('param_required', { param => 'limit', function => 'Bug.search()' }); } @@ -417,59 +464,85 @@ sub 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 %options = ( fields => ['bug_id'] ); + + # Find the highest custom field id + my @field_ids = grep(/^f(\d+)$/, keys %$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 $params->{'delta_ts'}) { + $params->{"f${last_field_id}"} = 'delta_ts'; + $params->{"o${last_field_id}"} = 'equals'; + $params->{"v${last_field_id}"} = $change_when; + $last_field_id++; } - if (my $when = delete $params->{creation_ts}) { - $params->{WHERE}->{'creation_ts >= ?'} = $when; + if (my $creation_when = delete $params->{'creation_ts'}) { + $params->{"f${last_field_id}"} = 'creation_ts'; + $params->{"o${last_field_id}"} = 'equals'; + $params->{"v${last_field_id}"} = $creation_when; + $last_field_id++; } - 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]; + + # 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 $params->{$param} && !defined $params->{$param . '_type'}) { + $params->{$param . '_type'} = 'allwordssubstr'; + } } - 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 (defined $params->{'keywords'} && !defined $params->{'keywords_type'}) { + $params->{'keywords_type'} = 'allwords'; } + # Backwards compatibility with old method regarding role search + $params->{'reporter'} = delete $params->{'creator'} if $params->{'creator'}; + foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) { + next if !exists $params->{$role}; + my $value = delete $params->{$role}; + $params->{"f${last_field_id}"} = $role; + $params->{"o${last_field_id}"} = "anywordssubstr"; + $params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value; + $last_field_id++; + } + + my %match_params = %{ $params }; + delete $params->{include_fields}; + delete $params->{exclude_fields}; + # 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)$/i, keys %$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_columns} = [ split(/\s*,\s*/, delete $params->{order}) ] if $params->{order}; + $options{params} = $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 = Bugzilla::Bug->new_from_list(\@bug_ids); + + my @bugs = map { $self->_bug_to_hash($_, \%match_params) } @$bug_objects; + + return { bugs => \@bugs }; } sub possible_duplicates { my ($self, $params) = validate(@_, 'product'); 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' }); @@ -490,6 +563,12 @@ sub possible_duplicates { sub update { my ($self, $params) = validate(@_, 'ids'); + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + my $user = Bugzilla->login(LOGIN_REQUIRED); my $dbh = Bugzilla->dbh; @@ -584,6 +663,13 @@ sub update { sub create { my ($self, $params) = @_; + + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + Bugzilla->login(LOGIN_REQUIRED); $params = Bugzilla::Bug::map_fields($params); my $bug = Bugzilla::Bug->create($params); @@ -594,6 +680,8 @@ sub create { sub legal_values { my ($self, $params) = @_; + Bugzilla->switch_to_shadow_db(); + defined $params->{field} or ThrowCodeError('param_required', { param => 'field' }); @@ -646,6 +734,12 @@ sub add_attachment { my ($self, $params) = validate(@_, 'ids'); my $dbh = Bugzilla->dbh; + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + Bugzilla->login(LOGIN_REQUIRED); defined $params->{ids} || ThrowCodeError('param_required', { param => 'ids' }); @@ -694,6 +788,12 @@ sub add_attachment { sub add_comment { my ($self, $params) = @_; + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + #The user must login in order add a comment Bugzilla->login(LOGIN_REQUIRED); @@ -738,6 +838,12 @@ sub add_comment { sub update_see_also { my ($self, $params) = @_; + # BMO: Don't allow updating of bugs if disabled + if (Bugzilla->params->{disable_bug_updates}) { + ThrowErrorPage('bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.'); + } + my $user = Bugzilla->login(LOGIN_REQUIRED); # Check parameters @@ -785,6 +891,8 @@ sub update_see_also { sub attachments { my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); + Bugzilla->switch_to_shadow_db(); + if (!(defined $params->{ids} or defined $params->{attachment_ids})) { @@ -863,18 +971,18 @@ sub _bug_to_hash { # We don't do the SQL calls at all if the filter would just # eliminate them anyway. 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); } if (filter_wants $params, 'blocks') { my @blocks = map { $self->type('int', $_) } @{ $bug->blocked }; $item{'blocks'} = \@blocks; } if (filter_wants $params, 'cc') { - my @cc = map { $self->type('string', $_) } @{ $bug->cc || [] }; + my @cc = map { $self->type('email', $_) } @{ $bug->cc || [] }; $item{'cc'} = \@cc; } if (filter_wants $params, 'creator') { - $item{'creator'} = $self->type('string', $bug->reporter->login); + $item{'creator'} = $self->type('email', $bug->reporter->login); } if (filter_wants $params, 'depends_on') { my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson }; @@ -898,13 +1006,16 @@ sub _bug_to_hash { } 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 (filter_wants $params, 'see_also') { my @see_also = map { $self->type('string', $_->name) } @{ $bug->see_also }; $item{'see_also'} = \@see_also; } + if (filter_wants $params, 'flags') { + $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; + } # And now custom fields my @custom_fields = Bugzilla->active_custom_fields; @@ -914,7 +1025,9 @@ sub _bug_to_hash { 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) { @@ -933,11 +1046,7 @@ sub _bug_to_hash { # 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); + $item{'actual_time'} = $self->type('double', $bug->actual_time); } # The "accessible" bits go here because they have long names and it @@ -953,9 +1062,6 @@ sub _bug_to_hash { sub _attachment_to_hash { my ($self, $attach, $filters) = @_; - # Skipping attachment flags for now. - delete $attach->{flags}; - my $item = filter $filters, { creation_time => $self->type('dateTime', $attach->attached), last_change_time => $self->type('dateTime', $attach->modification_time), @@ -974,7 +1080,7 @@ sub _attachment_to_hash { # the filter wants them. foreach my $field (qw(creator attacher)) { if (filter_wants $filters, $field) { - $item->{$field} = $self->type('string', $attach->attacher->login); + $item->{$field} = $self->type('email', $attach->attacher->login); } } @@ -982,6 +1088,31 @@ sub _attachment_to_hash { $item->{'data'} = $self->type('base64', $attach->data); } + if (filter_wants $filters, 'flags') { + $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; } @@ -1004,6 +1135,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 @@ -1017,11 +1152,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 /field/bug + +To get information related to a single field: + +GET /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 @@ -1120,7 +1270,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<component>, C<version>, and C<target_milestone> +populated for the C<component>, C<version>, C<target_milestone>, and C<keywords> fields, but not for the C<product> field (you must use L<Product.get_accessible_products|Bugzilla::WebService::Product/get_accessible_products> for that. @@ -1153,6 +1303,11 @@ if the C<value_field> is set to one of the values listed in this array. Note that for per-product fields, C<value_field> is set to C<'product'> and C<visibility_values> will reflect which product(s) this value appears in. +=item C<description> + +C<string> The description of the value. This item is only included for the +C<keywords> field. + =item C<is_open> C<boolean> For C<bug_status> values, determines whether this status @@ -1206,6 +1361,8 @@ You specified an invalid field name or id. =back +=item REST API call added in Bugzilla B<5.0> + =back @@ -1219,6 +1376,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 /field/bug/<field_name>/values + +To get information based on field name and a specific product: + +GET /field/bug/<field_name>/<product_id>/values + +The returned data format is the same as below. + =item B<Params> =over @@ -1251,6 +1420,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 @@ -1269,6 +1446,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 /bug/<bug_id>/attachment + +To get a specific attachment based on attachment ID: + +GET /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. @@ -1382,6 +1571,48 @@ 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 +for each attachment. Each flag hash contains the following items: + +=over + +=item C<id> + +C<int> The id of the flag. + +=item C<name> + +C<string> The name of the flag. + +=item C<type_id> + +C<int> The type id of the flag. + +=item C<creation_date> + +C<dateTime> The timestamp when this flag was originally created. + +=item C<modification_date> + +C<dateTime> The timestamp when the flag was last modified. + +=item C<status> + +C<string> The current status of the flag. + +=item C<setter> + +C<string> The login name of the user who created or last modified the flag. + +=item C<requestee> + +C<string> 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 =item B<Errors> @@ -1418,6 +1649,10 @@ C<summary>. =back +=item The C<flags> array was added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + =back @@ -1432,6 +1667,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 /bug/<id_or_alias>/comment + +To get a specific comment based on the comment ID: + +GET /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. @@ -1522,6 +1769,13 @@ Bugzillas. (However, this backwards-compatibility will go away in Bugzilla C<dateTime> The time (in Bugzilla's timezone) that the comment was added. +=item creation_time + +C<dateTime> This is exactly same as the C<time> key. Use this field instead of +C<time> for consistency with other methods including L</get> and L</attachments>. +For compatibility, C<time> is still usable. However, please note that C<time> +may be deprecated and removed in a future release. + =item is_private C<boolean> True if this comment is private (only visible to a certain @@ -1563,6 +1817,10 @@ C<creator>. =back +=item C<creation_time> was added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + =back @@ -1578,6 +1836,14 @@ 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 /bug/<id_or_alias> + +The returned data format is the same as below. + =item B<Params> In addition to the parameters below, this method also accepts the @@ -1622,6 +1888,13 @@ the valid ids. Each hash contains the following items: =over +=item C<actual_time> + +C<double> The total number of hours that this bug has taken (so far). + +If you are not in the time-tracking group, this field will not be included +in the return value. + =item C<alias> C<string> The unique alias of this bug. @@ -1680,6 +1953,48 @@ take. If you are not in the time-tracking group, this field will not be included in the return value. +=item C<flags> + +An array of hashes containing the information about flags currently set +for the bug. Each flag hash contains the following items: + +=over + +=item C<id> + +C<int> The id of the flag. + +=item C<name> + +C<string> The name of the flag. + +=item C<type_id> + +C<int> The type id of the flag. + +=item C<creation_date> + +C<dateTime> The timestamp when this flag was originally created. + +=item C<modification_date> + +C<dateTime> The timestamp when the flag was last modified. + +=item C<status> + +C<string> The current status of the flag. + +=item C<setter> + +C<string> The login name of the user who created or last modified the flag. + +=item C<requestee> + +C<string> 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 + =item C<groups> C<array> of C<string>s. The names of all the groups that this bug is in. @@ -1869,6 +2184,8 @@ You do not have access to the bug_id you specified. =item The following properties were added to this method's return values in Bugzilla B<3.4>: +=item REST API call added in Bugzilla B<5.0> + =over =item For C<bugs> @@ -1907,8 +2224,12 @@ C<op_sys>, C<platform>, C<qa_contact>, C<remaining_time>, C<see_also>, C<target_milestone>, C<update_token>, C<url>, C<version>, C<whiteboard>, and all custom fields. -=back +=item The C<flags> array was added in Bugzilla B<4.4>. +=item The C<actual_time> item was added to the C<bugs> return value +in Bugzilla B<4.4>. + +=back =back @@ -1922,6 +2243,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 /bug/<bug_id>/history + +The returned data format will be the same as below. + =item B<Params> =over @@ -1939,6 +2268,11 @@ Note that it's possible for aliases to be disabled in Bugzilla, in which case you will be told that you have specified an invalid bug_id if you try to specify an alias. (It will be error 100.) +=item C<start_time> + +An optional C<datetime> string that only shows changes at and after a specific +time. + =back =item B<Returns> @@ -2002,6 +2336,8 @@ present in this hash. =back +=item REST API call added Bugzilla B<5.0>. + =back =item B<Errors> @@ -2014,6 +2350,10 @@ The same as L</get>. =item Added in Bugzilla B<3.4>. +=item Field names changed to be more consistent with other methods in Bugzilla B<4.4>. + +=item As of Bugzilla B<4.4>, field names now match names used by L<Bug.update|/"update"> for consistency. + =back =back @@ -2082,6 +2422,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 @@ -2102,10 +2450,19 @@ 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> @@ -2230,6 +2587,11 @@ 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<count_only> + +C<boolean> If count_only set to true, only a single hash key called C<bug_count> +will be returned which is the number of bugs that matched the search. + =back =item B<Returns> @@ -2269,6 +2631,11 @@ 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>. + =back =back @@ -2295,10 +2662,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 /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 @@ -2453,6 +2829,8 @@ loop errors had a generic code of C<32000>. =back +=item REST API call added in Bugzilla B<5.0>. + =back @@ -2466,6 +2844,16 @@ B<UNSTABLE> This allows you to add an attachment to a bug in Bugzilla. +=item B<REST> + +To create attachment on a current bug: + +POST /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 @@ -2564,6 +2952,8 @@ You set the "data" field to an empty string. =back +=item REST API call added in Bugzilla B<5.0>. + =back @@ -2577,6 +2967,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 /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 @@ -2653,6 +3052,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 @@ -2669,6 +3070,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 /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 @@ -3114,6 +3525,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 diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm index efc822311..1fc15c3c3 100644 --- a/Bugzilla/WebService/Bugzilla.pm +++ b/Bugzilla/WebService/Bugzilla.pm @@ -98,6 +98,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> @@ -108,6 +112,12 @@ B<STABLE> Returns the current version of Bugzilla. +=item B<REST> + +GET /version + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -117,6 +127,14 @@ string. =item B<Errors> (none) +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head2 extensions @@ -130,6 +148,12 @@ B<EXPERIMENTAL> Gets information about the extensions that are currently installed and enabled in this Bugzilla. +=item B<REST> + +GET /extensions + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -160,6 +184,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 @@ -175,6 +201,12 @@ Use L</time> instead. Returns the timezone that Bugzilla expects dates and times in. +=item B<REST> + +GET /timezone + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -189,6 +221,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 @@ -205,6 +239,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 /time + +The returned data format is the same as below. + =item B<Params> (none) =item B<Returns> @@ -215,7 +255,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 @@ -225,7 +265,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 @@ -241,7 +281,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> @@ -265,6 +305,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 diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm new file mode 100644 index 000000000..22358c784 --- /dev/null +++ b/Bugzilla/WebService/Classification.pm @@ -0,0 +1,210 @@ +# 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::Classification; + +use 5.10.1; +use strict; + +use parent qw (Bugzilla::WebService); + +use Bugzilla::Classification; +use Bugzilla::Error; +use Bugzilla::WebService::Util qw(filter validate params_to_objects); + +use constant READ_ONLY => qw( + get +); + +sub get { + my ($self, $params) = validate(@_, 'names', 'ids'); + + defined $params->{names} || defined $params->{ids} + || ThrowCodeError('params_required', { function => 'Classification.get', + params => ['names', 'ids'] }); + + my $user = Bugzilla->user; + + Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications') + || ThrowUserError('auth_classification_not_enabled'); + + Bugzilla->switch_to_shadow_db; + + my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') }; + unless ($user->in_group('editclassifications')) { + my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications}; + @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; + } + + my @classifications = map { filter($params, $self->_classification_to_hash($_)) } @classification_objs; + + return { classifications => \@classifications }; +} + +sub _classification_to_hash { + my ($self, $classification) = @_; + + 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 = ( + 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; +} + +sub _product_to_hash { + my ($self, $product) = @_; + my %hash = ( + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + ); + + return \%hash; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Webservice::Classification - The Classification API + +=head1 DESCRIPTION + +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 + +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 + +B<EXPERIMENTAL> + +=over + +=item B<Description> + +Returns a hash containing information about a set of classifications. + +=item B<REST> + +To return information on a single classification: + +GET /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. +So, this method accepts the following parameters: + +=over + +=item C<ids> + +An array of classification ids. + +=item C<names> + +An array of classification names. + +=back + +=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 +and has the following keys: + +=over + +=item C<id> + +C<int> The id of the classification. + +=item C<name> + +C<string> The name of the classification. + +=item C<description> + +C<string> The description of the classificaion. + +=item C<sort_key> + +C<int> The value which determines the order the classification is sorted. + +=item C<products> + +An array of hashes. The array contains the products the user is authorized to +access within the classification. Each hash has the following keys: + +=over + +=item C<name> + +C<string> The name of the product. + +=item C<id> + +C<int> The id of the product. + +=item C<description> + +C<string> The description of the product. + +=back + +=back + +=item B<Errors> + +=over + +=item 900 (Classification not enabled) + +Classification is not enabled on this installation. + +=back + +=item B<History> + +=over + +=item Added in Bugzilla B<4.4>. + +=item REST API call added in Bugzilla B<5.0>. + +=back + +=back + diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 3207356fa..0aa6c1bbd 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -22,9 +22,22 @@ use base 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 ); @@ -177,8 +190,47 @@ use constant WS_ERROR_CODE => { 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. +use constant REST_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, + _default => STATUS_BAD_REQUEST }; # These are the fallback defaults for errors not in ERROR_CODE. @@ -192,6 +244,13 @@ use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw( application/xml ); +use constant REST_CONTENT_TYPE_WHITELIST => qw( + text/html + application/javascript + application/json + text/javascript +); + sub WS_DISPATCH { # We "require" here instead of "use" above to avoid a dependency loop. require Bugzilla::Hook; @@ -199,11 +258,12 @@ sub WS_DISPATCH { Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch }); my $dispatch = { - 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', - 'Bug' => 'Bugzilla::WebService::Bug', - 'User' => 'Bugzilla::WebService::User', - 'Product' => 'Bugzilla::WebService::Product', - 'Group' => 'Bugzilla::WebService::Group', + 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', + 'Bug' => 'Bugzilla::WebService::Bug', + 'Classification' => 'Bugzilla::WebService::Classification', + 'User' => 'Bugzilla::WebService::User', + 'Product' => 'Bugzilla::WebService::Product', + 'Group' => 'Bugzilla::WebService::Group', %hook_dispatch }; return $dispatch; diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm index 65feb7a1a..b571a1062 100644 --- a/Bugzilla/WebService/Group.pm +++ b/Bugzilla/WebService/Group.pm @@ -61,6 +61,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 =head2 create @@ -73,9 +77,16 @@ B<UNSTABLE> This allows you to create a new group in Bugzilla. -=item B<Params> +=item B<REST> + +POST /group + +The params to include in the POST body as well as the returned data format, +are the same as below. -Some params must be set, or an error will be thrown. These params are +=item B<Params> + +Some params must be set, or an error will be thrown. These params are marked B<Required>. =over @@ -96,7 +107,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 @@ -110,7 +121,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. @@ -136,6 +147,14 @@ 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 =cut diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 3cd0d0a6c..63981ae89 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -47,57 +47,95 @@ BEGIN { *get_products = \&get } # Get the ids of the products the user can search sub get_selectable_products { - return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; } # Get the ids of the products the user can enter bugs against sub get_enterable_products { - return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; } # Get the union of the products the user can search and enter bugs against. sub get_accessible_products { - return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; } # Get a list of actual products, based on list of ids or names sub get { - my ($self, $params) = validate(@_, 'ids', 'names'); - - # Only products that are in the users accessible products, - # can be allowed to be returned - my $accessible_products = Bugzilla->user->get_accessible_products; + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + my $user = Bugzilla->user; + + defined $params->{ids} || defined $params->{names} || defined $params->{type} + || ThrowCodeError("params_required", { function => "Product.get", + params => ['ids', 'names', 'type'] }); + + Bugzilla->switch_to_shadow_db(); + + my $products = []; + if (defined $params->{type}) { + my %product_hash; + foreach my $type (@{ $params->{type} }) { + my $result = []; + if ($type eq 'accessible') { + $result = $user->get_accessible_products(); + } + elsif ($type eq 'enterable') { + $result = $user->get_enterable_products(); + } + elsif ($type eq 'selectable') { + $result = $user->get_selectable_products(); + } + else { + ThrowUserError('get_products_invalid_type', + { type => $type }); + } + map { $product_hash{$_->id} = $_ } @$result; + } + $products = [ values %product_hash ]; + } + else { + $products = $user->get_accessible_products; + } - my @requested_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 + + # Return the intersection of this, by grepping the ids from # accessible products. - push(@requested_accessible, - grep { $ids{$_->id} } @$accessible_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 + + # Return the intersection of this, by grepping the names from # accessible products, union'ed with products found by ID to # avoid duplicates foreach my $product (grep { $names{lc $_->name} } - @$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 }; } @@ -105,7 +143,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"}); @@ -148,40 +186,41 @@ sub _product_to_hash { } if (filter_wants($params, 'versions')) { $field_data->{versions} = [map { - $self->_version_to_hash($_) + $self->_version_to_hash($_, $params) } @{$product->versions}]; } if (filter_wants($params, 'milestones')) { $field_data->{milestones} = [map { - $self->_milestone_to_hash($_) + $self->_milestone_to_hash($_, $params) } @{$product->milestones}]; } return filter($params, $field_data); } sub _component_to_hash { - my ($self, $component) = @_; - return { + my ($self, $component, $params) = @_; + my $field_data = { id => $self->type('int', $component->id), name => $self->type('string', $component->name), description => - $self->type('string' , $component->description), + $self->type('string', $component->description), default_assigned_to => - $self->type('string' , $component->default_assignee->login), + $self->type('email', $component->default_assignee->login), default_qa_contact => - $self->type('string' , $component->default_qa_contact->login), + $self->type('email', $component->default_qa_contact->login), sort_key => # sort_key is returned to match Bug.fields 0, is_active => $self->type('boolean', $component->is_active), }; + return filter($params, $field_data, 'components'); } sub _version_to_hash { - my ($self, $version) = @_; - return { + my ($self, $version, $params) = @_; + my $field_data = { id => $self->type('int', $version->id), name => @@ -191,11 +230,12 @@ sub _version_to_hash { is_active => $self->type('boolean', $version->is_active), }; + return filter($params, $field_data, 'versions'); } sub _milestone_to_hash { - my ($self, $milestone) = @_; - return { + my ($self, $milestone, $params) = @_; + my $field_data = { id => $self->type('int', $milestone->id), name => @@ -205,6 +245,7 @@ sub _milestone_to_hash { is_active => $self->type('boolean', $milestone->is_active), }; + return filter($params, $field_data, 'milestones'); } 1; @@ -225,6 +266,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 @@ -237,15 +282,29 @@ B<EXPERIMENTAL> Returns a list of the ids of the products the user can search on. +=item B<REST> + +GET /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 @@ -259,6 +318,12 @@ B<EXPERIMENTAL> Returns a list of the ids of the products the user can enter bugs against. +=item B<REST> + +GET /product_enterable + +the returned data format is same as below. + =item B<Params> (none) =item B<Returns> @@ -268,6 +333,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 @@ -281,6 +354,12 @@ B<UNSTABLE> Returns a list of the ids of the products the user can search or enter bugs against. +=item B<REST> + +GET /product_accessible + +the returned data format is same as below. + =item B<Params> (none) =item B<Returns> @@ -290,6 +369,14 @@ ids. =item B<Errors> (none) +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =head2 get @@ -304,12 +391,32 @@ Returns a list of information about the products passed to it. 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 /product?type=accessible + +To return information about a specific product by C<id> or C<name>: + +GET /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 /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 standard L<include_fields|Bugzilla::WebService/include_fields> and L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments. +This RPC call supports sub field restrictions. + =over =item C<ids> @@ -320,9 +427,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: @@ -432,6 +545,8 @@ C<milestones>, C<default_milestone> and C<has_unconfirmed> were added to the fields returned by C<get> as a replacement for C<internals>, which has been removed. +=item REST API call added in Bugzilla B<5.0>. + =back =back @@ -448,9 +563,16 @@ B<EXPERIMENTAL> This allows you to create a new product in Bugzilla. -=item B<Params> +=item B<REST> + +POST /product + +The params to include in the POST body as well as the returned data format, +are the same as below. -Some params must be set, or an error will be thrown. These params are +=item B<Params> + +Some params must be set, or an error will be thrown. These params are marked B<Required>. =over @@ -464,11 +586,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. @@ -477,11 +599,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. @@ -493,7 +615,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. @@ -529,4 +651,12 @@ 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 diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm index 206f0c657..9727dcbcb 100644 --- a/Bugzilla/WebService/Server.pm +++ b/Bugzilla/WebService/Server.pm @@ -22,6 +22,9 @@ 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) = @_; @@ -37,7 +40,7 @@ sub handle_login { sub datetime_format_inbound { my ($self, $time) = @_; - + my $converted = datetime_from($time, Bugzilla->local_timezone); if (!defined $converted) { ThrowUserError('illegal_date', { date => $time }); @@ -63,4 +66,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 cec1c29ea..109c530b7 100644 --- a/Bugzilla/WebService/Server/JSONRPC.pm +++ b/Bugzilla/WebService/Server/JSONRPC.pm @@ -37,8 +37,8 @@ BEGIN { use Bugzilla::Error; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(taint_data); -use Bugzilla::Util qw(correct_urlbase trim disable_utf8); +use Bugzilla::WebService::Util qw(taint_data fix_credentials); +use Bugzilla::Util; use HTTP::Message; use MIME::Base64 qw(decode_base64 encode_base64); @@ -87,12 +87,12 @@ sub response_header { sub response { my ($self, $response) = @_; + my $cgi = $self->cgi; # Implement JSONP. if (my $callback = $self->_bz_callback) { my $content = $response->content; $response->content("$callback($content)"); - } # Use $cgi->header properly instead of just printing text directly. @@ -107,9 +107,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 @@ -221,6 +230,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; } @@ -266,7 +278,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 @@ -363,6 +385,10 @@ sub _argument_type_check { } } + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); + Bugzilla->input_params($params); if ($self->request->method eq 'POST') { diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm new file mode 100644 index 000000000..4145455ec --- /dev/null +++ b/Bugzilla/WebService/Server/REST.pm @@ -0,0 +1,638 @@ +# 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 parent qw(Bugzilla::WebService::Server::JSONRPC); + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::WebService::Constants; +use Bugzilla::WebService::Util qw(taint_data fix_credentials); +use Bugzilla::Util qw(correct_urlbase html_quote); + +# 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::Group; +use Bugzilla::WebService::Server::REST::Resources::Product; +use Bugzilla::WebService::Server::REST::Resources::User; + +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. + $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); + delete $result->{'name'}; # Remove JSONRPCError + } + elsif (exists $json_data->{result}) { + $result = $json_data->{result}; + } + + # Access Control + $response->header("Access-Control-Allow-Origin", "*"); + + # 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); + + Bugzilla->input_params($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; + + 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) = @_; + + # _all is same as default columns + if ($params->{'include_fields'} + && ($params->{'include_fields'} eq '_all' + || $params->{'include_fields'} eq '_default')) + { + delete $params->{'include_fields'}; + delete $params->{'exclude_fields'} if $params->{'exclude_fields'}; + } + + 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 params we were able to pull out of the path + # based on the resource regexp + %{$params} = (%{$params}, %{$self->bz_rest_params}) if $self->bz_rest_params; + + # Merge any additional query key/values with $obj->{params} if not a GET request + # We do this manually cause CGI.pm doesn't understand JSON strings. + if ($self->request->method ne 'GET') { + my $extra_params = {}; + 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 not GET. + # Note: query string parameters will override any matching params + # also specified 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; + } + + # 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(); + 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, '*/*'; # Allows allow for */* +} + +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. If using Apache and mod_rewrite is installed +and enabled, you can also use /rest/ as your endpoint. For example, if your +Bugzilla is at C<bugzilla.yourdomain.com>, then your REST client would +access the API via: C<http://bugzilla.yourdomain.com/rest/bug/35> which +looks cleaner. + +=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..a82625be7 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm @@ -0,0 +1,152 @@ +# 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 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', + 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/([^/]+)/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] ] }; + } + } + }, + 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/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm new file mode 100644 index 000000000..1c86f77bc --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm @@ -0,0 +1,69 @@ +# 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 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..5bb697ac1 --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm @@ -0,0 +1,49 @@ +# 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 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::Bug> 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..9200d609d --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm @@ -0,0 +1,56 @@ +# 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 Bugzilla::WebService::Constants; +use Bugzilla::WebService::Group; + +BEGIN { + *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; +}; + +sub _rest_resources { + my $rest_resources = [ + qr{^/group$}, { + 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..acee3887b --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm @@ -0,0 +1,82 @@ +# 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 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::Bug> 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..0a2fe455f --- /dev/null +++ b/Bugzilla/WebService/Server/REST/Resources/User.pm @@ -0,0 +1,75 @@ +# 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 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{^/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::Bug> 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 fc297421a..8d9108122 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -30,9 +30,10 @@ if ($ENV{MOD_PERL}) { } use Bugzilla::WebService::Constants; +use Bugzilla::Util; -# 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') { @@ -41,8 +42,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 { @@ -56,22 +68,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 { diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index deb7518ec..32e11a2a4 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -29,6 +29,9 @@ use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Util qw(trim); use Bugzilla::WebService::Util qw(filter validate); +use Bugzilla::Hook; + +use List::Util qw(first); # Don't need auth to login use constant LOGIN_EXEMPT => { @@ -70,14 +73,25 @@ sub login { $input_params->{'Bugzilla_password'} = $params->{password}; $input_params->{'Bugzilla_remember'} = $remember; - Bugzilla->login(); - return { id => $self->type('int', Bugzilla->user->id) }; + my $user = Bugzilla->login(); + + my $result = { id => $self->type('int', $user->id) }; + + # We will use the stored cookie value combined with the user id + # to create a token that can be used with future requests in the + # query parameters + my $login_cookie = first { $_->name eq 'Bugzilla_logincookie' } + @{ Bugzilla->cgi->{'Bugzilla_cookie_list'} }; + if ($login_cookie) { + $result->{'token'} = $user->id . "-" . $login_cookie->value; + } + + return $result; } sub logout { my $self = shift; Bugzilla->logout; - return undef; } ################# @@ -124,7 +138,9 @@ 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(); defined($params->{names}) || defined($params->{ids}) || defined($params->{match}) @@ -154,8 +170,8 @@ sub get { \@user_objects, $params); @users = map {filter $params, { id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('string', $_->login), + real_name => $self->type('string', $_->name), + name => $self->type('email', $_->login), }} @$in_group; return { users => \@users }; @@ -196,33 +212,39 @@ sub get { } } } - + my $in_group = $self->_filter_users_by_group( \@user_objects, $params); if (Bugzilla->user->in_group('editusers')) { - @users = + @users = map {filter $params, { id => $self->type('int', $_->id), real_name => $self->type('string', $_->name), - name => $self->type('string', $_->login), - email => $self->type('string', $_->email), + name => $self->type('email', $_->login), + email => $self->type('email', $_->email), can_login => $self->type('boolean', $_->is_enabled ? 1 : 0), + groups => $self->_filter_bless_groups($_->groups), email_enabled => $self->type('boolean', $_->email_enabled), login_denied_text => $self->type('string', $_->disabledtext), + saved_searches => [map { $self->_query_to_hash($_) } @{ $_->queries }], }} @$in_group; - } else { @users = map {filter $params, { id => $self->type('int', $_->id), real_name => $self->type('string', $_->name), - name => $self->type('string', $_->login), - email => $self->type('string', $_->email), + name => $self->type('email', $_->login), + email => $self->type('email', $_->email), can_login => $self->type('boolean', $_->is_enabled ? 1 : 0), + groups => $self->_filter_bless_groups($_->groups), + saved_searches => [map { $self->_query_to_hash($_) } @{ $_->queries }], }} @$in_group; } + Bugzilla::Hook::process('webservice_user_get', + { webservice => $self, params => $params, users => \@users }); + return { users => \@users }; } @@ -259,6 +281,40 @@ sub _user_in_any_group { return 0; } +sub _filter_bless_groups { + my ($self, $groups) = @_; + my $user = Bugzilla->user; + + my @filtered_groups; + foreach my $group (@$groups) { + next unless ($user->in_group('editusers') || $user->can_bless($group->id)); + push(@filtered_groups, $self->_group_to_hash($group)); + } + + return \@filtered_groups; +} + +sub _group_to_hash { + my ($self, $group) = @_; + my $item = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + return $item; +} + +sub _query_to_hash { + my ($self, $query) = @_; + my $item = { + id => $self->type('int', $query->id), + name => $self->type('string', $query->name), + url => $self->type('string', $query->url), + }; + + return $item; +} + 1; __END__ @@ -277,6 +333,10 @@ 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 =head2 login @@ -295,7 +355,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. @@ -311,10 +371,14 @@ management of cookies across sessions. =item B<Returns> -On success, a hash containing one item, C<id>, the numeric id of the -user that was logged in. A set of http cookies is also sent with the -response. These cookies must be sent along with any future requests -to the webservice, for the duration of the session. +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. A set of http cookies +is also sent with the response. These cookies *or* the token can be sent +along with any future requests to the webservice, for the duration of the +session. Note that cookies are not accepted for GET requests for JSONRPC +and REST for security reasons. You may, however, use the token or valid +login parameters for those requests. =item B<Errors> @@ -419,6 +483,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 /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 @@ -462,6 +533,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 @@ -478,6 +551,18 @@ B<STABLE> Gets information about user accounts in Bugzilla. +=item B<REST> + +To get information about a single user: + +GET /user/<user_id_or_name> + +To search for users by name, group using URL params same as below: + +GET /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. @@ -539,6 +624,14 @@ match string. Setting C<include_disabled> to C<true> will include disabled users in the returned results even if their username doesn't fully match the input string. +=item B<History> + +=over + +=item REST API call added in Bugzilla B<5.0>. + +=back + =back =item B<Returns> @@ -581,10 +674,60 @@ 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 groups + +C<array> An array of group hashes the user is a member of. Each hash describes +the group and contains the following items: + +=over + +=item id + +C<int> The group id + +=item name + +C<string> The name of the group + +=item description + +C<string> The description for the group + +=back + +=over + +=item saved_searches + +C<array> An array of hashes, each of which represents a user's saved search and has +the following keys: + +=over + +=item id + +C<int> An integer id uniquely identifying the saved search. + +=item name + +C<string> The name of the saved search. + +=item url + +C<string> The CGI parameters for the saved search. + +=back + +B<Note>: The elements of the returned array (i.e. hashes) are ordered by the +name of each saved search. + +=back + B<Note>: If you are not logged in to Bugzilla when you call this function, you will only be returned the C<id>, C<name>, and C<real_name> items. If you are logged in and not in editusers group, you will only be returned the C<id>, C<name>, -C<real_name>, C<email>, and C<can_login> items. +C<real_name>, C<email>, and C<can_login> items. The groups returned are filtered +based on your permission to bless each group. =back @@ -625,9 +768,15 @@ exist or you do not belong to it. =item C<include_disabled> added in Bugzilla B<4.0>. Default behavior for C<match> has changed to only returning enabled accounts. +=item C<groups> Added in Bugzilla B<4.4>. + +=item C<saved_searches> Added in Bugzilla B<4.4>. + =item Error 804 has been added in Bugzilla 4.0.9 and 4.2.4. It's now illegal to pass a group name you don't belong to. =back +=item REST API call added in Bugzilla B<5.0>. + =back diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index fe4105ca2..622e6890b 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -32,32 +32,55 @@ our @EXPORT_OK = qw( filter_wants taint_data validate + params_to_objects + fix_credentials ); -sub filter ($$) { - my ($params, $hash) = @_; +sub filter ($$;$) { + my ($params, $hash, $prefix) = @_; my %newhash = %$hash; foreach my $key (keys %$hash) { - delete $newhash{$key} if !filter_wants($params, $key); + delete $newhash{$key} if !filter_wants($params, $key, $prefix); } return \%newhash; } -sub filter_wants ($$) { - my ($params, $field) = @_; +sub filter_wants ($$;$) { + my ($params, $field, $prefix) = @_; + + # Since this is operation is resource intensive, we will cache the results + # This assumes that $params->{*_fields} doesn't change between calls + my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; + $field = "${prefix}.${field}" if $prefix; + + if (exists $cache->{$field}) { + return $cache->{$field}; + } + my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] }; my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] }; - if (defined $params->{include_fields}) { - return 0 if !$include{$field}; + my $wants = 1; + if (defined $params->{exclude_fields} && $exclude{$field}) { + $wants = 0; } - if (defined $params->{exclude_fields}) { - return 0 if $exclude{$field}; + 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; + } } - return 1; + $cache->{$field} = $wants; + return $wants; } sub taint_data { @@ -108,6 +131,38 @@ sub validate { return ($self, $params); } +sub params_to_objects { + my ($params, $class) = @_; + my (@objects, @objects_by_ids); + + @objects = map { $class->check($_) } + @{ $params->{names} } if $params->{names}; + + @objects_by_ids = map { $class->check({ id => $_ }) } + @{ $params->{ids} } if $params->{ids}; + + push(@objects, @objects_by_ids); + my %seen; + @objects = grep { !$seen{$_->id}++ } @objects; + return \@objects; +} + +sub fix_credentials { + my ($params) = @_; + # Allow user to pass in login=foo&password=bar as a convenience + # even if not calling GET /login. We also do not delete them as + # GET /login requires "login" and "password". + if (exists $params->{'login'} && exists $params->{'password'}) { + $params->{'Bugzilla_login'} = $params->{'login'}; + $params->{'Bugzilla_password'} = $params->{'password'}; + } + # 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'} = $params->{'token'}; + } +} + __END__ =head1 NAME @@ -136,6 +191,13 @@ of WebService methods. Given a hash (the second argument to this subroutine), this will remove any keys that are I<not> in C<include_fields> and then remove any keys that I<are> in C<exclude_fields>. +An optional third option can be passed that prefixes the field name to allow +filtering of data two or more levels deep. + +For example, if you want to filter out the C<id> key/value in components returned +by Product.get, you would use the value C<component.id> in your C<exclude_fields> +list. + =head2 filter_wants Returns C<1> if a filter would preserve the specified field when passing @@ -147,3 +209,25 @@ This helps in the validation of parameters passed into the WebService methods. Currently it converts listed parameters into an array reference if the client only passed a single scalar value. It modifies the parameters hash in place so other parameters should be unaltered. + +=head2 params_to_objects + +Creates objects of the type passed in as the second parameter, using the +parameters passed to a WebService method (the first parameter to this function). +Helps make life simpler for WebService methods that internally create objects +via both "ids" and "names" fields. Also de-duplicates objects that were loaded +by both "ids" and "names". Returns an arrayref of objects. + +=head2 fix_credentials + +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. + +=head1 B<Methods in need of POD> + +=over + +=item taint_data + +=back |