summaryrefslogtreecommitdiffstats
path: root/Bugzilla/API/1_0/Resource
diff options
context:
space:
mode:
authorDavid Lawrence <dkl@mozilla.com>2015-04-08 19:48:36 +0200
committerDavid Lawrence <dkl@mozilla.com>2015-04-08 19:48:36 +0200
commitdbfd6207290d1eee53fddec4c7c3b4aac0b2d47a (patch)
treeaa190d8cc9e2b313dd7e85bf763c1bfe5502b75d /Bugzilla/API/1_0/Resource
parente6d2fb75aa3c183323c534a214f3dd9be5638676 (diff)
downloadbugzilla-dbfd6207290d1eee53fddec4c7c3b4aac0b2d47a.tar.gz
bugzilla-dbfd6207290d1eee53fddec4c7c3b4aac0b2d47a.tar.xz
Bug 1051056: The REST API needs to be versioned so that new changes can be made that do not break compatibility
r=dylan,a=glob
Diffstat (limited to 'Bugzilla/API/1_0/Resource')
-rw-r--r--Bugzilla/API/1_0/Resource/Bug.pm4881
-rw-r--r--Bugzilla/API/1_0/Resource/BugUserLastVisit.pm239
-rw-r--r--Bugzilla/API/1_0/Resource/Bugzilla.pm547
-rw-r--r--Bugzilla/API/1_0/Resource/Classification.pm235
-rw-r--r--Bugzilla/API/1_0/Resource/Component.pm639
-rw-r--r--Bugzilla/API/1_0/Resource/FlagType.pm890
-rw-r--r--Bugzilla/API/1_0/Resource/Group.pm636
-rw-r--r--Bugzilla/API/1_0/Resource/Product.pm1013
-rw-r--r--Bugzilla/API/1_0/Resource/User.pm1151
9 files changed, 10231 insertions, 0 deletions
diff --git a/Bugzilla/API/1_0/Resource/Bug.pm b/Bugzilla/API/1_0/Resource/Bug.pm
new file mode 100644
index 000000000..c61b2c6c2
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/Bug.pm
@@ -0,0 +1,4881 @@
+# 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::API::1_0::Resource::Bug;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Constants;
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Comment;
+use Bugzilla::Comment::TagWeights;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::Bug;
+use Bugzilla::BugMail;
+use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural);
+use Bugzilla::Version;
+use Bugzilla::Milestone;
+use Bugzilla::Status;
+use Bugzilla::Token qw(issue_hash_token);
+use Bugzilla::Search;
+use Bugzilla::Product;
+use Bugzilla::FlagType;
+use Bugzilla::Search::Quicksearch;
+
+use Moo;
+use List::Util qw(max);
+use List::MoreUtils qw(uniq);
+use Storable qw(dclone);
+
+extends 'Bugzilla::API::1_0::Resource';
+
+#############
+# Constants #
+#############
+
+use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component);
+
+use constant DATE_FIELDS => {
+ comments => ['new_since'],
+ history => ['new_since'],
+ search => ['last_change_time', 'creation_time'],
+};
+
+use constant BASE64_FIELDS => {
+ add_attachment => ['data'],
+};
+
+use constant READ_ONLY => qw(
+ attachments
+ comments
+ fields
+ get
+ history
+ legal_values
+ search
+ search_comment_tags
+);
+
+use constant PUBLIC_METHODS => qw(
+ add_attachment
+ add_comment
+ attachments
+ comments
+ create
+ fields
+ get
+ history
+ legal_values
+ possible_duplicates
+ render_comment
+ search
+ search_comment_tags
+ update
+ update_attachment
+ update_comment_tags
+ update_see_also
+ update_tags
+);
+
+use constant ATTACHMENT_MAPPED_SETTERS => {
+ file_name => 'filename',
+ summary => 'description',
+};
+
+use constant ATTACHMENT_MAPPED_RETURNS => {
+ description => 'summary',
+ ispatch => 'is_patch',
+ isprivate => 'is_private',
+ isobsolete => 'is_obsolete',
+ filename => 'file_name',
+ mimetype => 'content_type',
+};
+
+sub REST_RESOURCES {
+ my $rest_resources = [
+ qr{^/bug$}, {
+ GET => {
+ method => 'search',
+ },
+ POST => {
+ method => 'create',
+ status_code => STATUS_CREATED
+ }
+ },
+ qr{^/bug/$}, {
+ GET => {
+ method => 'get'
+ }
+ },
+ qr{^/bug/([^/]+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ },
+ PUT => {
+ method => 'update',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ }
+ },
+ qr{^/bug/([^/]+)/comment$}, {
+ GET => {
+ method => 'comments',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ },
+ POST => {
+ method => 'add_comment',
+ params => sub {
+ return { id => $_[0] };
+ },
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/bug/comment/([^/]+)$}, {
+ GET => {
+ method => 'comments',
+ params => sub {
+ return { comment_ids => [ $_[0] ] };
+ }
+ }
+ },
+ qr{^/bug/comment/tags/([^/]+)$}, {
+ GET => {
+ method => 'search_comment_tags',
+ params => sub {
+ return { query => $_[0] };
+ },
+ },
+ },
+ qr{^/bug/comment/([^/]+)/tags$}, {
+ PUT => {
+ method => 'update_comment_tags',
+ params => sub {
+ return { comment_id => $_[0] };
+ },
+ },
+ },
+ qr{^/bug/([^/]+)/history$}, {
+ GET => {
+ method => 'history',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ },
+ }
+ },
+ qr{^/bug/([^/]+)/attachment$}, {
+ GET => {
+ method => 'attachments',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ },
+ POST => {
+ method => 'add_attachment',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ },
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/bug/attachment/([^/]+)$}, {
+ GET => {
+ method => 'attachments',
+ params => sub {
+ return { attachment_ids => [ $_[0] ] };
+ }
+ },
+ PUT => {
+ method => 'update_attachment',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ }
+ },
+ qr{^/field/bug$}, {
+ GET => {
+ method => 'fields',
+ }
+ },
+ qr{^/field/bug/([^/]+)$}, {
+ GET => {
+ method => 'fields',
+ params => sub {
+ my $value = $_[0];
+ my $param = 'names';
+ $param = 'ids' if $value =~ /^\d+$/;
+ return { $param => [ $_[0] ] };
+ }
+ }
+ },
+ qr{^/field/bug/([^/]+)/values$}, {
+ GET => {
+ method => 'legal_values',
+ params => sub {
+ return { field => $_[0] };
+ }
+ }
+ },
+ qr{^/field/bug/([^/]+)/([^/]+)/values$}, {
+ GET => {
+ method => 'legal_values',
+ params => sub {
+ return { field => $_[0],
+ product_id => $_[1] };
+ }
+ }
+ },
+ ];
+ return $rest_resources;
+}
+
+######################################################
+# Add aliases here for old method name compatibility #
+######################################################
+
+BEGIN {
+ # In 3.0, get was called get_bugs
+ *get_bugs = \&get;
+ # Before 3.4rc1, "history" was get_history.
+ *get_history = \&history;
+}
+
+###########
+# Methods #
+###########
+
+sub fields {
+ my ($self, $params) = validate(@_, 'ids', 'names');
+
+ Bugzilla->switch_to_shadow_db();
+
+ my @fields;
+ if (defined $params->{ids}) {
+ my $ids = $params->{ids};
+ foreach my $id (@$ids) {
+ my $loop_field = Bugzilla::Field->check({ id => $id });
+ push(@fields, $loop_field);
+ }
+ }
+
+ if (defined $params->{names}) {
+ my $names = $params->{names};
+ foreach my $field_name (@$names) {
+ my $loop_field = Bugzilla::Field->check($field_name);
+ # Don't push in duplicate fields if we also asked for this field
+ # in "ids".
+ if (!grep($_->id == $loop_field->id, @fields)) {
+ push(@fields, $loop_field);
+ }
+ }
+ }
+
+ if (!defined $params->{ids} and !defined $params->{names}) {
+ @fields = @{ Bugzilla->fields({ obsolete => 0 }) };
+ }
+
+ my @fields_out;
+ foreach my $field (@fields) {
+ my $visibility_field = $field->visibility_field
+ ? $field->visibility_field->name : undef;
+ my $vis_values = $field->visibility_values;
+ my $value_field = $field->value_field
+ ? $field->value_field->name : undef;
+
+ my (@values, $has_values);
+ if ( ($field->is_select and $field->name ne 'product')
+ or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)
+ or $field->name eq 'keywords')
+ {
+ $has_values = 1;
+ @values = @{ $self->_legal_field_values({ field => $field }) };
+ }
+
+ if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) {
+ $value_field = 'product';
+ }
+
+ my %field_data = (
+ id => as_int($field->id),
+ type => as_int($field->type),
+ is_custom => as_boolean($field->custom),
+ name => as_string($field->name),
+ display_name => as_string($field->description),
+ is_mandatory => as_boolean($field->is_mandatory),
+ is_on_bug_entry => as_boolean($field->enter_bug),
+ visibility_field => as_string($visibility_field),
+ visibility_values => as_name_array($vis_values)
+ );
+ if ($has_values) {
+ $field_data{value_field} = as_string($value_field);
+ $field_data{values} = \@values;
+ };
+ push(@fields_out, filter $params, \%field_data);
+ }
+
+ return { fields => \@fields_out };
+}
+
+sub _legal_field_values {
+ my ($self, $params) = @_;
+ my $field = $params->{field};
+ my $field_name = $field->name;
+ my $user = Bugzilla->user;
+
+ my @result;
+ if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) {
+ my @list;
+ if ($field_name eq 'version') {
+ @list = Bugzilla::Version->get_all;
+ }
+ elsif ($field_name eq 'component') {
+ @list = Bugzilla::Component->get_all;
+ }
+ else {
+ @list = Bugzilla::Milestone->get_all;
+ }
+
+ foreach my $value (@list) {
+ my $sortkey = $field_name eq 'target_milestone'
+ ? $value->sortkey : 0;
+ # XXX This is very slow for large numbers of values.
+ my $product_name = $value->product->name;
+ if ($user->can_see_product($product_name)) {
+ push(@result, {
+ name => as_string($value->name),
+ sort_key => as_int($sortkey),
+ sortkey => as_int($sortkey), # deprecated
+ visibility_values => [ as_string($product_name) ],
+ is_active => as_boolean($value->is_active),
+ });
+ }
+ }
+ }
+
+ elsif ($field_name eq 'bug_status') {
+ my @status_all = Bugzilla::Status->get_all;
+ my $initial_status = bless({ id => 0, name => '', is_open => 1, sortkey => 0,
+ can_change_to => Bugzilla::Status->can_change_to },
+ 'Bugzilla::Status');
+ unshift(@status_all, $initial_status);
+
+ foreach my $status (@status_all) {
+ my @can_change_to;
+ foreach my $change_to (@{ $status->can_change_to }) {
+ # There's no need to note that a status can transition
+ # to itself.
+ next if $change_to->id == $status->id;
+ my %change_to_hash = (
+ name => as_string($change_to->name),
+ comment_required => as_boolean(
+ $change_to->comment_required_on_change_from($status)),
+ );
+ push(@can_change_to, \%change_to_hash);
+ }
+
+ push (@result, {
+ name => as_string($status->name),
+ is_open => as_boolean($status->is_open),
+ sort_key => as_int($status->sortkey),
+ sortkey => as_int($status->sortkey), # deprecated
+ can_change_to => \@can_change_to,
+ visibility_values => [],
+ });
+ }
+ }
+
+ elsif ($field_name eq 'keywords') {
+ my @legal_keywords = Bugzilla::Keyword->get_all;
+ foreach my $value (@legal_keywords) {
+ push (@result, {
+ name => as_string($value->name),
+ description => as_string($value->description),
+ });
+ }
+ }
+ else {
+ my @values = Bugzilla::Field::Choice->type($field)->get_all();
+ foreach my $value (@values) {
+ my $vis_val = $value->visibility_value;
+ push(@result, {
+ name => as_string($value->name),
+ sort_key => as_int($value->sortkey),
+ sortkey => as_int($value->sortkey), # deprecated
+ visibility_values => [
+ defined $vis_val ? as_string($vis_val->name)
+ : ()
+ ],
+ });
+ }
+ }
+
+ return \@result;
+}
+
+sub comments {
+ my ($self, $params) = validate(@_, 'ids', 'comment_ids');
+
+ if (!(defined $params->{ids} || defined $params->{comment_ids})) {
+ ThrowCodeError('params_required',
+ { function => 'Bug.comments',
+ params => ['ids', 'comment_ids'] });
+ }
+
+ my $bug_ids = $params->{ids} || [];
+ my $comment_ids = $params->{comment_ids} || [];
+
+ my $dbh = Bugzilla->switch_to_shadow_db();
+ my $user = Bugzilla->user;
+
+ my %bugs;
+ foreach my $bug_id (@$bug_ids) {
+ my $bug = Bugzilla::Bug->check($bug_id);
+ # We want the API to always return comments in the same order.
+
+ my $comments = $bug->comments({ order => 'oldest_to_newest',
+ after => $params->{new_since} });
+ my @result;
+ foreach my $comment (@$comments) {
+ next if $comment->is_private && !$user->is_insider;
+ push(@result, $self->_translate_comment($comment, $params));
+ }
+ $bugs{$bug->id}{'comments'} = \@result;
+ }
+
+ my %comments;
+ if (scalar @$comment_ids) {
+ my @ids = map { trim($_) } @$comment_ids;
+ my $comment_data = Bugzilla::Comment->new_from_list(\@ids);
+
+ # See if we were passed any invalid comment ids.
+ my %got_ids = map { $_->id => 1 } @$comment_data;
+ foreach my $comment_id (@ids) {
+ if (!$got_ids{$comment_id}) {
+ ThrowUserError('comment_id_invalid', { id => $comment_id });
+ }
+ }
+
+ # Now make sure that we can see all the associated bugs.
+ my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
+ Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);
+
+ foreach my $comment (@$comment_data) {
+ if ($comment->is_private && !$user->is_insider) {
+ ThrowUserError('comment_is_private', { id => $comment->id });
+ }
+ $comments{$comment->id} =
+ $self->_translate_comment($comment, $params);
+ }
+ }
+
+ return { bugs => \%bugs, comments => \%comments };
+}
+
+sub render_comment {
+ my ($self, $params) = @_;
+
+ unless (defined $params->{text}) {
+ ThrowCodeError('params_required',
+ { function => 'Bug.render_comment',
+ params => ['text'] });
+ }
+
+ Bugzilla->switch_to_shadow_db();
+ my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
+
+ my $markdown = $params->{markdown} ? 1 : 0;
+ my $tmpl = $markdown ? '[% text FILTER markdown(bug, { is_markdown => 1 }) %]' : '[% text FILTER markdown(bug) %]';
+
+ my $html;
+ my $template = Bugzilla->template;
+ $template->process(
+ \$tmpl,
+ { bug => $bug, text => $params->{text}},
+ \$html
+ );
+
+ return { html => $html };
+}
+
+# Helper for Bug.comments
+sub _translate_comment {
+ my ($self, $comment, $filters, $types, $prefix) = @_;
+ my $attach_id = $comment->is_about_attachment ? $comment->extra_data
+ : undef;
+
+ my $comment_hash = {
+ id => as_int($comment->id),
+ bug_id => as_int($comment->bug_id),
+ creator => as_email($comment->author->login),
+ time => as_datetime($comment->creation_ts),
+ creation_time => as_datetime($comment->creation_ts),
+ is_private => as_boolean($comment->is_private),
+ is_markdown => as_boolean($comment->is_markdown),
+ text => as_string($comment->body_full),
+ attachment_id => as_int($attach_id),
+ count => as_int($comment->count),
+ };
+
+ # Don't load comment tags unless enabled
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ $comment_hash->{tags} = as_string_array($comment->tags);
+ }
+
+ return filter($filters, $comment_hash, $types, $prefix);
+}
+
+sub get {
+ my ($self, $params) = validate(@_, 'ids');
+
+ Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+
+ my $ids = $params->{ids};
+ defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+
+ my (@bugs, @faults, @hashes);
+
+ # Cache permissions for bugs. This highly reduces the number of calls to the DB.
+ # visible_bugs() is only able to handle bug IDs, so we have to skip aliases.
+ my @int = grep { $_ =~ /^\d+$/ } @$ids;
+ Bugzilla->user->visible_bugs(\@int);
+
+ foreach my $bug_id (@$ids) {
+ my $bug;
+ if ($params->{permissive}) {
+ eval { $bug = Bugzilla::Bug->check($bug_id); };
+ if ($@) {
+ push(@faults, {id => $bug_id,
+ faultString => $@->faultstring,
+ faultCode => $@->faultcode,
+ }
+ );
+ undef $@;
+ next;
+ }
+ }
+ else {
+ $bug = Bugzilla::Bug->check($bug_id);
+ }
+ push(@bugs, $bug);
+ push(@hashes, $self->_bug_to_hash($bug, $params));
+ }
+
+ # Set the ETag before inserting the update tokens
+ # since the tokens will always be unique even if
+ # the data has not changed.
+ Bugzilla->api_server->etag(\@hashes);
+
+ $self->_add_update_tokens($params, \@bugs, \@hashes);
+
+ return { bugs => \@hashes, faults => \@faults };
+}
+
+# this is a function that gets bug activity for list of bug ids
+# it can be called as the following:
+# $call = $rpc->call( 'Bug.history', { ids => [1,2] });
+sub history {
+ my ($self, $params) = validate(@_, 'ids');
+
+ Bugzilla->switch_to_shadow_db();
+
+ my $ids = $params->{ids};
+ defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+
+ my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
+ $api_name{'bug_group'} = 'groups';
+
+ my @return;
+ foreach my $bug_id (@$ids) {
+ my %item;
+ my $bug = Bugzilla::Bug->check($bug_id);
+ $bug_id = $bug->id;
+ $item{id} = as_int($bug_id);
+
+ my ($activity) = $bug->get_activity(undef, $params->{new_since});
+
+ my @history;
+ foreach my $changeset (@$activity) {
+ my %bug_history;
+ $bug_history{when} = as_datetime($changeset->{when});
+ $bug_history{who} = as_string($changeset->{who});
+ $bug_history{changes} = [];
+ foreach my $change (@{ $changeset->{changes} }) {
+ my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname};
+ my $attach_id = delete $change->{attachid};
+ if ($attach_id) {
+ $change->{attachment_id} = as_int($attach_id);
+ }
+ $change->{removed} = as_string($change->{removed});
+ $change->{added} = as_string($change->{added});
+ $change->{field_name} = as_string($api_field);
+ delete $change->{fieldname};
+ push (@{$bug_history{changes}}, $change);
+ }
+
+ push (@history, \%bug_history);
+ }
+
+ $item{history} = \@history;
+
+ # alias is returned in case users passes a mixture of ids and aliases
+ # then they get to know which bug activity relates to which value
+ # they passed
+ $item{alias} = as_string_array($bug->alias);
+
+ push(@return, \%item);
+ }
+
+ return { bugs => \@return };
+}
+
+sub search {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ Bugzilla->switch_to_shadow_db();
+
+ my $match_params = dclone($params);
+ delete $match_params->{include_fields};
+ delete $match_params->{exclude_fields};
+
+ # Determine whether this is a quicksearch query
+ if (exists $match_params->{quicksearch}) {
+ my $quicksearch = quicksearch($match_params->{'quicksearch'});
+ my $cgi = Bugzilla::CGI->new($quicksearch);
+ $match_params = $cgi->Vars;
+ }
+
+ if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) {
+ ThrowCodeError('param_required',
+ { param => 'limit', function => 'Bug.search()' });
+ }
+
+ my $max_results = Bugzilla->params->{max_search_results};
+ unless (defined $match_params->{limit} && $match_params->{limit} == 0) {
+ if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) {
+ $match_params->{limit} = $max_results;
+ }
+ }
+ else {
+ delete $match_params->{limit};
+ delete $match_params->{offset};
+ }
+
+ $match_params = Bugzilla::Bug::map_fields($match_params);
+
+ my %options = ( fields => ['bug_id'] );
+
+ # Find the highest custom field id
+ my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
+ my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+
+ # Do special search types for certain fields.
+ if (my $change_when = delete $match_params->{'delta_ts'}) {
+ $match_params->{"f${last_field_id}"} = 'delta_ts';
+ $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+ $match_params->{"v${last_field_id}"} = $change_when;
+ $last_field_id++;
+ }
+ if (my $creation_when = delete $match_params->{'creation_ts'}) {
+ $match_params->{"f${last_field_id}"} = 'creation_ts';
+ $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+ $match_params->{"v${last_field_id}"} = $creation_when;
+ $last_field_id++;
+ }
+
+ # Some fields require a search type such as short desc, keywords, etc.
+ foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) {
+ if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) {
+ $match_params->{$param . '_type'} = 'allwordssubstr';
+ }
+ }
+ if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) {
+ $match_params->{'keywords_type'} = 'allwords';
+ }
+
+ # Backwards compatibility with old method regarding role search
+ $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'};
+ foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) {
+ next if !exists $match_params->{$role};
+ my $value = delete $match_params->{$role};
+ $match_params->{"f${last_field_id}"} = $role;
+ $match_params->{"o${last_field_id}"} = "anywordssubstr";
+ $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value;
+ $last_field_id++;
+ }
+
+ # If no other parameters have been passed other than limit and offset
+ # then we throw error if system is configured to do so.
+ if (!grep(!/^(limit|offset)$/, keys %$match_params)
+ && !Bugzilla->params->{search_allow_no_criteria})
+ {
+ ThrowUserError('buglist_parameters_required');
+ }
+
+ $options{order} = [ split(/\s*,\s*/, delete $match_params->{order}) ] if $match_params->{order};
+ $options{params} = $match_params;
+
+ my $search = new Bugzilla::Search(%options);
+ my ($data) = $search->data;
+
+ if (!scalar @$data) {
+ return { bugs => [] };
+ }
+
+ # Search.pm won't return bugs that the user shouldn't see so no filtering is needed.
+ my @bug_ids = map { $_->[0] } @$data;
+ my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
+ my @bugs = map { $bug_objects{$_} } @bug_ids;
+ @bugs = map { $self->_bug_to_hash($_, $params) } @bugs;
+
+ return { bugs => \@bugs };
+}
+
+sub possible_duplicates {
+ my ($self, $params) = validate(@_, 'products');
+ my $user = Bugzilla->user;
+
+ Bugzilla->switch_to_shadow_db();
+
+ # Undo the array-ification that validate() does, for "summary".
+ $params->{summary} || ThrowCodeError('param_required',
+ { function => 'Bug.possible_duplicates', param => 'summary' });
+
+ my @products;
+ foreach my $name (@{ $params->{'products'} || [] }) {
+ my $object = $user->can_enter_product($name, THROW_ERROR);
+ push(@products, $object);
+ }
+
+ my $possible_dupes = Bugzilla::Bug->possible_duplicates(
+ { summary => $params->{summary}, products => \@products,
+ limit => $params->{limit} });
+ my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
+ $self->_add_update_tokens($params, $possible_dupes, \@hashes);
+ return { bugs => \@hashes };
+}
+
+sub update {
+ my ($self, $params) = validate(@_, 'ids');
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $dbh = Bugzilla->dbh;
+
+ # We skip certain fields because their set_ methods actually use
+ # the external names instead of the internal names.
+ $params = Bugzilla::Bug::map_fields($params,
+ { summary => 1, platform => 1, severity => 1, url => 1 });
+
+ my $ids = delete $params->{ids};
+ defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+
+ my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids;
+
+ my %values = %$params;
+ $values{other_bugs} = \@bugs;
+
+ if (exists $values{comment} and exists $values{comment}{comment}) {
+ $values{comment}{body} = delete $values{comment}{comment};
+ }
+
+ # Prevent bugs that could be triggered by specifying fields that
+ # have valid "set_" functions in Bugzilla::Bug, but shouldn't be
+ # called using those field names.
+ delete $values{dependencies};
+
+ # For backwards compatibility, treat alias string or array as a set action
+ if (exists $values{alias}) {
+ if (not ref $values{alias}) {
+ $values{alias} = { set => [ $values{alias} ] };
+ }
+ elsif (ref $values{alias} eq 'ARRAY') {
+ $values{alias} = { set => $values{alias} };
+ }
+ }
+
+ my $flags = delete $values{flags};
+
+ foreach my $bug (@bugs) {
+ $bug->set_all(\%values);
+ if ($flags) {
+ my ($old_flags, $new_flags) = extract_flags($flags, $bug);
+ $bug->set_flags($old_flags, $new_flags);
+ }
+ }
+
+ my %all_changes;
+ $dbh->bz_start_transaction();
+ foreach my $bug (@bugs) {
+ $all_changes{$bug->id} = $bug->update();
+ }
+ $dbh->bz_commit_transaction();
+
+ foreach my $bug (@bugs) {
+ $bug->send_changes($all_changes{$bug->id});
+ }
+
+ my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
+ # This doesn't normally belong in FIELD_MAP, but we do want to translate
+ # "bug_group" back into "groups".
+ $api_name{'bug_group'} = 'groups';
+
+ my @result;
+ foreach my $bug (@bugs) {
+ my %hash = (
+ id => as_int($bug->id),
+ last_change_time => as_datetime($bug->delta_ts),
+ changes => {},
+ );
+
+ # alias is returned in case users pass a mixture of ids and aliases,
+ # so that they can know which set of changes relates to which value
+ # they passed.
+ $hash{alias} = as_string_array($bug->alias);
+
+ my %changes = %{ $all_changes{$bug->id} };
+ foreach my $field (keys %changes) {
+ my $change = $changes{$field};
+ my $api_field = $api_name{$field} || $field;
+ # We normalize undef to an empty string, so that the API
+ # stays consistent for things like Deadline that can become
+ # empty.
+ $change->[0] = '' if !defined $change->[0];
+ $change->[1] = '' if !defined $change->[1];
+ $hash{changes}->{$api_field} = {
+ removed => as_string($change->[0]),
+ added => as_string($change->[1])
+ };
+ }
+
+ push(@result, \%hash);
+ }
+
+ return { bugs => \@result };
+}
+
+sub create {
+ my ($self, $api, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+
+ $params = Bugzilla::Bug::map_fields($params);
+
+ my $flags = delete $params->{flags};
+
+ # We start a nested transaction in case flag setting fails
+ # we want the bug creation to roll back as well.
+ $dbh->bz_start_transaction();
+
+ my $bug = Bugzilla::Bug->create($params);
+
+ # Set bug flags
+ if ($flags) {
+ my ($flags, $new_flags) = extract_flags($flags, $bug);
+ $bug->set_flags($flags, $new_flags);
+ $bug->update($bug->creation_ts);
+ }
+
+ $dbh->bz_commit_transaction();
+
+ $bug->send_changes();
+
+ return { id => as_int($bug->bug_id) };
+}
+
+sub legal_values {
+ my ($self, $params) = @_;
+
+ Bugzilla->switch_to_shadow_db();
+
+ defined $params->{field}
+ or ThrowCodeError('param_required', { param => 'field' });
+
+ my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}}
+ || $params->{field};
+
+ my @global_selects =
+ @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) };
+
+ my $values;
+ if (grep($_->name eq $field, @global_selects)) {
+ # The field is a valid one.
+ trick_taint($field);
+ $values = get_legal_field_values($field);
+ }
+ elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) {
+ my $id = $params->{product_id};
+ defined $id || ThrowCodeError('param_required',
+ { function => 'Bug.legal_values', param => 'product_id' });
+ grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
+ || ThrowUserError('product_access_denied', { id => $id });
+
+ my $product = new Bugzilla::Product($id);
+ my @objects;
+ if ($field eq 'version') {
+ @objects = @{$product->versions};
+ }
+ elsif ($field eq 'target_milestone') {
+ @objects = @{$product->milestones};
+ }
+ elsif ($field eq 'component') {
+ @objects = @{$product->components};
+ }
+
+ $values = [map { $_->name } @objects];
+ }
+ else {
+ ThrowCodeError('invalid_field_name', { field => $params->{field} });
+ }
+
+ my @result;
+ foreach my $val (@$values) {
+ push(@result, as_string($val));
+ }
+
+ return { values => \@result };
+}
+
+sub add_attachment {
+ my ($self, $params) = validate(@_, 'ids');
+ my $dbh = Bugzilla->dbh;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ defined $params->{ids}
+ || ThrowCodeError('param_required', { param => 'ids' });
+ defined $params->{data}
+ || ThrowCodeError('param_required', { param => 'data' });
+
+ my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{ $params->{ids} };
+
+ my @created;
+ $dbh->bz_start_transaction();
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ my $flags = delete $params->{flags};
+
+ foreach my $bug (@bugs) {
+ my $attachment = Bugzilla::Attachment->create({
+ bug => $bug,
+ creation_ts => $timestamp,
+ data => $params->{data},
+ description => $params->{summary},
+ filename => $params->{file_name},
+ mimetype => $params->{content_type},
+ ispatch => $params->{is_patch},
+ isprivate => $params->{is_private},
+ });
+
+ if ($flags) {
+ my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment);
+ $attachment->set_flags($old_flags, $new_flags);
+ }
+
+ $attachment->update($timestamp);
+ my $comment = $params->{comment} || '';
+
+ my $is_markdown = 0;
+ if (ref $params->{comment} eq 'HASH') {
+ $is_markdown = $params->{comment}->{is_markdown};
+ $comment = $params->{comment}->{body};
+ }
+
+ ThrowUserError('markdown_disabled')
+ if $is_markdown && !Bugzilla->user->use_markdown();
+
+ $attachment->bug->add_comment($comment,
+ { is_markdown => $is_markdown,
+ isprivate => $attachment->isprivate,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ push(@created, $attachment);
+ }
+ $_->bug->update($timestamp) foreach @created;
+ $dbh->bz_commit_transaction();
+
+ $_->send_changes() foreach @bugs;
+
+ my @created_ids = map { $_->id } @created;
+
+ return { ids => \@created_ids };
+}
+
+sub update_attachment {
+ my ($self, $params) = validate(@_, 'ids');
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ my $dbh = Bugzilla->dbh;
+
+ my $ids = delete $params->{ids};
+ defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+
+ # Some fields cannot be sent to set_all
+ foreach my $key (qw(login password token)) {
+ delete $params->{$key};
+ }
+
+ $params = translate($params, ATTACHMENT_MAPPED_SETTERS);
+
+ # Get all the attachments, after verifying that they exist and are editable
+ my @attachments = ();
+ my %bugs = ();
+ foreach my $id (@$ids) {
+ my $attachment = Bugzilla::Attachment->new($id)
+ || ThrowUserError("invalid_attach_id", { attach_id => $id });
+ my $bug = $attachment->bug;
+ $attachment->_check_bug;
+ $attachment->validate_can_edit
+ || ThrowUserError("illegal_attachment_edit", { attach_id => $id });
+
+ push @attachments, $attachment;
+ $bugs{$bug->id} = $bug;
+ }
+
+ my $flags = delete $params->{flags};
+ my $comment = delete $params->{comment};
+ my $is_markdown = 0;
+
+ if (ref $comment eq 'HASH') {
+ $is_markdown = $comment->{is_markdown};
+ $comment = $comment->{body};
+ }
+
+ ThrowUserError('markdown_disabled')
+ if $is_markdown && !$user->use_markdown();
+
+ # Update the values
+ foreach my $attachment (@attachments) {
+ $attachment->set_all($params);
+ if ($flags) {
+ my ($old_flags, $new_flags) = extract_flags($flags, $attachment->bug, $attachment);
+ $attachment->set_flags($old_flags, $new_flags);
+ }
+ }
+
+ $dbh->bz_start_transaction();
+
+ # Do the actual update and get information to return to user
+ my @result;
+ foreach my $attachment (@attachments) {
+ my $changes = $attachment->update();
+
+ if ($comment = trim($comment)) {
+ $attachment->bug->add_comment($comment,
+ { is_markdown => $is_markdown,
+ isprivate => $attachment->isprivate,
+ type => CMT_ATTACHMENT_UPDATED,
+ extra_data => $attachment->id });
+ }
+
+ $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
+
+ my %hash = (
+ id => as_int($attachment->id),
+ last_change_time => as_datetime($attachment->modification_time),
+ changes => {},
+ );
+
+ foreach my $field (keys %$changes) {
+ my $change = $changes->{$field};
+
+ # We normalize undef to an empty string, so that the API
+ # stays consistent for things like Deadline that can become
+ # empty.
+ $hash{changes}->{$field} = {
+ removed => as_string($change->[0] // ''),
+ added => as_string($change->[1] // '')
+ };
+ }
+
+ push(@result, \%hash);
+ }
+
+ $dbh->bz_commit_transaction();
+
+ # Email users about the change
+ foreach my $bug (values %bugs) {
+ $bug->update();
+ $bug->send_changes();
+ }
+
+ # Return the information to the user
+ return { attachments => \@result };
+}
+
+sub add_comment {
+ my ($self, $params) = @_;
+
+ # The user must login in order add a comment
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ # Check parameters
+ defined $params->{id}
+ || ThrowCodeError('param_required', { param => 'id' });
+ my $comment = $params->{comment};
+ (defined $comment && trim($comment) ne '')
+ || ThrowCodeError('param_required', { param => 'comment' });
+
+ my $bug = Bugzilla::Bug->check_for_edit($params->{id});
+
+ # Backwards-compatibility for versions before 3.6
+ if (defined $params->{private}) {
+ $params->{is_private} = delete $params->{private};
+ }
+
+ ThrowUserError('markdown_disabled')
+ if $params->{is_markdown} && !$user->use_markdown();
+
+ # Append comment
+ $bug->add_comment($comment, { isprivate => $params->{is_private},
+ is_markdown => $params->{is_markdown},
+ work_time => $params->{work_time} });
+ $bug->update();
+
+ my $new_comment_id = $bug->{added_comments}[0]->id;
+
+ # Send mail.
+ Bugzilla::BugMail::Send($bug->bug_id, { changer => $user });
+
+ return { id => as_int($new_comment_id) };
+}
+
+sub update_see_also {
+ my ($self, $params) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ # Check parameters
+ $params->{ids}
+ || ThrowCodeError('param_required', { param => 'id' });
+ my ($add, $remove) = @$params{qw(add remove)};
+ ($add || $remove)
+ or ThrowCodeError('params_required', { params => ['add', 'remove'] });
+
+ my @bugs;
+ foreach my $id (@{ $params->{ids} }) {
+ my $bug = Bugzilla::Bug->check_for_edit($id);
+ push(@bugs, $bug);
+ if ($remove) {
+ $bug->remove_see_also($_) foreach @$remove;
+ }
+ if ($add) {
+ $bug->add_see_also($_) foreach @$add;
+ }
+ }
+
+ my %changes;
+ foreach my $bug (@bugs) {
+ my $change = $bug->update();
+ if (my $see_also = $change->{see_also}) {
+ $changes{$bug->id}->{see_also} = {
+ removed => [split(', ', $see_also->[0])],
+ added => [split(', ', $see_also->[1])],
+ };
+ }
+ else {
+ # We still want a changes entry, for API consistency.
+ $changes{$bug->id}->{see_also} = { added => [], removed => [] };
+ }
+
+ Bugzilla::BugMail::Send($bug->id, { changer => $user });
+ }
+
+ return { changes => \%changes };
+}
+
+sub attachments {
+ my ($self, $params) = validate(@_, 'ids', 'attachment_ids');
+
+ Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+
+ if (!(defined $params->{ids}
+ or defined $params->{attachment_ids}))
+ {
+ ThrowCodeError('param_required',
+ { function => 'Bug.attachments',
+ params => ['ids', 'attachment_ids'] });
+ }
+
+ my $ids = $params->{ids} || [];
+ my $attach_ids = $params->{attachment_ids} || [];
+
+ my %bugs;
+ foreach my $bug_id (@$ids) {
+ my $bug = Bugzilla::Bug->check($bug_id);
+ $bugs{$bug->id} = [];
+ foreach my $attach (@{$bug->attachments}) {
+ push @{$bugs{$bug->id}},
+ $self->_attachment_to_hash($attach, $params);
+ }
+ }
+
+ my %attachments;
+ foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) {
+ Bugzilla::Bug->check($attach->bug_id);
+ if ($attach->isprivate && !Bugzilla->user->is_insider) {
+ ThrowUserError('auth_failure', {action => 'access',
+ object => 'attachment',
+ attach_id => $attach->id});
+ }
+ $attachments{$attach->id} =
+ $self->_attachment_to_hash($attach, $params);
+ }
+
+ return { bugs => \%bugs, attachments => \%attachments };
+}
+
+sub update_tags {
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+
+ my $ids = $params->{ids};
+ my $tags = $params->{tags};
+
+ ThrowCodeError('param_required',
+ { function => 'Bug.update_tags',
+ param => 'ids' }) if !defined $ids;
+
+ ThrowCodeError('param_required',
+ { function => 'Bug.update_tags',
+ param => 'tags' }) if !defined $tags;
+
+ my %changes;
+ foreach my $bug_id (@$ids) {
+ my $bug = Bugzilla::Bug->check($bug_id);
+ my @old_tags = @{ $bug->tags };
+
+ $bug->remove_tag($_) foreach @{ $tags->{remove} || [] };
+ $bug->add_tag($_) foreach @{ $tags->{add} || [] };
+
+ my ($removed, $added) = diff_arrays(\@old_tags, $bug->tags);
+
+ $removed = as_string_array($removed);
+ $added = as_string_array($added);
+
+ $changes{$bug->id}->{tags} = {
+ removed => $removed,
+ added => $added
+ };
+ }
+
+ return { changes => \%changes };
+}
+
+sub update_comment_tags {
+ my ($self, $params) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->params->{'comment_taggers_group'}
+ || ThrowUserError("comment_tag_disabled");
+ $user->can_tag_comments
+ || ThrowUserError("auth_failure",
+ { group => Bugzilla->params->{'comment_taggers_group'},
+ action => "update",
+ object => "comment_tags" });
+
+ my $comment_id = $params->{comment_id}
+ // ThrowCodeError('param_required',
+ { function => 'Bug.update_comment_tags',
+ param => 'comment_id' });
+
+ my $comment = Bugzilla::Comment->new($comment_id)
+ || return [];
+ $comment->bug->check_is_visible();
+ if ($comment->is_private && !$user->is_insider) {
+ ThrowUserError('comment_is_private', { id => $comment_id });
+ }
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ foreach my $tag (@{ $params->{add} || [] }) {
+ $comment->add_tag($tag) if defined $tag;
+ }
+ foreach my $tag (@{ $params->{remove} || [] }) {
+ $comment->remove_tag($tag) if defined $tag;
+ }
+ $comment->update();
+ $dbh->bz_commit_transaction();
+
+ return $comment->tags;
+}
+
+sub search_comment_tags {
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->params->{'comment_taggers_group'}
+ || ThrowUserError("comment_tag_disabled");
+ Bugzilla->user->can_tag_comments
+ || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'},
+ action => "search",
+ object => "comment_tags"});
+
+ my $query = $params->{query};
+ $query
+ // ThrowCodeError('param_required', { param => 'query' });
+ my $limit = $params->{limit} || 7;
+ detaint_natural($limit)
+ || ThrowCodeError('param_must_be_numeric', { param => 'limit',
+ function => 'Bug.search_comment_tags' });
+
+
+ my $tags = Bugzilla::Comment::TagWeights->match({
+ WHERE => {
+ 'tag LIKE ?' => "\%$query\%",
+ },
+ LIMIT => $limit,
+ });
+ return [ map { $_->tag } @$tags ];
+}
+
+##############################
+# Private Helper Subroutines #
+##############################
+
+# A helper for get() and search(). This is done in this fashion in order
+# to produce a stable API and to explicitly type return values.
+# The internals of Bugzilla::Bug are not stable enough to just
+# return them directly.
+
+sub _bug_to_hash {
+ my ($self, $bug, $params) = @_;
+
+ # All the basic bug attributes are here, in alphabetical order.
+ # A bug attribute is "basic" if it doesn't require an additional
+ # database call to get the info.
+ my %item = %{ filter $params, {
+ # No need to format $bug->deadline specially, because Bugzilla::Bug
+ # already does it for us.
+ deadline => as_string($bug->deadline),
+ id => as_int($bug->bug_id),
+ is_confirmed => as_boolean($bug->everconfirmed),
+ op_sys => as_string($bug->op_sys),
+ platform => as_string($bug->rep_platform),
+ priority => as_string($bug->priority),
+ resolution => as_string($bug->resolution),
+ severity => as_string($bug->bug_severity),
+ status => as_string($bug->bug_status),
+ summary => as_string($bug->short_desc),
+ target_milestone => as_string($bug->target_milestone),
+ url => as_string($bug->bug_file_loc),
+ version => as_string($bug->version),
+ whiteboard => as_string($bug->status_whiteboard),
+ } };
+
+ # First we handle any fields that require extra work (such as date parsing
+ # or SQL calls).
+ if (filter_wants $params, 'alias') {
+ $item{alias} = as_string_array($bug->alias);
+ }
+ if (filter_wants $params, 'assigned_to') {
+ $item{'assigned_to'} = as_email($bug->assigned_to->login);
+ $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
+ }
+ if (filter_wants $params, 'blocks') {
+ $item{'blocks'} = as_int_array($bug->blocked);
+ }
+ if (filter_wants $params, 'classification') {
+ $item{classification} = as_string($bug->classification);
+ }
+ if (filter_wants $params, 'component') {
+ $item{component} = as_string($bug->component);
+ }
+ if (filter_wants $params, 'cc') {
+ $item{'cc'} = as_email_array($bug->cc);
+ $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ];
+ }
+ if (filter_wants $params, 'creation_time') {
+ $item{'creation_time'} = as_datetime($bug->creation_ts);
+ }
+ if (filter_wants $params, 'creator') {
+ $item{'creator'} = as_email($bug->reporter->login);
+ $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
+ }
+ if (filter_wants $params, 'depends_on') {
+ $item{'depends_on'} = as_int_array($bug->dependson);
+ }
+ if (filter_wants $params, 'dupe_of') {
+ $item{'dupe_of'} = as_int($bug->dup_id);
+ }
+ if (filter_wants $params, 'groups') {
+ $item{'groups'} = as_name_array($bug->groups_in);
+ }
+ if (filter_wants $params, 'is_open') {
+ $item{'is_open'} = as_boolean($bug->status->is_open);
+ }
+ if (filter_wants $params, 'keywords') {
+ $item{'keywords'} = as_name_array($bug->keyword_objects);
+ }
+ if (filter_wants $params, 'last_change_time') {
+ $item{'last_change_time'} = as_datetime($bug->delta_ts);
+ }
+ if (filter_wants $params, 'product') {
+ $item{product} = as_string($bug->product);
+ }
+ if (filter_wants $params, 'qa_contact') {
+ my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
+ $item{'qa_contact'} = as_email($qa_login);
+ if ($bug->qa_contact) {
+ $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
+ }
+ }
+ if (filter_wants $params, 'see_also') {
+ $item{'see_also'} = as_string_array($bug->see_also);
+ }
+ if (filter_wants $params, 'flags') {
+ $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ];
+ }
+ if (filter_wants $params, 'tags', 'extra') {
+ $item{'tags'} = $bug->tags;
+ }
+
+ # And now custom fields
+ my @custom_fields = Bugzilla->active_custom_fields;
+ foreach my $field (@custom_fields) {
+ my $name = $field->name;
+ next if !filter_wants($params, $name, ['default', 'custom']);
+ if ($field->type == FIELD_TYPE_BUG_ID) {
+ $item{$name} = as_int($bug->$name);
+ }
+ elsif ($field->type == FIELD_TYPE_DATETIME
+ || $field->type == FIELD_TYPE_DATE)
+ {
+ $item{$name} = as_datetime($bug->$name);
+ }
+ elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+ $item{$name} = as_string_array($bug->$name);
+ }
+ else {
+ $item{$name} = as_string($bug->$name);
+ }
+ }
+
+ # Timetracking fields are only sent if the user can see them.
+ if (Bugzilla->user->is_timetracker) {
+ if (filter_wants $params, 'estimated_time') {
+ $item{'estimated_time'} = as_double($bug->estimated_time);
+ }
+ if (filter_wants $params, 'remaining_time') {
+ $item{'remaining_time'} = as_double($bug->remaining_time);
+ }
+ if (filter_wants $params, 'actual_time') {
+ $item{'actual_time'} = as_double($bug->actual_time);
+ }
+ }
+
+ # The "accessible" bits go here because they have long names and it
+ # makes the code look nicer to separate them out.
+ if (filter_wants $params, 'is_cc_accessible') {
+ $item{'is_cc_accessible'} = as_boolean($bug->cclist_accessible);
+ }
+ if (filter_wants $params, 'is_creator_accessible') {
+ $item{'is_creator_accessible'} = as_boolean($bug->reporter_accessible);
+ }
+
+ return \%item;
+}
+
+sub _user_to_hash {
+ my ($self, $user, $filters, $types, $prefix) = @_;
+ my $item = filter $filters, {
+ id => as_int($user->id),
+ real_name => as_string($user->name),
+ name => as_email($user->login),
+ email => as_email($user->email),
+ }, $types, $prefix;
+ return $item;
+}
+
+sub _attachment_to_hash {
+ my ($self, $attach, $filters, $types, $prefix) = @_;
+
+ my $item = filter $filters, {
+ creation_time => as_datetime($attach->attached),
+ last_change_time => as_datetime($attach->modification_time),
+ id => as_int($attach->id),
+ bug_id => as_int($attach->bug_id),
+ file_name => as_string($attach->filename),
+ summary => as_string($attach->description),
+ content_type => as_string($attach->contenttype),
+ is_private => as_boolean($attach->isprivate),
+ is_obsolete => as_boolean($attach->isobsolete),
+ is_patch => as_boolean($attach->ispatch),
+ }, $types, $prefix;
+
+ # creator requires an extra lookup, so we only send them if
+ # the filter wants them.
+ if (filter_wants $filters, 'creator', $types, $prefix) {
+ $item->{'creator'} = as_email($attach->attacher->login);
+ }
+
+ if (filter_wants $filters, 'data', $types, $prefix) {
+ $item->{'data'} = as_base64($attach->data);
+ }
+
+ if (filter_wants $filters, 'size', $types, $prefix) {
+ $item->{'size'} = as_int($attach->datasize);
+ }
+
+ if (filter_wants $filters, 'flags', $types, $prefix) {
+ $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ];
+ }
+
+ return $item;
+}
+
+sub _flag_to_hash {
+ my ($self, $flag) = @_;
+
+ my $item = {
+ id => as_int($flag->id),
+ name => as_string($flag->name),
+ type_id => as_int($flag->type_id),
+ creation_date => as_datetime($flag->creation_date),
+ modification_date => as_datetime($flag->modification_date),
+ status => as_string($flag->status)
+ };
+
+ foreach my $field (qw(setter requestee)) {
+ my $field_id = $field . "_id";
+ $item->{$field} = as_email($flag->$field->login)
+ if $flag->$field_id;
+ }
+
+ return $item;
+}
+
+sub _add_update_tokens {
+ my ($self, $params, $bugs, $hashes) = @_;
+
+ return if !Bugzilla->user->id;
+ return if !filter_wants($params, 'update_token');
+
+ for(my $i = 0; $i < @$bugs; $i++) {
+ my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]);
+ $hashes->[$i]->{'update_token'} = as_string($token);
+ }
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::Bug - The API for creating, changing, and getting the
+details of bugs.
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla API allows you to file a new bug in Bugzilla,
+or get information about bugs that have already been filed.
+
+=head1 USAGE
+
+Full documentation on how to use the Bugzilla API can be found at
+L<https://bugzilla.readthedocs.org/en/latest/api/core/v1/bug.html>.
+
+=head1 METHODS
+
+=head2 fields
+
+=over
+
+=item B<Description>
+
+Get information about valid bug fields, including the lists of legal values
+for each field.
+
+=item B<REST>
+
+You have several options for retreiving information about fields. The first
+part is the request method and the rest is the related path needed.
+
+To get information about all fields:
+
+GET /rest/field/bug
+
+To get information related to a single field:
+
+GET /rest/field/bug/<id_or_name>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+You can pass either field ids or field names.
+
+B<Note>: If neither C<ids> nor C<names> is specified, then all
+non-obsolete fields will be returned.
+
+In addition to the parameters below, this method also accepts the
+standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and
+L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments.
+
+=over
+
+=item C<ids> (array) - An array of integer field ids.
+
+=item C<names> (array) - An array of strings representing field names.
+
+=back
+
+=item B<Returns>
+
+A hash containing a single element, C<fields>. This is an array of hashes,
+containing the following keys:
+
+=over
+
+=item C<id>
+
+C<int> An integer id uniquely identifying this field in this installation only.
+
+=item C<type>
+
+C<int> The number of the fieldtype. The following values are defined:
+
+=over
+
+=item C<0> Unknown
+
+=item C<1> Free Text
+
+=item C<2> Drop Down
+
+=item C<3> Multiple-Selection Box
+
+=item C<4> Large Text Box
+
+=item C<5> Date/Time
+
+=item C<6> Bug Id
+
+=item C<7> Bug URLs ("See Also")
+
+=item C<8> Keywords
+
+=item C<9> Date
+
+=item C<10> Integer value
+
+=back
+
+=item C<is_custom>
+
+C<boolean> True when this is a custom field, false otherwise.
+
+=item C<name>
+
+C<string> The internal name of this field. This is a unique identifier for
+this field. If this is not a custom field, then this name will be the same
+across all Bugzilla installations.
+
+=item C<display_name>
+
+C<string> The name of the field, as it is shown in the user interface.
+
+=item C<is_mandatory>
+
+C<boolean> True if the field must have a value when filing new bugs.
+Also, mandatory fields cannot have their value cleared when updating
+bugs.
+
+=item C<is_on_bug_entry>
+
+C<boolean> For custom fields, this is true if the field is shown when you
+enter a new bug. For standard fields, this is currently always false,
+even if the field shows up when entering a bug. (To know whether or not
+a standard field is valid on bug entry, see L</create>.)
+
+=item C<visibility_field>
+
+C<string> The name of a field that controls the visibility of this field
+in the user interface. This field only appears in the user interface when
+the named field is equal to one of the values in C<visibility_values>.
+Can be null.
+
+=item C<visibility_values>
+
+C<array> of C<string>s This field is only shown when C<visibility_field>
+matches one of these values. When C<visibility_field> is null,
+then this is an empty array.
+
+=item C<value_field>
+
+C<string> The name of the field that controls whether or not particular
+values of the field are shown in the user interface. Can be null.
+
+=item C<values>
+
+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>, C<target_milestone>, and C<keywords>
+fields, but not for the C<product> field (you must use
+L<Product.get_accessible_products|Bugzilla::API::1_0::Resource::Product/get_accessible_products>
+for that.
+
+For fields that aren't select-type fields, this will simply be an empty
+array.
+
+Each hash has the following keys:
+
+=over
+
+=item C<name>
+
+C<string> The actual value--this is what you would specify for this
+field in L</create>, etc.
+
+=item C<sort_key>
+
+C<int> Values, when displayed in a list, are sorted first by this integer
+and then secondly by their name.
+
+=item C<sortkey>
+
+B<DEPRECATED> - Use C<sort_key> instead.
+
+=item C<visibility_values>
+
+If C<value_field> is defined for this field, then this value is only shown
+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<is_active>
+
+C<boolean> This value is defined only for certain product specific fields
+such as version, target_milestone or component. When true, the value is active,
+otherwise the value is not active.
+
+=item C<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
+specifies that the bug is "open" (true) or "closed" (false). This item
+is only included for the C<bug_status> field.
+
+=item C<can_change_to>
+
+For C<bug_status> values, this is an array of hashes that determines which
+statuses you can transition to from this status. (This item is only included
+for the C<bug_status> field.)
+
+Each hash contains the following items:
+
+=over
+
+=item C<name>
+
+the name of the new status
+
+=item C<comment_required>
+
+this C<boolean> True if a comment is required when you change a bug into
+this status using this transition.
+
+=back
+
+=back
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 51 (Invalid Field Name or Id)
+
+You specified an invalid field name or id.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.6>.
+
+=item The C<is_mandatory> return value was added in Bugzilla B<4.0>.
+
+=item C<sortkey> was renamed to C<sort_key> in Bugzilla B<4.2>.
+
+=item C<is_active> return key for C<values> was added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>
+
+=back
+
+=back
+
+=head2 legal_values
+
+B<DEPRECATED> - Use L</fields> instead.
+
+=over
+
+=item B<Description>
+
+Tells you what values are allowed for a particular field.
+
+=item B<REST>
+
+To get information on the values for a field based on field name:
+
+GET /rest/field/bug/<field_name>/values
+
+To get information based on field name and a specific product:
+
+GET /rest/field/bug/<field_name>/<product_id>/values
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<field> - The name of the field you want information about.
+This should be the same as the name you would use in L</create>, below.
+
+=item C<product_id> - If you're picking a product-specific field, you have
+to specify the id of the product you want the values for.
+
+=back
+
+=item B<Returns>
+
+C<values> - An array of strings: the legal values for this field.
+The values will be sorted as they normally would be in Bugzilla.
+
+=item B<Errors>
+
+=over
+
+=item 106 (Invalid Product)
+
+You were required to specify a product, and either you didn't, or you
+specified an invalid product (or a product that you can't access).
+
+=item 108 (Invalid Field Name)
+
+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
+
+=head2 attachments
+
+=over
+
+=item B<Description>
+
+It allows you to get data about attachments, given a list of bugs
+and/or attachment ids.
+
+B<Note>: Private attachments will only be returned if you are in the
+insidergroup or if you are the submitter of the attachment.
+
+=item B<REST>
+
+To get all current attachments for a bug:
+
+GET /rest/bug/<bug_id>/attachment
+
+To get a specific attachment based on attachment ID:
+
+GET /rest/bug/attachment/<attachment_id>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+B<Note>: At least one of C<ids> or C<attachment_ids> is required.
+
+=over
+
+=item C<ids>
+
+See the description of the C<ids> parameter in the L</get> method.
+
+=item C<attachment_ids>
+
+C<array> An array of integer attachment ids.
+
+=back
+
+Also accepts the L<include_fields|Bugzilla::API::1_0::Resource/include_fields>,
+and L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments.
+
+=item B<Returns>
+
+A hash containing two elements: C<bugs> and C<attachments>. The return
+value looks like this:
+
+ {
+ bugs => {
+ 1345 => [
+ { (attachment) },
+ { (attachment) }
+ ],
+ 9874 => [
+ { (attachment) },
+ { (attachment) }
+ ],
+ },
+
+ attachments => {
+ 234 => { (attachment) },
+ 123 => { (attachment) },
+ }
+ }
+
+The attachments of any bugs that you specified in the C<ids> argument in
+input are returned in C<bugs> on output. C<bugs> is a hash that has integer
+bug IDs for keys and the values are arrayrefs that contain hashes as attachments.
+(Fields for attachments are described below.)
+
+For any attachments that you specified directly in C<attachment_ids>, they
+are returned in C<attachments> on output. This is a hash where the attachment
+ids point directly to hashes describing the individual attachment.
+
+The fields for each attachment (where it says C<(attachment)> in the
+diagram above) are:
+
+=over
+
+=item C<data>
+
+C<base64> The raw data of the attachment, encoded as Base64.
+
+=item C<size>
+
+C<int> The length (in bytes) of the attachment.
+
+=item C<creation_time>
+
+C<dateTime> The time the attachment was created.
+
+=item C<last_change_time>
+
+C<dateTime> The last time the attachment was modified.
+
+=item C<id>
+
+C<int> The numeric id of the attachment.
+
+=item C<bug_id>
+
+C<int> The numeric id of the bug that the attachment is attached to.
+
+=item C<file_name>
+
+C<string> The file name of the attachment.
+
+=item C<summary>
+
+C<string> A short string describing the attachment.
+
+=item C<content_type>
+
+C<string> The MIME type of the attachment.
+
+=item C<is_private>
+
+C<boolean> True if the attachment is private (only visible to a certain
+group called the "insidergroup"), False otherwise.
+
+=item C<is_obsolete>
+
+C<boolean> True if the attachment is obsolete, False otherwise.
+
+=item C<is_patch>
+
+C<boolean> True if the attachment is a patch, False otherwise.
+
+=item C<creator>
+
+C<string> The login name of the user that created the attachment.
+
+=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>
+
+This method can throw all the same errors as L</get>. In addition,
+it can also throw the following error:
+
+=over
+
+=item 304 (Auth Failure, Attachment is Private)
+
+You specified the id of a private attachment in the C<attachment_ids>
+argument, and you are not in the "insider group" that can see
+private attachments.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.6>.
+
+=item In Bugzilla B<4.0>, the C<attacher> return value was renamed to
+C<creator>.
+
+=item In Bugzilla B<4.0>, the C<description> return value was renamed to
+C<summary>.
+
+=item The C<data> return value was added in Bugzilla B<4.0>.
+
+=item In Bugzilla B<4.2>, the C<is_url> return value was removed
+(this attribute no longer exists for attachments).
+
+=item The C<size> return value was added in Bugzilla B<4.4>.
+
+=item The C<flags> array was added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+
+=head2 comments
+
+=over
+
+=item B<Description>
+
+This allows you to get data about comments, given a list of bugs
+and/or comment ids.
+
+=item B<REST>
+
+To get all comments for a particular bug using the bug ID or alias:
+
+GET /rest/bug/<id_or_alias>/comment
+
+To get a specific comment based on the comment ID:
+
+GET /rest/bug/comment/<comment_id>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+B<Note>: At least one of C<ids> or C<comment_ids> is required.
+
+In addition to the parameters below, this method also accepts the
+standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and
+L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments.
+
+=over
+
+=item C<ids>
+
+C<array> An array that can contain both bug IDs and bug aliases.
+All of the comments (that are visible to you) will be returned for the
+specified bugs.
+
+=item C<comment_ids>
+
+C<array> An array of integer comment_ids. These comments will be
+returned individually, separate from any other comments in their
+respective bugs.
+
+=item C<new_since>
+
+C<dateTime> If specified, the method will only return comments I<newer>
+than this time. This only affects comments returned from the C<ids>
+argument. You will always be returned all comments you request in the
+C<comment_ids> argument, even if they are older than this date.
+
+=back
+
+=item B<Returns>
+
+Two items are returned:
+
+=over
+
+=item C<bugs>
+
+This is used for bugs specified in C<ids>. This is a hash,
+where the keys are the numeric ids of the bugs, and the value is
+a hash with a single key, C<comments>, which is an array of comments.
+(The format of comments is described below.)
+
+Note that any individual bug will only be returned once, so if you
+specify an id multiple times in C<ids>, it will still only be
+returned once.
+
+=item C<comments>
+
+Each individual comment requested in C<comment_ids> is returned here,
+in a hash where the numeric comment id is the key, and the value
+is the comment. (The format of comments is described below.)
+
+=back
+
+A "comment" as described above is a hash that contains the following
+keys:
+
+=over
+
+=item id
+
+C<int> The globally unique ID for the comment.
+
+=item bug_id
+
+C<int> The ID of the bug that this comment is on.
+
+=item attachment_id
+
+C<int> If the comment was made on an attachment, this will be the
+ID of that attachment. Otherwise it will be null.
+
+=item count
+
+C<int> The number of the comment local to the bug. The Description is 0,
+comments start with 1.
+
+=item text
+
+C<string> The actual text of the comment.
+
+=item creator
+
+C<string> The login name of the comment's author.
+
+=item time
+
+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
+group called the "insidergroup"), False otherwise.
+
+=item is_markdown
+
+C<boolean> True if this comment needs Markdown processing, false otherwise.
+
+=back
+
+=item B<Errors>
+
+This method can throw all the same errors as L</get>. In addition,
+it can also throw the following errors:
+
+=over
+
+=item 110 (Comment Is Private)
+
+You specified the id of a private comment in the C<comment_ids>
+argument, and you are not in the "insider group" that can see
+private comments.
+
+=item 111 (Invalid Comment ID)
+
+You specified an id in the C<comment_ids> argument that is invalid--either
+you specified something that wasn't a number, or there is no comment with
+that id.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.4>.
+
+=item C<attachment_id> was added to the return value in Bugzilla B<3.6>.
+
+=item In Bugzilla B<4.0>, the C<author> return value was renamed to
+C<creator>.
+
+=item C<count> was added to the return value in Bugzilla B<4.4>.
+
+=item C<creation_time> was added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+
+=head2 get
+
+=over
+
+=item B<Description>
+
+Gets information about particular bugs in the database.
+
+Note: Can also be called as "get_bugs" for compatibilty with Bugzilla 3.0 API.
+
+=item B<REST>
+
+To get information about a particular bug using its ID or alias:
+
+GET /rest/bug/<id_or_alias>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+In addition to the parameters below, this method also accepts the
+standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and
+L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments.
+
+=over
+
+=item C<ids>
+
+An array of numbers and strings.
+
+If an element in the array is entirely numeric, it represents a bug_id
+from the Bugzilla database to fetch. If it contains any non-numeric
+characters, it is considered to be a bug alias instead, and the bug with
+that alias will be loaded.
+
+=item C<permissive>
+
+C<boolean> Normally, if you request any inaccessible or invalid bug ids,
+Bug.get will throw an error. If this parameter is True, instead of throwing an
+error we return an array of hashes with a C<id>, C<faultString> and C<faultCode>
+for each bug that fails, and return normal information for the other bugs that
+were accessible.
+
+=back
+
+=item B<Returns>
+
+Two items are returned:
+
+=over
+
+=item C<bugs>
+
+An array of hashes that contains information about the bugs with
+the valid ids. Each hash contains the following items:
+
+These fields are returned by default or by specifying C<_default>
+in C<include_fields>.
+
+=over
+
+=item C<actual_time>
+
+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<array> of C<string>s The unique aliases of this bug. An empty array will be
+returned if this bug has no aliases.
+
+=item C<assigned_to>
+
+C<string> The login name of the user to whom the bug is assigned.
+
+=item C<assigned_to_detail>
+
+C<hash> A hash containing detailed user information for the assigned_to. To see the
+keys included in the user detail hash, see below.
+
+=item C<blocks>
+
+C<array> of C<int>s. The ids of bugs that are "blocked" by this bug.
+
+=item C<cc>
+
+C<array> of C<string>s. The login names of users on the CC list of this
+bug.
+
+=item C<cc_detail>
+
+C<array> of hashes containing detailed user information for each of the cc list
+members. To see the keys included in the user detail hash, see below.
+
+=item C<classification>
+
+C<string> The name of the current classification the bug is in.
+
+=item C<component>
+
+C<string> The name of the current component of this bug.
+
+=item C<creation_time>
+
+C<dateTime> When the bug was created.
+
+=item C<creator>
+
+C<string> The login name of the person who filed this bug (the reporter).
+
+=item C<creator_detail>
+
+C<hash> A hash containing detailed user information for the creator. To see the
+keys included in the user detail hash, see below.
+
+=item C<deadline>
+
+C<string> The day that this bug is due to be completed, in the format
+C<YYYY-MM-DD>.
+
+=item C<depends_on>
+
+C<array> of C<int>s. The ids of bugs that this bug "depends on".
+
+=item C<dupe_of>
+
+C<int> The bug ID of the bug that this bug is a duplicate of. If this bug
+isn't a duplicate of any bug, this will be null.
+
+=item C<estimated_time>
+
+C<double> The number of hours that it was estimated that this bug would
+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.
+
+=item C<id>
+
+C<int> The unique numeric id of this bug.
+
+=item C<is_cc_accessible>
+
+C<boolean> If true, this bug can be accessed by members of the CC list,
+even if they are not in the groups the bug is restricted to.
+
+=item C<is_confirmed>
+
+C<boolean> True if the bug has been confirmed. Usually this means that
+the bug has at some point been moved out of the C<UNCONFIRMED> status
+and into another open status.
+
+=item C<is_open>
+
+C<boolean> True if this bug is open, false if it is closed.
+
+=item C<is_creator_accessible>
+
+C<boolean> If true, this bug can be accessed by the creator (reporter)
+of the bug, even if they are not a member of the groups the bug
+is restricted to.
+
+=item C<keywords>
+
+C<array> of C<string>s. Each keyword that is on this bug.
+
+=item C<last_change_time>
+
+C<dateTime> When the bug was last changed.
+
+=item C<op_sys>
+
+C<string> The name of the operating system that the bug was filed against.
+
+=item C<platform>
+
+C<string> The name of the platform (hardware) that the bug was filed against.
+
+=item C<priority>
+
+C<string> The priority of the bug.
+
+=item C<product>
+
+C<string> The name of the product this bug is in.
+
+=item C<qa_contact>
+
+C<string> The login name of the current QA Contact on the bug.
+
+=item C<qa_contact_detail>
+
+C<hash> A hash containing detailed user information for the qa_contact. To see the
+keys included in the user detail hash, see below.
+
+=item C<remaining_time>
+
+C<double> The number of hours of work remaining until work on this bug
+is complete.
+
+If you are not in the time-tracking group, this field will not be included
+in the return value.
+
+=item C<resolution>
+
+C<string> The current resolution of the bug, or an empty string if the bug
+is open.
+
+=item C<see_also>
+
+C<array> of C<string>s. The URLs in the See Also field on the bug.
+
+=item C<severity>
+
+C<string> The current severity of the bug.
+
+=item C<status>
+
+C<string> The current status of the bug.
+
+=item C<summary>
+
+C<string> The summary of this bug.
+
+=item C<target_milestone>
+
+C<string> The milestone that this bug is supposed to be fixed by, or for
+closed bugs, the milestone that it was fixed for.
+
+=item C<update_token>
+
+C<string> The token that you would have to pass to the F<process_bug.cgi>
+page in order to update this bug. This changes every time the bug is
+updated.
+
+This field is not returned to logged-out users.
+
+=item C<url>
+
+C<string> A URL that demonstrates the problem described in
+the bug, or is somehow related to the bug report.
+
+=item C<version>
+
+C<string> The version the bug was reported against.
+
+=item C<whiteboard>
+
+C<string> The value of the "status whiteboard" field on the bug.
+
+=item I<custom fields>
+
+Every custom field in this installation will also be included in the
+return value. Most fields are returned as C<string>s. However, some
+field types have different return values.
+
+Normally custom fields are returned by default similar to normal bug
+fields or you can specify only custom fields by using C<_custom> in
+C<include_fields>.
+
+=over
+
+=item Bug ID Fields - C<int>
+
+=item Multiple-Selection Fields - C<array> of C<string>s.
+
+=item Date/Time Fields - C<dateTime>
+
+=back
+
+=item I<user detail hashes>
+
+Each user detail hash contains the following items:
+
+=over
+
+=item C<id>
+
+C<int> The user id for this user.
+
+=item C<real_name>
+
+C<string> The 'real' name for this user, if any.
+
+=item C<name>
+
+C<string> The user's Bugzilla login.
+
+=item C<email>
+
+C<string> The user's email address. Currently this is the same value as the name.
+
+=back
+
+=back
+
+These fields are returned only by specifying "_extra" or the field name in "include_fields".
+
+=over
+
+=item C<tags>
+
+C<array> of C<string>s. Each array item is a tag name.
+
+Note that tags are personal to the currently logged in user.
+
+=back
+
+=item C<faults>
+
+An array of hashes that contains invalid bug ids with error messages
+returned for them. Each hash contains the following items:
+
+=over
+
+=item id
+
+C<int> The numeric bug_id of this bug.
+
+=item faultString
+
+C<string> This will only be returned for invalid bugs if the C<permissive>
+argument was set when calling Bug.get, and it is an error indicating that
+the bug id was invalid.
+
+=item faultCode
+
+C<int> This will only be returned for invalid bugs if the C<permissive>
+argument was set when calling Bug.get, and it is the error code for the
+invalid bug error.
+
+=back
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 100 (Invalid Bug Alias)
+
+If you specified an alias and there is no bug with that alias.
+
+=item 101 (Invalid Bug ID)
+
+The bug_id you specified doesn't exist in the database.
+
+=item 102 (Access Denied)
+
+You do not have access to the bug_id you specified.
+
+=back
+
+=item B<History>
+
+=over
+
+=item C<permissive> argument added to this method's params in Bugzilla B<3.4>.
+
+=item The following properties were added to this method's return values
+in Bugzilla B<3.4>:
+
+=over
+
+=item For C<bugs>
+
+=over
+
+=item assigned_to
+
+=item component
+
+=item dupe_of
+
+=item is_open
+
+=item priority
+
+=item product
+
+=item resolution
+
+=item severity
+
+=item status
+
+=back
+
+=item C<faults>
+
+=back
+
+=item In Bugzilla B<4.0>, the following items were added to the C<bugs>
+return value: C<blocks>, C<cc>, C<classification>, C<creator>,
+C<deadline>, C<depends_on>, C<estimated_time>, C<is_cc_accessible>,
+C<is_confirmed>, C<is_creator_accessible>, C<groups>, C<keywords>,
+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.
+
+=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>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=item In Bugzilla B<5.0>, the following items were added to the bugs return value: C<assigned_to_detail>, C<creator_detail>, C<qa_contact_detail>.
+
+=back
+
+=back
+
+=head2 history
+
+=over
+
+=item B<Description>
+
+Gets the history of changes for particular bugs in the database.
+
+=item B<REST>
+
+To get the history for a specific bug ID:
+
+GET /rest/bug/<bug_id>/history
+
+The returned data format will be the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<ids>
+
+An array of numbers and strings.
+
+If an element in the array is entirely numeric, it represents a bug_id
+from the Bugzilla database to fetch. If it contains any non-numeric
+characters, it is considered to be a bug alias instead, and the data bug
+with that alias will be loaded.
+
+item C<new_since>
+
+C<dateTime> If specified, the method will only return changes I<newer>
+than this time.
+
+=back
+
+=item B<Returns>
+
+A hash containing a single element, C<bugs>. This is an array of hashes,
+containing the following keys:
+
+=over
+
+=item id
+
+C<int> The numeric id of the bug.
+
+=item alias
+
+C<array> of C<string>s The unique aliases of this bug. An empty array will be
+returned if this bug has no aliases.
+
+=item history
+
+C<array> An array of hashes, each hash having the following keys:
+
+=over
+
+=item when
+
+C<dateTime> The date the bug activity/change happened.
+
+=item who
+
+C<string> The login name of the user who performed the bug change.
+
+=item changes
+
+C<array> An array of hashes which contain all the changes that happened
+to the bug at this time (as specified by C<when>). Each hash contains
+the following items:
+
+=over
+
+=item field_name
+
+C<string> The name of the bug field that has changed.
+
+=item removed
+
+C<string> The previous value of the bug field which has been deleted
+by the change.
+
+=item added
+
+C<string> The new value of the bug field which has been added by the change.
+
+=item attachment_id
+
+C<int> The id of the attachment that was changed. This only appears if
+the change was to an attachment, otherwise C<attachment_id> will not be
+present in this hash.
+
+=back
+
+=back
+
+=back
+
+=item B<Errors>
+
+The same as L</get>.
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.4>.
+
+=item Field names returned by the C<field_name> field changed to be
+consistent with other methods. Since Bugzilla B<4.4>, they now match
+names used by L<Bug.update|/"update"> for consistency.
+
+=item REST API call added Bugzilla B<5.0>.
+
+=item Added C<new_since> parameter if Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 possible_duplicates
+
+=over
+
+=item B<Description>
+
+Allows a user to find possible duplicate bugs based on a set of keywords
+such as a user may use as a bug summary. Optionally the search can be
+narrowed down to specific products.
+
+=item B<Params>
+
+=over
+
+=item C<summary> (string) B<Required> - A string of keywords defining
+the type of bug you are trying to report.
+
+=item C<products> (array) - One or more product names to narrow the
+duplicate search to. If omitted, all bugs are searched.
+
+=back
+
+=item B<Returns>
+
+The same as L</get>.
+
+Note that you will only be returned information about bugs that you
+can see. Bugs that you can't see will be entirely excluded from the
+results. So, if you want to see private bugs, you will have to first
+log in and I<then> call this method.
+
+=item B<Errors>
+
+=over
+
+=item 50 (Param Required)
+
+You must specify a value for C<summary> containing a string of keywords to
+search for duplicates.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<4.0>.
+
+=item The C<product> parameter has been renamed to C<products> in
+Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 search
+
+=over
+
+=item B<Description>
+
+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
+returned if they match I<exactly> the criteria you specify in these
+parameters. That is, we don't match against substrings--if a bug is in
+the "Widgets" product and you ask for bugs in the "Widg" product, you
+won't get anything.
+
+Criteria are joined in a logical AND. That is, you will be returned
+bugs that match I<all> of the criteria, not bugs that match I<any> of
+the criteria.
+
+Each parameter can be either the type it says, or an array of the types
+it says. If you pass an array, it means "Give me bugs with I<any> of
+these values." For example, if you wanted bugs that were in either
+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
+most-common database to use with Bugzilla, and MySQL is not case sensitive).
+
+In addition to the fields listed below, you may also use criteria that
+is similar to what is used in the Advanced Search screen of the Bugzilla
+UI. This includes fields specified by C<Search by Change History> and
+C<Custom Search>. The easiest way to determine what the field names are and what
+format Bugzilla expects, is to first construct your query using the
+Advanced Search UI, execute it and use the query parameters in they URL
+as your key/value pairs for the WebService call. With REST, you can
+just reuse the query parameter portion in the REST call itself.
+
+=over
+
+=item C<alias>
+
+C<array> of C<string>s The unique aliases of this bug. An empty array will be
+returned if this bug has no aliases.
+
+=item C<assigned_to>
+
+C<string> The login name of a user that a bug is assigned to.
+
+=item C<component>
+
+C<string> The name of the Component that the bug is in. Note that
+if there are multiple Components with the same name, and you search
+for that name, bugs in I<all> those Components will be returned. If you
+don't want this, be sure to also specify the C<product> argument.
+
+=item C<creation_time>
+
+C<dateTime> Searches for bugs that were created at this time or later.
+May not be an array.
+
+=item C<creator>
+
+C<string> The login name of the user who created the bug.
+
+You can also pass this argument with the name C<reporter>, for
+backwards compatibility with older Bugzillas.
+
+=item C<id>
+
+C<int> The numeric id of the bug.
+
+=item C<last_change_time>
+
+C<dateTime> Searches for bugs that were modified at this time or later.
+May not be an array.
+
+=item C<limit>
+
+C<int> Limit the number of results returned to C<int> records. If the limit
+is more than zero and higher than the maximum limit set by the administrator,
+then the maximum limit will be used instead. If you set the limit equal to zero,
+then all matching results will be returned instead.
+
+=item C<offset>
+
+C<int> Used in conjunction with the C<limit> argument, C<offset> defines
+the starting position for the search. For example, given a search that
+would return 100 bugs, setting C<limit> to 10 and C<offset> to 10 would return
+bugs 11 through 20 from the set of 100.
+
+=item C<op_sys>
+
+C<string> The "Operating System" field of a bug.
+
+=item C<platform>
+
+C<string> The Platform (sometimes called "Hardware") field of a bug.
+
+=item C<priority>
+
+C<string> The Priority field on a bug.
+
+=item C<product>
+
+C<string> The name of the Product that the bug is in.
+
+=item C<resolution>
+
+C<string> The current resolution--only set if a bug is closed. You can
+find open bugs by searching for bugs with an empty resolution.
+
+=item C<severity>
+
+C<string> The Severity field on a bug.
+
+=item C<status>
+
+C<string> The current status of a bug (not including its resolution,
+if it has one, which is a separate field above).
+
+=item C<summary>
+
+C<string> Searches for substrings in the single-line Summary field on
+bugs. If you specify an array, then bugs whose summaries match I<any> of the
+passed substrings will be returned.
+
+Note that unlike searching in the Bugzilla UI, substrings are not split
+on spaces. So searching for C<foo bar> will match "This is a foo bar"
+but not "This foo is a bar". C<['foo', 'bar']>, would, however, match
+the second item.
+
+=item C<tags>
+
+C<string> Searches for a bug with the specified tag. If you specify an
+array, then any bugs that match I<any> of the tags will be returned.
+
+Note that tags are personal to the currently logged in user.
+
+=item C<target_milestone>
+
+C<string> The Target Milestone field of a bug. Note that even if this
+Bugzilla does not have the Target Milestone field enabled, you can
+still search for bugs by Target Milestone. However, it is likely that
+in that case, most bugs will not have a Target Milestone set (it
+defaults to "---" when the field isn't enabled).
+
+=item C<qa_contact>
+
+C<string> The login name of the bug's QA Contact. Note that even if
+this Bugzilla does not have the QA Contact field enabled, you can
+still search for bugs by QA Contact (though it is likely that no bug
+will have a QA Contact set, if the field is disabled).
+
+=item C<url>
+
+C<string> The "URL" field of a bug.
+
+=item C<version>
+
+C<string> The Version field of a bug.
+
+=item C<whiteboard>
+
+C<string> Search the "Status Whiteboard" field on bugs for a substring.
+Works the same as the C<summary> field described above, but searches the
+Status Whiteboard field.
+
+=item C<quicksearch>
+
+C<string> Search for bugs using quicksearch syntax.
+
+=back
+
+=item B<Returns>
+
+The same as L</get>.
+
+Note that you will only be returned information about bugs that you
+can see. Bugs that you can't see will be entirely excluded from the
+results. So, if you want to see private bugs, you will have to first
+log in and I<then> call this method.
+
+=item B<Errors>
+
+If you specify an invalid value for a particular field, you just won't
+get any results for that value.
+
+=over
+
+=item 1000 (Parameters Required)
+
+You may not search without any search terms.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.4>.
+
+=item Searching by C<votes> was removed in Bugzilla B<4.0>.
+
+=item The C<reporter> input parameter was renamed to C<creator>
+in Bugzilla B<4.0>.
+
+=item In B<4.2.6> and newer, added the ability to return all results if
+C<limit> is set equal to zero. Otherwise maximum results returned are limited
+by system configuration.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=item Updated to allow for full search capability similar to the Bugzilla UI
+in Bugzilla B<5.0>.
+
+=item Updated to allow quicksearch capability in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 Bug Creation and Modification
+
+=head2 create
+
+=over
+
+=item B<Description>
+
+This allows you to create a new bug in Bugzilla. If you specify any
+invalid fields, an error will be thrown stating which field is invalid.
+If you specify any fields you are not allowed to set, they will just be
+set to their defaults or ignored.
+
+You cannot currently set all the items here that you can set on enter_bug.cgi.
+
+=item B<REST>
+
+To create a new bug in Bugzilla:
+
+POST /rest/bug
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+Some params must be set, or an error will be thrown. These params are
+marked B<Required>.
+
+Some parameters can have defaults set in Bugzilla, by the administrator.
+If these parameters have defaults set, you can omit them. These parameters
+are marked B<Defaulted>.
+
+Clients that want to be able to interact uniformly with multiple
+Bugzillas should always set both the params marked B<Required> and those
+marked B<Defaulted>, because some Bugzillas may not have defaults set
+for B<Defaulted> parameters, and then this method will throw an error
+if you don't specify them.
+
+The descriptions of the parameters below are what they mean when Bugzilla is
+being used to track software bugs. They may have other meanings in some
+installations.
+
+=over
+
+=item C<product> (string) B<Required> - The name of the product the bug
+is being filed against.
+
+=item C<component> (string) B<Required> - The name of a component in the
+product above.
+
+=item C<summary> (string) B<Required> - A brief description of the bug being
+filed.
+
+=item C<version> (string) B<Required> - A version of the product above;
+the version the bug was found in.
+
+=item C<description> (string) B<Defaulted> - The initial description for
+this bug. Some Bugzilla installations require this to not be blank.
+
+=item C<op_sys> (string) B<Defaulted> - The operating system the bug was
+discovered on.
+
+=item C<platform> (string) B<Defaulted> - What type of hardware the bug was
+experienced on.
+
+=item C<priority> (string) B<Defaulted> - What order the bug will be fixed
+in by the developer, compared to the developer's other bugs.
+
+=item C<severity> (string) B<Defaulted> - How severe the bug is.
+
+=item C<alias> (array) - A brief alias for the bug that can be used
+instead of a bug number when accessing this bug. Must be unique in
+all of this Bugzilla.
+
+=item C<assigned_to> (username) - A user to assign this bug to, if you
+don't want it to be assigned to the component owner.
+
+=item C<cc> (array) - An array of usernames to CC on this bug.
+
+=item C<comment_is_private> (boolean) - If set to true, the description
+is private, otherwise it is assumed to be public.
+
+=item C<is_markdown> (boolean) - If set to true, the description
+has Markdown structures, otherwise it is a normal text.
+
+=item C<groups> (array) - An array of group names to put this
+bug into. You can see valid group names on the Permissions
+tab of the Preferences screen, or, if you are an administrator,
+in the Groups control panel.
+If you don't specify this argument, then the bug will be added into
+all the groups that are set as being "Default" for this product. (If
+you want to avoid that, you should specify C<groups> as an empty array.)
+
+=item C<qa_contact> (username) - If this installation has QA Contacts
+enabled, you can set the QA Contact here if you don't want to use
+the component's default QA Contact.
+
+=item C<status> (string) - The status that this bug should start out as.
+Note that only certain statuses can be set on bug creation.
+
+=item C<resolution> (string) - If you are filing a closed bug, then
+you will have to specify a resolution. You cannot currently specify
+a resolution of C<DUPLICATE> for new bugs, though. That must be done
+with L</update>.
+
+=item C<target_milestone> (string) - A valid target milestone for this
+product.
+
+=item C<flags>
+
+C<array> An array of hashes with flags to add to the bug. To create a flag,
+at least the status and the type_id or name must be provided. An optional
+requestee can be passed if the flag type is requestable to a specific user.
+
+=over
+
+=item C<name>
+
+C<string> The name of the flag type.
+
+=item C<type_id>
+
+C<int> The internal flag type id.
+
+=item C<status>
+
+C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag).
+
+=item C<requestee>
+
+C<string> The login of the requestee if the flag type is requestable to a specific user.
+
+=back
+
+=back
+
+In addition to the above parameters, if your installation has any custom
+fields, you can set them just by passing in the name of the field and
+its value as a string.
+
+=item B<Returns>
+
+A hash with one element, C<id>. This is the id of the newly-filed bug.
+
+=item B<Errors>
+
+=over
+
+=item 51 (Invalid Object)
+
+You specified a field value that is invalid. The error message will have
+more details.
+
+=item 103 (Invalid Alias)
+
+The alias you specified is invalid for some reason. See the error message
+for more details.
+
+=item 104 (Invalid Field)
+
+One of the drop-down fields has an invalid value, or a value entered in a
+text field is too long. The error message will have more detail.
+
+=item 105 (Invalid Component)
+
+You didn't specify a component.
+
+=item 106 (Invalid Product)
+
+Either you didn't specify a product, this product doesn't exist, or
+you don't have permission to enter bugs in this product.
+
+=item 107 (Invalid Summary)
+
+You didn't specify a summary for the bug.
+
+=item 116 (Dependency Loop)
+
+You specified values in the C<blocks> or C<depends_on> fields
+that would cause a circular dependency between bugs.
+
+=item 120 (Group Restriction Denied)
+
+You tried to restrict the bug to a group which does not exist, or which
+you cannot use with this product.
+
+=item 129 (Flag Status Invalid)
+
+The flag status is invalid.
+
+=item 130 (Flag Modification Denied)
+
+You tried to request, grant, or deny a flag but only a user with the required
+permissions may make the change.
+
+=item 131 (Flag not Requestable from Specific Person)
+
+You can't ask a specific person for the flag.
+
+=item 133 (Flag Type not Unique)
+
+The flag type specified matches several flag types. You must specify
+the type id value to update or add a flag.
+
+=item 134 (Inactive Flag Type)
+
+The flag type is inactive and cannot be used to create new flags.
+
+=item 504 (Invalid User)
+
+Either the QA Contact, Assignee, or CC lists have some invalid user
+in them. The error message will have more details.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Before B<3.0.4>, parameters marked as B<Defaulted> were actually
+B<Required>, due to a bug in Bugzilla.
+
+=item The C<groups> argument was added in Bugzilla B<4.0>. Before
+Bugzilla 4.0, bugs were only added into Mandatory groups by this
+method. Since Bugzilla B<4.0.2>, passing an illegal group name will
+throw an error. In Bugzilla 4.0 and 4.0.1, illegal group names were
+silently ignored.
+
+=item The C<comment_is_private> argument was added in Bugzilla B<4.0>.
+Before Bugzilla 4.0, you had to use the undocumented C<commentprivacy>
+argument.
+
+=item Error 116 was added in Bugzilla B<4.0>. Before that, dependency
+loop errors had a generic code of C<32000>.
+
+=item The ability to file new bugs with a C<resolution> was added in
+Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=item C<is_markdown> option added in Bugzilla B<6.0>.
+
+=back
+
+=back
+
+
+=head2 add_attachment
+
+=over
+
+=item B<Description>
+
+This allows you to add an attachment to a bug in Bugzilla.
+
+=item B<REST>
+
+To create attachment on a current bug:
+
+POST /rest/bug/<bug_id>/attachment
+
+The params to include in the POST body, as well as the returned
+data format are the same as below. The C<ids> param will be
+overridden as it it pulled from the URL path.
+
+=item B<Params>
+
+=over
+
+=item C<ids>
+
+B<Required> C<array> An array of ints and/or strings--the ids
+or aliases of bugs that you want to add this attachment to.
+The same attachment and comment will be added to all
+these bugs.
+
+=item C<data>
+
+B<Required> C<base64> or C<string> The content of the attachment.
+If the content of the attachment is not ASCII text, you must encode
+it in base64 and declare it as the C<base64> type.
+
+=item C<file_name>
+
+B<Required> C<string> The "file name" that will be displayed
+in the UI for this attachment.
+
+=item C<summary>
+
+B<Required> C<string> A short string describing the
+attachment.
+
+=item C<content_type>
+
+B<Required> C<string> The MIME type of the attachment, like
+C<text/plain> or C<image/png>.
+
+=item C<comment>
+
+C<string> or hash. A comment to add along with this attachment. If C<comment>
+is a hash, it has the following keys:
+
+=over
+
+=item C<body>
+
+C<string> The body of the comment.
+
+=item C<is_markdown>
+
+C<boolean> If set to true, the comment has Markdown structures; otherwise, it
+is an ordinary text.
+
+=back
+
+=item C<is_patch>
+
+C<boolean> True if Bugzilla should treat this attachment as a patch.
+If you specify this, you do not need to specify a C<content_type>.
+The C<content_type> of the attachment will be forced to C<text/plain>.
+
+Defaults to False if not specified.
+
+=item C<is_private>
+
+C<boolean> True if the attachment should be private (restricted
+to the "insidergroup"), False if the attachment should be public.
+
+Defaults to False if not specified.
+
+=item C<flags>
+
+C<array> An array of hashes with flags to add to the attachment. to create a flag,
+at least the status and the type_id or name must be provided. An optional requestee
+can be passed if the flag type is requestable to a specific user.
+
+=over
+
+=item C<name>
+
+C<string> The name of the flag type.
+
+=item C<type_id>
+
+C<int> The internal flag type id.
+
+=item C<status>
+
+C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag).
+
+=item C<requestee>
+
+C<string> The login of the requestee if the flag type is requestable to a specific user.
+
+=back
+
+=item C<minor_update>
+
+C<boolean> If set to true, this is considered a minor update and no mail is sent
+to users who do not want minor update emails. If current user is not in the
+minor_update_group, this parameter is simply ignored.
+
+=back
+
+=item B<Returns>
+
+A single item C<ids>, which contains an array of the
+attachment id(s) created.
+
+=item B<Errors>
+
+This method can throw all the same errors as L</get>, plus:
+
+=over
+
+=item 129 (Flag Status Invalid)
+
+The flag status is invalid.
+
+=item 130 (Flag Modification Denied)
+
+You tried to request, grant, or deny a flag but only a user with the required
+permissions may make the change.
+
+=item 131 (Flag not Requestable from Specific Person)
+
+You can't ask a specific person for the flag.
+
+=item 133 (Flag Type not Unique)
+
+The flag type specified matches several flag types. You must specify
+the type id value to update or add a flag.
+
+=item 134 (Inactive Flag Type)
+
+The flag type is inactive and cannot be used to create new flags.
+
+=item 140 (Markdown Disabled)
+
+You tried to set the C<is_markdown> flag of the comment to true but the Markdown feature is not enabled.
+
+=item 600 (Attachment Too Large)
+
+You tried to attach a file that was larger than Bugzilla will accept.
+
+=item 601 (Invalid MIME Type)
+
+You specified a C<content_type> argument that was blank, not a valid
+MIME type, or not a MIME type that Bugzilla accepts for attachments.
+
+=item 603 (File Name Not Specified)
+
+You did not specify a valid for the C<file_name> argument.
+
+=item 604 (Summary Required)
+
+You did not specify a value for the C<summary> argument.
+
+=item 606 (Empty Data)
+
+You set the "data" field to an empty string.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<4.0>.
+
+=item The C<is_url> parameter was removed in Bugzilla B<4.2>.
+
+=item The return value has changed in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=item C<is_markdown> added in Bugzilla B<6.0>.
+
+=back
+
+=back
+
+
+=head2 update_attachment
+
+=over
+
+=item B<Description>
+
+This allows you to update attachment metadata in Bugzilla.
+
+=item B<REST>
+
+To update attachment metadata on a current attachment:
+
+PUT /rest/bug/attachment/<attach_id>
+
+The params to include in the POST body, as well as the returned
+data format are the same as below. The C<ids> param will be
+overridden as it it pulled from the URL path.
+
+=item B<Params>
+
+=over
+
+=item C<ids>
+
+B<Required> C<array> An array of integers -- the ids of the attachments you
+want to update.
+
+=item C<file_name>
+
+C<string> The "file name" that will be displayed
+in the UI for this attachment.
+
+=item C<summary>
+
+C<string> A short string describing the
+attachment.
+
+=item C<comment>
+
+C<string> or hash: An optional comment to add to the attachment's bug. If C<comment> is
+a hash, it has the following keys:
+
+=over
+
+=item C<body>
+
+C<string> The body of the comment to be added.
+
+=item C<is_markdown>
+
+C<boolean> If set to true, the comment has Markdown structures; otherwise it is a normal
+text.
+
+=back
+
+=item C<content_type>
+
+C<string> The MIME type of the attachment, like
+C<text/plain> or C<image/png>.
+
+=item C<is_patch>
+
+C<boolean> True if Bugzilla should treat this attachment as a patch.
+If you specify this, you do not need to specify a C<content_type>.
+The C<content_type> of the attachment will be forced to C<text/plain>.
+
+=item C<is_private>
+
+C<boolean> True if the attachment should be private (restricted
+to the "insidergroup"), False if the attachment should be public.
+
+=item C<is_obsolete>
+
+C<boolean> True if the attachment is obsolete, False otherwise.
+
+=item C<flags>
+
+C<array> An array of hashes with changes to the flags. The following values
+can be specified. At least the status and one of type_id, id, or name must
+be specified. If a type_id or name matches a single currently set flag,
+the flag will be updated unless new is specified.
+
+=over
+
+=item C<name>
+
+C<string> The name of the flag that will be created or updated.
+
+=item C<type_id>
+
+C<int> The internal flag type id that will be created or updated. You will
+need to specify the C<type_id> if more than one flag type of the same name exists.
+
+=item C<status>
+
+C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag).
+
+=item C<requestee>
+
+C<string> The login of the requestee if the flag type is requestable to a specific user.
+
+=item C<id>
+
+C<int> Use id to specify the flag to be updated. You will need to specify the C<id>
+if more than one flag is set of the same name.
+
+=item C<new>
+
+C<boolean> Set to true if you specifically want a new flag to be created.
+
+=back
+
+=item C<minor_update>
+
+C<boolean> If set to true, this is considered a minor update and no mail is sent
+to users who do not want minor update emails. If current user is not in the
+minor_update_group, this parameter is simply ignored.
+
+=back
+
+=item B<Returns>
+
+A C<hash> with a single field, "attachments". This points to an array of hashes
+with the following fields:
+
+=over
+
+=item C<id>
+
+C<int> The id of the attachment that was updated.
+
+=item C<last_change_time>
+
+C<dateTime> The exact time that this update was done at, for this attachment.
+If no update was done (that is, no fields had their values changed and
+no comment was added) then this will instead be the last time the attachment
+was updated.
+
+=item C<changes>
+
+C<hash> The changes that were actually done on this bug. The keys are
+the names of the fields that were changed, and the values are a hash
+with two keys:
+
+=over
+
+=item C<added> (C<string>) The values that were added to this field.
+possibly a comma-and-space-separated list if multiple values were added.
+
+=item C<removed> (C<string>) The values that were removed from this
+field.
+
+=back
+
+=back
+
+Here's an example of what a return value might look like:
+
+ {
+ attachments => [
+ {
+ id => 123,
+ last_change_time => '2010-01-01T12:34:56',
+ changes => {
+ summary => {
+ removed => 'Sample ptach',
+ added => 'Sample patch'
+ },
+ is_obsolete => {
+ removed => '0',
+ added => '1',
+ }
+ },
+ }
+ ]
+ }
+
+=item B<Errors>
+
+This method can throw all the same errors as L</get>, plus:
+
+=over
+
+=item 129 (Flag Status Invalid)
+
+The flag status is invalid.
+
+=item 130 (Flag Modification Denied)
+
+You tried to request, grant, or deny a flag but only a user with the required
+permissions may make the change.
+
+=item 131 (Flag not Requestable from Specific Person)
+
+You can't ask a specific person for the flag.
+
+=item 132 (Flag not Unique)
+
+The flag specified has been set multiple times. You must specify the id
+value to update the flag.
+
+=item 133 (Flag Type not Unique)
+
+The flag type specified matches several flag types. You must specify
+the type id value to update or add a flag.
+
+=item 134 (Inactive Flag Type)
+
+The flag type is inactive and cannot be used to create new flags.
+
+=item 140 (Markdown Disabled)
+
+You tried to set the C<is_markdown> flag of the C<comment> to true but Markdown feature is
+not enabled.
+
+=item 601 (Invalid MIME Type)
+
+You specified a C<content_type> argument that was blank, not a valid
+MIME type, or not a MIME type that Bugzilla accepts for attachments.
+
+=item 603 (File Name Not Specified)
+
+You did not specify a valid for the C<file_name> argument.
+
+=item 604 (Summary Required)
+
+You did not specify a value for the C<summary> argument.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 add_comment
+
+=over
+
+=item B<Description>
+
+This allows you to add a comment to a bug in Bugzilla.
+
+=item B<REST>
+
+To create a comment on a current bug:
+
+POST /rest/bug/<bug_id>/comment
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<id> (int or string) B<Required> - The id or alias of the bug to append a
+comment to.
+
+=item C<comment> (string) B<Required> - The comment to append to the bug.
+If this is empty or all whitespace, an error will be thrown saying that
+you did not set the C<comment> parameter.
+
+=item C<is_private> (boolean) - If set to true, the comment is private,
+otherwise it is assumed to be public.
+
+=item C<is_markdown> (boolean) - If set to true, the comment has Markdown
+structures, otherwise it is a normal text.
+
+=item C<work_time> (double) - Adds this many hours to the "Hours Worked"
+on the bug. If you are not in the time tracking group, this value will
+be ignored.
+
+=item C<minor_update> (boolean) - If set to true, this is considered a minor update
+and no mail is sent to users who do not want minor update emails. If current user
+is not in the minor_update_group, this parameter is simply ignored.
+
+=back
+
+=item B<Returns>
+
+A hash with one element, C<id> whose value is the id of the newly-created comment.
+
+=item B<Errors>
+
+=over
+
+=item 54 (Hours Worked Too Large)
+
+You specified a C<work_time> larger than the maximum allowed value of
+C<99999.99>.
+
+=item 100 (Invalid Bug Alias)
+
+If you specified an alias and there is no bug with that alias.
+
+=item 101 (Invalid Bug ID)
+
+The id you specified doesn't exist in the database.
+
+=item 109 (Bug Edit Denied)
+
+You did not have the necessary rights to edit the bug.
+
+=item 113 (Can't Make Private Comments)
+
+You tried to add a private comment, but don't have the necessary rights.
+
+=item 114 (Comment Too Long)
+
+You tried to add a comment longer than the maximum allowed length
+(65,535 characters).
+
+=item 140 (Markdown Disabled)
+
+You tried to set the C<is_markdown> flag to true but the Markdown feature
+is not enabled.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.2>.
+
+=item Modified to return the new comment's id in Bugzilla B<3.4>
+
+=item Modified to throw an error if you try to add a private comment
+but can't, in Bugzilla B<3.4>.
+
+=item Before Bugzilla B<3.6>, the C<is_private> argument was called
+C<private>, and you can still call it C<private> for backwards-compatibility
+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>.
+
+=item C<is_markdown> option added in Bugzilla B<6.0>.
+
+=back
+
+=back
+
+
+=head2 update
+
+=over
+
+=item B<Description>
+
+Allows you to update the fields of a bug. Automatically sends emails
+out about the changes.
+
+=item B<REST>
+
+To update the fields of a current bug:
+
+PUT /rest/bug/<bug_id>
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below. The C<ids> param will be overridden as it is
+pulled from the URL path.
+
+=item B<Params>
+
+=over
+
+=item C<ids>
+
+Array of C<int>s or C<string>s. The ids or aliases of the bugs that
+you want to modify.
+
+=item C<minor_update>
+
+C<boolean> If set to true, this is considered a minor update and no mail is sent
+to users who do not want minor update emails. If current user is not in the
+minor_update_group, this parameter is simply ignored.
+
+=back
+
+B<Note>: All following fields specify the values you want to set on the
+bugs you are updating.
+
+=over
+
+=item C<alias>
+
+C<hash> These specify the aliases of a bug that can be used instead of a bug
+number when acessing this bug. To set these, you should pass a hash as the
+value. The hash may contain the following fields:
+
+=over
+
+=item C<add> An array of C<string>s. Aliases to add to this field.
+
+=item C<remove> An array of C<string>s. Aliases to remove from this field.
+If the aliases are not already in the field, they will be ignored.
+
+=item C<set> An array of C<string>s. An exact set of aliases to set this
+field to, overriding the current value. If you specify C<set>, then C<add>
+and C<remove> will be ignored.
+
+=back
+
+You can only set this if you are modifying a single bug. If there is more
+than one bug specified in C<ids>, passing in a value for C<alias> will cause
+an error to be thrown.
+
+For backwards compatibility, you can also specify a single string. This will
+be treated as if you specified the set key above.
+
+=item C<assigned_to>
+
+C<string> The full login name of the user this bug is assigned to.
+
+=item C<blocks>
+
+=item C<depends_on>
+
+C<hash> These specify the bugs that this bug blocks or depends on,
+respectively. To set these, you should pass a hash as the value. The hash
+may contain the following fields:
+
+=over
+
+=item C<add> An array of C<int>s. Bug ids to add to this field.
+
+=item C<remove> An array of C<int>s. Bug ids to remove from this field.
+If the bug ids are not already in the field, they will be ignored.
+
+=item C<set> An array of C<int>s. An exact set of bug ids to set this
+field to, overriding the current value. If you specify C<set>, then C<add>
+and C<remove> will be ignored.
+
+=back
+
+=item C<cc>
+
+C<hash> The users on the cc list. To modify this field, pass a hash, which
+may have the following fields:
+
+=over
+
+=item C<add> Array of C<string>s. User names to add to the CC list.
+They must be full user names, and an error will be thrown if you pass
+in an invalid user name.
+
+=item C<remove> Array of C<string>s. User names to remove from the CC
+list. They must be full user names, and an error will be thrown if you
+pass in an invalid user name.
+
+=back
+
+=item C<is_cc_accessible>
+
+C<boolean> Whether or not users in the CC list are allowed to access
+the bug, even if they aren't in a group that can normally access the bug.
+
+=item C<comment>
+
+C<hash>. A comment on the change. The hash may contain the following fields:
+
+=over
+
+=item C<body> C<string> The actual text of the comment.
+B<Note>: For compatibility with the parameters to L</add_comment>,
+you can also call this field C<comment>, if you want.
+
+=item C<is_private> C<boolean> Whether the comment is private or not.
+If you try to make a comment private and you don't have the permission
+to, an error will be thrown.
+
+=back
+
+=item C<comment_is_private>
+
+C<hash> This is how you update the privacy of comments that are already
+on a bug. This is a hash, where the keys are the C<int> id of comments (not
+their count on a bug, like #1, #2, #3, but their globally-unique id,
+as returned by L</comments>) and the value is a C<boolean> which specifies
+whether that comment should become private (C<true>) or public (C<false>).
+
+The comment ids must be valid for the bug being updated. Thus, it is not
+practical to use this while updating multiple bugs at once, as a single
+comment id will never be valid on multiple bugs.
+
+=item C<component>
+
+C<string> The Component the bug is in.
+
+=item C<deadline>
+
+C<string> The Deadline field--a date specifying when the bug must
+be completed by, in the format C<YYYY-MM-DD>.
+
+=item C<dupe_of>
+
+C<int> The bug that this bug is a duplicate of. If you want to mark
+a bug as a duplicate, the safest thing to do is to set this value
+and I<not> set the C<status> or C<resolution> fields. They will
+automatically be set by Bugzilla to the appropriate values for
+duplicate bugs.
+
+=item C<estimated_time>
+
+C<double> The total estimate of time required to fix the bug, in hours.
+This is the I<total> estimate, not the amount of time remaining to fix it.
+
+=item C<flags>
+
+C<array> An array of hashes with changes to the flags. The following values
+can be specified. At least the status and one of type_id, id, or name must
+be specified. If a type_id or name matches a single currently set flag,
+the flag will be updated unless new is specified.
+
+=over
+
+=item C<name>
+
+C<string> The name of the flag that will be created or updated.
+
+=item C<type_id>
+
+C<int> The internal flag type id that will be created or updated. You will
+need to specify the C<type_id> if more than one flag type of the same name exists.
+
+=item C<status>
+
+C<string> The flags new status (i.e. "?", "+", "-" or "X" to clear a flag).
+
+=item C<requestee>
+
+C<string> The login of the requestee if the flag type is requestable to a specific user.
+
+=item C<id>
+
+C<int> Use id to specify the flag to be updated. You will need to specify the C<id>
+if more than one flag is set of the same name.
+
+=item C<new>
+
+C<boolean> Set to true if you specifically want a new flag to be created.
+
+=back
+
+=item C<groups>
+
+C<hash> The groups a bug is in. To modify this field, pass a hash, which
+may have the following fields:
+
+=over
+
+=item C<add> Array of C<string>s. The names of groups to add. Passing
+in an invalid group name or a group that you cannot add to this bug will
+cause an error to be thrown.
+
+=item C<remove> Array of C<string>s. The names of groups to remove. Passing
+in an invalid group name or a group that you cannot remove from this bug
+will cause an error to be thrown.
+
+=back
+
+=item C<keywords>
+
+C<hash> Keywords on the bug. To modify this field, pass a hash, which
+may have the following fields:
+
+=over
+
+=item C<add> An array of C<strings>s. The names of keywords to add to
+the field on the bug. Passing something that isn't a valid keyword name
+will cause an error to be thrown.
+
+=item C<remove> An array of C<string>s. The names of keywords to remove
+from the field on the bug. Passing something that isn't a valid keyword
+name will cause an error to be thrown.
+
+=item C<set> An array of C<strings>s. An exact set of keywords to set the
+field to, on the bug. Passing something that isn't a valid keyword name
+will cause an error to be thrown. Specifying C<set> overrides C<add> and
+C<remove>.
+
+=back
+
+=item C<op_sys>
+
+C<string> The Operating System ("OS") field on the bug.
+
+=item C<platform>
+
+C<string> The Platform or "Hardware" field on the bug.
+
+=item C<priority>
+
+C<string> The Priority field on the bug.
+
+=item C<product>
+
+C<string> The name of the product that the bug is in. If you change
+this, you will probably also want to change C<target_milestone>,
+C<version>, and C<component>, since those have different legal
+values in every product.
+
+If you cannot change the C<target_milestone> field, it will be reset to
+the default for the product, when you move a bug to a new product.
+
+You may also wish to add or remove groups, as which groups are
+valid on a bug depends on the product. Groups that are not valid
+in the new product will be automatically removed, and groups which
+are mandatory in the new product will be automaticaly added, but no
+other automatic group changes will be done.
+
+Note that users can only move a bug into a product if they would
+normally have permission to file new bugs in that product.
+
+=item C<qa_contact>
+
+C<string> The full login name of the bug's QA Contact.
+
+=item C<is_creator_accessible>
+
+C<boolean> Whether or not the bug's reporter is allowed to access
+the bug, even if they aren't in a group that can normally access
+the bug.
+
+=item C<remaining_time>
+
+C<double> How much work time is remaining to fix the bug, in hours.
+If you set C<work_time> but don't explicitly set C<remaining_time>,
+then the C<work_time> will be deducted from the bug's C<remaining_time>.
+
+=item C<reset_assigned_to>
+
+C<boolean> If true, the C<assigned_to> field will be reset to the
+default for the component that the bug is in. (If you have set the
+component at the same time as using this, then the component used
+will be the new component, not the old one.)
+
+=item C<reset_qa_contact>
+
+C<boolean> If true, the C<qa_contact> field will be reset to the
+default for the component that the bug is in. (If you have set the
+component at the same time as using this, then the component used
+will be the new component, not the old one.)
+
+=item C<resolution>
+
+C<string> The current resolution. May only be set if you are closing
+a bug or if you are modifying an already-closed bug. Attempting to set
+the resolution to I<any> value (even an empty or null string) on an
+open bug will cause an error to be thrown.
+
+If you change the C<status> field to an open status, the resolution
+field will automatically be cleared, so you don't have to clear it
+manually.
+
+=item C<see_also>
+
+C<hash> The See Also field on a bug, specifying URLs to bugs in other
+bug trackers. To modify this field, pass a hash, which may have the
+following fields:
+
+=over
+
+=item C<add> An array of C<strings>s. URLs to add to the field.
+Each URL must be a valid URL to a bug-tracker, or an error will
+be thrown.
+
+=item C<remove> An array of C<string>s. URLs to remove from the field.
+Invalid URLs will be ignored.
+
+=back
+
+=item C<severity>
+
+C<string> The Severity field of a bug.
+
+=item C<status>
+
+C<string> The status you want to change the bug to. Note that if
+a bug is changing from open to closed, you should also specify
+a C<resolution>.
+
+=item C<summary>
+
+C<string> The Summary field of the bug.
+
+=item C<target_milestone>
+
+C<string> The bug's Target Milestone.
+
+=item C<url>
+
+C<string> The "URL" field of a bug.
+
+=item C<version>
+
+C<string> The bug's Version field.
+
+=item C<whiteboard>
+
+C<string> The Status Whiteboard field of a bug.
+
+=item C<work_time>
+
+C<double> The number of hours worked on this bug as part of this change.
+If you set C<work_time> but don't explicitly set C<remaining_time>,
+then the C<work_time> will be deducted from the bug's C<remaining_time>.
+
+=back
+
+You can also set the value of any custom field by passing its name as
+a parameter, and the value to set the field to. For multiple-selection
+fields, the value should be an array of strings.
+
+=item B<Returns>
+
+A C<hash> with a single field, "bugs". This points to an array of hashes
+with the following fields:
+
+=over
+
+=item C<id>
+
+C<int> The id of the bug that was updated.
+
+=item C<alias>
+
+C<array> of C<string>s The aliases of the bug that was updated, if this bug
+has any alias.
+
+=item C<last_change_time>
+
+C<dateTime> The exact time that this update was done at, for this bug.
+If no update was done (that is, no fields had their values changed and
+no comment was added) then this will instead be the last time the bug
+was updated.
+
+=item C<changes>
+
+C<hash> The changes that were actually done on this bug. The keys are
+the names of the fields that were changed, and the values are a hash
+with two keys:
+
+=over
+
+=item C<added> (C<string>) The values that were added to this field,
+possibly a comma-and-space-separated list if multiple values were added.
+
+=item C<removed> (C<string>) The values that were removed from this
+field, possibly a comma-and-space-separated list if multiple values were
+removed.
+
+=back
+
+=back
+
+Here's an example of what a return value might look like:
+
+ {
+ bugs => [
+ {
+ id => 123,
+ alias => [ 'foo' ],
+ last_change_time => '2010-01-01T12:34:56',
+ changes => {
+ status => {
+ removed => 'NEW',
+ added => 'ASSIGNED'
+ },
+ keywords => {
+ removed => 'bar',
+ added => 'qux, quo, qui',
+ }
+ },
+ }
+ ]
+ }
+
+Currently, some fields are not tracked in changes: C<comment>,
+C<comment_is_private>, and C<work_time>. This means that they will not
+show up in the return value even if they were successfully updated.
+This may change in a future version of Bugzilla.
+
+=item B<Errors>
+
+This function can throw all of the errors that L</get>, L</create>,
+and L</add_comment> can throw, plus:
+
+=over
+
+=item 50 (Empty Field)
+
+You tried to set some field to be empty, but that field cannot be empty.
+The error message will have more details.
+
+=item 52 (Input Not A Number)
+
+You tried to set a numeric field to a value that wasn't numeric.
+
+=item 54 (Number Too Large)
+
+You tried to set a numeric field to a value larger than that field can
+accept.
+
+=item 55 (Number Too Small)
+
+You tried to set a negative value in a numeric field that does not accept
+negative values.
+
+=item 56 (Bad Date/Time)
+
+You specified an invalid date or time in a date/time field (such as
+the C<deadline> field or a custom date/time field).
+
+=item 112 (See Also Invalid)
+
+You attempted to add an invalid value to the C<see_also> field.
+
+=item 115 (Permission Denied)
+
+You don't have permission to change a particular field to a particular value.
+The error message will have more detail.
+
+=item 116 (Dependency Loop)
+
+You specified a value in the C<blocks> or C<depends_on> fields that causes
+a dependency loop.
+
+=item 117 (Invalid Comment ID)
+
+You specified a comment id in C<comment_is_private> that isn't on this bug.
+
+=item 118 (Duplicate Loop)
+
+You specified a value for C<dupe_of> that causes an infinite loop of
+duplicates.
+
+=item 119 (dupe_of Required)
+
+You changed the resolution to C<DUPLICATE> but did not specify a value
+for the C<dupe_of> field.
+
+=item 120 (Group Add/Remove Denied)
+
+You tried to add or remove a group that you don't have permission to modify
+for this bug, or you tried to add a group that isn't valid in this product.
+
+=item 121 (Resolution Required)
+
+You tried to set the C<status> field to a closed status, but you didn't
+specify a resolution.
+
+=item 122 (Resolution On Open Status)
+
+This bug has an open status, but you specified a value for the C<resolution>
+field.
+
+=item 123 (Invalid Status Transition)
+
+You tried to change from one status to another, but the status workflow
+rules don't allow that change.
+
+=item 129 (Flag Status Invalid)
+
+The flag status is invalid.
+
+=item 130 (Flag Modification Denied)
+
+You tried to request, grant, or deny a flag but only a user with the required
+permissions may make the change.
+
+=item 131 (Flag not Requestable from Specific Person)
+
+You can't ask a specific person for the flag.
+
+=item 132 (Flag not Unique)
+
+The flag specified has been set multiple times. You must specify the id
+value to update the flag.
+
+=item 133 (Flag Type not Unique)
+
+The flag type specified matches several flag types. You must specify
+the type id value to update or add a flag.
+
+=item 134 (Inactive Flag Type)
+
+The flag type is inactive and cannot be used to create new flags.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<4.0>.
+
+=item REST API call added Bugzilla B<5.0>.
+
+=back
+
+=back
+
+
+=head2 update_see_also
+
+=over
+
+=item B<Description>
+
+Adds or removes URLs for the "See Also" field on bugs. These URLs must
+point to some valid bug in some Bugzilla installation or in Launchpad.
+
+=item B<Params>
+
+=over
+
+=item C<ids>
+
+Array of C<int>s or C<string>s. The ids or aliases of bugs that you want
+to modify.
+
+=item C<add>
+
+Array of C<string>s. URLs to Bugzilla bugs. These URLs will be added to
+the See Also field. They must be valid URLs to C<show_bug.cgi> in a
+Bugzilla installation or to a bug filed at launchpad.net.
+
+If the URLs don't start with C<http://> or C<https://>, it will be assumed
+that C<http://> should be added to the beginning of the string.
+
+It is safe to specify URLs that are already in the "See Also" field on
+a bug--they will just be silently ignored.
+
+=item C<remove>
+
+Array of C<string>s. These URLs will be removed from the See Also field.
+You must specify the full URL that you want removed. However, matching
+is done case-insensitively, so you don't have to specify the URL in
+exact case, if you don't want to.
+
+If you specify a URL that is not in the See Also field of a particular bug,
+it will just be silently ignored. Invaild URLs are currently silently ignored,
+though this may change in some future version of Bugzilla.
+
+=item C<minor_update>
+
+C<boolean> If set to true, this is considered a minor update and no mail is sent
+to users who do not want minor update emails. If current user is not in the
+minor_update_group, this parameter is simply ignored.
+
+=back
+
+NOTE: If you specify the same URL in both C<add> and C<remove>, it will
+be I<added>. (That is, C<add> overrides C<remove>.)
+
+=item B<Returns>
+
+C<changes>, a hash where the keys are numeric bug ids and the contents
+are a hash with one key, C<see_also>. C<see_also> points to a hash, which
+contains two keys, C<added> and C<removed>. These are arrays of strings,
+representing the actual changes that were made to the bug.
+
+Here's a diagram of what the return value looks like for updating
+bug ids 1 and 2:
+
+ {
+ changes => {
+ 1 => {
+ see_also => {
+ added => (an array of bug URLs),
+ removed => (an array of bug URLs),
+ }
+ },
+ 2 => {
+ see_also => {
+ added => (an array of bug URLs),
+ removed => (an array of bug URLs),
+ }
+ }
+ }
+ }
+
+This return value allows you to tell what this method actually did. It is in
+this format to be compatible with the return value of a future C<Bug.update>
+method.
+
+=item B<Errors>
+
+This method can throw all of the errors that L</get> throws, plus:
+
+=over
+
+=item 109 (Bug Edit Denied)
+
+You did not have the necessary rights to edit the bug.
+
+=item 112 (Invalid Bug URL)
+
+One of the URLs you provided did not look like a valid bug URL.
+
+=item 115 (See Also Edit Denied)
+
+You did not have the necessary rights to edit the See Also field for
+this bug.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.4>.
+
+=item Before Bugzilla B<3.6>, error 115 had a generic error code of 32000.
+
+=back
+
+=back
+
+
+=head2 update_tags
+
+=over
+
+=item B<Description>
+
+Adds or removes tags on bugs.
+
+=item B<Params>
+
+=over
+
+=item C<ids>
+
+B<Required> C<array> An array of ints and/or strings--the ids
+or aliases of bugs that you want to add or remove tags to. All the tags
+will be added or removed to all these bugs.
+
+=item C<tags>
+
+B<Required> C<hash> A hash representing tags to be added and/or removed.
+The hash has the following fields:
+
+=over
+
+=item C<add> An array of C<string>s representing tag names
+to be added to the bugs.
+
+It is safe to specify tags that are already associated with the
+bugs--they will just be silently ignored.
+
+=item C<remove> An array of C<string>s representing tag names
+to be removed from the bugs.
+
+It is safe to specify tags that are not associated with any
+bugs--they will just be silently ignored.
+
+=back
+
+=back
+
+=item B<Returns>
+
+C<changes>, a hash containing bug IDs as keys and one single value
+name "tags" which is also a hash, with C<added> and C<removed> as keys.
+See L</update_see_also> for an example of how it looks like.
+
+=item B<Errors>
+
+This method can throw the same errors as L</get>.
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<4.4>.
+
+=back
+
+=back
+
+=head2 search_comment_tags
+
+=over
+
+=item B<Description>
+
+Searches for tags which contain the provided substring.
+
+=item B<REST>
+
+To search for comment tags:
+
+GET /rest/bug/comment/tags/<query>
+
+=item B<Params>
+
+=over
+
+=item C<query>
+
+B<Required> C<string> Only tags containg this substring will be returned.
+
+=item C<limit>
+
+C<int> If provided will return no more than C<limit> tags. Defaults to C<10>.
+
+=back
+
+=item B<Returns>
+
+An C<array of strings> of matching tags.
+
+=item B<Errors>
+
+This method can throw all of the errors that L</get> throws, plus:
+
+=over
+
+=item 125 (Comment Tagging Disabled)
+
+Comment tagging support is not available or enabled.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 update_comment_tags
+
+=over
+
+=item B<Description>
+
+Adds or removes tags from a comment.
+
+=item B<REST>
+
+To update the tags comments attached to a comment:
+
+PUT /rest/bug/comment/tags
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<comment_id>
+
+B<Required> C<int> The ID of the comment to update.
+
+=item C<add>
+
+C<array of strings> The tags to attach to the comment.
+
+=item C<remove>
+
+C<array of strings> The tags to detach from the comment.
+
+=back
+
+=item B<Returns>
+
+An C<array of strings> containing the comment's updated tags.
+
+=item B<Errors>
+
+This method can throw all of the errors that L</get> throws, plus:
+
+=over
+
+=item 125 (Comment Tagging Disabled)
+
+Comment tagging support is not available or enabled.
+
+=item 126 (Invalid Comment Tag)
+
+The comment tag provided was not valid (eg. contains invalid characters).
+
+=item 127 (Comment Tag Too Short)
+
+The comment tag provided is shorter than the minimum length.
+
+=item 128 (Comment Tag Too Long)
+
+The comment tag provided is longer than the maximum length.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 render_comment
+
+=over
+
+=item B<Description>
+
+Returns the HTML rendering of the provided comment text.
+
+=item B<Params>
+
+=over
+
+=item C<text>
+
+B<Required> C<strings> Text comment text to render.
+
+=item C<id>
+
+C<int> The ID of the bug to render the comment against.
+
+=back
+
+=item B<Returns>
+
+C<html> containing the HTML rendering.
+
+=item B<Errors>
+
+This method can throw all of the errors that L</get> throws.
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 SEE ALSO
+
+=over
+
+=item L<Bugzilla::API::1_0::Resource>
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=item get_bugs
+
+=item get_history
+
+=back
diff --git a/Bugzilla/API/1_0/Resource/BugUserLastVisit.pm b/Bugzilla/API/1_0/Resource/BugUserLastVisit.pm
new file mode 100644
index 000000000..7568fc30f
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/BugUserLastVisit.pm
@@ -0,0 +1,239 @@
+# 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::API::1_0::Resource::BugUserLastVisit;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+use Bugzilla::Constants;
+
+use Moo;
+
+extends 'Bugzilla::API::1_0::Resource';
+
+##############
+# Constants #
+##############
+
+use constant READ_ONLY => qw(
+ get
+);
+
+use constant PUBLIC_METHODS => qw(
+ get
+ update
+);
+
+sub REST_RESOURCES {
+ return [
+ # bug-id
+ qr{^/bug_user_last_visit/(\d+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ return { ids => $_[0] };
+ },
+ },
+ POST => {
+ method => 'update',
+ params => sub {
+ return { ids => $_[0] };
+ },
+ },
+ },
+ ];
+}
+
+############
+# Methods #
+############
+
+sub update {
+ my ($self, $params) = validate(@_, 'ids');
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ $user->login(LOGIN_REQUIRED);
+
+ my $ids = $params->{ids} // [];
+ ThrowCodeError('param_required', { param => 'ids' }) unless @$ids;
+
+ # Cache permissions for bugs. This highly reduces the number of calls to the
+ # DB. visible_bugs() is only able to handle bug IDs, so we have to skip
+ # aliases.
+ $user->visible_bugs([grep /^[0-9]$/, @$ids]);
+
+ $dbh->bz_start_transaction();
+ my @results;
+ my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
+ foreach my $bug_id (@$ids) {
+ my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 });
+
+ ThrowUserError('user_not_involved', { bug_id => $bug->id })
+ unless $user->is_involved_in_bug($bug);
+
+ $bug->update_user_last_visit($user, $last_visit_ts);
+
+ push(
+ @results,
+ $self->_bug_user_last_visit_to_hash(
+ $bug, $last_visit_ts, $params
+ ));
+ }
+ $dbh->bz_commit_transaction();
+
+ return \@results;
+}
+
+sub get {
+ my ($self, $params) = validate(@_, 'ids');
+ my $user = Bugzilla->user;
+ my $ids = $params->{ids};
+
+ $user->login(LOGIN_REQUIRED);
+
+ if ($ids) {
+ # Cache permissions for bugs. This highly reduces the number of calls to
+ # the DB. visible_bugs() is only able to handle bug IDs, so we have to
+ # skip aliases.
+ $user->visible_bugs([grep /^[0-9]$/, @$ids]);
+ }
+
+ my @last_visits = @{ $user->last_visited };
+
+ if ($ids) {
+ # remove bugs that we are not interested in if ids is passed in.
+ my %id_set = map { ($_ => 1) } @$ids;
+ @last_visits = grep { $id_set{ $_->bug_id } } @last_visits;
+ }
+
+ return [
+ map {
+ $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts,
+ $params)
+ } @last_visits
+ ];
+}
+
+sub _bug_user_last_visit_to_hash {
+ my ($self, $bug_id, $last_visit_ts, $params) = @_;
+
+ my %result = (id => as_int($bug_id),
+ last_visit_ts => as_datetime($last_visit_ts));
+
+ return filter($params, \%result);
+}
+
+1;
+
+__END__
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::BugUserLastVisit - Find and Store the last time a
+user visited a bug.
+
+=head1 METHODS
+
+=head2 update
+
+=over
+
+=item B<Description>
+
+Update the last visit time for the specified bug and current user.
+
+=item B<REST>
+
+To add a single bug id:
+
+ POST /rest/bug_user_last_visit/<bug-id>
+
+Tp add one or more bug ids at once:
+
+ POST /rest/bug_user_last_visit
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<ids> (array) - One or more bug ids to add.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=item C<array> - An array of hashes containing the following:
+
+=over
+
+=item C<id> - (int) The bug id.
+
+=item C<last_visit_ts> - (string) The timestamp the user last visited the bug.
+
+=back
+
+=back
+
+=back
+
+=head2 get
+
+=over
+
+=item B<Description>
+
+Get the last visited timestamp for one or more specified bug ids.
+
+=item B<REST>
+
+To return the last visited timestamp for a single bug id:
+
+ GET /rest/bug_user_last_visit/<bug-id>
+
+=item B<Params>
+
+=over
+
+=item C<ids> (integer) - One or more optional bug ids to get.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=item C<array> - An array of hashes containing the following:
+
+=over
+
+=item C<id> - (int) The bug id.
+
+=item C<last_visit_ts> - (string) The timestamp the user last visited the bug.
+
+=back
+
+=back
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=back
diff --git a/Bugzilla/API/1_0/Resource/Bugzilla.pm b/Bugzilla/API/1_0/Resource/Bugzilla.pm
new file mode 100644
index 000000000..8a0c6baac
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/Bugzilla.pm
@@ -0,0 +1,547 @@
+# 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::API::1_0::Resource::Bugzilla;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Constants;
+use Bugzilla::Util qw(datetime_from);
+use Bugzilla::Util qw(trick_taint);
+
+use DateTime;
+use Moo;
+
+extends 'Bugzilla::API::1_0::Resource';
+
+##############
+# Constants #
+##############
+
+# Basic info that is needed before logins
+use constant LOGIN_EXEMPT => {
+ parameters => 1,
+ timezone => 1,
+ version => 1,
+};
+
+use constant READ_ONLY => qw(
+ extensions
+ parameters
+ timezone
+ time
+ version
+);
+
+use constant PUBLIC_METHODS => qw(
+ extensions
+ last_audit_time
+ parameters
+ time
+ timezone
+ version
+);
+
+# Logged-out users do not need to know more than that.
+use constant PARAMETERS_LOGGED_OUT => qw(
+ maintainer
+ requirelogin
+);
+
+# These parameters are guessable from the web UI when the user
+# is logged in. So it's safe to access them.
+use constant PARAMETERS_LOGGED_IN => qw(
+ allowemailchange
+ attachment_base
+ commentonchange_resolution
+ commentonduplicate
+ cookiepath
+ defaultopsys
+ defaultplatform
+ defaultpriority
+ defaultseverity
+ duplicate_or_move_bug_status
+ emailregexpdesc
+ emailsuffix
+ letsubmitterchoosemilestone
+ letsubmitterchoosepriority
+ mailfrom
+ maintainer
+ maxattachmentsize
+ maxlocalattachment
+ musthavemilestoneonaccept
+ noresolveonopenblockers
+ password_complexity
+ rememberlogin
+ requirelogin
+ search_allow_no_criteria
+ urlbase
+ use_see_also
+ useclassification
+ usemenuforusers
+ useqacontact
+ usestatuswhiteboard
+ usetargetmilestone
+);
+
+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;
+}
+
+############
+# Methods #
+############
+
+sub version {
+ my $self = shift;
+ return { version => as_string(BUGZILLA_VERSION) };
+}
+
+sub extensions {
+ my $self = shift;
+
+ my %retval;
+ foreach my $extension (@{ Bugzilla->extensions }) {
+ my $version = $extension->VERSION || 0;
+ my $name = $extension->NAME;
+ $retval{$name}->{version} = as_string($version);
+ }
+ return { extensions => \%retval };
+}
+
+sub timezone {
+ my $self = shift;
+ # All Webservices return times in UTC; Use UTC here for backwards compat.
+ return { timezone => as_string("+0000") };
+}
+
+sub time {
+ my ($self) = @_;
+ # All Webservices return times in UTC; Use UTC here for backwards compat.
+ # Hardcode values where appropriate
+ my $dbh = Bugzilla->dbh;
+
+ my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $db_time = datetime_from($db_time, 'UTC');
+ my $now_utc = DateTime->now();
+
+ return {
+ db_time => as_datetime($db_time),
+ web_time => as_datetime($now_utc),
+ };
+}
+
+sub last_audit_time {
+ my ($self, $params) = validate(@_, 'class');
+ my $dbh = Bugzilla->dbh;
+
+ my $sql_statement = "SELECT MAX(at_time) FROM audit_log";
+ my $class_values = $params->{class};
+ my @class_values_quoted;
+ foreach my $class_value (@$class_values) {
+ push (@class_values_quoted, $dbh->quote($class_value))
+ if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/;
+ }
+
+ if (@class_values_quoted) {
+ $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted);
+ }
+
+ my $last_audit_time = $dbh->selectrow_array("$sql_statement");
+
+ # All Webservices return times in UTC; Use UTC here for backwards compat.
+ # Hardcode values where appropriate
+ $last_audit_time = datetime_from($last_audit_time, 'UTC');
+
+ return {
+ last_audit_time => as_datetime($last_audit_time)
+ };
+}
+
+sub parameters {
+ my ($self, $args) = @_;
+ my $user = Bugzilla->login();
+ my $params = Bugzilla->params;
+ $args ||= {};
+
+ my @params_list = $user->in_group('tweakparams')
+ ? keys(%$params)
+ : $user->id ? PARAMETERS_LOGGED_IN : PARAMETERS_LOGGED_OUT;
+
+ my %parameters;
+ foreach my $param (@params_list) {
+ next unless filter_wants($args, $param);
+ $parameters{$param} = as_string($params->{$param});
+ }
+
+ return { parameters => \%parameters };
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::Bugzilla - Global functions for the webservice interface.
+
+=head1 DESCRIPTION
+
+This provides functions that tell you about Bugzilla in general.
+
+=head1 METHODS
+
+=head2 version
+
+=over
+
+=item B<Description>
+
+Returns the current version of Bugzilla.
+
+=item B<REST>
+
+GET /rest/version
+
+The returned data format is the same as below.
+
+=item B<Params> (none)
+
+=item B<Returns>
+
+A hash with a single item, C<version>, that is the version as a
+string.
+
+=item B<Errors> (none)
+
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 extensions
+
+=over
+
+=item B<Description>
+
+Gets information about the extensions that are currently installed and enabled
+in this Bugzilla.
+
+=item B<REST>
+
+GET /rest/extensions
+
+The returned data format is the same as below.
+
+=item B<Params> (none)
+
+=item B<Returns>
+
+A hash with a single item, C<extensions>. This points to a hash. I<That> hash
+contains the names of extensions as keys, and the values are a hash.
+That hash contains a single key C<version>, which is the version of the
+extension, or C<0> if the extension hasn't defined a version.
+
+The return value looks something like this:
+
+ extensions => {
+ Example => {
+ version => '3.6',
+ },
+ BmpConvert => {
+ version => '1.0',
+ },
+ }
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.2>.
+
+=item As of Bugzilla B<3.6>, the names of extensions are canonical names
+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
+
+=head2 timezone
+
+B<DEPRECATED> This method may be removed in a future version of Bugzilla.
+Use L</time> instead.
+
+=over
+
+=item B<Description>
+
+Returns the timezone that Bugzilla expects dates and times in.
+
+=item B<REST>
+
+GET /rest/timezone
+
+The returned data format is the same as below.
+
+=item B<Params> (none)
+
+=item B<Returns>
+
+A hash with a single item, C<timezone>, that is the timezone offset as a
+string in (+/-)XXXX (RFC 2822) format.
+
+=item B<History>
+
+=over
+
+=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
+
+
+=head2 time
+
+=over
+
+=item B<Description>
+
+Gets information about what time the Bugzilla server thinks it is, and
+what timezone it's running in.
+
+=item B<REST>
+
+GET /rest/time
+
+The returned data format is the same as below.
+
+=item B<Params> (none)
+
+=item B<Returns>
+
+A struct with the following items:
+
+=over
+
+=item C<db_time>
+
+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
+in the same time zone. However, if the web server and the database server
+aren't synchronized for some reason, I<this> is the time that you should
+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
+I<web server>.
+
+This might be different by a second from C<db_time> since this comes from
+a different source. If it's any more different than a second, then there is
+likely some problem with this Bugzilla instance. In this case you should
+rely on the C<db_time>, not the C<web_time>.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.4>.
+
+=item As of Bugzilla B<3.6>, this method returns all data as though the server
+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
+
+=head2 parameters
+
+=over
+
+=item B<Description>
+
+Returns parameter values currently used in this Bugzilla.
+
+=item B<REST>
+
+GET /rest/parameters
+
+The returned data format is the same as below.
+
+=item B<Params> (none)
+
+=item B<Returns>
+
+A hash with a single item C<parameters> which contains a hash with
+the name of the parameters as keys and their value as values. All
+values are returned as strings.
+The list of parameters returned by this method depends on the user
+credentials:
+
+A logged-out user can only access the C<maintainer> and C<requirelogin> parameters.
+
+A logged-in user can access the following parameters (listed alphabetically):
+ C<allowemailchange>,
+ C<attachment_base>,
+ C<commentonchange_resolution>,
+ C<commentonduplicate>,
+ C<cookiepath>,
+ C<defaultopsys>,
+ C<defaultplatform>,
+ C<defaultpriority>,
+ C<defaultseverity>,
+ C<duplicate_or_move_bug_status>,
+ C<emailregexpdesc>,
+ C<emailsuffix>,
+ C<letsubmitterchoosemilestone>,
+ C<letsubmitterchoosepriority>,
+ C<mailfrom>,
+ C<maintainer>,
+ C<maxattachmentsize>,
+ C<maxlocalattachment>,
+ C<musthavemilestoneonaccept>,
+ C<noresolveonopenblockers>,
+ C<password_complexity>,
+ C<rememberlogin>,
+ C<requirelogin>,
+ C<search_allow_no_criteria>,
+ C<urlbase>,
+ C<use_see_also>,
+ C<useclassification>,
+ C<usemenuforusers>,
+ C<useqacontact>,
+ C<usestatuswhiteboard>,
+ C<usetargetmilestone>.
+
+A user in the tweakparams group can access all existing parameters.
+New parameters can appear or obsolete parameters can disappear depending
+on the version of Bugzilla and on extensions being installed.
+The list of parameters returned by this method is not stable and will
+never be stable.
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 last_audit_time
+
+=over
+
+=item B<Description>
+
+Gets the latest time of the audit_log table.
+
+=item B<REST>
+
+GET /rest/last_audit_time
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+You can pass the optional parameter C<class> to get the maximum for only
+the listed classes.
+
+=over
+
+=item C<class> (array) - An array of strings representing the class names.
+
+B<Note:> The class names are defined as "Bugzilla::<class_name>". For the product
+use Bugzilla:Product.
+
+=back
+
+=item B<Returns>
+
+A hash with a single item, C<last_audit_time>, that is the maximum of the
+at_time from the audit_log.
+
+=item B<Errors> (none)
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=back
diff --git a/Bugzilla/API/1_0/Resource/Classification.pm b/Bugzilla/API/1_0/Resource/Classification.pm
new file mode 100644
index 000000000..7a59e381d
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/Classification.pm
@@ -0,0 +1,235 @@
+# 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::API::1_0::Resource::Classification;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Classification;
+use Bugzilla::Error;
+
+use Moo;
+
+extends 'Bugzilla::API::1_0::Resource';
+
+##############
+# Constants #
+##############
+
+use constant READ_ONLY => qw(
+ get
+);
+
+use constant PUBLIC_METHODS => qw(
+ get
+);
+
+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;
+}
+
+############
+# Methods #
+############
+
+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 { $self->_classification_to_hash($_, $params) } @classification_objs;
+
+ return { classifications => \@classifications };
+}
+
+sub _classification_to_hash {
+ my ($self, $classification, $params) = @_;
+
+ my $user = Bugzilla->user;
+ return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications'));
+
+ my $products = $user->in_group('editclassifications') ?
+ $classification->products : $user->get_selectable_products($classification->id);
+
+ return filter $params, {
+ id => as_int($classification->id),
+ name => as_string($classification->name),
+ description => as_string($classification->description),
+ sort_key => as_int($classification->sortkey),
+ products => [ map { $self->_product_to_hash($_, $params) } @$products ],
+ };
+}
+
+sub _product_to_hash {
+ my ($self, $product, $params) = @_;
+
+ return filter $params, {
+ id => as_int($product->id),
+ name => as_string($product->name),
+ description => as_string($product->description),
+ }, undef, 'products';
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::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
+
+=head2 get
+
+=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 /rest/classification/<classification_id_or_name>
+
+The returned data format will be the same as below.
+
+=item B<Params>
+
+In addition to the parameters below, this method also accepts the
+standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and
+L<exclude_fields|Bugzilla::API::1_0::Resource/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
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=back
diff --git a/Bugzilla/API/1_0/Resource/Component.pm b/Bugzilla/API/1_0/Resource/Component.pm
new file mode 100644
index 000000000..c3bdc0386
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/Component.pm
@@ -0,0 +1,639 @@
+# 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::API::1_0::Resource::Component;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Constants;
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+
+use Moo;
+
+extends 'Bugzilla::API::1_0::Resource';
+
+##############
+# Constants #
+##############
+
+use constant PUBLIC_METHODS => qw(
+ create
+);
+
+use constant CREATE_MAPPED_FIELDS => {
+ default_assignee => 'initialowner',
+ default_qa_contact => 'initialqacontact',
+ default_cc => 'initial_cc',
+ is_open => 'isactive',
+};
+
+use constant MAPPED_FIELDS => {
+ is_open => 'is_active',
+};
+
+use constant MAPPED_RETURNS => {
+ initialowner => 'default_assignee',
+ initialqacontact => 'default_qa_contact',
+ cc_list => 'default_cc',
+ isactive => 'isopen',
+};
+
+sub REST_RESOURCES {
+ my $rest_resources = [
+ qr{^/component$}, {
+ POST => {
+ method => 'create',
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/component/(\d+)$}, {
+ PUT => {
+ method => 'update',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ },
+ DELETE => {
+ method => 'delete',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ },
+ },
+ qr{^/component/([^/]+)/([^/]+)$}, {
+ PUT => {
+ method => 'update',
+ params => sub {
+ return { names => [ { product => $_[0], component => $_[1] } ] };
+ }
+ },
+ DELETE => {
+ method => 'delete',
+ params => sub {
+ return { names => [ { product => $_[0], component => $_[1] } ] };
+ }
+ },
+ },
+ ];
+ return $rest_resources;
+}
+
+############
+# Methods #
+############
+
+sub create {
+ my ($self, $params) = @_;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ $user->in_group('editcomponents')
+ || scalar @{ $user->get_products_by_permission('editcomponents') }
+ || ThrowUserError('auth_failure', { group => 'editcomponents',
+ action => 'edit',
+ object => 'components' });
+
+ my $product = $user->check_can_admin_product($params->{product});
+
+ # Translate the fields
+ my $values = translate($params, CREATE_MAPPED_FIELDS);
+ $values->{product} = $product;
+
+ # Create the component and return the newly created id.
+ my $component = Bugzilla::Component->create($values);
+ return { id => as_int($component->id) };
+}
+
+sub _component_params_to_objects {
+ # We can't use Util's _param_to_objects since name is a hash
+ my $params = shift;
+ my $user = Bugzilla->user;
+
+ my @components = ();
+
+ if (defined $params->{ids}) {
+ push @components, @{ Bugzilla::Component->new_from_list($params->{ids}) };
+ }
+
+ if (defined $params->{names}) {
+ # To get the component objects for product/component combination
+ # first obtain the product object from the passed product name
+ foreach my $name_hash (@{$params->{names}}) {
+ my $product = $user->can_admin_product($name_hash->{product});
+ push @components, @{ Bugzilla::Component->match({
+ product_id => $product->id,
+ name => $name_hash->{component}
+ })};
+ }
+ }
+
+ my %seen_component_ids = ();
+
+ my @accessible_components;
+ foreach my $component (@components) {
+ # Skip if we already included this component
+ next if $seen_component_ids{$component->id}++;
+
+ # Can the user see and admin this product?
+ my $product = $component->product;
+ $user->check_can_admin_product($product->name);
+
+ push @accessible_components, $component;
+ }
+
+ return \@accessible_components;
+}
+
+sub update {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ $user->in_group('editcomponents')
+ || scalar @{ $user->get_products_by_permission('editcomponents') }
+ || ThrowUserError("auth_failure", { group => "editcomponents",
+ action => "edit",
+ object => "components" });
+
+ defined($params->{names}) || defined($params->{ids})
+ || ThrowCodeError('params_required',
+ { function => 'Component.update', params => ['ids', 'names'] });
+
+ my $component_objects = _component_params_to_objects($params);
+
+ # If the user tries to change component name for several
+ # components of the same product then throw an error
+ if ($params->{name}) {
+ my %unique_product_comps;
+ foreach my $comp (@$component_objects) {
+ if($unique_product_comps{$comp->product_id}) {
+ ThrowUserError("multiple_components_update_not_allowed");
+ }
+ else {
+ $unique_product_comps{$comp->product_id} = 1;
+ }
+ }
+ }
+
+ my $values = translate($params, MAPPED_FIELDS);
+
+ # We delete names and ids to keep only new values to set.
+ delete $values->{names};
+ delete $values->{ids};
+
+ $dbh->bz_start_transaction();
+ foreach my $component (@$component_objects) {
+ $component->set_all($values);
+ }
+
+ my %changes;
+ foreach my $component (@$component_objects) {
+ my $returned_changes = $component->update();
+ $changes{$component->id} = translate($returned_changes, MAPPED_RETURNS);
+ }
+ $dbh->bz_commit_transaction();
+
+ my @result;
+ foreach my $component (@$component_objects) {
+ my %hash = (
+ id => $component->id,
+ changes => {},
+ );
+
+ foreach my $field (keys %{ $changes{$component->id} }) {
+ my $change = $changes{$component->id}->{$field};
+
+ if ($field eq 'default_assignee'
+ || $field eq 'default_qa_contact'
+ || $field eq 'default_cc'
+ ) {
+ # We need to convert user ids to login names
+ my @old_user_ids = split(/[,\s]+/, $change->[0]);
+ my @new_user_ids = split(/[,\s]+/, $change->[1]);
+
+ my @old_users = map { $_->login }
+ @{Bugzilla::User->new_from_list(\@old_user_ids)};
+ my @new_users = map { $_->login }
+ @{Bugzilla::User->new_from_list(\@new_user_ids)};
+
+ $hash{changes}{$field} = {
+ removed => as_string(join(', ', @old_users)),
+ added => as_string(join(', ', @new_users)),
+ };
+ }
+ else {
+ $hash{changes}{$field} = {
+ removed => as_string($change->[0]),
+ added => as_string($change->[1])
+ };
+ }
+ }
+
+ push(@result, \%hash);
+ }
+
+ return { components => \@result };
+}
+
+sub delete {
+ my ($self, $params) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ $user->in_group('editcomponents')
+ || scalar @{ $user->get_products_by_permission('editcomponents') }
+ || ThrowUserError("auth_failure", { group => "editcomponents",
+ action => "edit",
+ object => "components" });
+
+ defined($params->{names}) || defined($params->{ids})
+ || ThrowCodeError('params_required',
+ { function => 'Component.delete', params => ['ids', 'names'] });
+
+ my $component_objects = _component_params_to_objects($params);
+
+ $dbh->bz_start_transaction();
+ my %changes;
+ foreach my $component (@$component_objects) {
+ my $returned_changes = $component->remove_from_db();
+ }
+ $dbh->bz_commit_transaction();
+
+ my @result;
+ foreach my $component (@$component_objects) {
+ push @result, { id => $component->id };
+ }
+
+ return { components => \@result };
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::Component - The Component API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla API allows you to deal with the available product components.
+You will be able to get information about them as well as manipulate them.
+
+=head1 METHODS
+
+=head2 create
+
+=over
+
+=item B<Description>
+
+This allows you to create a new component in Bugzilla.
+
+=item B<Params>
+
+Some params must be set, or an error will be thrown. These params are
+marked B<Required>.
+
+=over
+
+=item C<name>
+
+B<Required> C<string> The name of the new component.
+
+=item C<product>
+
+B<Required> C<string> The name of the product that the component must be
+added to. This product must already exist, and the user have the necessary
+permissions to edit components for it.
+
+=item C<description>
+
+B<Required> C<string> The description of the new component.
+
+=item C<default_assignee>
+
+B<Required> C<string> The login name of the default assignee of the component.
+
+=item C<default_cc>
+
+C<array> An array of strings with each element representing one login name of the default CC list.
+
+=item C<default_qa_contact>
+
+C<string> The login name of the default QA contact for the component.
+
+=item C<is_open>
+
+C<boolean> 1 if you want to enable the component for bug creations. 0 otherwise. Default is 1.
+
+=back
+
+=item B<Returns>
+
+A hash with one key: C<id>. This will represent the ID of the newly-added
+component.
+
+=item B<Errors>
+
+=over
+
+=item 304 (Authorization Failure)
+
+You are not authorized to create a new component.
+
+=item 1200 (Component already exists)
+
+The name that you specified for the new component already exists in the
+specified product.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 update
+
+=over
+
+=item B<Description>
+
+This allows you to update one or more components in Bugzilla.
+
+=item B<REST>
+
+PUT /rest/component/<component_id>
+
+PUT /rest/component/<product_name>/<component_name>
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below. The C<ids> and C<names> params will be overridden as
+it is pulled from the URL path.
+
+=item B<Params>
+
+B<Note:> The following parameters specify which components you are updating.
+You must set one or both of these parameters.
+
+=over
+
+=item C<ids>
+
+C<array> of C<int>s. Numeric ids of the components that you wish to update.
+
+=item C<names>
+
+C<array> of C<hash>es. Names of the components that you wish to update. The
+hash keys are C<product> and C<component>, representing the name of the product
+and the component you wish to change.
+
+=back
+
+B<Note:> The following parameters specify the new values you want to set for
+the components you are updating.
+
+=over
+
+=item C<name>
+
+C<string> A new name for this component. If you try to set this while updating
+more than one component for a product, an error will occur, as component names
+must be unique per product.
+
+=item C<description>
+
+C<string> Update the long description for these components to this value.
+
+=item C<default_assignee>
+
+C<string> The login name of the default assignee of the component.
+
+=item C<default_cc>
+
+C<array> An array of strings with each element representing one login name of the default CC list.
+
+=item C<default_qa_contact>
+
+C<string> The login name of the default QA contact for the component.
+
+=item C<is_open>
+
+C<boolean> True if the component is currently allowing bugs to be entered
+into it, False otherwise.
+
+=back
+
+=item B<Returns>
+
+A C<hash> with a single field "components". This points to an array of hashes
+with the following fields:
+
+=over
+
+=item C<id>
+
+C<int> The id of the component that was updated.
+
+=item C<changes>
+
+C<hash> The changes that were actually done on this component. The keys are
+the names of the fields that were changed, and the values are a hash
+with two keys:
+
+=over
+
+=item C<added>
+
+C<string> The value that this field was changed to.
+
+=item C<removed>
+
+C<string> The value that was previously set in this field.
+
+=back
+
+Note that booleans will be represented with the strings '1' and '0'.
+
+Here's an example of what a return value might look like:
+
+ {
+ components => [
+ {
+ id => 123,
+ changes => {
+ name => {
+ removed => 'FooName',
+ added => 'BarName'
+ },
+ default_assignee => {
+ removed => 'foo@company.com',
+ added => 'bar@company.com',
+ }
+ }
+ }
+ ]
+ }
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 51 (User does not exist)
+
+One of the contact e-mail addresses is not a valid Bugzilla user.
+
+=item 106 (Product access denied)
+
+The product you are trying to modify does not exist or you don't have access to it.
+
+=item 706 (Product admin denied)
+
+You do not have the permission to change components for this product.
+
+=item 105 (Component name too long)
+
+The name specified for this component was longer than the maximum
+allowed length.
+
+=item 1200 (Component name already exists)
+
+You specified the name of a component that already exists.
+(Component names must be unique per product in Bugzilla.)
+
+=item 1210 (Component blank name)
+
+You must specify a non-blank name for this component.
+
+=item 1211 (Component must have description)
+
+You must specify a description for this component.
+
+=item 1212 (Component name is not unique)
+
+You have attempted to set more than one component in the same product with the
+same name. Component names must be unique in each product.
+
+=item 1213 (Component needs a default assignee)
+
+A default assignee is required for this component.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 delete
+
+=over
+
+=item B<Description>
+
+This allows you to delete one or more components in Bugzilla.
+
+=item B<REST>
+
+DELETE /rest/component/<component_id>
+
+DELETE /rest/component/<product_name>/<component_name>
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below. The C<ids> and C<names> params will be overridden as
+it is pulled from the URL path.
+
+=item B<Params>
+
+B<Note:> The following parameters specify which components you are deleting.
+You must set one or both of these parameters.
+
+=over
+
+=item C<ids>
+
+C<array> of C<int>s. Numeric ids of the components that you wish to delete.
+
+=item C<names>
+
+C<array> of C<hash>es. Names of the components that you wish to delete. The
+hash keys are C<product> and C<component>, representing the name of the product
+and the component you wish to delete.
+
+=back
+
+=item B<Returns>
+
+A C<hash> with a single field "components". This points to an array of hashes
+with the following field:
+
+=over
+
+=item C<id>
+
+C<int> The id of the component that was deleted.
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 106 (Product access denied)
+
+The product you are trying to modify does not exist or you don't have access to it.
+
+=item 706 (Product admin denied)
+
+You do not have the permission to delete components for this product.
+
+=item 1202 (Component has bugs)
+
+The component you are trying to delete currently has bugs assigned to it.
+You must move these bugs before trying to delete the component.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>
+
+=back
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=back
diff --git a/Bugzilla/API/1_0/Resource/FlagType.pm b/Bugzilla/API/1_0/Resource/FlagType.pm
new file mode 100644
index 000000000..297be1510
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/FlagType.pm
@@ -0,0 +1,890 @@
+# 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::API::1_0::Resource::FlagType;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Constants;
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::FlagType;
+use Bugzilla::Product;
+use Bugzilla::Util qw(trim);
+
+use List::MoreUtils qw(uniq);
+use Moo;
+
+extends 'Bugzilla::API::1_0::Resource';
+
+##############
+# Constants #
+##############
+
+use constant READ_ONLY => qw(
+ get
+);
+
+use constant PUBLIC_METHODS => qw(
+ create
+ get
+ update
+);
+
+sub REST_RESOURCES {
+ my $rest_resources = [
+ qr{^/flag_type$}, {
+ POST => {
+ method => 'create',
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/flag_type/([^/]+)/([^/]+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ return { product => $_[0],
+ component => $_[1] };
+ }
+ }
+ },
+ qr{^/flag_type/([^/]+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ return { product => $_[0] };
+ }
+ },
+ PUT => {
+ method => 'update',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ }
+ },
+ ];
+ return $rest_resources;
+}
+
+############
+# Methods #
+############
+
+sub get {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->switch_to_shadow_db();
+ my $user = Bugzilla->user;
+
+ defined $params->{product}
+ || ThrowCodeError('param_required',
+ { function => 'Bug.flag_types',
+ param => 'product' });
+
+ my $product = delete $params->{product};
+ my $component = delete $params->{component};
+
+ $product = Bugzilla::Product->check({ name => $product, cache => 1 });
+ $component = Bugzilla::Component->check(
+ { name => $component, product => $product, cache => 1 }) if $component;
+
+ my $flag_params = { product_id => $product->id };
+ $flag_params->{component_id} = $component->id if $component;
+ my $matched_flag_types = Bugzilla::FlagType::match($flag_params);
+
+ my $flag_types = { bug => [], attachment => [] };
+ foreach my $flag_type (@$matched_flag_types) {
+ push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product))
+ if $flag_type->target_type eq 'bug';
+ push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product))
+ if $flag_type->target_type eq 'attachment';
+ }
+
+ return $flag_types;
+}
+
+sub create {
+ my ($self, $params) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ Bugzilla->user->in_group('editcomponents')
+ || scalar(@{$user->get_products_by_permission('editcomponents')})
+ || ThrowUserError("auth_failure", { group => "editcomponents",
+ action => "add",
+ object => "flagtypes" });
+
+ $params->{name} || ThrowCodeError('param_required', { param => 'name' });
+ $params->{description} || ThrowCodeError('param_required', { param => 'description' });
+
+ my %args = (
+ sortkey => 1,
+ name => undef,
+ inclusions => ['0:0'], # Default to __ALL__:__ALL__
+ cc_list => '',
+ description => undef,
+ is_requestable => 'on',
+ exclusions => [],
+ is_multiplicable => 'on',
+ request_group => '',
+ is_active => 'on',
+ is_specifically_requestable => 'on',
+ target_type => 'bug',
+ grant_group => '',
+ );
+
+ foreach my $key (keys %args) {
+ $args{$key} = $params->{$key} if defined($params->{$key});
+ }
+
+ $args{name} = trim($params->{name});
+ $args{description} = trim($params->{description});
+
+ # Is specifically requestable is actually is_requesteeable
+ if (exists $args{is_specifically_requestable}) {
+ $args{is_requesteeble} = delete $args{is_specifically_requestable};
+ }
+
+ # Default is on for the tickbox flags.
+ # If the user has set them to 'off' then undefine them so the flags are not ticked
+ foreach my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) {
+ if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) {
+ $args{$arg_name} = undef;
+ }
+ }
+
+ # Process group inclusions and exclusions
+ $args{inclusions} = _process_lists($params->{inclusions}) if defined $params->{inclusions};
+ $args{exclusions} = _process_lists($params->{exclusions}) if defined $params->{exclusions};
+
+ my $flagtype = Bugzilla::FlagType->create(\%args);
+
+ return { id => as_int($flagtype->id) };
+}
+
+sub update {
+ my ($self, $params) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ $user->in_group('editcomponents')
+ || scalar(@{$user->get_products_by_permission('editcomponents')})
+ || ThrowUserError("auth_failure", { group => "editcomponents",
+ action => "edit",
+ object => "flagtypes" });
+
+ defined($params->{names}) || defined($params->{ids})
+ || ThrowCodeError('params_required',
+ { function => 'FlagType.update', params => ['ids', 'names'] });
+
+ # Get the list of unique flag type ids we are updating
+ my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : ();
+ if (defined $params->{names}) {
+ push @flag_type_ids, map { $_->id }
+ @{ Bugzilla::FlagType::match({ name => $params->{names} }) };
+ }
+ @flag_type_ids = uniq @flag_type_ids;
+
+ # We delete names and ids to keep only new values to set.
+ delete $params->{names};
+ delete $params->{ids};
+
+ # Process group inclusions and exclusions
+ # We removed them from $params because these are handled differently
+ my $inclusions = _process_lists(delete $params->{inclusions}) if defined $params->{inclusions};
+ my $exclusions = _process_lists(delete $params->{exclusions}) if defined $params->{exclusions};
+
+ $dbh->bz_start_transaction();
+ my %changes = ();
+
+ foreach my $flag_type_id (@flag_type_ids) {
+ my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id);
+
+ if ($can_fully_edit) {
+ $flagtype->set_all($params);
+ }
+ elsif (scalar keys %$params) {
+ ThrowUserError('flag_type_not_editable', { flagtype => $flagtype });
+ }
+
+ # Process the clusions
+ foreach my $type ('inclusions', 'exclusions') {
+ my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions;
+ next if not defined $clusions;
+
+ my @extra_clusions = ();
+ if (!$user->in_group('editcomponents')) {
+ my $products = $user->get_products_by_permission('editcomponents');
+ # Bring back the products the user cannot edit.
+ foreach my $item (values %{$flagtype->$type}) {
+ my ($prod_id, $comp_id) = split(':', $item);
+ push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products;
+ }
+ }
+
+ $flagtype->set_clusions({
+ $type => [@$clusions, @extra_clusions],
+ });
+ }
+
+ my $returned_changes = $flagtype->update();
+ $changes{$flagtype->id} = {
+ name => $flagtype->name,
+ changes => $returned_changes,
+ };
+ }
+ $dbh->bz_commit_transaction();
+
+ my @result;
+ foreach my $flag_type_id (keys %changes) {
+ my %hash = (
+ id => as_int($flag_type_id),
+ name => as_string($changes{$flag_type_id}{name}),
+ changes => {},
+ );
+
+ foreach my $field (keys %{ $changes{$flag_type_id}{changes} }) {
+ my $change = $changes{$flag_type_id}{changes}{$field};
+ $hash{changes}{$field} = {
+ removed => as_string($change->[0]),
+ added => as_string($change->[1])
+ };
+ }
+
+ push(@result, \%hash);
+ }
+
+ return { flagtypes => \@result };
+}
+
+sub _flagtype_to_hash {
+ my ($self, $flagtype, $product) = @_;
+ my $user = Bugzilla->user;
+
+ my @values = ('X');
+ push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype));
+ push(@values, '+', '-') if $user->can_set_flag($flagtype);
+
+ my $item = {
+ id => as_int($flagtype->id),
+ name => as_string($flagtype->name),
+ description => as_string($flagtype->description),
+ type => as_string($flagtype->target_type),
+ values => \@values,
+ is_active => as_boolean($flagtype->is_active),
+ is_requesteeble => as_boolean($flagtype->is_requesteeble),
+ is_multiplicable => as_boolean($flagtype->is_multiplicable)
+ };
+
+ if ($product) {
+ my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id);
+ my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id);
+ # if we have both inclusions and exclusions, the exclusions are redundant
+ $exclusions = [] if @$inclusions && @$exclusions;
+ # no need to return anything if there's just "any component"
+ $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne '';
+ $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne '';
+ }
+
+ return $item;
+}
+
+sub _flagtype_clusions_to_hash {
+ my ($self, $clusions, $product_id) = @_;
+ my $result = [];
+ foreach my $key (keys %$clusions) {
+ my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2);
+ if ($prod_id == 0 || $prod_id == $product_id) {
+ if ($comp_id) {
+ my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 });
+ push @$result, $component->name;
+ }
+ else {
+ return [ '' ];
+ }
+ }
+ }
+ return $result;
+}
+
+sub _process_lists {
+ my $list = shift;
+ my $user = Bugzilla->user;
+
+ my @products;
+ if ($user->in_group('editcomponents')) {
+ @products = Bugzilla::Product->get_all;
+ }
+ else {
+ @products = @{$user->get_products_by_permission('editcomponents')};
+ }
+
+ my @component_list;
+
+ foreach my $item (@$list) {
+ # A hash with products as the key and component names as the values
+ if(ref($item) eq 'HASH') {
+ while (my ($product_name, $component_names) = each %$item) {
+ my $product = Bugzilla::Product->check({name => $product_name});
+ unless (grep { $product->name eq $_->name } @products) {
+ ThrowUserError('product_access_denied', { name => $product_name });
+ }
+ my @component_ids;
+
+ foreach my $comp_name (@$component_names) {
+ my $component = Bugzilla::Component->check({product => $product, name => $comp_name});
+ ThrowCodeError('param_invalid', { param => $comp_name}) unless defined $component;
+ push @component_list, $product->id . ':' . $component->id;
+ }
+ }
+ }
+ elsif(!ref($item)) {
+ # These are whole products
+ my $product = Bugzilla::Product->check({name => $item});
+ unless (grep { $product->name eq $_->name } @products) {
+ ThrowUserError('product_access_denied', { name => $item });
+ }
+ push @component_list, $product->id . ':0';
+ }
+ else {
+ # The user has passed something invalid
+ ThrowCodeError('param_invalid', { param => $item });
+ }
+ }
+
+ return \@component_list;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::FlagType - API for creating flags.
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla API allows you to create new flags
+
+=head1 METHODS
+
+=head2 Get Flag Types
+
+=over
+
+=item C<get>
+
+=item B<Description>
+
+Get information about valid flag types that can be set for bugs and attachments.
+
+=item B<REST>
+
+You have several options for retreiving information about flag types. The first
+part is the request method and the rest is the related path needed.
+
+To get information about all flag types for a product:
+
+GET /rest/flag_type/<product>
+
+To get information about flag_types for a product and component:
+
+GET /rest/flag_type/<product>/<component>
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+You must pass a product name and an optional component name.
+
+=over
+
+=item C<product> (string) - The name of a valid product.
+
+=item C<component> (string) - An optional valid component name associated with the product.
+
+=back
+
+=item B<Returns>
+
+A hash containing two keys, C<bug> and C<attachment>. Each key value is an array of hashes,
+containing the following keys:
+
+=over
+
+=item C<id>
+
+C<int> An integer id uniquely identifying this flag type.
+
+=item C<name>
+
+C<string> The name for the flag type.
+
+=item C<type>
+
+C<string> The target of the flag type which is either C<bug> or C<attachment>.
+
+=item C<description>
+
+C<string> The description of the flag type.
+
+=item C<values>
+
+C<array> An array of string values that the user can set on the flag type.
+
+=item C<is_requesteeble>
+
+C<boolean> Users can ask specific other users to set flags of this type.
+
+=item C<is_multiplicable>
+
+C<boolean> Multiple flags of this type can be set for the same bug or attachment.
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 106 (Product Access Denied)
+
+Either the product does not exist or you don't have access to it.
+
+=item 51 (Invalid Component)
+
+The component provided does not exist in the product.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 Create Flag
+
+=over
+
+=item C<create>
+
+=item B<Description>
+
+Creates a new FlagType
+
+=item B<REST>
+
+POST /rest/flag_type
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+At a minimum the following two arguments must be supplied:
+
+=over
+
+=item C<name> (string) - The name of the new Flag Type.
+
+=item C<description> (string) - A description for the Flag Type object.
+
+=back
+
+=item B<Returns>
+
+C<int> flag_id
+
+The ID of the new FlagType object is returned.
+
+=item B<Params>
+
+=over
+
+=item name B<required>
+
+C<string> A short name identifying this type.
+
+=item description B<required>
+
+C<string> A comprehensive description of this type.
+
+=item inclusions B<optional>
+
+An array of strings or a hash containing product names, and optionally
+component names. If you provide a string, the flag type will be shown on
+all bugs in that product. If you provide a hash, the key represents the
+product name, and the value is the components of the product to be included.
+
+For example:
+
+ [ 'FooProduct',
+ {
+ BarProduct => [ 'C1', 'C3' ],
+ BazProduct => [ 'C7' ]
+ }
+ ]
+
+This flag will be added to B<All> components of I<FooProduct>,
+components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>.
+
+=item exclusions B<optional>
+
+An array of strings or hashes containing product names. This uses the same
+fromat as inclusions.
+
+This will exclude the flag from all products and components specified.
+
+=item sortkey B<optional>
+
+C<int> A number between 1 and 32767 by which this type will be sorted when
+displayed to users in a list; ignore if you don't care what order the types
+appear in or if you want them to appear in alphabetical order.
+
+=item is_active B<optional>
+
+C<boolean> Flag of this type appear in the UI and can be set. Default is B<true>.
+
+=item is_requestable B<optional>
+
+C<boolean> Users can ask for flags of this type to be set. Default is B<true>.
+
+=item cc_list B<optional>
+
+C<array> An array of strings. If the flag type is requestable, who should
+receive e-mail notification of requests. This is an array of e-mail addresses
+which do not need to be Bugzilla logins.
+
+=item is_specifically_requestable B<optional>
+
+C<boolean> Users can ask specific other users to set flags of this type as
+opposed to just asking the wind. Default is B<true>.
+
+=item is_multiplicable B<optional>
+
+C<boolean> Multiple flags of this type can be set on the same bug. Default is B<true>.
+
+=item grant_group B<optional>
+
+C<string> The group allowed to grant/deny flags of this type (to allow all
+users to grant/deny these flags, select no group). Default is B<no group>.
+
+=item request_group B<optional>
+
+C<string> If flags of this type are requestable, the group allowed to request
+them (to allow all users to request these flags, select no group). Note that
+the request group alone has no effect if the grant group is not defined!
+Default is B<no group>.
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 51 (Group Does Not Exist)
+
+The group name you entered does not exist, or you do not have access to it.
+
+=item 105 (Unknown component)
+
+The component does not exist for this product.
+
+=item 106 (Product Access Denied)
+
+Either the product does not exist or you don't have editcomponents privileges
+to it.
+
+=item 501 (Illegal Email Address)
+
+One of the e-mail address in the CC list is invalid. An e-mail in the CC
+list does NOT need to be a valid Bugzilla user.
+
+=item 1101 (Flag Type Name invalid)
+
+You must specify a non-blank name for this flag type. It must
+no contain spaces or commas, and must be 50 characters or less.
+
+=item 1102 (Flag type must have description)
+
+You must specify a description for this flag type.
+
+=item 1103 (Flag type CC list is invalid
+
+The CC list must be 200 characters or less.
+
+=item 1104 (Flag Type Sort Key Not Valid)
+
+The sort key is not a valid number.
+
+=item 1105 (Flag Type Not Editable)
+
+This flag type is not available for the products you can administer. Therefore
+you can not edit attributes of the flag type, other than the inclusion and
+exclusion list.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 update
+
+=over
+
+=item B<Description>
+
+This allows you to update a flag type in Bugzilla.
+
+=item B<REST>
+
+PUT /rest/flag_type/<product_id_or_name>
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below. The C<ids> and C<names> params will be overridden as
+it is pulled from the URL path.
+
+=item B<Params>
+
+B<Note:> The following parameters specify which products you are updating.
+You must set one or both of these parameters.
+
+=over
+
+=item C<ids>
+
+C<array> of C<int>s. Numeric ids of the flag types that you wish to update.
+
+=item C<names>
+
+C<array> of C<string>s. Names of the flag types that you wish to update. If
+many flag types have the same name, this will change ALL of them.
+
+=back
+
+B<Note:> The following parameters specify the new values you want to set for
+the products you are updating.
+
+=over
+
+=item name
+
+C<string> A short name identifying this type.
+
+=item description
+
+C<string> A comprehensive description of this type.
+
+=item inclusions B<optional>
+
+An array of strings or a hash containing product names, and optionally
+component names. If you provide a string, the flag type will be shown on
+all bugs in that product. If you provide a hash, the key represents the
+product name, and the value is the components of the product to be included.
+
+for example
+
+ [ 'FooProduct',
+ {
+ BarProduct => [ 'C1', 'C3' ],
+ BazProduct => [ 'C7' ]
+ }
+ ]
+
+This flag will be added to B<All> components of I<FooProduct>,
+components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>.
+
+=item exclusions B<optional>
+
+An array of strings or hashes containing product names.
+This uses the same fromat as inclusions.
+
+This will exclude the flag from all products and components specified.
+
+=item sortkey
+
+C<int> A number between 1 and 32767 by which this type will be sorted when
+displayed to users in a list; ignore if you don't care what order the types
+appear in or if you want them to appear in alphabetical order.
+
+=item is_active
+
+C<boolean> Flag of this type appear in the UI and can be set.
+
+=item is_requestable
+
+C<boolean> Users can ask for flags of this type to be set.
+
+=item cc_list
+
+C<array> An array of strings. If the flag type is requestable, who should
+receive e-mail notification of requests. This is an array of e-mail addresses
+which do not need to be Bugzilla logins.
+
+=item is_specifically_requestable
+
+C<boolean> Users can ask specific other users to set flags of this type as
+opposed to just asking the wind.
+
+=item is_multiplicable
+
+C<boolean> Multiple flags of this type can be set on the same bug.
+
+=item grant_group
+
+C<string> The group allowed to grant/deny flags of this type (to allow all
+users to grant/deny these flags, select no group).
+
+=item request_group
+
+C<string> If flags of this type are requestable, the group allowed to request
+them (to allow all users to request these flags, select no group). Note that
+the request group alone has no effect if the grant group is not defined!
+
+=back
+
+=item B<Returns>
+
+A C<hash> with a single field "flagtypes". This points to an array of hashes
+with the following fields:
+
+=over
+
+=item C<id>
+
+C<int> The id of the product that was updated.
+
+=item C<name>
+
+C<string> The name of the product that was updated.
+
+=item C<changes>
+
+C<hash> The changes that were actually done on this product. The keys are
+the names of the fields that were changed, and the values are a hash
+with two keys:
+
+=over
+
+=item C<added>
+
+C<string> The value that this field was changed to.
+
+=item C<removed>
+
+C<string> The value that was previously set in this field.
+
+=back
+
+Note that booleans will be represented with the strings '1' and '0'.
+
+Here's an example of what a return value might look like:
+
+ {
+ products => [
+ {
+ id => 123,
+ changes => {
+ name => {
+ removed => 'FooFlagType',
+ added => 'BarFlagType'
+ },
+ is_requestable => {
+ removed => '1',
+ added => '0',
+ }
+ }
+ }
+ ]
+ }
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 51 (Group Does Not Exist)
+
+The group name you entered does not exist, or you do not have access to it.
+
+=item 105 (Unknown component)
+
+The component does not exist for this product.
+
+=item 106 (Product Access Denied)
+
+Either the product does not exist or you don't have editcomponents privileges
+to it.
+
+=item 501 (Illegal Email Address)
+
+One of the e-mail address in the CC list is invalid. An e-mail in the CC
+list does NOT need to be a valid Bugzilla user.
+
+=item 1101 (Flag Type Name invalid)
+
+You must specify a non-blank name for this flag type. It must
+no contain spaces or commas, and must be 50 characters or less.
+
+=item 1102 (Flag type must have description)
+
+You must specify a description for this flag type.
+
+=item 1103 (Flag type CC list is invalid
+
+The CC list must be 200 characters or less.
+
+=item 1104 (Flag Type Sort Key Not Valid)
+
+The sort key is not a valid number.
+
+=item 1105 (Flag Type Not Editable)
+
+This flag type is not available for the products you can administer. Therefore
+you can not edit attributes of the flag type, other than the inclusion and
+exclusion list.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=back
diff --git a/Bugzilla/API/1_0/Resource/Group.pm b/Bugzilla/API/1_0/Resource/Group.pm
new file mode 100644
index 000000000..aee8a7492
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/Group.pm
@@ -0,0 +1,636 @@
+# 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::API::1_0::Resource::Group;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Constants;
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+
+use Moo;
+
+extends 'Bugzilla::API::1_0::Resource';
+
+##############
+# Constants #
+##############
+
+use constant PUBLIC_METHODS => qw(
+ create
+ get
+ update
+);
+
+use constant MAPPED_RETURNS => {
+ userregexp => 'user_regexp',
+ isactive => 'is_active'
+};
+
+sub REST_RESOURCES {
+ my $rest_resources = [
+ qr{^/group$}, {
+ GET => {
+ method => 'get'
+ },
+ POST => {
+ method => 'create',
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/group/([^/]+)$}, {
+ PUT => {
+ method => 'update',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ }
+ }
+ ];
+ return $rest_resources;
+}
+
+############
+# Methods #
+############
+
+sub create {
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->user->in_group('creategroups')
+ || ThrowUserError("auth_failure", { group => "creategroups",
+ action => "add",
+ object => "group"});
+ # Create group
+ my $group = Bugzilla::Group->create({
+ name => $params->{name},
+ description => $params->{description},
+ userregexp => $params->{user_regexp},
+ isactive => $params->{is_active},
+ isbuggroup => 1,
+ icon_url => $params->{icon_url}
+ });
+ return { id => as_int($group->id) };
+}
+
+sub update {
+ my ($self, $params) = @_;
+
+ my $dbh = Bugzilla->dbh;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->user->in_group('creategroups')
+ || ThrowUserError("auth_failure", { group => "creategroups",
+ action => "edit",
+ object => "group" });
+
+ defined($params->{names}) || defined($params->{ids})
+ || ThrowCodeError('params_required',
+ { function => 'Group.update', params => ['ids', 'names'] });
+
+ my $group_objects = params_to_objects($params, 'Bugzilla::Group');
+
+ my %values = %$params;
+
+ # We delete names and ids to keep only new values to set.
+ delete $values{names};
+ delete $values{ids};
+
+ $dbh->bz_start_transaction();
+ foreach my $group (@$group_objects) {
+ $group->set_all(\%values);
+ }
+
+ my %changes;
+ foreach my $group (@$group_objects) {
+ my $returned_changes = $group->update();
+ $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS);
+ }
+ $dbh->bz_commit_transaction();
+
+ my @result;
+ foreach my $group (@$group_objects) {
+ my %hash = (
+ id => $group->id,
+ changes => {},
+ );
+ foreach my $field (keys %{ $changes{$group->id} }) {
+ my $change = $changes{$group->id}->{$field};
+ $hash{changes}{$field} = {
+ removed => as_string($change->[0]),
+ added => as_string($change->[1])
+ };
+ }
+ push(@result, \%hash);
+ }
+
+ return { groups => \@result };
+}
+
+sub get {
+ my ($self, $params) = validate(@_, 'ids', 'names', 'type');
+
+ Bugzilla->login(LOGIN_REQUIRED);
+
+ # Reject access if there is no sense in continuing.
+ my $user = Bugzilla->user;
+ my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups');
+ if (!$all_groups && !$user->can_bless) {
+ ThrowUserError('group_cannot_view');
+ }
+
+ Bugzilla->switch_to_shadow_db();
+
+ my $groups = [];
+
+ if (defined $params->{ids}) {
+ # Get the groups by id
+ $groups = Bugzilla::Group->new_from_list($params->{ids});
+ }
+
+ if (defined $params->{names}) {
+ # Get the groups by name. Check will throw an error if a bad name is given
+ foreach my $name (@{$params->{names}}) {
+ # Skip if we got this from params->{id}
+ next if grep { $_->name eq $name } @$groups;
+
+ push @$groups, Bugzilla::Group->check({ name => $name });
+ }
+ }
+
+ if (!defined $params->{ids} && !defined $params->{names}) {
+ if ($all_groups) {
+ @$groups = Bugzilla::Group->get_all;
+ }
+ else {
+ # Get only groups the user has bless groups too
+ $groups = $user->bless_groups;
+ }
+ }
+
+ # Now create a result entry for each.
+ my @groups = map { $self->_group_to_hash($params, $_) } @$groups;
+ return { groups => \@groups };
+}
+
+sub _group_to_hash {
+ my ($self, $params, $group) = @_;
+ my $user = Bugzilla->user;
+
+ my $field_data = {
+ id => as_int($group->id),
+ name => as_string($group->name),
+ description => as_string($group->description),
+ };
+
+ if ($user->in_group('creategroups')) {
+ $field_data->{is_active} = as_boolean($group->is_active);
+ $field_data->{is_bug_group} = as_boolean($group->is_bug_group);
+ $field_data->{user_regexp} = as_string($group->user_regexp);
+ }
+
+ if ($params->{membership}) {
+ $field_data->{membership} = $self->_get_group_membership($group, $params);
+ }
+ return $field_data;
+}
+
+sub _get_group_membership {
+ my ($self, $group, $params) = @_;
+ my $user = Bugzilla->user;
+
+ my %users_only;
+ my $dbh = Bugzilla->dbh;
+ my $editusers = $user->in_group('editusers');
+
+ my $query = 'SELECT userid FROM profiles';
+ my $visibleGroups;
+
+ if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
+ # Show only users in visible groups.
+ $visibleGroups = $user->visible_groups_inherited;
+
+ if (scalar @$visibleGroups) {
+ $query .= qq{, user_group_map AS ugm
+ WHERE ugm.user_id = profiles.userid
+ AND ugm.isbless = 0
+ AND } . $dbh->sql_in('ugm.group_id', $visibleGroups);
+ }
+ } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) {
+ $visibleGroups = 1;
+ $query .= qq{, user_group_map AS ugm
+ WHERE ugm.user_id = profiles.userid
+ AND ugm.isbless = 0
+ };
+ }
+ if (!$visibleGroups) {
+ ThrowUserError('group_not_visible', { group => $group });
+ }
+
+ my $grouplist = Bugzilla::Group->flatten_group_membership($group->id);
+ $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist);
+
+ my $userids = $dbh->selectcol_arrayref($query);
+ my $user_objects = Bugzilla::User->new_from_list($userids);
+ my @users =
+ map {{
+ id => as_int($_->id),
+ real_name => as_string($_->name),
+ name => as_string($_->login),
+ email => as_string($_->email),
+ can_login => as_boolean($_->is_enabled),
+ email_enabled => as_boolean($_->email_enabled),
+ login_denied_text => as_string($_->disabledtext),
+ }} @$user_objects;
+
+ return \@users;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::Group - The API for creating, changing, and getting
+information about Groups.
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla API allows you to create Groups and
+get information about them.
+
+=head1 METHODS
+
+=head2 create
+
+=over
+
+=item B<Description>
+
+This allows you to create a new group in Bugzilla.
+
+=item B<REST>
+
+POST /rest/group
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+Some params must be set, or an error will be thrown. These params are
+marked B<Required>.
+
+=over
+
+=item C<name>
+
+B<Required> C<string> A short name for this group. Must be unique. This
+is not usually displayed in the user interface, except in a few places.
+
+=item C<description>
+
+B<Required> C<string> A human-readable name for this group. Should be
+relatively short. This is what will normally appear in the UI as the
+name of the group.
+
+=item C<user_regexp>
+
+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>
+
+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
+to it.
+
+=item C<icon_url>
+
+C<string> A URL pointing to a small icon used to identify the group.
+This icon will show up next to users' names in various parts of Bugzilla
+if they are in this group.
+
+=back
+
+=item B<Returns>
+
+A hash with one element, C<id>. This is the id of the newly-created group.
+
+=item B<Errors>
+
+=over
+
+=item 800 (Empty Group Name)
+
+You must specify a value for the C<name> field.
+
+=item 801 (Group Exists)
+
+There is already another group with the same C<name>.
+
+=item 802 (Group Missing Description)
+
+You must specify a value for the C<description> field.
+
+=item 803 (Group Regexp Invalid)
+
+You specified an invalid regular expression in the C<user_regexp> field.
+
+=back
+
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 update
+
+=over
+
+=item B<Description>
+
+This allows you to update a group in Bugzilla.
+
+=item B<REST>
+
+PUT /rest/group/<group_name_or_id>
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below. The C<ids> param will be overridden as it is pulled
+from the URL path.
+
+=item B<Params>
+
+At least C<ids> or C<names> must be set, or an error will be thrown.
+
+=over
+
+=item C<ids>
+
+B<Required> C<array> Contain ids of groups to update.
+
+=item C<names>
+
+B<Required> C<array> Contain names of groups to update.
+
+=item C<name>
+
+C<string> A new name for group.
+
+=item C<description>
+
+C<string> A new description for groups. This is what will appear in the UI
+as the name of the groups.
+
+=item C<user_regexp>
+
+C<string> A new regular expression for email. Will automatically grant
+membership to these groups to anyone with an email address that matches
+this perl regular expression.
+
+=item C<is_active>
+
+C<boolean> Set if groups are active and eligible to be used for bugs.
+True if bugs can be restricted to this group, false otherwise.
+
+=item C<icon_url>
+
+C<string> A URL pointing to an icon that will appear next to the name of
+users who are in this group.
+
+=back
+
+=item B<Returns>
+
+A C<hash> with a single field "groups". This points to an array of hashes
+with the following fields:
+
+=over
+
+=item C<id>
+
+C<int> The id of the group that was updated.
+
+=item C<changes>
+
+C<hash> The changes that were actually done on this group. The keys are
+the names of the fields that were changed, and the values are a hash
+with two keys:
+
+=over
+
+=item C<added>
+
+C<string> The values that were added to this field,
+possibly a comma-and-space-separated list if multiple values were added.
+
+=item C<removed>
+
+C<string> The values that were removed from this field, possibly a
+comma-and-space-separated list if multiple values were removed.
+
+=back
+
+=back
+
+=item B<Errors>
+
+The same as L</create>.
+
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 Group Information
+
+=head2 get
+
+=over
+
+=item B<Description>
+
+Returns information about L<Bugzilla::Group|Groups>.
+
+=item B<REST>
+
+To return information about a specific group by C<id> or C<name>:
+
+GET /rest/group/<group_id_or_name>
+
+You can also return information about more than one specific group
+by using the following in your query string:
+
+GET /rest/group?ids=1&ids=2&ids=3 or GET /group?names=ProductOne&names=Product2
+
+the returned data format is same as below.
+
+=item B<Params>
+
+If neither ids or names is passed, and you are in the creategroups or
+editusers group, then all groups will be retrieved. Otherwise, only groups
+that you have bless privileges for will be returned.
+
+=over
+
+=item C<ids>
+
+C<array> Contain ids of groups to update.
+
+=item C<names>
+
+C<array> Contain names of groups to update.
+
+=item C<membership>
+
+C<boolean> Set to 1 then a list of members of the passed groups' names and
+ids will be returned.
+
+=back
+
+=item B<Returns>
+
+If the user is a member of the "creategroups" group they will receive
+information about all groups or groups matching the criteria that they passed.
+You have to be in the creategroups group unless you're requesting membership
+information.
+
+If the user is not a member of the "creategroups" group, but they are in the
+"editusers" group or have bless privileges to the groups they require
+membership information for, the is_active, is_bug_group and user_regexp values
+are not supplied.
+
+The return value will be a hash containing group names as the keys, each group
+name will point to a hash that describes the group and has the following items:
+
+=over
+
+=item id
+
+C<int> The unique integer ID that Bugzilla uses to identify this group.
+Even if the name of the group changes, this ID will stay the same.
+
+=item name
+
+C<string> The name of the group.
+
+=item description
+
+C<string> The description of the group.
+
+=item is_bug_group
+
+C<int> Whether this groups is to be used for bug reports or is only administrative specific.
+
+=item user_regexp
+
+C<string> A regular expression that allows users to be added to this group if their login matches.
+
+=item is_active
+
+C<int> Whether this group is currently active or not.
+
+=item users
+
+C<array> An array of hashes, each hash contains a user object for one of the
+members of this group, only returned if the user sets the C<membership>
+parameter to 1, the user hash has the following items:
+
+=over
+
+=item id
+
+C<int> The id of the user.
+
+=item real_name
+
+C<string> The actual name of the user.
+
+=item email
+
+C<string> The email address of the user.
+
+=item name
+
+C<string> The login name of the user. Note that in some situations this is
+different than their email.
+
+=item can_login
+
+C<boolean> A boolean value to indicate if the user can login into bugzilla.
+
+=item email_enabled
+
+C<boolean> A boolean value to indicate if bug-related mail will be sent
+to the user or not.
+
+=item disabled_text
+
+C<string> A text field that holds the reason for disabling a user from logging
+into bugzilla, if empty then the user account is enabled otherwise it is
+disabled/closed.
+
+=back
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 51 (Invalid Object)
+
+A non existing group name was passed to the function, as a result no
+group object existed for that invalid name.
+
+=item 805 (Cannot view groups)
+
+Logged-in users are not authorized to edit bugzilla groups as they are not
+members of the creategroups group in bugzilla, or they are not authorized to
+access group member's information as they are not members of the "editusers"
+group or can bless the group.
+
+=back
+
+=item B<History>
+
+=over
+
+=item This function was added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=back
diff --git a/Bugzilla/API/1_0/Resource/Product.pm b/Bugzilla/API/1_0/Resource/Product.pm
new file mode 100644
index 000000000..0f393e207
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/Product.pm
@@ -0,0 +1,1013 @@
+# 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::API::1_0::Resource::Product;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Constants;
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::Error;
+use Bugzilla::Constants;
+
+use Moo;
+
+extends 'Bugzilla::API::1_0::Resource';
+
+##############
+# Constants #
+##############
+
+use constant READ_ONLY => qw(
+ get
+ get_accessible_products
+ get_enterable_products
+ get_selectable_products
+);
+
+use constant PUBLIC_METHODS => qw(
+ create
+ get
+ get_accessible_products
+ get_enterable_products
+ get_selectable_products
+ update
+);
+
+use constant MAPPED_FIELDS => {
+ has_unconfirmed => 'allows_unconfirmed',
+ is_open => 'is_active',
+};
+
+use constant MAPPED_RETURNS => {
+ allows_unconfirmed => 'has_unconfirmed',
+ defaultmilestone => 'default_milestone',
+ isactive => 'is_open',
+};
+
+use constant FIELD_MAP => {
+ has_unconfirmed => 'allows_unconfirmed',
+ is_open => 'isactive',
+};
+
+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;
+}
+
+############
+# Methods #
+############
+
+BEGIN { *get_products = \&get }
+
+# Get the ids of the products the user can search
+sub get_selectable_products {
+ Bugzilla->switch_to_shadow_db();
+ return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
+}
+
+# Get the ids of the products the user can enter bugs against
+sub get_enterable_products {
+ Bugzilla->switch_to_shadow_db();
+ return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
+}
+
+# Get the union of the products the user can search and enter bugs against.
+sub get_accessible_products {
+ Bugzilla->switch_to_shadow_db();
+ return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
+}
+
+# Get a list of actual products, based on list of ids or names
+sub get {
+ 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_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 $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 $products, union'ed with products found by ID to
+ # avoid duplicates
+ foreach my $product (grep { $names{lc $_->name} }
+ @$products) {
+ next if grep { $_->id == $product->id }
+ @requested_products;
+ push @requested_products, $product;
+ }
+ }
+
+ # If we just requested a specific type of products without
+ # specifying ids or names, then return the entire list.
+ if (!defined $params->{ids} && !defined $params->{names}) {
+ @requested_products = @$products;
+ }
+
+ # Now create a result entry for each.
+ my @products = map { $self->_product_to_hash($params, $_) }
+ @requested_products;
+ return { products => \@products };
+}
+
+sub create {
+ my ($self, $params) = @_;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->user->in_group('editcomponents')
+ || ThrowUserError("auth_failure", { group => "editcomponents",
+ action => "add",
+ object => "products"});
+ # Create product
+ my $args = {
+ name => $params->{name},
+ description => $params->{description},
+ version => $params->{version},
+ defaultmilestone => $params->{default_milestone},
+ # create_series has no default value.
+ create_series => defined $params->{create_series} ?
+ $params->{create_series} : 1
+ };
+ foreach my $field (qw(has_unconfirmed is_open classification)) {
+ if (defined $params->{$field}) {
+ my $name = FIELD_MAP->{$field} || $field;
+ $args->{$name} = $params->{$field};
+ }
+ }
+ my $product = Bugzilla::Product->create($args);
+ return { id => as_int($product->id) };
+}
+
+sub update {
+ my ($self, $params) = @_;
+
+ my $dbh = Bugzilla->dbh;
+
+ Bugzilla->login(LOGIN_REQUIRED);
+ Bugzilla->user->in_group('editcomponents')
+ || ThrowUserError("auth_failure", { group => "editcomponents",
+ action => "edit",
+ object => "products" });
+
+ defined($params->{names}) || defined($params->{ids})
+ || ThrowCodeError('params_required',
+ { function => 'Product.update', params => ['ids', 'names'] });
+
+ my $product_objects = params_to_objects($params, 'Bugzilla::Product');
+
+ my $values = translate($params, MAPPED_FIELDS);
+
+ # We delete names and ids to keep only new values to set.
+ delete $values->{names};
+ delete $values->{ids};
+
+ $dbh->bz_start_transaction();
+ foreach my $product (@$product_objects) {
+ $product->set_all($values);
+ }
+
+ my %changes;
+ foreach my $product (@$product_objects) {
+ my $returned_changes = $product->update();
+ $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS);
+ }
+ $dbh->bz_commit_transaction();
+
+ my @result;
+ foreach my $product (@$product_objects) {
+ my %hash = (
+ id => $product->id,
+ changes => {},
+ );
+
+ foreach my $field (keys %{ $changes{$product->id} }) {
+ my $change = $changes{$product->id}->{$field};
+ $hash{changes}{$field} = {
+ removed => as_string($change->[0]),
+ added => as_string($change->[1])
+ };
+ }
+
+ push(@result, \%hash);
+ }
+
+ return { products => \@result };
+}
+
+sub _product_to_hash {
+ my ($self, $params, $product) = @_;
+
+ my $field_data = {
+ id => as_int($product->id),
+ name => as_string($product->name),
+ description => as_string($product->description),
+ is_active => as_boolean($product->is_active),
+ default_milestone => as_string($product->default_milestone),
+ has_unconfirmed => as_boolean($product->allows_unconfirmed),
+ classification => as_string($product->classification->name),
+ };
+ if (filter_wants($params, 'components')) {
+ $field_data->{components} = [map {
+ $self->_component_to_hash($_, $params)
+ } @{$product->components}];
+ }
+ if (filter_wants($params, 'versions')) {
+ $field_data->{versions} = [map {
+ $self->_version_to_hash($_, $params)
+ } @{$product->versions}];
+ }
+ if (filter_wants($params, 'milestones')) {
+ $field_data->{milestones} = [map {
+ $self->_milestone_to_hash($_, $params)
+ } @{$product->milestones}];
+ }
+ return filter($params, $field_data);
+}
+
+sub _component_to_hash {
+ my ($self, $component, $params) = @_;
+ my $field_data = filter $params, {
+ id => as_int($component->id),
+ name => as_string($component->name),
+ description => as_string($component->description),
+ default_assigned_to =>
+ as_email($component->default_assignee->login),
+ default_qa_contact =>
+ as_email($component->default_qa_contact ?
+ $component->default_qa_contact->login : ""),
+ sort_key => 0, # sort_key is returned to match Bug.fields
+ is_active => as_boolean($component->is_active),
+ }, undef, 'components';
+
+ if (filter_wants($params, 'flag_types', undef, 'components')) {
+ $field_data->{flag_types} = {
+ bug =>
+ [map {
+ $self->_flag_type_to_hash($_)
+ } @{$component->flag_types->{'bug'}}],
+ attachment =>
+ [map {
+ $self->_flag_type_to_hash($_)
+ } @{$component->flag_types->{'attachment'}}],
+ };
+ }
+
+ return $field_data;
+}
+
+sub _flag_type_to_hash {
+ my ($self, $flag_type, $params) = @_;
+ return filter $params, {
+ id => as_int($flag_type->id),
+ name => as_string($flag_type->name),
+ description => as_string($flag_type->description),
+ cc_list => as_string($flag_type->cc_list),
+ sort_key => as_int($flag_type->sortkey),
+ is_active => as_boolean($flag_type->is_active),
+ is_requestable => as_boolean($flag_type->is_requestable),
+ is_requesteeble => as_boolean($flag_type->is_requesteeble),
+ is_multiplicable => as_boolean($flag_type->is_multiplicable),
+ grant_group => as_int($flag_type->grant_group_id),
+ request_group => as_int($flag_type->request_group_id),
+ }, undef, 'flag_types';
+}
+
+sub _version_to_hash {
+ my ($self, $version, $params) = @_;
+ return filter $params, {
+ id => as_int($version->id),
+ name => as_string($version->name),
+ sort_key => 0, # sort_key is returened to match Bug.fields
+ is_active => as_boolean($version->is_active),
+ }, undef, 'versions';
+}
+
+sub _milestone_to_hash {
+ my ($self, $milestone, $params) = @_;
+ return filter $params, {
+ id => as_int($milestone->id),
+ name => as_string($milestone->name),
+ sort_key => as_int($milestone->sortkey),
+ is_active => as_boolean($milestone->is_active),
+ }, undef, 'milestones';
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::Product - The Product API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla API allows you to list the available Products and
+get information about them.
+
+=head1 METHODS
+
+=head2 get_selectable_products
+
+=over
+
+=item B<Description>
+
+Returns a list of the ids of the products the user can search on.
+
+=item B<REST>
+
+GET /rest/product_selectable
+
+the returned data format is same as below.
+
+=item B<Params> (none)
+
+=item B<Returns>
+
+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
+
+=over
+
+=item B<Description>
+
+Returns a list of the ids of the products the user can enter bugs
+against.
+
+=item B<REST>
+
+GET /rest/product_enterable
+
+the returned data format is same as below.
+
+=item B<Params> (none)
+
+=item B<Returns>
+
+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_accessible_products
+
+=over
+
+=item B<Description>
+
+Returns a list of the ids of the products the user can search or enter
+bugs against.
+
+=item B<REST>
+
+GET /rest/product_accessible
+
+the returned data format is same as below.
+
+=item B<Params> (none)
+
+=item B<Returns>
+
+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
+
+=over
+
+=item B<Description>
+
+Returns a list of information about the products passed to it.
+
+B<Note>: You must at least specify one of C<ids> or C<names>.
+
+B<Note>: Can also be called as "get_products" for compatibilty with Bugzilla 3.0 API.
+
+=item B<REST>
+
+To return information about a specific groups of products such as
+C<accessible>, C<selectable>, or C<enterable>:
+
+GET /rest/product?type=accessible
+
+To return information about a specific product by C<id> or C<name>:
+
+GET /rest/product/<product_id_or_name>
+
+You can also return information about more than one specific product
+by using the following in your query string:
+
+GET /rest/product?ids=1&ids=2&ids=3 or GET /product?names=ProductOne&names=Product2
+
+the returned data format is same as below.
+
+=item B<Params>
+
+In addition to the parameters below, this method also accepts the
+standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and
+L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments.
+
+This RPC call supports sub field restrictions.
+
+=over
+
+=item C<ids>
+
+An array of product ids
+
+=item C<names>
+
+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>
+
+A hash containing one item, C<products>, that is an array of
+hashes. Each hash describes a product, and has the following items:
+
+=over
+
+=item C<id>
+
+C<int> An integer id uniquely identifying the product in this installation only.
+
+=item C<name>
+
+C<string> The name of the product. This is a unique identifier for the
+product.
+
+=item C<description>
+
+C<string> A description of the product, which may contain HTML.
+
+=item C<is_active>
+
+C<boolean> A boolean indicating if the product is active.
+
+=item C<default_milestone>
+
+C<string> The name of the default milestone for the product.
+
+=item C<has_unconfirmed>
+
+C<boolean> Indicates whether the UNCONFIRMED bug status is available
+for this product.
+
+=item C<classification>
+
+C<string> The classification name for the product.
+
+=item C<components>
+
+C<array> An array of hashes, where each hash describes a component, and has the
+following items:
+
+=over
+
+=item C<id>
+
+C<int> An integer id uniquely identifying the component in this installation
+only.
+
+=item C<name>
+
+C<string> The name of the component. This is a unique identifier for this
+component.
+
+=item C<description>
+
+C<string> A description of the component, which may contain HTML.
+
+=item C<default_assigned_to>
+
+C<string> The login name of the user to whom new bugs will be assigned by
+default.
+
+=item C<default_qa_contact>
+
+C<string> The login name of the user who will be set as the QA Contact for
+new bugs by default. Empty string if the QA contact is not defined.
+
+=item C<sort_key>
+
+C<int> Components, when displayed in a list, are sorted first by this integer
+and then secondly by their name.
+
+=item C<is_active>
+
+C<boolean> A boolean indicating if the component is active. Inactive
+components are not enabled for new bugs.
+
+=item C<flag_types>
+
+A hash containing the two items C<bug> and C<attachment> that each contains an
+array of hashes, where each hash describes a flagtype, and has the
+following items:
+
+=over
+
+=item C<id>
+
+C<int> Returns the ID of the flagtype.
+
+=item C<name>
+
+C<string> Returns the name of the flagtype.
+
+=item C<description>
+
+C<string> Returns the description of the flagtype.
+
+=item C<cc_list>
+
+C<string> Returns the concatenated CC list for the flagtype, as a single string.
+
+=item C<sort_key>
+
+C<int> Returns the sortkey of the flagtype.
+
+=item C<is_active>
+
+C<boolean> Returns whether the flagtype is active or disabled. Flags being
+in a disabled flagtype are not deleted. It only prevents you from
+adding new flags to it.
+
+=item C<is_requestable>
+
+C<boolean> Returns whether you can request for the given flagtype
+(i.e. whether the '?' flag is available or not).
+
+=item C<is_requesteeble>
+
+C<boolean> Returns whether you can ask someone specifically or not.
+
+=item C<is_multiplicable>
+
+C<boolean> Returns whether you can have more than one flag for the given
+flagtype in a given bug/attachment.
+
+=item C<grant_group>
+
+C<int> the group id that is allowed to grant/deny flags of this type.
+If the item is not included all users are allowed to grant/deny this
+flagtype.
+
+=item C<request_group>
+
+C<int> the group id that is allowed to request the flag if the flag
+is of the type requestable. If the item is not included all users
+are allowed request this flagtype.
+
+=back
+
+=back
+
+=item C<versions>
+
+C<array> An array of hashes, where each hash describes a version, and has the
+following items: C<name>, C<sort_key> and C<is_active>.
+
+=item C<milestones>
+
+C<array> An array of hashes, where each hash describes a milestone, and has the
+following items: C<name>, C<sort_key> and C<is_active>.
+
+=back
+
+Note, that if the user tries to access a product that is not in the
+list of accessible products for the user, or a product that does not
+exist, that is silently ignored, and no information about that product
+is returned.
+
+=item B<Errors> (none)
+
+=item B<History>
+
+=over
+
+=item In Bugzilla B<4.2>, C<names> was added as an input parameter.
+
+=item In Bugzilla B<4.2>, C<classification>, C<components>, C<versions>,
+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 In Bugzilla B<4.4>, C<flag_types> was added to the fields returned
+by C<get>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 Product Creation and Modification
+
+=head2 create
+
+=over
+
+=item B<Description>
+
+This allows you to create a new product in Bugzilla.
+
+=item B<REST>
+
+POST /rest/product
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+Some params must be set, or an error will be thrown. These params are
+marked B<Required>.
+
+=over
+
+=item C<name>
+
+B<Required> C<string> The name of this product. Must be globally unique
+within Bugzilla.
+
+=item C<description>
+
+B<Required> C<string> A description for this product. Allows some simple HTML.
+
+=item C<version>
+
+B<Required> C<string> The default version for this product.
+
+=item C<has_unconfirmed>
+
+C<boolean> Allow the UNCONFIRMED status to be set on bugs in this product.
+Default: true.
+
+=item C<classification>
+
+C<string> The name of the Classification which contains this product.
+
+=item C<default_milestone>
+
+C<string> The default milestone for this product. Default '---'.
+
+=item C<is_open>
+
+C<boolean> True if the product is currently allowing bugs to be entered
+into it. Default: true.
+
+=item C<create_series>
+
+C<boolean> True if you want series for New Charts to be created for this
+new product. Default: true.
+
+=back
+
+=item B<Returns>
+
+A hash with one element, id. This is the id of the newly-filed product.
+
+=item B<Errors>
+
+=over
+
+=item 51 (Classification does not exist)
+
+You must specify an existing classification name.
+
+=item 700 (Product blank name)
+
+You must specify a non-blank name for this product.
+
+=item 701 (Product name too long)
+
+The name specified for this product was longer than the maximum
+allowed length.
+
+=item 702 (Product name already exists)
+
+You specified the name of a product that already exists.
+(Product names must be globally unique in Bugzilla.)
+
+=item 703 (Product must have description)
+
+You must specify a description for this product.
+
+=item 704 (Product must have version)
+
+You must specify a version for this product.
+
+=back
+
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 update
+
+=over
+
+=item B<Description>
+
+This allows you to update a product in Bugzilla.
+
+=item B<REST>
+
+PUT /rest/product/<product_id_or_name>
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below. The C<ids> and C<names> params will be overridden as
+it is pulled from the URL path.
+
+=item B<Params>
+
+B<Note:> The following parameters specify which products you are updating.
+You must set one or both of these parameters.
+
+=over
+
+=item C<ids>
+
+C<array> of C<int>s. Numeric ids of the products that you wish to update.
+
+=item C<names>
+
+C<array> of C<string>s. Names of the products that you wish to update.
+
+=back
+
+B<Note:> The following parameters specify the new values you want to set for
+the products you are updating.
+
+=over
+
+=item C<name>
+
+C<string> A new name for this product. If you try to set this while updating more
+than one product, an error will occur, as product names must be unique.
+
+=item C<default_milestone>
+
+C<string> When a new bug is filed, what milestone does it get by default if the
+user does not choose one? Must represent a milestone that is valid for this product.
+
+=item C<description>
+
+C<string> Update the long description for these products to this value.
+
+=item C<has_unconfirmed>
+
+C<boolean> Allow the UNCONFIRMED status to be set on bugs in products.
+
+=item C<is_open>
+
+C<boolean> True if the product is currently allowing bugs to be entered
+into it, False otherwise.
+
+=back
+
+=item B<Returns>
+
+A C<hash> with a single field "products". This points to an array of hashes
+with the following fields:
+
+=over
+
+=item C<id>
+
+C<int> The id of the product that was updated.
+
+=item C<changes>
+
+C<hash> The changes that were actually done on this product. The keys are
+the names of the fields that were changed, and the values are a hash
+with two keys:
+
+=over
+
+=item C<added>
+
+C<string> The value that this field was changed to.
+
+=item C<removed>
+
+C<string> The value that was previously set in this field.
+
+=back
+
+Note that booleans will be represented with the strings '1' and '0'.
+
+Here's an example of what a return value might look like:
+
+ {
+ products => [
+ {
+ id => 123,
+ changes => {
+ name => {
+ removed => 'FooName',
+ added => 'BarName'
+ },
+ has_unconfirmed => {
+ removed => '1',
+ added => '0',
+ }
+ }
+ }
+ ]
+ }
+
+=item B<Errors>
+
+=over
+
+=item 700 (Product blank name)
+
+You must specify a non-blank name for this product.
+
+=item 701 (Product name too long)
+
+The name specified for this product was longer than the maximum
+allowed length.
+
+=item 702 (Product name already exists)
+
+You specified the name of a product that already exists.
+(Product names must be globally unique in Bugzilla.)
+
+=item 703 (Product must have description)
+
+You must specify a description for this product.
+
+=item 705 (Product must define a default milestone)
+
+You must define a default milestone.
+
+=back
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=item get_products
+
+=back
diff --git a/Bugzilla/API/1_0/Resource/User.pm b/Bugzilla/API/1_0/Resource/User.pm
new file mode 100644
index 000000000..d2c869907
--- /dev/null
+++ b/Bugzilla/API/1_0/Resource/User.pm
@@ -0,0 +1,1151 @@
+# 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::API::1_0::Resource::User;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::API::1_0::Constants;
+use Bugzilla::API::1_0::Util;
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::User;
+use Bugzilla::Util qw(trim detaint_natural);
+
+use List::Util qw(first min);
+use Moo;
+
+extends 'Bugzilla::API::1_0::Resource';
+
+##############
+# Constants #
+##############
+
+# Don't need auth to login
+use constant LOGIN_EXEMPT => {
+ login => 1,
+ offer_account_by_email => 1,
+};
+
+use constant READ_ONLY => qw(
+ get
+ login
+ logout
+ valid_login
+);
+
+use constant PUBLIC_METHODS => qw(
+ create
+ get
+ login
+ logout
+ offer_account_by_email
+ update
+ valid_login
+);
+
+use constant MAPPED_FIELDS => {
+ email => 'login',
+ full_name => 'name',
+ login_denied_text => 'disabledtext',
+ email_enabled => 'disable_mail'
+};
+
+use constant MAPPED_RETURNS => {
+ login_name => 'email',
+ realname => 'full_name',
+ disabledtext => 'login_denied_text',
+ disable_mail => 'email_enabled'
+};
+
+sub REST_RESOURCES {
+ my $rest_resources = [
+ qr{^/login$}, {
+ GET => {
+ method => 'login'
+ },
+ POST => {
+ method => 'login'
+ }
+ },
+ qr{^/logout$}, {
+ GET => {
+ method => 'logout'
+ }
+ },
+ qr{^/valid_login$}, {
+ GET => {
+ method => 'valid_login'
+ }
+ },
+ qr{^/user$}, {
+ GET => {
+ method => 'get'
+ },
+ POST => {
+ method => 'create',
+ success_code => STATUS_CREATED
+ }
+ },
+ qr{^/user/([^/]+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ },
+ PUT => {
+ method => 'update',
+ params => sub {
+ my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+ return { $param => [ $_[0] ] };
+ }
+ }
+ }
+ ];
+ return $rest_resources;
+}
+
+############
+# Methods #
+############
+
+sub login {
+ my ($self, $params) = @_;
+
+ # Check to see if we are already logged in
+ my $user = Bugzilla->user;
+ if ($user->id) {
+ return $self->_login_to_hash($user);
+ }
+
+ # Username and password params are required
+ foreach my $param ("login", "password") {
+ (defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
+ || ThrowCodeError('param_required', { param => $param });
+ }
+
+ $user = Bugzilla->login();
+ return $self->_login_to_hash($user);
+}
+
+sub logout {
+ my $self = shift;
+ Bugzilla->logout;
+}
+
+sub valid_login {
+ my ($self, $params) = @_;
+ defined $params->{login}
+ || ThrowCodeError('param_required', { param => 'login' });
+ Bugzilla->login();
+ if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
+ return as_boolean(1);
+ }
+ return as_boolean(0);
+}
+
+sub offer_account_by_email {
+ my $self = shift;
+ my ($params) = @_;
+ my $email = trim($params->{email})
+ || ThrowCodeError('param_required', { param => 'email' });
+
+ Bugzilla->user->check_account_creation_enabled;
+ Bugzilla->user->check_and_send_account_creation_confirmation($email);
+ return undef;
+}
+
+sub create {
+ my $self = shift;
+ my ($params) = @_;
+
+ Bugzilla->user->in_group('editusers')
+ || ThrowUserError("auth_failure", { group => "editusers",
+ action => "add",
+ object => "users"});
+
+ my $email = trim($params->{email})
+ || ThrowCodeError('param_required', { param => 'email' });
+ my $realname = trim($params->{full_name});
+ my $password = trim($params->{password}) || '*';
+
+ my $user = Bugzilla::User->create({
+ login_name => $email,
+ realname => $realname,
+ cryptpassword => $password
+ });
+
+ return { id => as_int($user->id) };
+}
+
+
+# function to return user information by passing either user ids or
+# login names or both together:
+# $call = $rpc->call( 'User.get', { ids => [1,2,3],
+# names => ['testusera@redhat.com', 'testuserb@redhat.com'] });
+sub get {
+ my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
+
+ Bugzilla->switch_to_shadow_db();
+
+ defined($params->{names}) || defined($params->{ids})
+ || defined($params->{match})
+ || ThrowCodeError('params_required',
+ { function => 'User.get', params => ['ids', 'names', 'match'] });
+
+ my @user_objects;
+ @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }
+ if $params->{names};
+
+ # start filtering to remove duplicate user ids
+ my %unique_users = map { $_->id => $_ } @user_objects;
+ @user_objects = values %unique_users;
+
+ my @users;
+
+ # If the user is not logged in: Return an error if they passed any user ids.
+ # Otherwise, return a limited amount of information based on login names.
+ if (!Bugzilla->user->id){
+ if ($params->{ids}){
+ ThrowUserError("user_access_by_id_denied");
+ }
+ if ($params->{match}) {
+ ThrowUserError('user_access_by_match_denied');
+ }
+ my $in_group = $self->_filter_users_by_group(
+ \@user_objects, $params);
+ @users = map { filter $params, {
+ id => as_int($_->id),
+ real_name => as_string($_->name),
+ name => as_email($_->login),
+ } } @$in_group;
+
+ return { users => \@users };
+ }
+
+ my $obj_by_ids;
+ $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
+
+ # obj_by_ids are only visible to the user if they can see
+ # the otheruser, for non visible otheruser throw an error
+ foreach my $obj (@$obj_by_ids) {
+ if (Bugzilla->user->can_see_user($obj)){
+ if (!$unique_users{$obj->id}) {
+ push (@user_objects, $obj);
+ $unique_users{$obj->id} = $obj;
+ }
+ }
+ else {
+ ThrowUserError('auth_failure', {reason => "not_visible",
+ action => "access",
+ object => "user",
+ userid => $obj->id});
+ }
+ }
+
+ # User Matching
+ my $limit = Bugzilla->params->{maxusermatches};
+ if ($params->{limit}) {
+ detaint_natural($params->{limit})
+ || ThrowCodeError('param_must_be_numeric',
+ { function => 'Bugzilla::API::1_0::Resource::User::match',
+ param => 'limit' });
+ $limit = $limit ? min($params->{limit}, $limit) : $params->{limit};
+ }
+
+ my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1;
+ foreach my $match_string (@{ $params->{'match'} || [] }) {
+ my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled);
+ foreach my $user (@$matched) {
+ if (!$unique_users{$user->id}) {
+ push(@user_objects, $user);
+ $unique_users{$user->id} = $user;
+ }
+ }
+ }
+
+ my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
+ foreach my $user (@$in_group) {
+ my $user_info = filter $params, {
+ id => as_int($user->id),
+ real_name => as_string($user->name),
+ name => as_email($user->login),
+ email => as_email($user->email),
+ can_login => as_boolean($user->is_enabled ? 1 : 0),
+ };
+
+ if (Bugzilla->user->in_group('editusers')) {
+ $user_info->{email_enabled} = as_boolean($user->email_enabled);
+ $user_info->{login_denied_text} = as_string($user->disabledtext);
+ }
+
+ if (Bugzilla->user->id == $user->id) {
+ if (filter_wants($params, 'saved_searches')) {
+ $user_info->{saved_searches} = [
+ map { $self->_query_to_hash($_) } @{ $user->queries }
+ ];
+ }
+ if (filter_wants($params, 'saved_reports')) {
+ $user_info->{saved_reports} = [
+ map { $self->_report_to_hash($_) } @{ $user->reports }
+ ];
+ }
+ }
+
+ if (filter_wants($params, 'groups')) {
+ if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) {
+ $user_info->{groups} = [
+ map { $self->_group_to_hash($_) } @{ $user->groups }
+ ];
+ }
+ else {
+ $user_info->{groups} = $self->_filter_bless_groups($user->groups);
+ }
+ }
+
+ push(@users, $user_info);
+ }
+
+ return { users => \@users };
+}
+
+sub update {
+ my ($self, $params) = @_;
+
+ my $dbh = Bugzilla->dbh;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ # Reject access if there is no sense in continuing.
+ $user->in_group('editusers')
+ || ThrowUserError("auth_failure", {group => "editusers",
+ action => "edit",
+ object => "users"});
+
+ defined($params->{names}) || defined($params->{ids})
+ || ThrowCodeError('params_required',
+ { function => 'User.update', params => ['ids', 'names'] });
+
+ my $user_objects = params_to_objects($params, 'Bugzilla::User');
+
+ my $values = translate($params, MAPPED_FIELDS);
+
+ # We delete names and ids to keep only new values to set.
+ delete $values->{names};
+ delete $values->{ids};
+
+ $dbh->bz_start_transaction();
+ foreach my $user (@$user_objects){
+ $user->set_all($values);
+ }
+
+ my %changes;
+ foreach my $user (@$user_objects){
+ my $returned_changes = $user->update();
+ $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
+ }
+ $dbh->bz_commit_transaction();
+
+ my @result;
+ foreach my $user (@$user_objects) {
+ my %hash = (
+ id => $user->id,
+ changes => {},
+ );
+
+ foreach my $field (keys %{ $changes{$user->id} }) {
+ my $change = $changes{$user->id}->{$field};
+ # We normalize undef to an empty string, so that the API
+ # stays consistent for things that can become empty.
+ $change->[0] = '' if !defined $change->[0];
+ $change->[1] = '' if !defined $change->[1];
+ # We also flatten arrays (used by groups and blessed_groups)
+ $change->[0] = join(',', @{$change->[0]}) if ref $change->[0];
+ $change->[1] = join(',', @{$change->[1]}) if ref $change->[1];
+
+ $hash{changes}{$field} = {
+ removed => as_string($change->[0]),
+ added => as_string($change->[1])
+ };
+ }
+
+ push(@result, \%hash);
+ }
+
+ return { users => \@result };
+}
+
+sub _filter_users_by_group {
+ my ($self, $users, $params) = @_;
+ my ($group_ids, $group_names) = @$params{qw(group_ids groups)};
+
+ # If no groups are specified, we return all users.
+ return $users if (!$group_ids and !$group_names);
+
+ my $user = Bugzilla->user;
+ my (@groups, %groups);
+
+ if ($group_ids) {
+ @groups = map { Bugzilla::Group->check({ id => $_ }) } @$group_ids;
+ $groups{$_->id} = $_ foreach @groups;
+ }
+ if ($group_names) {
+ foreach my $name (@$group_names) {
+ my $group = Bugzilla::Group->check({ name => $name, _error => 'invalid_group_name' });
+ $user->in_group($group) || ThrowUserError('invalid_group_name', { name => $name });
+ $groups{$group->id} = $group;
+ }
+ }
+ @groups = values %groups;
+
+ my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users;
+ return \@in_group;
+}
+
+sub _user_in_any_group {
+ my ($self, $user, $groups) = @_;
+ foreach my $group (@$groups) {
+ return 1 if $user->in_group($group);
+ }
+ return 0;
+}
+
+sub _filter_bless_groups {
+ my ($self, $groups) = @_;
+ my $user = Bugzilla->user;
+
+ my @filtered_groups;
+ foreach my $group (@$groups) {
+ next unless $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 => as_int($group->id),
+ name => as_string($group->name),
+ description => as_string($group->description),
+ };
+ return $item;
+}
+
+sub _query_to_hash {
+ my ($self, $query) = @_;
+ my $item = {
+ id => as_int($query->id),
+ name => as_string($query->name),
+ query => as_string($query->url),
+ };
+ return $item;
+}
+
+sub _report_to_hash {
+ my ($self, $report) = @_;
+ my $item = {
+ id => as_int($report->id),
+ name => as_string($report->name),
+ query => as_string($report->query),
+ };
+ return $item;
+}
+
+sub _login_to_hash {
+ my ($self, $user) = @_;
+ my $item = { id => as_int($user->id) };
+ if ($user->{_login_token}) {
+ $item->{'token'} = $user->id . "-" . $user->{_login_token};
+ }
+ return $item;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::API::1_0::Resource::User - The User Account and Login API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla API allows you to create User Accounts and
+log in/out using an existing account.
+
+=head1 METHODS
+
+The authentication methods listed here are now deprecated, and will be removed
+in the release after Bugzilla 5.0. The correct way to authenticate when making
+calls is noted in L<Bugzilla::API::Server>.
+
+=head2 login
+
+B<DEPRECATED>
+
+=over
+
+=item B<Description>
+
+Logging in, with a username and password, is required for many
+Bugzilla installations, in order to search for bugs, post new bugs,
+etc. This method logs in an user.
+
+=item B<Params>
+
+=over
+
+=item C<login> (string) - The user's login name.
+
+=item C<password> (string) - The user's password.
+
+=item C<restrict_login> (bool) B<Optional> - If set to a true value,
+the token returned by this method will only be valid from the IP address
+which called this method.
+
+=back
+
+=item B<Returns>
+
+On success, a hash containing two items, C<id>, the numeric id of the
+user that was logged in, and a C<token> which can be passed in
+the parameters as authentication in other calls. The token can be sent
+along with any future requests to the webservice, for the duration of the
+session, i.e. till L<User.logout|/logout> is called.
+
+=item B<Errors>
+
+=over
+
+=item 300 (Invalid Username or Password)
+
+The username does not exist, or the password is wrong.
+
+=item 301 (Login Disabled)
+
+The ability to login with this account has been disabled. A reason may be
+specified with the error.
+
+=item 305 (New Password Required)
+
+The current password is correct, but the user is asked to change
+their password.
+
+=item 50 (Param Required)
+
+A login or password parameter was not provided.
+
+=back
+
+=item B<History>
+
+=over
+
+=item C<remember> was removed in Bugzilla B<5.0> as this method no longer
+creates a login cookie.
+
+=item C<restrict_login> was added in Bugzilla B<5.0>.
+
+=item C<token> was added in Bugzilla B<4.4.3>.
+
+=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys.
+
+=back
+
+=back
+
+=head2 logout
+
+B<DEPRECATED>
+
+=over
+
+=item B<Description>
+
+Log out the user. Does nothing if there is no user logged in.
+
+=item B<Params> (none)
+
+=item B<Returns> (nothing)
+
+=item B<Errors> (none)
+
+=back
+
+=head2 valid_login
+
+B<DEPRECATED>
+
+=over
+
+=item B<Description>
+
+This method will verify whether a client's cookies or current login
+token is still valid or have expired. A valid username must be provided
+as well that matches.
+
+=item B<Params>
+
+=over
+
+=item C<login>
+
+The login name that matches the provided cookies or token.
+
+=item C<token>
+
+(string) Persistent login token current being used for authentication (optional).
+Cookies passed by client will be used before the token if both provided.
+
+=back
+
+=item B<Returns>
+
+Returns true/false depending on if the current cookies or token are valid
+for the provided username.
+
+=item B<Errors> (none)
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<5.0>.
+
+=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys.
+
+=back
+
+=back
+
+=head1 Account Creation and Modification
+
+=head2 offer_account_by_email
+
+=over
+
+=item B<Description>
+
+Sends an email to the user, offering to create an account. The user
+will have to click on a URL in the email, and choose their password
+and real name.
+
+This is the recommended way to create a Bugzilla account.
+
+=item B<Param>
+
+=over
+
+=item C<email> (string) - the email to send the offer to.
+
+=back
+
+=item B<Returns> (nothing)
+
+=item B<Errors>
+
+=over
+
+=item 500 (Account Already Exists)
+
+An account with that email address already exists in Bugzilla.
+
+=item 501 (Illegal Email Address)
+
+This Bugzilla does not allow you to create accounts with the format of
+email address you specified. Account creation may be entirely disabled.
+
+=back
+
+=back
+
+=head2 create
+
+=over
+
+=item B<Description>
+
+Creates a user account directly in Bugzilla, password and all.
+Instead of this, you should use L</offer_account_by_email> when
+possible, because that makes sure that the email address specified can
+actually receive an email. This function does not check that.
+
+You must be logged in and have the C<editusers> privilege in order to
+call this function.
+
+=item B<REST>
+
+POST /rest/user
+
+The params to include in the POST body as well as the returned data format,
+are the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<email> (string) - The email address for the new user.
+
+=item C<full_name> (string) B<Optional> - The user's full name. Will
+be set to empty if not specified.
+
+=item C<password> (string) B<Optional> - The password for the new user
+account, in plain text. It will be stripped of leading and trailing
+whitespace. If blank or not specified, the newly created account will
+exist in Bugzilla, but will not be allowed to log in using DB
+authentication until a password is set either by the user (through
+resetting their password) or by the administrator.
+
+=back
+
+=item B<Returns>
+
+A hash containing one item, C<id>, the numeric id of the user that was
+created.
+
+=item B<Errors>
+
+The same as L</offer_account_by_email>. If a password is specified,
+the function may also throw:
+
+=over
+
+=item 502 (Password Too Short)
+
+The password specified is too short. (Usually, this means the
+password is under three characters.)
+
+=back
+
+=item B<History>
+
+=over
+
+=item Error 503 (Password Too Long) removed in Bugzilla B<3.6>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head2 update
+
+=over
+
+=item B<Description>
+
+Updates user accounts in Bugzilla.
+
+=item B<REST>
+
+PUT /rest/user/<user_id_or_name>
+
+The params to include in the PUT body as well as the returned data format,
+are the same as below. The C<ids> and C<names> params are overridden as they
+are pulled from the URL path.
+
+=item B<Params>
+
+=over
+
+=item C<ids>
+
+C<array> Contains ids of user to update.
+
+=item C<names>
+
+C<array> Contains email/login of user to update.
+
+=item C<full_name>
+
+C<string> The new name of the user.
+
+=item C<email>
+
+C<string> The email of the user. Note that email used to login to bugzilla.
+Also note that you can only update one user at a time when changing the
+login name / email. (An error will be thrown if you try to update this field
+for multiple users at once.)
+
+=item C<password>
+
+C<string> The password of the user.
+
+=item C<email_enabled>
+
+C<boolean> A boolean value to enable/disable sending bug-related mail to the user.
+
+=item C<login_denied_text>
+
+C<string> A text field that holds the reason for disabling a user from logging
+into bugzilla, if empty then the user account is enabled otherwise it is
+disabled/closed.
+
+=item C<groups>
+
+C<hash> These specify the groups that this user is directly a member of.
+To set these, you should pass a hash as the value. The hash may contain
+the following fields:
+
+=over
+
+=item C<add> An array of C<int>s or C<string>s. The group ids or group names
+that the user should be added to.
+
+=item C<remove> An array of C<int>s or C<string>s. The group ids or group names
+that the user should be removed from.
+
+=item C<set> An array of C<int>s or C<string>s. An exact set of group ids
+and group names that the user should be a member of. NOTE: This does not
+remove groups from the user where the person making the change does not
+have the bless privilege for.
+
+If you specify C<set>, then C<add> and C<remove> will be ignored. A group in
+both the C<add> and C<remove> list will be added. Specifying a group that the
+user making the change does not have bless rights will generate an error.
+
+=back
+
+=item C<bless_groups>
+
+C<hash> - This is the same as groups, but affects what groups a user
+has direct membership to bless that group. It takes the same inputs as
+groups.
+
+=back
+
+=item B<Returns>
+
+A C<hash> with a single field "users". This points to an array of hashes
+with the following fields:
+
+=over
+
+=item C<id>
+
+C<int> The id of the user that was updated.
+
+=item C<changes>
+
+C<hash> The changes that were actually done on this user. The keys are
+the names of the fields that were changed, and the values are a hash
+with two keys:
+
+=over
+
+=item C<added>
+
+C<string> The values that were added to this field,
+possibly a comma-and-space-separated list if multiple values were added.
+
+=item C<removed>
+
+C<string> The values that were removed from this field, possibly a
+comma-and-space-separated list if multiple values were removed.
+
+=back
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 51 (Bad Login Name)
+
+You passed an invalid login name in the "names" array.
+
+=item 304 (Authorization Required)
+
+Logged-in users are not authorized to edit other users.
+
+=back
+
+=item B<History>
+
+=over
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 User Info
+
+=head2 get
+
+=over
+
+=item B<Description>
+
+Gets information about user accounts in Bugzilla.
+
+=item B<REST>
+
+To get information about a single user:
+
+GET /rest/user/<user_id_or_name>
+
+To search for users by name, group using URL params same as below:
+
+GET /rest/user
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified.
+
+B<Note>: Users will not be returned more than once, so even if a user
+is matched by more than one argument, only one user will be returned.
+
+In addition to the parameters below, this method also accepts the
+standard L<include_fields|Bugzilla::API::1_0::Resource/include_fields> and
+L<exclude_fields|Bugzilla::API::1_0::Resource/exclude_fields> arguments.
+
+=over
+
+=item C<ids> (array)
+
+An array of integers, representing user ids.
+
+Logged-out users cannot pass this parameter to this function. If they try,
+they will get an error. Logged-in users will get an error if they specify
+the id of a user they cannot see.
+
+=item C<names> (array)
+
+An array of login names (strings).
+
+=item C<match> (array)
+
+An array of strings. This works just like "user matching" in
+Bugzilla itself. Users will be returned whose real name or login name
+contains any one of the specified strings. Users that you cannot see will
+not be included in the returned list.
+
+Most installations have a limit on how many matches are returned for
+each string, which defaults to 1000 but can be changed by the Bugzilla
+administrator.
+
+Logged-out users cannot use this argument, and an error will be thrown
+if they try. (This is to make it harder for spammers to harvest email
+addresses from Bugzilla, and also to enforce the user visibility
+restrictions that are implemented on some Bugzillas.)
+
+=item C<limit> (int)
+
+Limit the number of users matched by the C<match> parameter. If value
+is greater than the system limit, the system limit will be used. This
+parameter is only used when user matching using the C<match> parameter
+is being performed.
+
+=item C<group_ids> (array)
+
+=item C<groups> (array)
+
+C<group_ids> is an array of numeric ids for groups that a user can be in.
+C<groups> is an array of names of groups that a user can be in.
+If these are specified, they limit the return value to users who are
+in I<any> of the groups specified.
+
+=item C<include_disabled> (boolean)
+
+By default, when using the C<match> parameter, disabled users are excluded
+from the returned results unless their full username is identical to the
+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.
+
+=back
+
+=item B<Returns>
+
+A hash containing one item, C<users>, that is an array of
+hashes. Each hash describes a user, and has the following items:
+
+=over
+
+=item id
+
+C<int> The unique integer ID that Bugzilla uses to represent this user.
+Even if the user's login name changes, this will not change.
+
+=item real_name
+
+C<string> The actual name of the user. May be blank.
+
+=item email
+
+C<string> The email address of the user.
+
+=item name
+
+C<string> The login name of the user. Note that in some situations this is
+different than their email.
+
+=item can_login
+
+C<boolean> A boolean value to indicate if the user can login into bugzilla.
+
+=item email_enabled
+
+C<boolean> A boolean value to indicate if bug-related mail will be sent
+to the user or not.
+
+=item login_denied_text
+
+C<string> A text field that holds the reason for disabling a user from logging
+into bugzilla, if empty then the user account is enabled. Otherwise it is
+disabled/closed.
+
+=item groups
+
+C<array> An array of group hashes the user is a member of. If the currently
+logged in user is querying their own account or is a member of the 'editusers'
+group, the array will contain all the groups that the user is a
+member of. Otherwise, the array will only contain groups that the logged in
+user can bless. Each hash describes the group and contains the following items:
+
+=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
+
+=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 query
+
+C<string> The CGI parameters for the saved search.
+
+=back
+
+=item saved_reports
+
+C<array> An array of hashes, each of which represents a user's saved report and has
+the following keys:
+
+=over
+
+=item id
+
+C<int> An integer id uniquely identifying the saved report.
+
+=item name
+
+C<string> The name of the saved report.
+
+=item query
+
+C<string> The CGI parameters for the saved report.
+
+=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>, C<can_login>, and C<groups> items. The groups returned are
+filtered based on your permission to bless each group.
+The C<saved_searches> and C<saved_reports> items are only returned if you are
+querying your own account, even if you are in the editusers group.
+
+=back
+
+=item B<Errors>
+
+=over
+
+=item 51 (Bad Login Name or Group ID)
+
+You passed an invalid login name in the "names" array or a bad
+group ID in the C<group_ids> argument.
+
+=item 52 (Invalid Parameter)
+
+The value used must be an integer greater than zero.
+
+=item 304 (Authorization Required)
+
+You are logged in, but you are not authorized to see one of the users you
+wanted to get information about by user id.
+
+=item 505 (User Access By Id or User-Matching Denied)
+
+Logged-out users cannot use the "ids" or "match" arguments to this
+function.
+
+=item 804 (Invalid Group Name)
+
+You passed a group name in the C<groups> argument which either does not
+exist or you do not belong to it.
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in Bugzilla B<3.4>.
+
+=item C<group_ids> and C<groups> were added in Bugzilla B<4.0>.
+
+=item C<include_disabled> was added in Bugzilla B<4.0>. Default
+behavior for C<match> was changed to only return enabled accounts.
+
+=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.
+
+=item C<groups>, C<saved_searches>, and C<saved_reports> were added
+in Bugzilla B<4.4>.
+
+=item REST API call added in Bugzilla B<5.0>.
+
+=back
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item REST_RESOURCES
+
+=back