summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Lawrence <dkl@redhat.com>2014-06-16 17:15:35 +0200
committerDavid Lawrence <dkl@redhat.com>2014-06-16 17:15:35 +0200
commit0a2592d00b9d230f78b69c5808cbca108af54967 (patch)
tree6a0d72b2433b75c2ef8879ec23ff1ff507eb6165
parentdd10df6857319589e15cc404ad8690cdf54a6768 (diff)
downloadbugzilla-0a2592d00b9d230f78b69c5808cbca108af54967.tar.gz
bugzilla-0a2592d00b9d230f78b69c5808cbca108af54967.tar.xz
Bug 880669 - Extend current BzAPI BMO extension to contain compatibility changes on top of native rest
r=glob
-rw-r--r--.htaccess1
-rw-r--r--Bugzilla/Util.pm8
-rw-r--r--Bugzilla/WebService/Bug.pm29
-rw-r--r--Bugzilla/WebService/Server/REST.pm26
-rw-r--r--Bugzilla/WebService/Util.pm10
-rw-r--r--extensions/BzAPI/Config.pm61
-rw-r--r--extensions/BzAPI/Extension.pm291
-rwxr-xr-xextensions/BzAPI/bin/rest.cgi34
-rw-r--r--extensions/BzAPI/lib/Constants.pm150
-rw-r--r--extensions/BzAPI/lib/Resources/Bug.pm860
-rw-r--r--extensions/BzAPI/lib/Resources/Bugzilla.pm118
-rw-r--r--extensions/BzAPI/lib/Resources/User.pm79
-rw-r--r--extensions/BzAPI/lib/Util.pm442
-rw-r--r--extensions/BzAPI/template/en/default/config.json.tmpl47
-rw-r--r--extensions/BzAPI/template/en/default/hook/global/user-error-errors.html.tmpl11
15 files changed, 2014 insertions, 153 deletions
diff --git a/.htaccess b/.htaccess
index f6ea870c3..a901a444a 100644
--- a/.htaccess
+++ b/.htaccess
@@ -75,3 +75,4 @@ RewriteRule ^form[\.:]mobile[\.\-:]compat$ enter_bug.cgi?product=Tech+Evangelism
RewriteRule ^form[\.:]web[\.:]bounty$ enter_bug.cgi?product=mozilla.org&format=web-bounty
RewriteRule ^form[\.:]automative$ enter_bug.cgi?product=Testing&format=automative
RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE]
+RewriteRule ^bzapi/(.*)$ extensions/BzAPI/bin/rest.cgi/$1 [NE]
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index 225edbe4a..27a7e5e23 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -45,7 +45,7 @@ use base qw(Exporter);
bz_crypt generate_random_password
validate_email_syntax clean_text
get_text template_var disable_utf8
- detect_encoding email_filter);
+ enable_utf8 detect_encoding email_filter);
use Bugzilla::Constants;
use Bugzilla::RNG qw(irand);
@@ -778,6 +778,12 @@ sub disable_utf8 {
}
}
+sub enable_utf8 {
+ if (Bugzilla->params->{'utf8'}) {
+ binmode STDOUT, ':utf8'; # Turn on UTF8 encoding.
+ }
+}
+
use constant UTF8_ACCIDENTAL => qw(shiftjis big5-eten euc-kr euc-jp);
sub detect_encoding {
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 710bff112..a82f55d3b 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -54,11 +54,25 @@ use Storable qw(dclone);
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'],
-};
+sub DATE_FIELDS {
+ my $fields = {
+ comments => ['new_since'],
+ create => [],
+ history => ['new_since'],
+ search => ['last_change_time', 'creation_time'],
+ update => []
+ };
+
+ # Add date related custom fields
+ foreach my $field (Bugzilla->active_custom_fields) {
+ next unless ($field->type == FIELD_TYPE_DATETIME
+ || $field->type == FIELD_TYPE_DATE);
+ push(@{ $fields->{create} }, $field->name);
+ push(@{ $fields->{update} }, $field->name);
+ }
+
+ return $fields;
+}
use constant BASE64_FIELDS => {
add_attachment => ['data'],
@@ -558,7 +572,7 @@ sub search {
# 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)) {
+ foreach my $role (qw(assigned_to reporter qa_contact commenter cc)) {
next if !exists $match_params->{$role};
my $value = delete $match_params->{$role};
$match_params->{"f${last_field_id}"} = $role;
@@ -594,6 +608,9 @@ sub search {
my @bugs = map { $bug_objects{$_} } @bug_ids;
@bugs = map { $self->_bug_to_hash($_, $params) } @bugs;
+ # BzAPI
+ Bugzilla->request_cache->{bzapi_search_bugs} = [ map { $bug_objects{$_} } @bug_ids ];
+
return { bugs => \@bugs };
}
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
index 96e4b3179..1be457c8f 100644
--- a/Bugzilla/WebService/Server/REST.pm
+++ b/Bugzilla/WebService/Server/REST.pm
@@ -16,7 +16,7 @@ use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Hook;
-use Bugzilla::Util qw(correct_urlbase html_quote);
+use Bugzilla::Util qw(correct_urlbase html_quote disable_utf8 enable_utf8);
use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(taint_data fix_credentials);
@@ -78,8 +78,12 @@ sub handle {
# Fix includes/excludes for each call
rest_include_exclude($params);
- # Set callback name if exists
- $self->_bz_callback($params->{'callback'}) if $params->{'callback'};
+ # Set callback name if content-type is 'application/javascript'
+ if ($params->{'callback'}
+ || $self->content_type eq 'application/javascript')
+ {
+ $self->_bz_callback($params->{'callback'} || 'callback');
+ }
Bugzilla->input_params($params);
@@ -111,8 +115,13 @@ sub response {
# along with the result/error such as version and id which
# we will strip off for REST calls.
my $content = $response->content;
+
my $json_data = {};
if ($content) {
+ # Content is in bytes at this point and needs to be converted
+ # back to utf8 string.
+ enable_utf8();
+ utf8::decode($content) if !utf8::is_utf8($content);
$json_data = $self->json->decode($content);
}
@@ -152,6 +161,9 @@ sub response {
$content = $self->json->encode($result);
}
+ utf8::encode($content) if utf8::is_utf8($content);
+ disable_utf8();
+
$response->content($content);
$self->SUPER::response($response);
@@ -307,16 +319,16 @@ sub bz_rest_params {
sub bz_rest_options {
my ($self, $options) = @_;
$self->{_bz_rest_options} = $options if $options;
- return $self->{_bz_rest_options};
+ return [ sort { $a cmp $b } @{ $self->{_bz_rest_options} } ];
}
sub rest_include_exclude {
my ($params) = @_;
- if ($params->{'include_fields'} && !ref $params->{'include_fields'}) {
+ if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) {
$params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ];
}
- if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
+ if (exists $params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
$params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ];
}
@@ -344,7 +356,7 @@ sub _retrieve_json_params {
my $extra_params = {};
my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
if ($json) {
- eval { $extra_params = $self->json->decode($json); };
+ eval { $extra_params = $self->json->utf8(0)->decode($json); };
if ($@) {
ThrowUserError('json_rpc_invalid_params', { err_msg => $@ });
}
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index 9f053095c..856fd3481 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -166,14 +166,14 @@ sub filter_wants($$;$$) {
delete $exclude{$key};
}
- # If the user has asked to include all or exclude all
- return $cache->{$field} = 0 if $exclude_types{'all'};
- return $cache->{$field} = 1 if $include_types{'all'};
-
# Explicit inclusion/exclusion
return $cache->{$field} = 0 if $exclude{$field};
return $cache->{$field} = 1 if $include{$field};
+ # If the user has asked to include all or exclude all
+ return $cache->{$field} = 0 if $exclude_types{'all'};
+ return $cache->{$field} = 1 if $include_types{'all'};
+
# If the user has not asked for any fields specifically or if the user has asked
# for one or more of the field's types (and not excluded them)
foreach my $type (keys %field_types) {
@@ -214,7 +214,7 @@ sub _delete_bad_keys {
# However, we need to validate our argument names in some way.
# We know that all hash keys passed in to the WebService will
# match \w+, so we delete any key that doesn't match that.
- if ($key !~ /^\w+$/) {
+ if ($key !~ /^[\w\.\-]+$/) {
delete $item->{$key};
}
}
diff --git a/extensions/BzAPI/Config.pm b/extensions/BzAPI/Config.pm
index 0de081097..89b8c1e02 100644
--- a/extensions/BzAPI/Config.pm
+++ b/extensions/BzAPI/Config.pm
@@ -1,63 +1,16 @@
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is the BzAPI Bugzilla Extension.
-#
-# The Initial Developer of the Original Code is
-# the Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2010
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# Gervase Markham <gerv@gerv.net>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Extension::BzAPI;
+
use strict;
use constant NAME => 'BzAPI';
-use constant REQUIRED_MODULES => [
- {
- package => 'SOAP-Lite',
- module => 'SOAP::Lite',
- # 0.710.04 is required for correct UTF-8 handling, but .04 and .05 are
- # affected by bug 468009.
- version => '0.710.06',
- },
- {
- package => 'Test-Taint',
- module => 'Test::Taint',
- version => 0,
- },
- {
- package => 'JSON',
- module => 'JSON',
- version => 0,
- },
-];
+use constant REQUIRED_MODULES => [];
__PACKAGE__->NAME;
diff --git a/extensions/BzAPI/Extension.pm b/extensions/BzAPI/Extension.pm
index aeaa0bce4..4d144b881 100644
--- a/extensions/BzAPI/Extension.pm
+++ b/extensions/BzAPI/Extension.pm
@@ -1,71 +1,262 @@
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is the BzAPI Bugzilla Extension.
-#
-# The Initial Developer of the Original Code is
-# the Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2010
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-# Gervase Markham <gerv@gerv.net>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Extension::BzAPI;
+
use strict;
use base qw(Bugzilla::Extension);
+use Bugzilla::Extension::BzAPI::Constants;
+use Bugzilla::Extension::BzAPI::Util qw(fix_credentials filter_wants_nocache);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trick_taint datetime_from);
+use Bugzilla::Constants;
+use Bugzilla::Install::Filesystem;
+
+use File::Basename;
+
our $VERSION = '0.1';
-# Add JSON filter for JSON templates
-sub template_before_create {
- my ($self, $args) = @_;
- my $config = $args->{'config'};
-
- $config->{'FILTERS'}->{'json'} = sub {
- my ($var) = @_;
- $var =~ s/([\\\"\/])/\\$1/g;
- $var =~ s/\n/\\n/g;
- $var =~ s/\r/\\r/g;
- $var =~ s/\f/\\f/g;
- $var =~ s/\t/\\t/g;
- return $var;
+################
+# Installation #
+################
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{'files'};
+
+ my $extensionsdir = bz_locations()->{'extensionsdir'};
+ my $scriptname = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/rest.cgi";
+
+ $files->{$scriptname} = {
+ perms => Bugzilla::Install::Filesystem::WS_EXECUTE
};
}
+##################
+# Template Hooks #
+##################
+
sub template_before_process {
my ($self, $args) = @_;
my $vars = $args->{'vars'};
my $file = $args->{'file'};
-
+
if ($file =~ /config\.json\.tmpl$/) {
$vars->{'initial_status'} = Bugzilla::Status->can_change_to;
- $vars->{'status_objects'} = [Bugzilla::Status->get_all];
+ $vars->{'status_objects'} = [ Bugzilla::Status->get_all ];
}
}
+##############
+# Code Hooks #
+##############
+
+sub bug_start_of_update {
+ my ($self, $args) = @_;
+ my $old_bug = $args->{old_bug};
+ my $params = Bugzilla->input_params;
+
+ return if !Bugzilla->request_cache->{bzapi};
+
+ # Check for a mid-air collision. Currently this only works when updating
+ # an individual bug and if last_changed_time is provided. Otherwise it
+ # allows the changes.
+ my $delta_ts = $params->{last_change_time} || '';
+
+ if ($delta_ts && exists $params->{ids} && @{ $params->{ids} } == 1) {
+ _midair_check($delta_ts, $old_bug->delta_ts);
+ }
+}
+
+sub object_end_of_set_all {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ my $params = Bugzilla->input_params;
+
+ return if !Bugzilla->request_cache->{bzapi};
+ return if !$object->isa('Bugzilla::Attachment');
+
+ # Check for a mid-air collision. Currently this only works when updating
+ # an individual attachment and if last_changed_time is provided. Otherwise it
+ # allows the changes.
+ my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {};
+ my $delta_ts = $stash->{last_change_time};
+
+ _midair_check($delta_ts, $object->modification_time) if $delta_ts;
+}
+
+sub _midair_check {
+ my ($delta_ts, $old_delta_ts) = @_;
+ my $delta_ts_z = datetime_from($delta_ts)
+ || ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts });
+ my $old_delta_tz_z = datetime_from($old_delta_ts);
+ if ($old_delta_tz_z ne $delta_ts_z) {
+ ThrowUserError('bzapi_midair_collision');
+ }
+}
+
+sub webservice_error_codes {
+ my ($self, $args) = @_;
+ my $error_map = $args->{error_map};
+ $error_map->{'bzapi_midair_collision'} = 400;
+}
+
+sub webservice_fix_credentials {
+ my ($self, $args) = @_;
+ my $rpc = $args->{rpc};
+ my $params = $args->{params};
+ fix_credentials($params);
+}
+
+sub webservice_rest_request {
+ my ($self, $args) = @_;
+ my $rpc = $args->{rpc};
+ my $params = $args->{params};
+ my $cache = Bugzilla->request_cache;
+
+ return if !$cache->{bzapi};
+
+ # Stash certain values for later use
+ $cache->{bzapi_rpc} = $rpc;
+
+ # Internal websevice method being used
+ $cache->{bzapi_rpc_method} = $rpc->path_info . "." . $rpc->bz_method_name;
+
+ # Load the appropriate request handler based on path and type
+ if (my $handler = _find_handler($rpc, 'request')) {
+ &$handler($params);
+ }
+}
+
+sub webservice_rest_response {
+ my ($self, $args) = @_;
+ my $rpc = $args->{rpc};
+ my $result = $args->{result};
+ my $response = $args->{response};
+ my $cache = Bugzilla->request_cache;
+
+ # Stash certain values for later use
+ $cache->{bzapi_rpc} ||= $rpc;
+
+ return if !Bugzilla->request_cache->{bzapi}
+ || ref $$result ne 'HASH'
+ || exists $$result->{error};
+
+ # Load the appropriate response handler based on path and type
+ if (my $handler = _find_handler($rpc, 'response')) {
+ &$handler($result, $response);
+ }
+}
+
+sub webservice_rest_resources {
+ my ($self, $args) = @_;
+ my $rpc = $args->{rpc};
+ my $resources = $args->{resources};
+
+ return if !Bugzilla->request_cache->{bzapi};
+
+ _add_resources($rpc, $resources);
+}
+
+#####################
+# Utility Functions #
+#####################
+
+sub _find_handler {
+ my ($rpc, $type) = @_;
+
+ my $path_info = $rpc->cgi->path_info;
+ my $request_method = $rpc->request->method;
+
+ my $module = $rpc->bz_class_name || '';
+ $module =~ s/^Bugzilla::WebService:://;
+
+ my $cache = _preload_handlers();
+
+ return undef if !exists $cache->{$module};
+
+ # Make a copy of the handler array so
+ # as to not alter the actual cached data.
+ my @handlers = @{ $cache->{$module} };
+
+ while (my $regex = shift @handlers) {
+ my $data = shift @handlers;
+ next if ref $data ne 'HASH';
+ if ($path_info =~ $regex
+ && exists $data->{$request_method}
+ && exists $data->{$request_method}->{$type})
+ {
+ return $data->{$request_method}->{$type};
+ }
+ }
+
+ return undef;
+}
+
+sub _add_resources {
+ my ($rpc, $native_resources) = @_;
+
+ my $cache = _preload_handlers();
+
+ foreach my $module (keys %$cache) {
+ my $native_module = "Bugzilla::WebService::$module";
+ next if !$native_resources->{$native_module};
+
+ # Make a copy of the handler array so
+ # as to not alter the actual cached data.
+ my @handlers = @{ $cache->{$module} };
+
+ my @ext_resources = ();
+ while (my $regex = shift @handlers) {
+ my $data = shift @handlers;
+ next if ref $data ne 'HASH';
+ my $new_data = {};
+ foreach my $request_method (keys %$data) {
+ next if !exists $data->{$request_method}->{resource};
+ $new_data->{$request_method} = $data->{$request_method}->{resource};
+ }
+ push(@ext_resources, $regex, $new_data);
+ }
+
+ # Places the new resources at the beginning of the list
+ # so we can capture specific paths before the native resources
+ unshift(@{$native_resources->{$native_module}}, @ext_resources);
+ }
+}
+
+sub _resource_modules {
+ my $extdir = bz_locations()->{extensionsdir};
+ return map { basename($_, '.pm') } glob("$extdir/" . __PACKAGE__->NAME . "/lib/Resources/*.pm");
+}
+
+# preload all handlers into cache
+# since we don't want to parse all
+# this multiple times
+sub _preload_handlers {
+ my $cache = Bugzilla->request_cache;
+
+ if (!exists $cache->{rest_handlers}) {
+ my $all_handlers = {};
+ foreach my $module (_resource_modules()) {
+ my $resource_class = "Bugzilla::Extension::BzAPI::Resources::$module";
+ trick_taint($resource_class);
+ eval("require $resource_class");
+ warn $@ if $@;
+ next if ($@ || !$resource_class->can('rest_handlers'));
+ my $handlers = $resource_class->rest_handlers;
+ next if (ref $handlers ne 'ARRAY' || scalar @$handlers % 2 != 0);
+ $all_handlers->{$module} = $handlers;
+ }
+ $cache->{rest_handlers} = $all_handlers;
+ }
+
+ return $cache->{rest_handlers};
+}
+
__PACKAGE__->NAME;
diff --git a/extensions/BzAPI/bin/rest.cgi b/extensions/BzAPI/bin/rest.cgi
new file mode 100755
index 000000000..37cbab437
--- /dev/null
+++ b/extensions/BzAPI/bin/rest.cgi
@@ -0,0 +1,34 @@
+#!/usr/bin/perl -wT
+# 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.
+
+use 5.10.1;
+use strict;
+use lib qw(../../.. ../../../lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::WebService::Constants;
+BEGIN {
+ if (!Bugzilla->feature('rest')
+ || !Bugzilla->feature('jsonrpc'))
+ {
+ ThrowUserError('feature_disabled', { feature => 'rest' });
+ }
+}
+
+# Set request_cache bzapi value to true in order to enable the
+# BzAPI extension functionality
+Bugzilla->request_cache->{bzapi} = 1;
+
+use Bugzilla::WebService::Server::REST;
+Bugzilla->usage_mode(USAGE_MODE_REST);
+local @INC = (bz_locations()->{extensionsdir}, @INC);
+my $server = new Bugzilla::WebService::Server::REST;
+$server->version('1.1');
+$server->handle();
diff --git a/extensions/BzAPI/lib/Constants.pm b/extensions/BzAPI/lib/Constants.pm
new file mode 100644
index 000000000..c36543daf
--- /dev/null
+++ b/extensions/BzAPI/lib/Constants.pm
@@ -0,0 +1,150 @@
+# 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::Extension::BzAPI::Constants;
+
+use strict;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ USER_FIELDS
+ BUG_FIELD_MAP
+ BOOLEAN_TYPE_MAP
+ ATTACHMENT_FIELD_MAP
+ DEFAULT_BUG_FIELDS
+ DEFAULT_ATTACHMENT_FIELDS
+);
+
+# These are fields that are normally exported as a single value such
+# as the user's email. BzAPI needs to convert them to user objects
+# where possible.
+use constant USER_FIELDS => (qw(
+ assigned_to
+ cc
+ creator
+ qa_contact
+ reporter
+));
+
+# Convert old field names from old to new
+use constant BUG_FIELD_MAP => {
+ 'opendate' => 'creation_time', # query
+ 'creation_ts' => 'creation_time',
+ 'changeddate' => 'last_change_time', # query
+ 'delta_ts' => 'last_change_time',
+ 'bug_id' => 'id',
+ 'rep_platform' => 'platform',
+ 'bug_severity' => 'severity',
+ 'bug_status' => 'status',
+ 'short_desc' => 'summary',
+ 'bug_file_loc' => 'url',
+ 'status_whiteboard' => 'whiteboard',
+ 'reporter' => 'creator',
+ 'reporter_realname' => 'creator_realname',
+ 'cclist_accessible' => 'is_cc_accessible',
+ 'reporter_accessible' => 'is_creator_accessible',
+ 'everconfirmed' => 'is_confirmed',
+ 'dependson' => 'depends_on',
+ 'blocked' => 'blocks',
+ 'attachment' => 'attachments',
+ 'flag' => 'flags',
+ 'flagtypes.name' => 'flag',
+ 'bug_group' => 'group',
+ 'group' => 'groups',
+ 'longdesc' => 'comment',
+ 'bug_file_loc_type' => 'url_type',
+ 'bugidtype' => 'id_mode',
+ 'longdesc_type' => 'comment_type',
+ 'short_desc_type' => 'summary_type',
+ 'status_whiteboard_type' => 'whiteboard_type',
+ 'emailassigned_to1' => 'email1_assigned_to',
+ 'emailassigned_to2' => 'email2_assigned_to',
+ 'emailcc1' => 'email1_cc',
+ 'emailcc2' => 'email2_cc',
+ 'emailqa_contact1' => 'email1_qa_contact',
+ 'emailqa_contact2' => 'email2_qa_contact',
+ 'emailreporter1' => 'email1_creator',
+ 'emailreporter2' => 'email2_creator',
+ 'emaillongdesc1' => 'email1_comment_creator',
+ 'emaillongdesc2' => 'email2_comment_creator',
+ 'emailtype1' => 'email1_type',
+ 'emailtype2' => 'email2_type',
+ 'chfieldfrom' => 'changed_after',
+ 'chfieldto' => 'changed_before',
+ 'chfield' => 'changed_field',
+ 'chfieldvalue' => 'changed_field_to',
+ 'deadlinefrom' => 'deadline_after',
+ 'deadlineto' => 'deadline_before',
+ 'attach_data.thedata' => 'attachment.data',
+ 'longdescs.isprivate' => 'comment.is_private',
+ 'commenter' => 'comment.creator',
+ 'requestees.login_name' => 'flag.requestee',
+ 'setters.login_name' => 'flag.setter',
+ 'days_elapsed' => 'idle',
+ 'owner_idle_time' => 'assignee_idle',
+ 'dup_id' => 'dupe_of',
+ 'isopened' => 'is_open',
+ 'flag_type' => 'flag_types',
+ 'attachments.submitter' => 'attachment.attacher',
+ 'attachments.filename' => 'attachment.file_name',
+ 'attachments.description' => 'attachment.description',
+ 'attachments.delta_ts' => 'attachment.last_change_time',
+ 'attachments.isobsolete' => 'attachment.is_obsolete',
+ 'attachments.ispatch' => 'attachment.is_patch',
+ 'attachments.isprivate' => 'attachment.is_private',
+ 'attachments.mimetype' => 'attachment.content_type',
+ 'attachments.date' => 'attachment.creation_time',
+ 'attachments.attachid' => 'attachment.id',
+ 'attachments.flag' => 'attachment.flags',
+ 'attachments.token' => 'attachment.update_token'
+};
+
+# Convert from old boolean chart type names to new names
+use constant BOOLEAN_TYPE_MAP => {
+ 'equals' => 'equals',
+ 'not_equals' => 'notequals',
+ 'equals_any' => 'anyexact',
+ 'contains' => 'substring',
+ 'not_contains' => 'notsubstring',
+ 'case_contains' => 'casesubstring',
+ 'contains_any' => 'anywordssubstr',
+ 'not_contains_any' => 'nowordssubstr',
+ 'contains_all' => 'allwordssubstr',
+ 'contains_any_words' => 'anywords',
+ 'not_contains_any_words' => 'nowords',
+ 'contains_all_words' => 'allwords',
+ 'regex' => 'regexp',
+ 'not_regex' => 'notregexp',
+ 'less_than' => 'lessthan',
+ 'greater_than' => 'greaterthan',
+ 'changed_before' => 'changedbefore',
+ 'changed_after' => 'changedafter',
+ 'changed_from' => 'changedfrom',
+ 'changed_to' => 'changedto',
+ 'changed_by' => 'changedby',
+ 'matches' => 'matches'
+};
+
+# Convert old attachment field names from old to new
+use constant ATTACHMENT_FIELD_MAP => {
+ 'submitter' => 'attacher',
+ 'description' => 'description',
+ 'filename' => 'file_name',
+ 'delta_ts' => 'last_change_time',
+ 'isobsolete' => 'is_obsolete',
+ 'ispatch' => 'is_patch',
+ 'isprivate' => 'is_private',
+ 'mimetype' => 'content_type',
+ 'contenttypeentry' => 'content_type',
+ 'date' => 'creation_time',
+ 'attachid' => 'id',
+ 'desc' => 'description',
+ 'flag' => 'flags',
+ 'type' => 'content_type',
+};
+
+1;
diff --git a/extensions/BzAPI/lib/Resources/Bug.pm b/extensions/BzAPI/lib/Resources/Bug.pm
new file mode 100644
index 000000000..eed439a00
--- /dev/null
+++ b/extensions/BzAPI/lib/Resources/Bug.pm
@@ -0,0 +1,860 @@
+# 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::Extension::BzAPI::Resources::Bug;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+use Bugzilla::Token qw(issue_hash_token);
+use Bugzilla::Util qw(trick_taint diff_arrays);
+use Bugzilla::WebService::Constants;
+
+use Bugzilla::Extension::BzAPI::Util;
+use Bugzilla::Extension::BzAPI::Constants;
+
+use List::MoreUtils qw(uniq);
+use List::Util qw(max);
+
+#################
+# REST Handlers #
+#################
+
+BEGIN {
+ require Bugzilla::WebService::Bug;
+ *Bugzilla::WebService::Bug::get_bug_count = \&get_bug_count_resource;
+}
+
+sub rest_handlers {
+ my $rest_handlers = [
+ qr{^/bug$}, {
+ GET => {
+ request => \&search_bugs_request,
+ response => \&search_bugs_response
+ },
+ POST => {
+ request => \&create_bug_request,
+ response => \&create_bug_response
+ }
+ },
+ qr{^/bug/([^/]+)$}, {
+ GET => {
+ response => \&get_bug_response
+ },
+ PUT => {
+ request => \&update_bug_request,
+ response => \&update_bug_response
+ }
+ },
+ qr{^/bug/([^/]+)/comment$}, {
+ GET => {
+ response => \&get_comments_response
+ },
+ POST => {
+ request => \&add_comment_request,
+ response => \&add_comment_response
+ }
+ },
+ qr{^/bug/([^/]+)/history$}, {
+ GET => {
+ response => \&get_history_response
+ }
+ },
+ qr{^/bug/([^/]+)/attachment$}, {
+ GET => {
+ response => \&get_attachments_response
+ },
+ POST => {
+ request => \&add_attachment_request,
+ response => \&add_attachment_response
+ }
+ },
+ qr{^/bug/attachment/([^/]+)$}, {
+ GET => {
+ response => \&get_attachment_response
+ },
+ PUT => {
+ request => \&update_attachment_request,
+ response => \&update_attachment_response
+ }
+ },
+ qr{^/attachment/([^/]+)$}, {
+ GET => {
+ response => \&get_attachment_response
+ },
+ PUT => {
+ request => \&update_attachment_request,
+ response => \&update_attachment_response
+ }
+ },
+ qr{^/bug/([^/]+)/flag$}, {
+ GET => {
+ resource => {
+ method => 'get',
+ params => sub {
+ return { ids => [ $_[0] ],
+ include_fields => ['flags'] };
+ }
+ },
+ response => \&get_bug_flags_response,
+ }
+ },
+ qr{^/count$}, {
+ GET => {
+ resource => {
+ method => 'get_bug_count'
+ }
+ }
+ },
+ qr{^/attachment/([^/]+)$}, {
+ GET => {
+ resource => {
+ method => 'attachments',
+ params => sub {
+ return { attachment_ids => [ $_[0] ] };
+ }
+ }
+ },
+ PUT => {
+ resource => {
+ method => 'update_attachment',
+ params => sub {
+ return { ids => [ $_[0] ] };
+ }
+ }
+ }
+ }
+ ];
+ return $rest_handlers;
+}
+
+#########################
+# REST Resource Methods #
+#########################
+
+# Return bug counts based on row/col/table fields
+# FIXME Borrowed a lot of code from report.cgi, eventually
+# this should be broken into it's own module so that report.cgi
+# and here can share the same code.
+sub get_bug_count_resource {
+ my ($self, $params) = @_;
+
+ Bugzilla->switch_to_shadow_db();
+
+ my $col_field = $params->{x_axis_field} || '';
+ my $row_field = $params->{y_axis_field} || '';
+ my $tbl_field = $params->{z_axis_field} || '';
+
+ my $dimensions = $col_field ?
+ $row_field ?
+ $tbl_field ? 3 : 2 : 1 : 0;
+
+ if ($dimensions == 0) {
+ $col_field = "bug_status";
+ $params->{x_axis_field} = "bug_status";
+ }
+
+ # Valid bug fields that can be reported on.
+ my $valid_columns = Bugzilla::Search::REPORT_COLUMNS;
+
+ # Convert external names to internal if necessary
+ $params = Bugzilla::Bug::map_fields($params);
+ $row_field = Bugzilla::Bug::FIELD_MAP->{$row_field} || $row_field;
+ $col_field = Bugzilla::Bug::FIELD_MAP->{$col_field} || $col_field;
+ $tbl_field = Bugzilla::Bug::FIELD_MAP->{$tbl_field} || $tbl_field;
+
+ # Validate the values in the axis fields or throw an error.
+ !$row_field
+ || ($valid_columns->{$row_field} && trick_taint($row_field))
+ || ThrowCodeError("report_axis_invalid", { fld => "x", val => $row_field });
+ !$col_field
+ || ($valid_columns->{$col_field} && trick_taint($col_field))
+ || ThrowCodeError("report_axis_invalid", { fld => "y", val => $col_field });
+ !$tbl_field
+ || ($valid_columns->{$tbl_field} && trick_taint($tbl_field))
+ || ThrowCodeError("report_axis_invalid", { fld => "z", val => $tbl_field });
+
+ my @axis_fields = grep { $_ } ($row_field, $col_field, $tbl_field);
+
+ my $search = new Bugzilla::Search(
+ fields => \@axis_fields,
+ params => $params,
+ allow_unlimited => 1,
+ );
+
+ my ($results, $extra_data) = $search->data;
+
+ # We have a hash of hashes for the data itself, and a hash to hold the
+ # row/col/table names.
+ my %data;
+ my %names;
+
+ # Read the bug data and count the bugs for each possible value of row, column
+ # and table.
+ #
+ # We detect a numerical field, and sort appropriately, if all the values are
+ # numeric.
+ my $col_isnumeric = 1;
+ my $row_isnumeric = 1;
+ my $tbl_isnumeric = 1;
+
+ foreach my $result (@$results) {
+ # handle empty dimension member names
+ my $row = check_value($row_field, $result);
+ my $col = check_value($col_field, $result);
+ my $tbl = check_value($tbl_field, $result);
+
+ $data{$tbl}{$col}{$row}++;
+ $names{"col"}{$col}++;
+ $names{"row"}{$row}++;
+ $names{"tbl"}{$tbl}++;
+
+ $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o);
+ $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o);
+ $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o);
+ }
+
+ my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field);
+ my @row_names = get_names($names{"row"}, $row_isnumeric, $row_field);
+ my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field);
+
+ push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1);
+
+ my @data;
+ foreach my $tbl (@tbl_names) {
+ my @tbl_data;
+ foreach my $row (@row_names) {
+ my @col_data;
+ foreach my $col (@col_names) {
+ $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0;
+ push(@col_data, $data{$tbl}{$col}{$row});
+ if ($tbl ne "-total-") {
+ # This is a bit sneaky. We spend every loop except the last
+ # building up the -total- data, and then last time round,
+ # we process it as another tbl, and push() the total values
+ # into the image_data array.
+ $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
+ }
+ }
+ push(@tbl_data, \@col_data);
+ }
+ push(@data, \@tbl_data);
+ }
+
+ my $result = {};
+ if ($dimensions == 0) {
+ my $sum = 0;
+
+ # If the search returns no results, we just get an 0-byte file back
+ # and so there is no data at all.
+ if (@data) {
+ foreach my $value (@{ $data[0][0] }) {
+ $sum += $value;
+ }
+ }
+
+ $result = {
+ 'data' => $sum
+ };
+ }
+ elsif ($dimensions == 1) {
+ $result = {
+ 'x_labels' => \@col_names,
+ 'data' => $data[0][0] || []
+ };
+ }
+ elsif ($dimensions == 2) {
+ $result = {
+ 'x_labels' => \@col_names,
+ 'y_labels' => \@row_names,
+ 'data' => $data[0] || [[]]
+ };
+ }
+ elsif ($dimensions == 3) {
+ if (@data > 1 && $tbl_names[-1] eq "-total-") {
+ # Last table is a total, which we discard
+ pop(@data);
+ pop(@tbl_names);
+ }
+
+ $result = {
+ 'x_labels' => \@col_names,
+ 'y_labels' => \@row_names,
+ 'z_labels' => \@tbl_names,
+ 'data' => @data ? \@data : [[[]]]
+ };
+ }
+
+ return $result;
+}
+
+sub get_names {
+ my ($names, $isnumeric, $field_name) = @_;
+ my ($field, @sorted);
+ # XXX - This is a hack to handle the actual_time/work_time field,
+ # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm.
+ $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time';
+
+ # _realname fields aren't real Bugzilla::Field objects, but they are a
+ # valid axis, so we don't vailidate them as Bugzilla::Field objects.
+ $field = Bugzilla::Field->check($field_name)
+ if ($field_name && $field_name !~ /_realname$/);
+
+ if ($field && $field->is_select) {
+ foreach my $value (@{$field->legal_values}) {
+ push(@sorted, $value->name) if $names->{$value->name};
+ }
+ unshift(@sorted, '---') if $field_name eq 'resolution';
+ @sorted = uniq @sorted;
+ }
+ elsif ($isnumeric) {
+ # It's not a field we are preserving the order of, so sort it
+ # numerically...
+ @sorted = sort { $a <=> $b } keys %$names;
+ }
+ else {
+ # ...or alphabetically, as appropriate.
+ @sorted = sort keys %$names;
+ }
+
+ return @sorted;
+}
+
+sub check_value {
+ my ($field, $result) = @_;
+
+ my $value;
+ if (!defined $field) {
+ $value = '';
+ }
+ elsif ($field eq '') {
+ $value = ' ';
+ }
+ else {
+ $value = shift @$result;
+ $value = ' ' if (!defined $value || $value eq '');
+ $value = '---' if ($field eq 'resolution' && $value eq ' ');
+ }
+ return $value;
+}
+
+########################
+# REST Request Methods #
+########################
+
+sub search_bugs_request {
+ my ($params) = @_;
+
+ if (defined $params->{changed_field}
+ && $params->{changed_field} eq "creation_time")
+ {
+ $params->{changed_field} = "[Bug creation]";
+ }
+
+ my $FIELD_NEW_TO_OLD = { reverse %{ BUG_FIELD_MAP() } };
+
+ # Update values of various forms.
+ foreach my $key (keys %$params) {
+ # First, search types. These are found in the value of any field ending
+ # _type, and the value of any field matching type\d-\d-\d.
+ if ($key =~ /^type(\d+)-(\d+)-(\d+)$|_type$/) {
+ $params->{$key}
+ = BOOLEAN_TYPE_MAP->{$params->{$key}} || $params->{$key};
+ }
+
+ # Field names hiding in values instead of keys: changed_field, boolean
+ # charts and axis names.
+ if ($key =~ /^(field\d+-\d+-\d+|
+ changed_field|
+ (x|y|z)_axis_field)$
+ /x) {
+ $params->{$key}
+ = $FIELD_NEW_TO_OLD->{$params->{$key}} || $params->{$key};
+ }
+ }
+
+ # Update field names
+ foreach my $field (keys %$FIELD_NEW_TO_OLD) {
+ if (defined $params->{$field}) {
+ $params->{$FIELD_NEW_TO_OLD->{$field}} = delete $params->{$field};
+ }
+ }
+
+ if (exists $params->{bug_id_type}) {
+ $params->{bug_id_type}
+ = BOOLEAN_TYPE_MAP->{$params->{bug_id_type}} || $params->{bug_id_type};
+ }
+
+ # Time field names are screwy, and got reused. We can't put this mapping
+ # in NEW2OLD as everything will go haywire. actual_time has to be queried
+ # as work_time even though work_time is the submit-only field for _adding_
+ # to actual_time, which can't be arbitrarily manipulated.
+ if (defined $params->{work_time}) {
+ $params->{actual_time} = delete $params->{work_time};
+ }
+
+ # Other convenience search ariables used by BzAPI
+ my @field_ids = grep(/^f(\d+)$/, keys %$params);
+ my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+ foreach my $field (qw(setters.login_name requestees.login_name)) {
+ if (my $value = delete $params->{$field}) {
+ $params->{"f${last_field_id}"} = $FIELD_NEW_TO_OLD->{$field} || $field;
+ $params->{"o${last_field_id}"} = 'equals';
+ $params->{"v${last_field_id}"} = $value;
+ $last_field_id++;
+ }
+ }
+}
+
+sub create_bug_request {
+ my ($params) = @_;
+
+ # User roles such as assigned_to and qa_contact should be just the
+ # email (login) of the user you want to set to.
+ foreach my $field (qw(assigned_to qa_contact)) {
+ if (exists $params->{$field}) {
+ $params->{$field} = $params->{$field}->{name};
+ }
+ }
+
+ # CC should just be a list of bugzilla logins
+ if (exists $params->{cc}) {
+ $params->{cc} = [ map { $_->{name} } @{ $params->{cc} } ];
+ }
+
+ # Comment
+ if (exists $params->{comments}) {
+ $params->{comment_is_private} = $params->{comments}->[0]->{is_private};
+ $params->{description} = $params->{comments}->[0]->{text};
+ delete $params->{comments};
+ }
+
+ # Some fields are not supported by Bugzilla::Bug->create but are supported
+ # by Bugzilla::Bug->update :(
+ my $cache = Bugzilla->request_cache->{bzapi_bug_create_extra} ||= {};
+ foreach my $field (qw(remaining_time)) {
+ next if !exists $params->{$field};
+ $cache->{$field} = delete $params->{$field};
+ }
+
+ # remove username/password
+ delete $params->{username};
+ delete $params->{password};
+}
+
+sub update_bug_request {
+ my ($params) = @_;
+
+ my $bug_id = ref $params->{ids} ? $params->{ids}->[0] : $params->{ids};
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ # Convert groups to proper add/remove lists
+ if (exists $params->{groups}) {
+ my @new_groups = map { $_->{name} } @{ $params->{groups} };
+ my @old_groups = map { $_->name } @{ $bug->groups_in };
+ my ($removed, $added) = diff_arrays(\@old_groups, \@new_groups);
+ if (@$added || @$removed) {
+ my $groups_data = {};
+ $groups_data->{add} = $added if @$added;
+ $groups_data->{remove} = $removed if @$removed;
+ $params->{groups} = $groups_data;
+ }
+ else {
+ delete $params->{groups};
+ }
+ }
+
+ # Other fields such as keywords, blocks depends_on
+ # support 'set' which will make the list exactly what
+ # the user passes in.
+ foreach my $field (qw(blocks depends_on dependson keywords)) {
+ if (exists $params->{$field}) {
+ $params->{$field} = { set => $params->{$field} };
+ }
+ }
+
+ # User roles such as assigned_to and qa_contact should be just the
+ # email (login) of the user you want to change to. Also if defined
+ # but set to NULL then we reset them to default
+ foreach my $field (qw(assigned_to qa_contact)) {
+ if (exists $params->{$field}) {
+ if (!$params->{$field}) {
+ $params->{"reset_$field"} = 1;
+ delete $params->{$field};
+ }
+ else {
+ $params->{$field} = $params->{$field}->{name};
+ }
+ }
+ }
+
+ # CC is treated like groups in that we need 'add' and 'remove' keys
+ if (exists $params->{cc}) {
+ my $new_cc = [ map { $_->{name} } @{ $params->{cc} } ];
+ my ($removed, $added) = diff_arrays($bug->cc, $new_cc);
+ if (@$added || @$removed) {
+ my $cc_data = {};
+ $cc_data->{add} = $added if @$added;
+ $cc_data->{remove} = $removed if @$removed;
+ $params->{cc} = $cc_data;
+ }
+ else {
+ delete $params->{cc};
+ }
+ }
+
+ # see_also is treated like groups in that we need 'add' and 'remove' keys
+ if (exists $params->{see_also}) {
+ my $old_see_also = [ map { $_->name } @{ $bug->see_also } ];
+ my ($removed, $added) = diff_arrays($old_see_also, $params->{see_also});
+ if (@$added || @$removed) {
+ my $data = {};
+ $data->{add} = $added if @$added;
+ $data->{remove} = $removed if @$removed;
+ $params->{see_also} = $data;
+ }
+ else {
+ delete $params->{see_also};
+ }
+ }
+
+ # BzAPI allows for adding comments by appending to the list of current
+ # comments and passing the whole list back.
+ # 1. If a comment id is specified, the user can update the comment privacy
+ # 2. If no id is specified it is considered a new comment but only the last
+ # one will be accepted.
+ my %comment_is_private;
+ foreach my $comment (@{ $params->{'comments'} }) {
+ if (my $id = $comment->{'id'}) {
+ # Existing comment; tweak privacy flags if necessary
+ $comment_is_private{$id}
+ = ($comment->{'is_private'} && $comment->{'is_private'} eq "true") ? 1 : 0;
+ }
+ else {
+ # New comment to be added
+ # If multiple new comments are specified, only the last one will be
+ # added.
+ $params->{comment} = {
+ body => $comment->{text},
+ is_private => ($comment->{'is_private'} &&
+ $comment->{'is_private'} eq "true") ? 1 : 0
+ };
+ }
+ }
+ $params->{comment_is_private} = \%comment_is_private if %comment_is_private;
+
+ # Remove setter and convert requestee to just name
+ if (exists $params->{flags}) {
+ foreach my $flag (@{ $params->{flags} }) {
+ delete $flag->{setter}; # Always use logged in user
+ if (exists $flag->{requestee} && ref $flag->{requestee}) {
+ $flag->{requestee} = $flag->{requestee}->{name};
+ }
+ # If no flag id provided, assume it is new
+ if (!exists $flag->{id}) {
+ $flag->{new} = 1;
+ }
+ }
+ }
+}
+
+sub add_comment_request {
+ my ($params) = @_;
+ $params->{comment} = delete $params->{text} if $params->{text};
+}
+
+sub add_attachment_request {
+ my ($params) = @_;
+
+ # Bug.add_attachment uses 'summary' for description.
+ if ($params->{description}) {
+ $params->{summary} = $params->{description};
+ delete $params->{description};
+ }
+
+ # Remove setter and convert requestee to just name
+ if (exists $params->{flags}) {
+ foreach my $flag (@{ $params->{flags} }) {
+ delete $flag->{setter}; # Always use logged in user
+ if (exists $flag->{requestee} && ref $flag->{requestee}) {
+ $flag->{requestee} = $flag->{requestee}->{name};
+ }
+ }
+ }
+}
+
+sub update_attachment_request {
+ my ($params) = @_;
+
+ # Stash away for midair checking later
+ if ($params->{last_change_time}) {
+ my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {};
+ $stash->{last_change_time} = delete $params->{last_change_time};
+ }
+
+ # Immutable values
+ foreach my $key (qw(attacher bug_id bug_ref creation_time
+ encoding id ref size update_token)) {
+ delete $params->{$key};
+ }
+
+ # Convert setter and requestee to standard values
+ if (exists $params->{flags}) {
+ foreach my $flag (@{ $params->{flags} }) {
+ delete $flag->{setter}; # Always use logged in user
+ if (exists $flag->{requestee} && ref $flag->{requestee}) {
+ $flag->{requestee} = $flag->{requestee}->{name};
+ }
+ }
+ }
+ # Add comment if one is provided
+ if ($params->{comments}) {
+ $params->{comment} = $params->{comments}->[0]->{text};
+ delete $params->{comments};
+ }
+}
+
+#########################
+# REST Response Methods #
+#########################
+
+sub search_bugs_response {
+ my ($result, $response) = @_;
+ my $cache = Bugzilla->request_cache;
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+
+ my $bug_objs = $cache->{bzapi_search_bugs};
+
+ my @fixed_bugs;
+ foreach my $bug_data (@{$$result->{bugs}}) {
+ my $bug_obj = shift @$bug_objs;
+ my $fixed = fix_bug($bug_data, $bug_obj);
+
+ # CC count and Dupe count
+ if (filter_wants_nocache($params, 'cc_count')) {
+ $fixed->{cc_count} = scalar @{ $bug_obj->cc }
+ if $bug_obj->cc;
+ }
+ if (filter_wants_nocache($params, 'dupe_count')) {
+ $fixed->{dupe_count} = scalar @{ $bug_obj->duplicate_ids }
+ if $bug_obj->duplicate_ids;
+ }
+
+ push(@fixed_bugs, $fixed);
+ }
+
+ $$result->{bugs} = \@fixed_bugs;
+}
+
+sub create_bug_response {
+ my ($result, $response) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ return if !exists $$result->{id};
+ my $bug_id = $$result->{id};
+
+ $$result = { ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id") };
+ $response->code(STATUS_CREATED);
+}
+
+sub get_bug_response {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ return if !exists $$result->{bugs};
+ my $bug_data = $$result->{bugs}->[0];
+
+ my $bug_id = $rpc->bz_rest_params->{ids}->[0];
+ my $bug_obj = Bugzilla::Bug->check($bug_id);
+ my $fixed = fix_bug($bug_data, $bug_obj);
+
+ $$result = $fixed;
+}
+
+sub update_bug_response {
+ my ($result) = @_;
+ return if !exists $$result->{bugs}
+ || !scalar @{$$result->{bugs}};
+ $$result = { ok => 1 };
+}
+
+# Get all comments for a bug
+sub get_comments_response {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+
+ my $bug_id = $rpc->bz_rest_params->{ids}->[0];
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ my $comment_objs = $bug->comments({ order => 'oldest_to_newest',
+ after => $params->{new_since} });
+ my @filtered_comment_objs;
+ foreach my $comment (@$comment_objs) {
+ next if $comment->is_private && !Bugzilla->user->is_insider;
+ push(@filtered_comment_objs, $comment);
+ }
+
+ my $comments_data = $$result->{bugs}->{$bug_id}->{comments};
+
+ my @fixed_comments;
+ foreach my $comment_data (@$comments_data) {
+ my $comment_obj = shift @filtered_comment_objs;
+ my $fixed = fix_comment($comment_data, $comment_obj);
+
+ if (exists $fixed->{creator}) {
+ # /bug/<ID>/comment returns full login for creator but not for /bug/<ID>?include_fields=comments :(
+ $fixed->{creator}->{name} = $rpc->type('string', $comment_obj->author->login);
+ # /bug/<ID>/comment does not return real_name for creator but returns ref
+ $fixed->{creator}->{'ref'} = $rpc->type('string', ref_urlbase() . "/user/" . $comment_obj->author->login);
+ delete $fixed->{creator}->{real_name};
+ }
+
+ push(@fixed_comments, filter($params, $fixed));
+ }
+
+ $$result = { comments => \@fixed_comments };
+}
+
+# Format the return response on successful comment creation
+sub add_comment_response {
+ my ($result, $response) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ return if !exists $$result->{id};
+ my $bug_id = $rpc->bz_rest_params->{id};
+
+ $$result = { ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id/comment") };
+ $response->code(STATUS_CREATED);
+}
+
+# Get the history for a bug
+sub get_history_response {
+ my ($result) = @_;
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+ my $history = $$result->{bugs}->[0]->{history};
+
+ my @new_history;
+ foreach my $changeset (@$history) {
+ $changeset = fix_changeset($changeset);
+ push(@new_history, filter($params, $changeset));
+ }
+
+ $$result = { history => \@new_history };
+}
+
+# Get all attachments for a bug
+sub get_attachments_response {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+ my $bug_id = $rpc->bz_rest_params->{ids}->[0];
+ my $bug = Bugzilla::Bug->check($bug_id);
+ my $attachment_objs = $bug->attachments;
+
+ my $attachments_data = $$result->{bugs}->{$bug_id};
+
+ my @fixed_attachments;
+ foreach my $attachment (@$attachments_data) {
+ my $attachment_obj = shift @$attachment_objs;
+ my $fixed = fix_attachment($attachment, $attachment_obj);
+
+ if ((filter_wants_nocache($params, 'data', 'extra')
+ || filter_wants_nocache($params, 'encoding', 'extra')
+ || $params->{attachmentdata}))
+ {
+ if (!$fixed->{data}) {
+ $fixed->{data} = $rpc->type('base64', $attachment_obj->data);
+ $fixed->{encoding} = $rpc->type('string', 'base64');
+ }
+ }
+ else {
+ delete $fixed->{data};
+ delete $fixed->{encoding};
+ }
+
+ push(@fixed_attachments, filter($params, $fixed));
+ }
+
+ $$result = { attachments => \@fixed_attachments };
+}
+
+# Format the return response on successful attachment creation
+sub add_attachment_response {
+ my ($result, $response) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ my ($attach_id) = keys %{ $$result->{attachments} };
+
+ $$result = { ref => $rpc->type('string', ref_urlbase() . "/attachment/$attach_id"), id => $attach_id };
+ $response->code(STATUS_CREATED);
+}
+
+# Update an attachment's metadata
+sub update_attachment_response {
+ my ($result) = @_;
+ $$result = { ok => 1 };
+}
+
+# Get a single attachment by attachment_id
+sub get_attachment_response {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{attachments};
+ my $attach_id = $rpc->bz_rest_params->{attachment_ids}->[0];
+ my $attachment_data = $$result->{attachments}->{$attach_id};
+ my $attachment_obj = Bugzilla::Attachment->new($attach_id);
+ my $fixed = fix_attachment($attachment_data, $attachment_obj);
+
+ if ((filter_wants_nocache($params, 'data', 'extra')
+ || filter_wants_nocache($params, 'encoding', 'extra')
+ || $params->{attachmentdata}))
+ {
+ if (!$fixed->{data}) {
+ $fixed->{data} = $rpc->type('base64', $attachment_obj->data);
+ $fixed->{encoding} = $rpc->type('string', 'base64');
+ }
+ }
+ else {
+ delete $fixed->{data};
+ delete $fixed->{encoding};
+ }
+
+ $fixed = filter($params, $fixed);
+
+ $$result = $fixed;
+}
+
+# Get a list of flags for a bug
+sub get_bug_flags_response {
+ my ($result) = @_;
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{bugs};
+ my $flags = $$result->{bugs}->[0]->{flags};
+
+ my @new_flags;
+ foreach my $flag (@$flags) {
+ push(@new_flags, fix_flag($flag));
+ }
+
+ $$result = { flags => \@new_flags };
+}
+
+1;
diff --git a/extensions/BzAPI/lib/Resources/Bugzilla.pm b/extensions/BzAPI/lib/Resources/Bugzilla.pm
new file mode 100644
index 000000000..6c436e861
--- /dev/null
+++ b/extensions/BzAPI/lib/Resources/Bugzilla.pm
@@ -0,0 +1,118 @@
+# 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::Extension::BzAPI::Resources::Bugzilla;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Keyword;
+use Bugzilla::Product;
+use Bugzilla::Status;
+use Bugzilla::Field;
+
+use Digest::MD5 qw(md5_base64);
+
+#########################
+# REST Resource Methods #
+#########################
+
+BEGIN {
+ require Bugzilla::WebService::Bugzilla;
+ *Bugzilla::WebService::Bugzilla::get_configuration = \&get_configuration;
+}
+
+sub rest_handlers {
+ my $rest_handlers = [
+ qr{^/configuration$}, {
+ GET => {
+ resource => {
+ method => 'get_configuration'
+ }
+ }
+ }
+ ];
+ return $rest_handlers;
+}
+
+sub get_configuration {
+ my ($self) = @_;
+ my $user = Bugzilla->user;
+ my $params = Bugzilla->input_params;
+
+ # Get data from the shadow DB as they don't change very often.
+ Bugzilla->switch_to_shadow_db;
+
+ # Pass a bunch of Bugzilla configuration to the templates.
+ my $vars = {};
+ $vars->{'priority'} = get_legal_field_values('priority');
+ $vars->{'severity'} = get_legal_field_values('bug_severity');
+ $vars->{'platform'} = get_legal_field_values('rep_platform');
+ $vars->{'op_sys'} = get_legal_field_values('op_sys');
+ $vars->{'keyword'} = [ map($_->name, Bugzilla::Keyword->get_all) ];
+ $vars->{'resolution'} = get_legal_field_values('resolution');
+ $vars->{'status'} = get_legal_field_values('bug_status');
+ $vars->{'custom_fields'} =
+ [ grep {$_->is_select} Bugzilla->active_custom_fields ];
+
+ # Include a list of product objects.
+ if ($params->{'product'}) {
+ my @products = $params->{'product'};
+ foreach my $product_name (@products) {
+ my $product = new Bugzilla::Product({ name => $product_name });
+ if ($product && $user->can_see_product($product->name)) {
+ push (@{$vars->{'products'}}, $product);
+ }
+ }
+ } else {
+ $vars->{'products'} = $user->get_selectable_products;
+ }
+
+ # We set the 2nd argument to 1 to also preload flag types.
+ Bugzilla::Product::preload($vars->{'products'}, 1, { is_active => 1 });
+
+ # Allow consumers to specify whether or not they want flag data.
+ if (defined $params->{'flags'}) {
+ $vars->{'show_flags'} = $params->{'flags'};
+ }
+ else {
+ # We default to sending flag data.
+ $vars->{'show_flags'} = 1;
+ }
+
+ # Create separate lists of open versus resolved statuses. This should really
+ # be made part of the configuration.
+ my @open_status;
+ my @closed_status;
+ foreach my $status (@{$vars->{'status'}}) {
+ is_open_state($status) ? push(@open_status, $status)
+ : push(@closed_status, $status);
+ }
+ $vars->{'open_status'} = \@open_status;
+ $vars->{'closed_status'} = \@closed_status;
+
+ # Generate a list of fields that can be queried.
+ my @fields = @{Bugzilla::Field->match({obsolete => 0})};
+ # Exclude fields the user cannot query.
+ if (!Bugzilla->user->is_timetracker) {
+ @fields = grep { $_->name !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ } @fields;
+ }
+ $vars->{'field'} = \@fields;
+
+ my $json;
+ Bugzilla->template->process('config.json.tmpl', $vars, \$json);
+ my $result = {};
+ if ($json) {
+ $result = $self->json->decode($json);
+ }
+ return $result;
+}
+
+1;
diff --git a/extensions/BzAPI/lib/Resources/User.pm b/extensions/BzAPI/lib/Resources/User.pm
new file mode 100644
index 000000000..7fbcdb871
--- /dev/null
+++ b/extensions/BzAPI/lib/Resources/User.pm
@@ -0,0 +1,79 @@
+# 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::Extension::BzAPI::Resources::User;
+
+use 5.10.1;
+use strict;
+
+use Bugzilla::Extension::BzAPI::Util;
+
+sub rest_handlers {
+ my $rest_handlers = [
+ qr{/user$}, {
+ GET => {
+ response => \&get_users,
+ },
+ },
+ qr{/user/([^/]+)$}, {
+ GET => {
+ response => \&get_user,
+ },
+ }
+ ];
+ return $rest_handlers;
+}
+
+sub get_users {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{users};
+
+ my @users;
+ foreach my $user (@{$$result->{users}}) {
+ my $object = Bugzilla::User->new(
+ { id => $user->{id}, cache => 1 });
+
+ $user = fix_user($user, $object);
+
+ # Use userid instead of email for 'ref' for /user calls
+ $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id);
+
+ # Emails are not filtered even if user is not logged in
+ $user->{name} = $rpc->type('string', $object->login);
+
+ push(@users, filter($params, $user));
+ }
+
+ $$result->{users} = \@users;
+}
+
+sub get_user {
+ my ($result) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $params = Bugzilla->input_params;
+
+ return if !exists $$result->{users};
+ my $user = $$result->{users}->[0] || return;
+ my $object = Bugzilla::User->new({ id => $user->{id}, cache => 1 });
+
+ $user = fix_user($user, $object);
+
+ # Use userid instead of email for 'ref' for /user calls
+ $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id);
+
+ # Emails are not filtered even if user is not logged in
+ $user->{name} = $rpc->type('string', $object->login);
+
+ $user = filter($params, $user);
+
+ $$result = $user;
+}
+
+1;
diff --git a/extensions/BzAPI/lib/Util.pm b/extensions/BzAPI/lib/Util.pm
new file mode 100644
index 000000000..ba16f0a13
--- /dev/null
+++ b/extensions/BzAPI/lib/Util.pm
@@ -0,0 +1,442 @@
+# 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.
+# 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::Extension::BzAPI::Util;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Bug;
+use Bugzilla::Constants;
+use Bugzilla::Extension::BzAPI::Constants;
+use Bugzilla::Token;
+use Bugzilla::Util qw(correct_urlbase email_filter);
+use Bugzilla::WebService::Util qw(filter_wants);
+
+use MIME::Base64;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ ref_urlbase
+ fix_bug
+ fix_user
+ fix_flag
+ fix_comment
+ fix_changeset
+ fix_attachment
+ fix_group
+ filter_wants_nocache
+ filter
+ fix_credentials
+ filter_email
+);
+
+# Return an URL base appropriate for constructing a ref link
+# normally required by REST API calls.
+sub ref_urlbase {
+ return correct_urlbase() . "bzapi";
+}
+
+# convert certain fields within a bug object
+# from a simple scalar value to their respective objects
+sub fix_bug {
+ my ($data, $bug) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $params = Bugzilla->input_params;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $method = Bugzilla->request_cache->{bzapi_rpc_method};
+
+ $bug = ref $bug ? $bug : Bugzilla::Bug->check($bug || $data->{id});
+
+ # Add REST API reference to the individual bug
+ if (filter_wants_nocache($params, 'ref')) {
+ $data->{'ref'} = ref_urlbase() . "/bug/" . $bug->id;
+ }
+
+ # User fields
+ foreach my $field (USER_FIELDS) {
+ next if !exists $data->{$field};
+ if ($field eq 'cc') {
+ my @new_cc;
+ foreach my $cc (@{ $bug->cc_users }) {
+ my $cc_data = { name => filter_email($cc->email) };
+ push(@new_cc, fix_user($cc_data, $cc));
+ }
+ $data->{$field} = \@new_cc;
+ }
+ else {
+ my $field_name = $field;
+ if ($field eq 'creator') {
+ $field_name = 'reporter';
+ }
+ $data->{$field}
+ = fix_user($data->{"${field}_detail"}, $bug->$field_name);
+ delete $data->{$field}->{id};
+ delete $data->{$field}->{email};
+ $data->{$field} = filter($params, $data->{$field}, undef, $field);
+ }
+
+ # Get rid of extra detail hash if exists since redundant
+ delete $data->{"${field}_detail"} if exists $data->{"${field}_detail"};
+ }
+
+ # Groups
+ if (filter_wants_nocache($params, 'groups')) {
+ my @new_groups;
+ foreach my $group (@{ $data->{groups} }) {
+ push(@new_groups, fix_group($group));
+ }
+ $data->{groups} = \@new_groups;
+ }
+
+ # Flags
+ if (exists $data->{flags}) {
+ my @new_flags;
+ foreach my $flag (@{ $data->{flags} }) {
+ push(@new_flags, fix_flag($flag));
+ }
+ $data->{flags} = \@new_flags;
+ }
+
+ # Attachment metadata is included by default but not data
+ if (filter_wants_nocache($params, 'attachments')) {
+ my $attachment_params = { ids => $data->{id} };
+ if (!filter_wants_nocache($params, 'data', 'extra', 'attachments')
+ && !$params->{attachmentdata})
+ {
+ $attachment_params->{exclude_fields} = ['data'];
+ }
+
+ my $attachments = $rpc->attachments($attachment_params);
+
+ my @fixed_attachments;
+ foreach my $attachment (@{ $attachments->{bugs}->{$data->{id}} }) {
+ my $fixed = fix_attachment($attachment);
+ push(@fixed_attachments, filter($params, $fixed, undef, 'attachments'));
+ }
+
+ $data->{attachments} = \@fixed_attachments;
+ }
+
+ # Comments and history are not part of _default and have to be requested
+
+ # Comments
+ if (filter_wants_nocache($params, 'comments', 'extra', 'comments')) {
+ my $comments = $rpc->comments({ ids => $data->{id} });
+ $comments = $comments->{bugs}->{$data->{id}}->{comments};
+ my @new_comments;
+ foreach my $comment (@$comments) {
+ $comment = fix_comment($comment);
+ push(@new_comments, filter($params, $comment, 'extra', 'comments'));
+ }
+ $data->{comments} = \@new_comments;
+ }
+
+ # History
+ if (filter_wants_nocache($params, 'history', 'extra', 'history')) {
+ my $history = $rpc->history({ ids => [ $data->{id} ] });
+ my @new_history;
+ foreach my $changeset (@{ $history->{bugs}->[0]->{history} }) {
+ push(@new_history, fix_changeset($changeset, $bug));
+ }
+ $data->{history} = \@new_history;
+ }
+
+ # Add in all custom fields even if not set or visible on this bug
+ my $custom_fields = Bugzilla->fields({ custom => 1, obsolete => 0 });
+ foreach my $field (@$custom_fields) {
+ my $name = $field->name;
+ next if !filter_wants_nocache($params, $name, ['default','custom']);
+ if ($field->type == FIELD_TYPE_BUG_ID) {
+ $data->{$name} = $rpc->type('int', $bug->$name);
+ }
+ elsif ($field->type == FIELD_TYPE_DATETIME
+ || $field->type == FIELD_TYPE_DATE)
+ {
+ $data->{$name} = $rpc->type('dateTime', $bug->$name);
+ }
+ elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+ # Bug.search, when include_fields=_all, returns array, otherwise return as comma delimited string :(
+ if ($method eq 'Bug.search' && !grep($_ eq '_all', @{ $params->{include_fields} })) {
+ $data->{$name} = $rpc->type('string', join(', ', @{ $bug->$name }));
+ }
+ else {
+ my @values = map { $rpc->type('string', $_) } @{ $bug->$name };
+ $data->{$name} = \@values;
+ }
+ }
+ else {
+ $data->{$name} = $rpc->type('string', $bug->$name);
+ }
+ }
+
+ foreach my $key (keys %$data) {
+ # Remove empty values in some cases
+ next if $key eq 'qa_contact'; # Return qa_contact even if null
+ next if $method eq 'Bug.search' && $key eq 'keywords'; # Return keywords even if empty
+ next if $method eq 'Bug.get' && grep($_ eq $key, TIMETRACKING_FIELDS);
+
+ next if ($method eq 'Bug.search'
+ && $key =~ /^(resolution|cc_count|dupe_count)$/
+ && !grep($_ eq '_all', @{ $params->{include_fields} }));
+
+ if (!ref $data->{$key}) {
+ delete $data->{$key} if !$data->{$key};
+ }
+ else {
+ if (ref $data->{$key} eq 'ARRAY' && !@{$data->{$key}}) {
+ # Return empty string if blocks or depends_on is empty
+ if ($key eq 'depends_on' || $key eq 'blocks') {
+ $data->{$key} = '';
+ }
+ else {
+ delete $data->{$key};
+ }
+ }
+ elsif (ref $data->{$key} eq 'HASH' && !%{$data->{$key}}) {
+ delete $data->{$key};
+ }
+ }
+ }
+
+ return $data;
+}
+
+# convert a user related field from being just login
+# names to user objects
+sub fix_user {
+ my ($data, $object) = @_;
+ my $user = Bugzilla->user;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ return { name => undef } if !$data;
+
+ if (!ref $data) {
+ $data = {
+ name => filter_email($object->login)
+ };
+ if ($user->id) {
+ $data->{real_name} = $rpc->type('string', $object->name);
+ }
+ }
+ else {
+ $data->{name} = filter_email($data->{name});
+ }
+
+ if ($user->id) {
+ $data->{ref} = $rpc->type('string', ref_urlbase . "/user/" . $object->login);
+ }
+
+ return $data;
+}
+
+# convert certain attributes of a comment to objects
+# and also remove other unwanted key/values.
+sub fix_comment {
+ my ($data, $object) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $method = Bugzilla->request_cache->{bzapi_rpc_method};
+
+ $object ||= Bugzilla::Comment->new({ id => $data->{id}, cache => 1 });
+
+ if (exists $data->{creator}) {
+ $data->{creator} = fix_user($data->{creator}, $object->author);
+ }
+
+ if ($data->{attachment_id} && $method ne 'Bug.search') {
+ $data->{attachment_ref} = $rpc->type('string', ref_urlbase() .
+ "/attachment/" . $data->{attachment_id});
+ }
+ else {
+ delete $data->{attachment_id};
+ }
+
+ delete $data->{author};
+ delete $data->{time};
+
+ return $data;
+}
+
+# convert certain attributes of a changeset object from
+# scalar values to related objects. Also remove other unwanted
+# key/values.
+sub fix_changeset {
+ my ($data, $object) = @_;
+ my $user = Bugzilla->user;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ if ($data->{who}) {
+ $data->{changer} = {
+ name => filter_email($data->{who}),
+ ref => $rpc->type('string', ref_urlbase() . "/user/" . $data->{who})
+ };
+ delete $data->{who};
+ }
+
+ if ($data->{when}) {
+ $data->{change_time} = $rpc->type('dateTime', $data->{when});
+ delete $data->{when};
+ }
+
+ foreach my $change (@{ $data->{changes} }) {
+ $change->{field_name} = 'flag' if $change->{field_name} eq 'flagtypes.name';
+ }
+
+ return $data;
+}
+
+# convert certain attributes of an attachment object from
+# scalar values to related objects. Also add in additional
+# key/values.
+sub fix_attachment {
+ my ($data, $object) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ my $method = Bugzilla->request_cache->{bzapi_rpc_method};
+ my $params = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ $object ||= Bugzilla::Attachment->new({ id => $data->{id}, cache => 1 });
+
+ if (exists $data->{attacher}) {
+ $data->{attacher} = fix_user($data->{attacher}, $object->attacher);
+ if ($method eq 'Bug.search') {
+ delete $data->{attacher}->{real_name};
+ }
+ else {
+ $data->{attacher}->{real_name} = $rpc->type('string', $object->attacher->name);
+ }
+ }
+
+ if ($data->{data}) {
+ $data->{encoding} = $rpc->type('string', 'base64');
+ }
+
+ if (exists $data->{bug_id}) {
+ $data->{bug_ref} = $rpc->type('string', ref_urlbase() . "/bug/" . $data->{bug_id});
+ }
+
+ # Upstream API returns these as integers where bzapi returns as booleans
+ if (exists $data->{is_patch}) {
+ $data->{is_patch} = $rpc->type('boolean', $data->{is_patch});
+ }
+ if (exists $data->{is_obsolete}) {
+ $data->{is_obsolete} = $rpc->type('boolean', $data->{is_obsolete});
+ }
+ if (exists $data->{is_private}) {
+ $data->{is_private} = $rpc->type('boolean', $data->{is_private});
+ }
+
+ if (exists $data->{flags} && @{ $data->{flags} }) {
+ my @new_flags;
+ foreach my $flag (@{ $data->{flags} }) {
+ push(@new_flags, fix_flag($flag));
+ }
+ $data->{flags} = \@new_flags;
+ }
+ else {
+ delete $data->{flags};
+ }
+
+ $data->{ref} = $rpc->type('string', ref_urlbase() . "/attachment/" . $data->{id});
+
+ # Add update token if we are getting an attachment outside of Bug.get and user is logged in
+ if ($user->id && ($method eq 'Bug.attachments'|| $method eq 'Bug.search')) {
+ $data->{update_token} = issue_hash_token([ $object->id, $object->modification_time ]);
+ }
+
+ delete $data->{creator};
+ delete $data->{summary};
+
+ return $data;
+}
+
+# convert certain attributes of a flag object from
+# scalar values to related objects. Also remove other unwanted
+# key/values.
+sub fix_flag {
+ my ($data, $object) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ $object ||= Bugzilla::Flag->new({ id => $data->{id}, cache => 1 });
+
+ if (exists $data->{setter}) {
+ $data->{setter} = fix_user($data->{setter}, $object->setter);
+ delete $data->{setter}->{real_name};
+ }
+
+ if (exists $data->{requestee}) {
+ $data->{requestee} = fix_user($data->{requestee}, $object->requestee);
+ delete $data->{requestee}->{real_name};
+ }
+
+ return $data;
+}
+
+# convert certain attributes of a group object from scalar
+# values to related objects
+sub fix_group {
+ my ($group, $object) = @_;
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+
+ $object ||= Bugzilla::Group->new({ name => $group });
+
+ if ($object) {
+ $group = {
+ id => $rpc->type('int', $object->id),
+ name => $rpc->type('string', $object->name),
+ };
+ }
+
+ return $group;
+}
+
+# Calls Bugzilla::WebService::Util::filter_wants but disables caching
+# as we make several webservice calls in a single REST call and the
+# caching can cause unexpected results.
+sub filter_wants_nocache {
+ my ($params, $field, $types, $prefix) = @_;
+ delete Bugzilla->request_cache->{filter_wants};
+ return filter_wants($params, $field, $types, $prefix);
+}
+
+sub filter {
+ my ($params, $hash, $types, $prefix) = @_;
+ my %newhash = %$hash;
+ foreach my $key (keys %$hash) {
+ delete $newhash{$key} if !filter_wants_nocache($params, $key, $types, $prefix);
+ }
+ return \%newhash;
+}
+
+sub fix_credentials {
+ my ($params) = @_;
+ # Allow user to pass in username=foo&password=bar to be compatible
+ $params->{'Bugzilla_login'} = delete $params->{'username'} if exists $params->{'username'};
+ $params->{'Bugzilla_password'} = delete $params->{'password'} if exists $params->{'password'};
+
+ # Allow user to pass userid=1&cookie=3iYGuKZdyz for compatibility with BzAPI
+ if (exists $params->{'userid'} && exists $params->{'cookie'}) {
+ my $userid = delete $params->{'userid'};
+ my $cookie = delete $params->{'cookie'};
+ $params->{'Bugzilla_token'} = "${userid}-${cookie}";
+ }
+}
+
+# Filter email addresses by default ignoring the system
+# webservice_email_filter setting
+sub filter_email {
+ my $rpc = Bugzilla->request_cache->{bzapi_rpc};
+ return $rpc->type('string', email_filter($_[0]));
+}
+
+1;
diff --git a/extensions/BzAPI/template/en/default/config.json.tmpl b/extensions/BzAPI/template/en/default/config.json.tmpl
index 9c6852346..993b34915 100644
--- a/extensions/BzAPI/template/en/default/config.json.tmpl
+++ b/extensions/BzAPI/template/en/default/config.json.tmpl
@@ -1,23 +1,10 @@
-[%# The contents of this file are subject to the Mozilla Public
- # License Version 1.1 (the "License"); you may not use this file
- # except in compliance with the License. You may obtain a copy of
- # the License at http://www.mozilla.org/MPL/
+[%# 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/.
#
- # Software distributed under the License is distributed on an "AS
- # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
- # implied. See the License for the specific language governing
- # rights and limitations under the License.
- #
- # The Original Code is the Bugzilla Bug Tracking System.
- #
- # The Initial Developer of the Original Code is Netscape Communications
- # Corporation. Portions created by Netscape are
- # Copyright (C) 1998 Netscape Communications Corporation. All
- # Rights Reserved.
- #
- # Contributor(s): Gervase Markham <gerv@gerv.net>
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
#%]
-
[%
# Pinched from Bugzilla/API/Model/Utils.pm in BzAPI - need to keep in sync
OLD2NEW = {
@@ -28,7 +15,7 @@ OLD2NEW = {
'bug_id' => 'id',
'rep_platform' => 'platform',
'bug_severity' => 'severity',
- 'bug_status' => 'status',
+ 'bug_status' => 'status',
'short_desc' => 'summary',
'short_short_desc' => 'summary',
'bug_file_loc' => 'url',
@@ -103,8 +90,8 @@ OLDATTACH2NEW = {
%]
-[%# Add attachment stuff to the main hash - but with right prefix. (This is
- # the way the code is structured in BzAPI, and changing it makes it harder
+[%# Add attachment stuff to the main hash - but with right prefix. (This is
+ # the way the code is structured in BzAPI, and changing it makes it harder
# to keep the two in sync.)
#%]
[% FOREACH entry IN OLDATTACH2NEW %]
@@ -119,7 +106,7 @@ OLDATTACH2NEW = {
"maintainer": "[% Param('maintainer') FILTER json %]",
"announcement": "[% Param('announcehtml') FILTER json %]",
"max_attachment_size": [% (Param('maxattachmentsize') * 1000) FILTER json %],
-
+
[% IF Param('useclassification') %]
[% cl_name_for = {} %]
"classification": {
@@ -154,7 +141,7 @@ OLDATTACH2NEW = {
"id": [% component.id FILTER json %],
[% IF show_flags %]
"flag_type": [
- [% flag_types =
+ [% flag_types =
component.flag_types(is_active=>1).bug.merge(component.flag_types(is_active=>1).attachment) %]
[%-# "first" flag used to get commas right; can't use loop.last() in case
# last flag is inactive %]
@@ -173,7 +160,7 @@ OLDATTACH2NEW = {
"[% version.name FILTER json %]"[% ',' UNLESS loop.last() %]
[% END %]
],
-
+
[% IF Param('usetargetmilestone') %]
"default_target_milestone": "[% product.defaultmilestone FILTER json %]",
"target_milestone": [
@@ -191,7 +178,7 @@ OLDATTACH2NEW = {
}[% ',' UNLESS loop.last() %]
[% END %]
},
-
+
"group": {
[% FOREACH group = product.groups_valid %]
"[% group.id FILTER json %]": {
@@ -227,7 +214,7 @@ OLDATTACH2NEW = {
[% END %]
[% PROCESS "global/field-descs.none.tmpl" %]
-
+
[%# Put custom field value data where below loop expects to find it %]
[% FOREACH cf = custom_fields %]
[% ${cf.name} = [] %]
@@ -235,8 +222,8 @@ OLDATTACH2NEW = {
[% ${cf.name}.push(value.name) %]
[% END %]
[% END %]
-
- [%# Built-in fields do not have type IDs. There aren't ID values for all
+
+ [%# Built-in fields do not have type IDs. There aren't ID values for all
# the types of the built-in fields, but we do what we can, and leave the
# rest as "0" (unknown).
#%]
@@ -257,12 +244,12 @@ OLDATTACH2NEW = {
"alias" => 1,
"deadline" => 5,
} %]
-
+
"field": {
[% FOREACH item = field %]
[% newname = OLD2NEW.${item.name} || item.name %]
"[% newname FILTER json %]": {
- "description": "[% (field_descs.${item.name} OR
+ "description": "[% (field_descs.${item.name} OR
item.description) FILTER json %]",
"is_active": [% field.obsolete ? "false" : "true" %],
[% blacklist = ["version", "group", "product", "component"] %]
diff --git a/extensions/BzAPI/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BzAPI/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..1ffe03d22
--- /dev/null
+++ b/extensions/BzAPI/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,11 @@
+[%# 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.
+ #%]
+
+[% IF error == "bzapi_midair_collision" %]
+ Mid-air collision
+[% END %]