Last merge with trunk
commit
b271aaa842
|
@ -26,3 +26,8 @@ Options -Indexes
|
|||
</IfModule>
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE]
|
||||
</IfModule>
|
||||
|
|
50
Bugzilla.pm
50
Bugzilla.pm
|
@ -20,7 +20,7 @@ use Bugzilla::Extension;
|
|||
use Bugzilla::DB;
|
||||
use Bugzilla::Install::Localconfig qw(read_localconfig);
|
||||
use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
|
||||
use Bugzilla::Install::Util qw(init_console);
|
||||
use Bugzilla::Install::Util qw(init_console include_languages);
|
||||
use Bugzilla::Template;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Error;
|
||||
|
@ -207,7 +207,7 @@ sub _tt_provider_load_compiled {
|
|||
# Global Code
|
||||
#####################################################################
|
||||
|
||||
# $::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess;
|
||||
#$::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess;
|
||||
|
||||
# Note that this is a raw subroutine, not a method, so $class isn't available.
|
||||
sub init_page {
|
||||
|
@ -400,12 +400,7 @@ sub feature {
|
|||
|
||||
my $success = 1;
|
||||
foreach my $module (@{ $feature_map->{$feature} }) {
|
||||
# We can't use a string eval and "use" here (it kills Template-Toolkit,
|
||||
# see https://rt.cpan.org/Public/Bug/Display.html?id=47929), so we have
|
||||
# to do a block eval.
|
||||
$module =~ s{::}{/}g;
|
||||
$module .= ".pm";
|
||||
eval { require $module; 1; } or $success = 0;
|
||||
eval "require $module" or $success = 0;
|
||||
}
|
||||
$cache->{feature}->{$feature} = $success;
|
||||
return $success;
|
||||
|
@ -601,6 +596,10 @@ sub languages {
|
|||
return Bugzilla::Install::Util::supported_languages();
|
||||
}
|
||||
|
||||
sub current_language {
|
||||
return $_[0]->request_cache->{current_language} ||= (include_languages())[0];
|
||||
}
|
||||
|
||||
sub error_mode {
|
||||
my ($class, $newval) = @_;
|
||||
if (defined $newval) {
|
||||
|
@ -643,6 +642,9 @@ sub usage_mode {
|
|||
elsif ($newval == USAGE_MODE_EMAIL) {
|
||||
$class->error_mode(ERROR_MODE_DIE);
|
||||
}
|
||||
elsif ($newval == USAGE_MODE_TEST) {
|
||||
$class->error_mode(ERROR_MODE_TEST);
|
||||
}
|
||||
else {
|
||||
ThrowCodeError('usage_mode_invalid',
|
||||
{'invalid_usage_mode', $newval});
|
||||
|
@ -953,7 +955,7 @@ sub has_flags {
|
|||
}
|
||||
|
||||
sub local_timezone {
|
||||
return $_[0]->request_cache->{local_timezone}
|
||||
return $_[0]->process_cache->{local_timezone}
|
||||
||= DateTime::TimeZone->new(name => 'local');
|
||||
}
|
||||
|
||||
|
@ -975,6 +977,18 @@ sub request_cache {
|
|||
return $_request_cache ||= {};
|
||||
}
|
||||
|
||||
sub clear_request_cache {
|
||||
$_request_cache = {};
|
||||
if ($ENV{MOD_PERL}) {
|
||||
require Apache2::RequestUtil;
|
||||
my $request = eval { Apache2::RequestUtil->request };
|
||||
if ($request) {
|
||||
my $pnotes = $request->pnotes;
|
||||
delete @$pnotes{(keys %$pnotes)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# This is a per-process cache. Under mod_cgi it's identical to the
|
||||
# request_cache. When using mod_perl, items in this cache live until the
|
||||
# worker process is terminated.
|
||||
|
@ -996,7 +1010,7 @@ sub _cleanup {
|
|||
$dbh->bz_rollback_transaction() if $dbh->bz_in_transaction;
|
||||
$dbh->disconnect;
|
||||
}
|
||||
undef $_request_cache;
|
||||
clear_request_cache();
|
||||
|
||||
# These are both set by CGI.pm but need to be undone so that
|
||||
# Apache can actually shut down its children if it needs to.
|
||||
|
@ -1118,10 +1132,10 @@ not an arrayref.
|
|||
|
||||
=item C<user>
|
||||
|
||||
C<undef> if there is no currently logged in user or if the login code has not
|
||||
yet been run. If an sudo session is in progress, the C<Bugzilla::User>
|
||||
corresponding to the person who is being impersonated. If no session is in
|
||||
progress, the current C<Bugzilla::User>.
|
||||
Default C<Bugzilla::User> object if there is no currently logged in user or
|
||||
if the login code has not yet been run. If an sudo session is in progress,
|
||||
the C<Bugzilla::User> corresponding to the person who is being impersonated.
|
||||
If no session is in progress, the current C<Bugzilla::User>.
|
||||
|
||||
=item C<set_user>
|
||||
|
||||
|
@ -1258,6 +1272,10 @@ The main database handle. See L<DBI>.
|
|||
Currently installed languages.
|
||||
Returns a reference to a list of RFC 1766 language tags of installed languages.
|
||||
|
||||
=item C<current_language>
|
||||
|
||||
The currently active language.
|
||||
|
||||
=item C<switch_to_shadow_db>
|
||||
|
||||
Switch from using the main database to using the shadow database.
|
||||
|
@ -1289,6 +1307,10 @@ this Bugzilla installation.
|
|||
Tells you whether or not a specific feature is enabled. For names
|
||||
of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
|
||||
|
||||
=item C<clear_request_cache>
|
||||
|
||||
Removes all entries from the C<request_cache>.
|
||||
|
||||
=back
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
|
|
@ -130,8 +130,7 @@ the ID of the bug to which the attachment is attached
|
|||
=cut
|
||||
|
||||
sub bug_id {
|
||||
my $self = shift;
|
||||
return $self->{bug_id};
|
||||
return $_[0]->{bug_id};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -145,11 +144,8 @@ the bug object to which the attachment is attached
|
|||
=cut
|
||||
|
||||
sub bug {
|
||||
my $self = shift;
|
||||
|
||||
require Bugzilla::Bug;
|
||||
$self->{bug} ||= Bugzilla::Bug->new({ id => $self->bug_id, cache => 1 });
|
||||
return $self->{bug};
|
||||
return $_[0]->{bug} //= Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 });
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -163,8 +159,7 @@ user-provided text describing the attachment
|
|||
=cut
|
||||
|
||||
sub description {
|
||||
my $self = shift;
|
||||
return $self->{description};
|
||||
return $_[0]->{description};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -178,8 +173,7 @@ the attachment's MIME media type
|
|||
=cut
|
||||
|
||||
sub contenttype {
|
||||
my $self = shift;
|
||||
return $self->{mimetype};
|
||||
return $_[0]->{mimetype};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -193,9 +187,8 @@ the user who attached the attachment
|
|||
=cut
|
||||
|
||||
sub attacher {
|
||||
my $self = shift;
|
||||
return $self->{attacher}
|
||||
||= new Bugzilla::User({ id => $self->{submitter_id}, cache => 1 });
|
||||
return $_[0]->{attacher}
|
||||
//= new Bugzilla::User({ id => $_[0]->{submitter_id}, cache => 1 });
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -209,8 +202,7 @@ the date and time on which the attacher attached the attachment
|
|||
=cut
|
||||
|
||||
sub attached {
|
||||
my $self = shift;
|
||||
return $self->{creation_ts};
|
||||
return $_[0]->{creation_ts};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -224,8 +216,7 @@ the date and time on which the attachment was last modified.
|
|||
=cut
|
||||
|
||||
sub modification_time {
|
||||
my $self = shift;
|
||||
return $self->{modification_time};
|
||||
return $_[0]->{modification_time};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -239,8 +230,7 @@ the name of the file the attacher attached
|
|||
=cut
|
||||
|
||||
sub filename {
|
||||
my $self = shift;
|
||||
return $self->{filename};
|
||||
return $_[0]->{filename};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -254,8 +244,7 @@ whether or not the attachment is a patch
|
|||
=cut
|
||||
|
||||
sub ispatch {
|
||||
my $self = shift;
|
||||
return $self->{ispatch};
|
||||
return $_[0]->{ispatch};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -269,8 +258,7 @@ whether or not the attachment is obsolete
|
|||
=cut
|
||||
|
||||
sub isobsolete {
|
||||
my $self = shift;
|
||||
return $self->{isobsolete};
|
||||
return $_[0]->{isobsolete};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -284,8 +272,7 @@ whether or not the attachment is private
|
|||
=cut
|
||||
|
||||
sub isprivate {
|
||||
my $self = shift;
|
||||
return $self->{isprivate};
|
||||
return $_[0]->{isprivate};
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -302,8 +289,7 @@ matches, because this will return a value even if it's matched by the generic
|
|||
=cut
|
||||
|
||||
sub is_viewable {
|
||||
my $self = shift;
|
||||
my $contenttype = $self->contenttype;
|
||||
my $contenttype = $_[0]->contenttype;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
# We assume we can view all text and image types.
|
||||
|
@ -443,7 +429,7 @@ the length (in bytes) of the attachment content
|
|||
|
||||
sub datasize {
|
||||
my $self = shift;
|
||||
return $self->{datasize} if exists $self->{datasize};
|
||||
return $self->{datasize} if defined $self->{datasize};
|
||||
|
||||
# If we have already retrieved the data, return its size.
|
||||
return length($self->{data}) if exists $self->{data};
|
||||
|
@ -500,11 +486,8 @@ flags that have been set on the attachment
|
|||
=cut
|
||||
|
||||
sub flags {
|
||||
my $self = shift;
|
||||
|
||||
# Don't cache it as it must be in sync with ->flag_types.
|
||||
$self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}];
|
||||
return $self->{flags};
|
||||
return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}];
|
||||
}
|
||||
|
||||
=over
|
||||
|
@ -529,8 +512,7 @@ sub flag_types {
|
|||
bug_obj => $self->bug,
|
||||
};
|
||||
|
||||
$self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
|
||||
return $self->{flag_types};
|
||||
return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
|
||||
}
|
||||
|
||||
###############################
|
||||
|
@ -634,6 +616,7 @@ sub _check_content_type {
|
|||
}
|
||||
|
||||
my $mimetype = mimetype($fh);
|
||||
$fh->seek(0, 0);
|
||||
$content_type = $mimetype if $mimetype;
|
||||
}
|
||||
|
||||
|
@ -771,7 +754,7 @@ sub get_attachments_by_bug {
|
|||
# To avoid $attachment->flags to run SQL queries itself for each
|
||||
# attachment listed here, we collect all the data at once and
|
||||
# populate $attachment->{flags} ourselves.
|
||||
# We also load all attachers at once for the same reason.
|
||||
# We also load all attachers and datasizes at once for the same reason.
|
||||
if ($vars->{preload}) {
|
||||
# Preload flags.
|
||||
$_->{flags} = [] foreach @$attachments;
|
||||
|
@ -793,6 +776,16 @@ sub get_attachments_by_bug {
|
|||
foreach my $attachment (@$attachments) {
|
||||
$attachment->{attacher} = $user_map{$attachment->{submitter_id}};
|
||||
}
|
||||
|
||||
# Preload datasizes.
|
||||
my $sizes =
|
||||
$dbh->selectall_hashref('SELECT attach_id, LENGTH(thedata) AS datasize
|
||||
FROM attachments LEFT JOIN attach_data ON attach_id = id
|
||||
WHERE bug_id = ?',
|
||||
'attach_id', undef, $bug->id);
|
||||
|
||||
# Force the size of attachments not in the DB to be recalculated.
|
||||
$_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments;
|
||||
}
|
||||
return $attachments;
|
||||
}
|
||||
|
@ -1004,12 +997,12 @@ sub update
|
|||
);
|
||||
}
|
||||
|
||||
if (scalar keys %$changes)
|
||||
{
|
||||
if (scalar(keys %$changes)) {
|
||||
$dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?',
|
||||
undef, $timestamp, $self->id);
|
||||
undef, ($timestamp, $self->id));
|
||||
$dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
|
||||
undef, $timestamp, $self->bug_id);
|
||||
undef, ($timestamp, $self->bug_id));
|
||||
$self->{modification_time} = $timestamp;
|
||||
}
|
||||
|
||||
return $changes;
|
||||
|
|
|
@ -10,6 +10,9 @@ package Bugzilla::Attachment::PatchReader;
|
|||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use IPC::Open3;
|
||||
use Symbol 'gensym';
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Attachment;
|
||||
use Bugzilla::Util;
|
||||
|
@ -100,8 +103,32 @@ sub process_interdiff {
|
|||
# Send through interdiff, send output directly to template.
|
||||
# Must hack path so that interdiff will work.
|
||||
$ENV{'PATH'} = $lc->{diffpath};
|
||||
open my $interdiff_fh, "$lc->{interdiffbin} $old_filename $new_filename|";
|
||||
binmode $interdiff_fh;
|
||||
|
||||
my ($pid, $interdiff_stdout, $interdiff_stderr);
|
||||
if ($ENV{MOD_PERL}) {
|
||||
require Apache2::RequestUtil;
|
||||
require Apache2::SubProcess;
|
||||
my $request = Apache2::RequestUtil->request;
|
||||
(undef, $interdiff_stdout, $interdiff_stderr) = $request->spawn_proc_prog(
|
||||
$lc->{interdiffbin}, [$old_filename, $new_filename]
|
||||
);
|
||||
} else {
|
||||
$interdiff_stderr = gensym;
|
||||
my $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr,
|
||||
$lc->{interdiffbin}, $old_filename, $new_filename);
|
||||
}
|
||||
binmode $interdiff_stdout;
|
||||
|
||||
# Check for errors
|
||||
{
|
||||
local $/ = undef;
|
||||
my $error = <$interdiff_stderr>;
|
||||
if ($error) {
|
||||
warn($error);
|
||||
$warning = 'interdiff3';
|
||||
}
|
||||
}
|
||||
|
||||
my ($reader, $last_reader) = setup_patch_readers("", $context);
|
||||
|
||||
if ($format eq 'raw') {
|
||||
|
@ -114,7 +141,7 @@ sub process_interdiff {
|
|||
}
|
||||
else {
|
||||
# In case the HTML page is displayed with the UTF-8 encoding.
|
||||
binmode $interdiff_fh, ':utf8' if Bugzilla->params->{'utf8'};
|
||||
binmode $interdiff_stdout, ':utf8' if Bugzilla->params->{'utf8'};
|
||||
|
||||
$vars->{'warning'} = $warning if $warning;
|
||||
$vars->{'bugid'} = $new_attachment->bug_id;
|
||||
|
@ -125,9 +152,9 @@ sub process_interdiff {
|
|||
|
||||
setup_template_patch_reader($last_reader, $format, $context, $vars);
|
||||
}
|
||||
$reader->iterate_fh($interdiff_fh, 'interdiff #' . $old_attachment->id .
|
||||
$reader->iterate_fh($interdiff_stdout, 'interdiff #' . $old_attachment->id .
|
||||
' #' . $new_attachment->id);
|
||||
close $interdiff_fh;
|
||||
waitpid($pid, 0) if $pid;
|
||||
$ENV{'PATH'} = '';
|
||||
|
||||
# Delete temporary files.
|
||||
|
|
|
@ -111,6 +111,15 @@ sub can_logout {
|
|||
return $getter->can_logout;
|
||||
}
|
||||
|
||||
sub login_token {
|
||||
my ($self) = @_;
|
||||
my $getter = $self->{_info_getter}->{successful};
|
||||
if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) {
|
||||
return $getter->login_token;
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub user_can_create_account {
|
||||
my ($self) = @_;
|
||||
my $verifier = $self->{_verifier}->{successful};
|
||||
|
@ -412,6 +421,14 @@ Params: None
|
|||
Returns: C<true> if users can change their own email address,
|
||||
C<false> otherwise.
|
||||
|
||||
=item C<login_token>
|
||||
|
||||
Description: If a login token was used instead of a cookie then this
|
||||
will return the current login token data such as user id
|
||||
and the token itself.
|
||||
Params: None
|
||||
Returns: A hash containing C<login_token> and C<user_id>.
|
||||
|
||||
=back
|
||||
|
||||
=head1 STRUCTURE
|
||||
|
|
|
@ -9,7 +9,7 @@ package Bugzilla::Auth::Login;
|
|||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use fields qw();
|
||||
use fields qw(_login_token);
|
||||
|
||||
# Determines whether or not a user can logout. It's really a subroutine,
|
||||
# but we implement it here as a constant. Override it in subclasses if
|
||||
|
|
|
@ -14,13 +14,15 @@ use parent qw(Bugzilla::Auth::Login);
|
|||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
use List::Util qw(first);
|
||||
|
||||
use constant requires_persistence => 0;
|
||||
use constant requires_verification => 0;
|
||||
use constant can_login => 0;
|
||||
use constant is_automatic => 1;
|
||||
|
||||
sub is_automatic { return $_[0]->login_token ? 0 : 1; }
|
||||
|
||||
# Note that Cookie never consults the Verifier, it always assumes
|
||||
# it has a valid DB account or it fails.
|
||||
|
@ -28,23 +30,34 @@ sub get_login_info {
|
|||
my ($self) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my ($user_id, $login_cookie);
|
||||
|
||||
if (!Bugzilla->request_cache->{auth_no_automatic_login}) {
|
||||
$login_cookie = $cgi->cookie("Bugzilla_logincookie");
|
||||
$user_id = $cgi->cookie("Bugzilla_login");
|
||||
|
||||
# If cookies cannot be found, this could mean that they haven't
|
||||
# been made available yet. In this case, look at Bugzilla_cookie_list.
|
||||
unless ($login_cookie) {
|
||||
my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
|
||||
@{$cgi->{'Bugzilla_cookie_list'}};
|
||||
$login_cookie = $cookie->value if $cookie;
|
||||
}
|
||||
unless ($user_id) {
|
||||
my $cookie = first {$_->name eq 'Bugzilla_login'}
|
||||
@{$cgi->{'Bugzilla_cookie_list'}};
|
||||
$user_id = $cookie->value if $cookie;
|
||||
}
|
||||
}
|
||||
|
||||
# If no cookies were provided, we also look for a login token
|
||||
# passed in the parameters of a webservice
|
||||
my $token = $self->login_token;
|
||||
if ($token && (!$login_cookie || !$user_id)) {
|
||||
($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'});
|
||||
}
|
||||
|
||||
my $ip_addr = remote_ip();
|
||||
my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
|
||||
my $user_id = $cgi->cookie("Bugzilla_login");
|
||||
|
||||
# If cookies cannot be found, this could mean that they haven't
|
||||
# been made available yet. In this case, look at Bugzilla_cookie_list.
|
||||
unless ($login_cookie) {
|
||||
my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
|
||||
@{$cgi->{'Bugzilla_cookie_list'}};
|
||||
$login_cookie = $cookie->value if $cookie;
|
||||
}
|
||||
unless ($user_id) {
|
||||
my $cookie = first {$_->name eq 'Bugzilla_login'}
|
||||
@{$cgi->{'Bugzilla_cookie_list'}};
|
||||
$user_id = $cookie->value if $cookie;
|
||||
}
|
||||
|
||||
if ($login_cookie && $user_id) {
|
||||
# Anything goes for these params - they're just strings which
|
||||
|
@ -58,23 +71,53 @@ sub get_login_info {
|
|||
{Slice=>{}}, ($login_cookie, $user_id, $ip_addr)) || [];
|
||||
$session = $session->[0];
|
||||
|
||||
# If the cookie is valid, return a valid username.
|
||||
if ($session) {
|
||||
# If the cookie or token is valid, return a valid username.
|
||||
# If they were not valid and we are using a webservice, then
|
||||
# throw an error notifying the client.
|
||||
if ($is_valid) {
|
||||
# If we logged in successfully, then update the lastused
|
||||
# time on the login cookie
|
||||
$dbh->do("UPDATE logincookies SET lastused = NOW()
|
||||
WHERE cookie = ?", undef, $login_cookie);
|
||||
return { user_id => $user_id, session => $session };
|
||||
}
|
||||
elsif (i_am_webservice()) {
|
||||
ThrowUserError('invalid_cookies_or_token');
|
||||
}
|
||||
}
|
||||
|
||||
# Either the he cookie is invalid, or we got no cookie. We don't want
|
||||
# to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to
|
||||
# actually throw an error when it gets a bad cookie. It should just
|
||||
# look like there was no cookie to begin with.
|
||||
$cgi->remove_cookie("Bugzilla_login");
|
||||
$cgi->remove_cookie("Bugzilla_logincookie");
|
||||
# Either the cookie or token is invalid and we are not authenticating
|
||||
# via a webservice, or we did not receive a cookie or token. We don't
|
||||
# want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to
|
||||
# actually throw an error when it gets a bad cookie or token. It should just
|
||||
# look like there was no cookie or token to begin with.
|
||||
return { failure => AUTH_NODATA };
|
||||
}
|
||||
|
||||
sub login_token {
|
||||
my ($self) = @_;
|
||||
my $input = Bugzilla->input_params;
|
||||
my $usage_mode = Bugzilla->usage_mode;
|
||||
|
||||
return $self->{'_login_token'} if exists $self->{'_login_token'};
|
||||
|
||||
if (!i_am_webservice()) {
|
||||
return $self->{'_login_token'} = undef;
|
||||
}
|
||||
|
||||
# Check if a token was passed in via requests for WebServices
|
||||
my $token = trim(delete $input->{'Bugzilla_token'});
|
||||
return $self->{'_login_token'} = undef if !$token;
|
||||
|
||||
my ($user_id, $login_token) = split('-', $token, 2);
|
||||
if (!detaint_natural($user_id) || !$login_token) {
|
||||
return $self->{'_login_token'} = undef;
|
||||
}
|
||||
|
||||
return $self->{'_login_token'} = {
|
||||
user_id => $user_id,
|
||||
login_token => $login_token
|
||||
};
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -15,6 +15,8 @@ use Bugzilla::Constants;
|
|||
use Bugzilla::Util;
|
||||
use Bugzilla::Token;
|
||||
|
||||
use Bugzilla::Auth::Login::Cookie qw(login_token);
|
||||
|
||||
use List::Util qw(first);
|
||||
|
||||
sub new {
|
||||
|
@ -86,6 +88,7 @@ sub logout {
|
|||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $input = Bugzilla->input_params;
|
||||
$param = {} unless $param;
|
||||
my $user = $param->{user} || Bugzilla->user;
|
||||
my $type = $param->{type} || LOGOUT_ALL;
|
||||
|
@ -99,16 +102,23 @@ sub logout {
|
|||
# The LOGOUT_*_CURRENT options require the current login cookie.
|
||||
# If a new cookie has been issued during this run, that's the current one.
|
||||
# If not, it's the one we've received.
|
||||
my @login_cookies;
|
||||
my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
|
||||
@{$cgi->{'Bugzilla_cookie_list'}};
|
||||
my $login_cookie;
|
||||
if ($cookie) {
|
||||
$login_cookie = $cookie->value;
|
||||
push(@login_cookies, $cookie->value);
|
||||
}
|
||||
else {
|
||||
$login_cookie = $cgi->cookie("Bugzilla_logincookie");
|
||||
push(@login_cookies, $cgi->cookie("Bugzilla_logincookie"));
|
||||
}
|
||||
trick_taint($login_cookie);
|
||||
|
||||
# If we are a webservice using a token instead of cookie
|
||||
# then add that as well to the login cookies to delete
|
||||
if (my $login_token = $user->authorizer->login_token) {
|
||||
push(@login_cookies, $login_token->{'login_token'});
|
||||
}
|
||||
|
||||
return if !@login_cookies;
|
||||
|
||||
# These queries use both the cookie ID and the user ID as keys. Even
|
||||
# though we know the userid must match, we still check it in the SQL
|
||||
|
@ -117,12 +127,18 @@ sub logout {
|
|||
# logged in and got the same cookie, we could be logging the other
|
||||
# user out here. Yes, this is very very very unlikely, but why take
|
||||
# chances? - bbaetz
|
||||
map { trick_taint($_) } @login_cookies;
|
||||
@login_cookies = map { $dbh->quote($_) } @login_cookies;
|
||||
if ($type == LOGOUT_KEEP_CURRENT) {
|
||||
$dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
|
||||
undef, $login_cookie, $user->id);
|
||||
$dbh->do("DELETE FROM logincookies WHERE " .
|
||||
$dbh->sql_in('cookie', \@login_cookies, 1) .
|
||||
" AND userid = ?",
|
||||
undef, $user->id);
|
||||
} elsif ($type == LOGOUT_CURRENT) {
|
||||
$dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
|
||||
undef, $login_cookie, $user->id);
|
||||
$dbh->do("DELETE FROM logincookies WHERE " .
|
||||
$dbh->sql_in('cookie', \@login_cookies) .
|
||||
" AND userid = ?",
|
||||
undef, $user->id);
|
||||
} else {
|
||||
die("Invalid type $type supplied to logout()");
|
||||
}
|
||||
|
|
271
Bugzilla/Bug.pm
271
Bugzilla/Bug.pm
|
@ -153,6 +153,9 @@ sub VALIDATORS {
|
|||
elsif ($field->type == FIELD_TYPE_DATETIME) {
|
||||
$validator = \&_check_datetime_field;
|
||||
}
|
||||
elsif ($field->type == FIELD_TYPE_DATE) {
|
||||
$validator = \&_check_date_field;
|
||||
}
|
||||
elsif ($field->type == FIELD_TYPE_FREETEXT) {
|
||||
$validator = \&_check_freetext_field;
|
||||
}
|
||||
|
@ -240,7 +243,9 @@ use constant NUMERIC_COLUMNS => qw(
|
|||
);
|
||||
|
||||
sub DATE_COLUMNS {
|
||||
my @fields = @{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) };
|
||||
my @fields = (@{ Bugzilla->fields({ type => [FIELD_TYPE_DATETIME,
|
||||
FIELD_TYPE_DATE] })
|
||||
});
|
||||
return map { $_->name } @fields;
|
||||
}
|
||||
|
||||
|
@ -272,10 +277,6 @@ use constant FIELD_MAP => {
|
|||
summary => 'short_desc',
|
||||
url => 'bug_file_loc',
|
||||
whiteboard => 'status_whiteboard',
|
||||
|
||||
# These are special values for the WebService Bug.search method.
|
||||
limit => 'LIMIT',
|
||||
offset => 'OFFSET',
|
||||
};
|
||||
|
||||
use constant REQUIRED_FIELD_MAP => {
|
||||
|
@ -303,21 +304,11 @@ use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_cont
|
|||
|
||||
#####################################################################
|
||||
|
||||
# This and "new" catch every single way of creating a bug, so that we
|
||||
# can call _create_cf_accessors.
|
||||
sub _do_list_select {
|
||||
my $invocant = shift;
|
||||
$invocant->_create_cf_accessors();
|
||||
return $invocant->SUPER::_do_list_select(@_);
|
||||
}
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $param = shift;
|
||||
|
||||
$class->_create_cf_accessors();
|
||||
|
||||
# Remove leading "#" mark if we've just been passed an id.
|
||||
if (!ref $param && $param =~ /^#(\d+)$/) {
|
||||
$param = $1;
|
||||
|
@ -365,6 +356,10 @@ sub new {
|
|||
return $self;
|
||||
}
|
||||
|
||||
sub initialize {
|
||||
$_[0]->_create_cf_accessors();
|
||||
}
|
||||
|
||||
sub cache_key {
|
||||
my $class = shift;
|
||||
my $key = $class->SUPER::cache_key(@_)
|
||||
|
@ -374,14 +369,16 @@ sub cache_key {
|
|||
|
||||
sub check {
|
||||
my $class = shift;
|
||||
my ($id, $field) = @_;
|
||||
|
||||
ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
|
||||
my ($param, $field) = @_;
|
||||
|
||||
# Bugzilla::Bug throws lots of special errors, so we don't call
|
||||
# SUPER::check, we just call our new and do our own checks.
|
||||
$id = trim($id);
|
||||
my $self = $class->new($id);
|
||||
my $id = ref($param)
|
||||
? ($param->{id} = trim($param->{id}))
|
||||
: ($param = trim($param));
|
||||
ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
|
||||
|
||||
my $self = $class->new($param);
|
||||
|
||||
if ($self->{error}) {
|
||||
# For error messages, use the id that was returned by new(), because
|
||||
|
@ -755,7 +752,7 @@ sub create {
|
|||
# Because MySQL doesn't support transactions on the fulltext table,
|
||||
# we do this after we've committed the transaction. That way we're
|
||||
# sure we're inserting a good Bug ID.
|
||||
$bug->_sync_fulltext('new bug');
|
||||
$bug->_sync_fulltext( new_bug => 1 );
|
||||
|
||||
return $bug;
|
||||
}
|
||||
|
@ -765,6 +762,17 @@ sub run_create_validators {
|
|||
|
||||
my $params = $class->SUPER::run_create_validators(@_);
|
||||
|
||||
# Add classification for checking mandatory fields which depend on it
|
||||
$params->{classification} = $params->{product}->classification->name;
|
||||
|
||||
my @mandatory_fields = @{ Bugzilla->fields({ is_mandatory => 1,
|
||||
enter_bug => 1,
|
||||
obsolete => 0 }) };
|
||||
foreach my $field (@mandatory_fields) {
|
||||
$class->_check_field_is_mandatory($params->{$field->name}, $field,
|
||||
$params);
|
||||
}
|
||||
|
||||
my $product = delete $params->{product};
|
||||
$params->{product_id} = $product->id;
|
||||
my $component = delete $params->{component};
|
||||
|
@ -784,18 +792,11 @@ sub run_create_validators {
|
|||
# You can't set these fields.
|
||||
delete $params->{lastdiffed};
|
||||
delete $params->{bug_id};
|
||||
delete $params->{classification};
|
||||
|
||||
Bugzilla::Hook::process('bug_end_of_create_validators',
|
||||
{ params => $params });
|
||||
|
||||
my @mandatory_fields = @{ Bugzilla->fields({ is_mandatory => 1,
|
||||
enter_bug => 1,
|
||||
obsolete => 0 }) };
|
||||
foreach my $field (@mandatory_fields) {
|
||||
$class->_check_field_is_mandatory($params->{$field->name}, $field,
|
||||
$params);
|
||||
}
|
||||
|
||||
# And this is not a valid DB field, it's just used as part of
|
||||
# _check_dependencies to avoid running it twice for both blocked
|
||||
# and dependson.
|
||||
|
@ -964,15 +965,9 @@ sub check_dependent_fields
|
|||
sub update
|
||||
{
|
||||
my $self = shift;
|
||||
|
||||
# First check dependent field values
|
||||
if ($self->{dependent_validators})
|
||||
{
|
||||
check_dependent_fields($self->{dependent_validators}, $self);
|
||||
}
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
# XXX This is just a temporary hack until all updating happens
|
||||
# inside this function.
|
||||
my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
||||
|
@ -1134,12 +1129,14 @@ sub update
|
|||
}
|
||||
|
||||
# Comments
|
||||
foreach my $comment (@{$self->{added_comments} || []})
|
||||
{
|
||||
if (Bugzilla->cgi->param('commentsilent'))
|
||||
{
|
||||
# Log silent comments
|
||||
SilentLog($self->bug_id, $comment->{thetext});
|
||||
foreach my $comment (@{$self->{added_comments} || []}) {
|
||||
# Override the Comment's timestamp to be identical to the update
|
||||
# timestamp.
|
||||
$comment->{bug_when} = $delta_ts;
|
||||
$comment = Bugzilla::Comment->insert_create_data($comment);
|
||||
if ($comment->work_time) {
|
||||
LogActivityEntry($self->id, "work_time", "", $comment->work_time,
|
||||
$user->id, $delta_ts);
|
||||
}
|
||||
$comment->{bug_id} = $self->bug_id;
|
||||
$comment->{who} ||= $user->id;
|
||||
|
@ -1241,8 +1238,8 @@ sub update
|
|||
my $change = $changes->{$field};
|
||||
my $from = defined $change->[0] ? $change->[0] : '';
|
||||
my $to = defined $change->[1] ? $change->[1] : '';
|
||||
LogActivityEntry($self->id, $field, $from, $to, Bugzilla->user->id,
|
||||
$delta_ts);
|
||||
LogActivityEntry($self->id, $field, $from, $to,
|
||||
$user->id, $delta_ts);
|
||||
}
|
||||
|
||||
# Check if we have to update the duplicates table and the other bug.
|
||||
|
@ -1273,17 +1270,35 @@ sub update
|
|||
$self->{delta_ts} = $delta_ts;
|
||||
}
|
||||
|
||||
Bugzilla::Hook::process('bug_end_of_update',
|
||||
{ bug => $self, timestamp => $delta_ts, changes => $changes,
|
||||
old_bug => $old_bug });
|
||||
# Update bug ignore data if user wants to ignore mail for this bug
|
||||
if (exists $self->{'bug_ignored'}) {
|
||||
my $bug_ignored_changed;
|
||||
if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) {
|
||||
$dbh->do('INSERT INTO email_bug_ignore
|
||||
(user_id, bug_id) VALUES (?, ?)',
|
||||
undef, $user->id, $self->id);
|
||||
$bug_ignored_changed = 1;
|
||||
|
||||
}
|
||||
elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) {
|
||||
$dbh->do('DELETE FROM email_bug_ignore
|
||||
WHERE user_id = ? AND bug_id = ?',
|
||||
undef, $user->id, $self->id);
|
||||
$bug_ignored_changed = 1;
|
||||
}
|
||||
delete $user->{bugs_ignored} if $bug_ignored_changed;
|
||||
}
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
# The only problem with this here is that update() is often called
|
||||
# in the middle of a transaction, and if that transaction is rolled
|
||||
# back, this change will *not* be rolled back. As we expect rollbacks
|
||||
# to be extremely rare, that is OK for us.
|
||||
$self->_sync_fulltext()
|
||||
if $self->{added_comments} || $changes->{short_desc}
|
||||
|| $self->{comment_isprivate} || $self->{edited_comments};
|
||||
$self->_sync_fulltext(
|
||||
update_short_desc => $changes->{short_desc},
|
||||
update_comments => $self->{added_comments} || $self->{comment_isprivate} || $self->{edited_comments}
|
||||
);
|
||||
|
||||
# Remove obsolete internal variables.
|
||||
delete $self->{'_old_assigned_to'};
|
||||
|
@ -1293,7 +1308,7 @@ sub update
|
|||
|
||||
# Also flush the visible_bugs cache for this bug as the user's
|
||||
# relationship with this bug may have changed.
|
||||
delete Bugzilla->user->{_visible_bugs_cache}->{$self->id};
|
||||
delete $user->{_visible_bugs_cache}->{$self->id};
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
@ -1318,40 +1333,44 @@ sub _extract_multi_selects {
|
|||
}
|
||||
|
||||
# Should be called any time you update short_desc or change a comment.
|
||||
sub _sync_fulltext
|
||||
{
|
||||
use utf8;
|
||||
my ($self, $new_bug) = @_;
|
||||
sub _sync_fulltext {
|
||||
my ($self, %options) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my ($short_desc) = $dbh->selectrow_array(
|
||||
"SELECT short_desc FROM bugs WHERE bug_id=?", undef, $self->id
|
||||
);
|
||||
my ($nopriv, $priv) = ([], []);
|
||||
for (@{ $dbh->selectall_arrayref(
|
||||
"SELECT thetext, isprivate FROM longdescs WHERE bug_id=?",
|
||||
undef, $self->id
|
||||
) || [] })
|
||||
{
|
||||
$_->[1] ? push @$priv, $_->[0] : push @$nopriv, $_->[0];
|
||||
|
||||
my($all_comments, $public_comments);
|
||||
if ($options{new_bug} || $options{update_comments}) {
|
||||
my $comments = $dbh->selectall_arrayref(
|
||||
'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?',
|
||||
undef, $self->id);
|
||||
$all_comments = join("\n", map { $_->[0] } @$comments);
|
||||
my @no_private = grep { !$_->[1] } @$comments;
|
||||
$public_comments = join("\n", map { $_->[0] } @no_private);
|
||||
}
|
||||
$nopriv = join "\n", @$nopriv;
|
||||
$priv = join "\n", @$priv;
|
||||
my $row = [ $short_desc, $nopriv, $priv ];
|
||||
$_ = $dbh->quote_fulltext($_) for @$row;
|
||||
## O_o Don't know how can it be tainted here, sometimes it was. Checking if it goes away.
|
||||
#trick_taint($row);
|
||||
my $sql;
|
||||
if ($new_bug)
|
||||
{
|
||||
$sql = "INSERT INTO bugs_fulltext (bug_id, short_desc, comments, comments_private)".
|
||||
" VALUES (".join(',', $self->id, @$row).")";
|
||||
|
||||
if ($options{new_bug}) {
|
||||
$dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc, comments,
|
||||
comments_noprivate)
|
||||
VALUES (?, ?, ?, ?)',
|
||||
undef,
|
||||
$self->id, $self->short_desc, $all_comments, $public_comments);
|
||||
} else {
|
||||
my(@names, @values);
|
||||
if ($options{update_short_desc}) {
|
||||
push @names, 'short_desc';
|
||||
push @values, $self->short_desc;
|
||||
}
|
||||
if ($options{update_comments}) {
|
||||
push @names, ('comments', 'comments_noprivate');
|
||||
push @values, ($all_comments, $public_comments);
|
||||
}
|
||||
if (@names) {
|
||||
$dbh->do('UPDATE bugs_fulltext SET ' .
|
||||
join(', ', map { "$_ = ?" } @names) .
|
||||
' WHERE bug_id = ?',
|
||||
undef,
|
||||
@values, $self->id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$sql = "UPDATE bugs_fulltext SET short_desc=$row->[0],".
|
||||
" comments=$row->[1], comments_private=$row->[2] WHERE bug_id=".$self->id;
|
||||
}
|
||||
return $dbh->do($sql);
|
||||
}
|
||||
|
||||
sub remove_from_db {
|
||||
|
@ -1621,8 +1640,12 @@ sub _check_component {
|
|||
$name || ThrowUserError("require_component");
|
||||
my $product = blessed($invocant) ? $invocant->product_obj
|
||||
: $params->{product};
|
||||
my $obj = Bugzilla::Component->check({ product => $product, name => $name });
|
||||
return $obj;
|
||||
my $old_comp = blessed($invocant) ? $invocant->component : '';
|
||||
my $object = Bugzilla::Component->check({ product => $product, name => $name });
|
||||
if ($object->name ne $old_comp && !$object->is_active) {
|
||||
ThrowUserError('value_inactive', { class => ref($object), value => $name });
|
||||
}
|
||||
return $object;
|
||||
}
|
||||
|
||||
sub _check_creation_ts {
|
||||
|
@ -2111,10 +2134,14 @@ sub _check_target_milestone {
|
|||
my ($invocant, $target, undef, $params) = @_;
|
||||
my $product = blessed($invocant) ? $invocant->product_obj
|
||||
: $params->{product};
|
||||
my $old_target = blessed($invocant) ? $invocant->target_milestone : '';
|
||||
$target = trim($target);
|
||||
$target = $product->default_milestone if !defined $target;
|
||||
my $object = Bugzilla::Milestone->check(
|
||||
{ product => $product, name => $target });
|
||||
if ($old_target && $object->name ne $old_target && !$object->is_active) {
|
||||
ThrowUserError('value_inactive', { class => ref($object), value => $target });
|
||||
}
|
||||
return $object->name;
|
||||
}
|
||||
|
||||
|
@ -2154,8 +2181,11 @@ sub _check_version {
|
|||
$version = trim($version);
|
||||
my $product = blessed($invocant) ? $invocant->product_obj
|
||||
: $params->{product};
|
||||
my $object =
|
||||
Bugzilla::Version->check({ product => $product, name => $version });
|
||||
my $old_vers = blessed($invocant) ? $invocant->version : '';
|
||||
my $object = Bugzilla::Version->check({ product => $product, name => $version });
|
||||
if ($object->name ne $old_vers && !$object->is_active) {
|
||||
ThrowUserError('value_inactive', { class => ref($object), value => $version });
|
||||
}
|
||||
return $object->name;
|
||||
}
|
||||
|
||||
|
@ -2194,8 +2224,13 @@ sub _check_field_is_mandatory {
|
|||
}
|
||||
}
|
||||
|
||||
sub _check_date_field {
|
||||
my ($invocant, $date) = @_;
|
||||
return $invocant->_check_datetime_field($date, undef, {date_only => 1});
|
||||
}
|
||||
|
||||
sub _check_datetime_field {
|
||||
my ($invocant, $date_time) = @_;
|
||||
my ($invocant, $date_time, $field, $params) = @_;
|
||||
|
||||
# Empty datetimes are empty strings or strings only containing
|
||||
# 0's, whitespace, and punctuation.
|
||||
|
@ -2209,6 +2244,10 @@ sub _check_datetime_field {
|
|||
ThrowUserError('illegal_date', { date => $date,
|
||||
format => 'YYYY-MM-DD' });
|
||||
}
|
||||
if ($time && $params->{date_only}) {
|
||||
ThrowUserError('illegal_date', { date => $date_time,
|
||||
format => 'YYYY-MM-DD' });
|
||||
}
|
||||
if ($time && !validate_time($time)) {
|
||||
ThrowUserError('illegal_time', { 'time' => $time,
|
||||
format => 'HH:MM:SS' });
|
||||
|
@ -2502,7 +2541,7 @@ sub set_all {
|
|||
# we have to check that the current assignee, qa, and CCs are still
|
||||
# valid if we've switched products, under strict_isolation. We can only
|
||||
# do that here, because if they *did* change the assignee, qa, or CC,
|
||||
# then we don't want to check the original ones, only the new ones.
|
||||
# then we don't want to check the original ones, only the new ones.
|
||||
$self->_check_strict_isolation() if $product_changed;
|
||||
}
|
||||
|
||||
|
@ -2532,6 +2571,7 @@ sub reset_assigned_to {
|
|||
my $comp = $self->component_obj;
|
||||
$self->set_assigned_to($comp->default_assignee);
|
||||
}
|
||||
sub set_bug_ignored { $_[0]->set('bug_ignored', $_[1]); }
|
||||
sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); }
|
||||
sub set_comment_is_private {
|
||||
my ($self, $comment_id, $isprivate) = @_;
|
||||
|
@ -2735,9 +2775,9 @@ sub _set_product {
|
|||
milestone => $milestone_ok ? $self->target_milestone
|
||||
: $product->default_milestone
|
||||
};
|
||||
$vars{components} = [map { $_->name } @{$product->components}];
|
||||
$vars{milestones} = [map { $_->name } @{$product->milestones}];
|
||||
$vars{versions} = [map { $_->name } @{$product->versions}];
|
||||
$vars{components} = [map { $_->name } grep($_->is_active, @{$product->components})];
|
||||
$vars{milestones} = [map { $_->name } grep($_->is_active, @{$product->milestones})];
|
||||
$vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})];
|
||||
}
|
||||
|
||||
if (!$verified) {
|
||||
|
@ -2761,6 +2801,10 @@ sub _set_product {
|
|||
OR gcm.othercontrol != ?) )',
|
||||
undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA));
|
||||
$vars{'old_groups'} = Bugzilla::Group->new_from_list($gids);
|
||||
|
||||
# Did we come here from editing multiple bugs? (affects how we
|
||||
# show optional group changes)
|
||||
$vars{multiple_bugs} = Bugzilla->cgi->param('id') ? 0 : 1;
|
||||
}
|
||||
|
||||
if (%vars) {
|
||||
|
@ -3530,11 +3574,8 @@ sub cc_users {
|
|||
|
||||
sub component {
|
||||
my ($self) = @_;
|
||||
return $self->{component} if exists $self->{component};
|
||||
return '' if $self->{error};
|
||||
($self->{component}) = Bugzilla->dbh->selectrow_array(
|
||||
'SELECT name FROM components WHERE id = ?',
|
||||
undef, $self->{component_id});
|
||||
$self->{component} //= $self->component_obj->name;
|
||||
return $self->{component};
|
||||
}
|
||||
|
||||
|
@ -3550,21 +3591,15 @@ sub component_obj {
|
|||
|
||||
sub classification_id {
|
||||
my ($self) = @_;
|
||||
return $self->{classification_id} if exists $self->{classification_id};
|
||||
return 0 if $self->{error};
|
||||
($self->{classification_id}) = Bugzilla->dbh->selectrow_array(
|
||||
'SELECT classification_id FROM products WHERE id = ?',
|
||||
undef, $self->{product_id});
|
||||
$self->{classification_id} //= $self->product_obj->classification_id;
|
||||
return $self->{classification_id};
|
||||
}
|
||||
|
||||
sub classification {
|
||||
my ($self) = @_;
|
||||
return $self->{classification} if exists $self->{classification};
|
||||
return '' if $self->{error};
|
||||
($self->{classification}) = Bugzilla->dbh->selectrow_array(
|
||||
'SELECT name FROM classifications WHERE id = ?',
|
||||
undef, $self->classification_id);
|
||||
$self->{classification} //= $self->product_obj->classification->name;
|
||||
return $self->{classification};
|
||||
}
|
||||
|
||||
|
@ -3746,11 +3781,8 @@ sub percentage_complete {
|
|||
|
||||
sub product {
|
||||
my ($self) = @_;
|
||||
return $self->{product} if exists $self->{product};
|
||||
return '' if $self->{error};
|
||||
($self->{product}) = Bugzilla->dbh->selectrow_array(
|
||||
'SELECT name FROM products WHERE id = ?',
|
||||
undef, $self->{product_id});
|
||||
$self->{product} //= $self->product_obj->name;
|
||||
return $self->{product};
|
||||
}
|
||||
|
||||
|
@ -4273,11 +4305,12 @@ sub get_activity {
|
|||
if ($operation->{'who'} && $who eq $operation->{'who'}
|
||||
&& $when eq $operation->{'when'}
|
||||
&& $fieldname eq $operation->{'fieldname'}
|
||||
&& ($comment_id || 0) == ($operation->{'comment_id'} || 0)
|
||||
&& ($attachid || 0) == ($operation->{'attachid'} || 0))
|
||||
{
|
||||
my $old_change = pop @$changes;
|
||||
$removed = _join_activity_entries($fieldname, $old_change->{'removed'}, $removed);
|
||||
$added = _join_activity_entries($fieldname, $old_change->{'added'}, $added);
|
||||
$removed = join_activity_entries($fieldname, $old_change->{'removed'}, $removed);
|
||||
$added = join_activity_entries($fieldname, $old_change->{'added'}, $added);
|
||||
}
|
||||
$operation->{'who'} = $who;
|
||||
$operation->{'when'} = $when;
|
||||
|
@ -4287,7 +4320,7 @@ sub get_activity {
|
|||
$change{'added'} = $added;
|
||||
|
||||
if ($comment_id) {
|
||||
$change{'comment'} = Bugzilla::Comment->new($comment_id);
|
||||
$operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id);
|
||||
}
|
||||
|
||||
push (@$changes, \%change);
|
||||
|
@ -4463,8 +4496,8 @@ sub check_can_change_field {
|
|||
return 1;
|
||||
}
|
||||
|
||||
# Allow anyone to change comments.
|
||||
if ($field =~ /^longdesc/) {
|
||||
# Allow anyone to change comments, or set flags
|
||||
if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@ -4713,6 +4746,16 @@ sub _multi_select_accessor {
|
|||
|
||||
1;
|
||||
|
||||
=head1 B<Methods>
|
||||
|
||||
=over
|
||||
|
||||
=item C<initialize>
|
||||
|
||||
Ensures the accessors for custom fields are always created.
|
||||
|
||||
=back
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
@ -4851,6 +4894,8 @@ sub _multi_select_accessor {
|
|||
|
||||
=item set_cclist_accessible
|
||||
|
||||
=item set_bug_ignored
|
||||
|
||||
=item product
|
||||
|
||||
=item VALIDATORS
|
||||
|
|
|
@ -24,6 +24,7 @@ use Date::Parse;
|
|||
use Date::Format;
|
||||
use Scalar::Util qw(blessed);
|
||||
use List::MoreUtils qw(uniq);
|
||||
use Storable qw(dclone);
|
||||
|
||||
use constant BIT_DIRECT => 1;
|
||||
use constant BIT_WATCHING => 2;
|
||||
|
@ -380,6 +381,9 @@ sub Send {
|
|||
# Skip empty comments.
|
||||
@$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
|
||||
|
||||
# If no changes have been made, there is no need to process further.
|
||||
return {'sent' => []} unless scalar(@diffs) || scalar(@$comments);
|
||||
|
||||
###########################################################################
|
||||
# Start of email filtering code
|
||||
###########################################################################
|
||||
|
@ -472,6 +476,10 @@ sub Send {
|
|||
# Deleted users must be excluded.
|
||||
next unless $user;
|
||||
|
||||
# If email notifications are disabled for this account, or the bug
|
||||
# is ignored, there is no need to do additional checks.
|
||||
next if ($user->email_disabled || $user->is_bug_ignored($id));
|
||||
|
||||
if ($user->can_see_bug($id)) {
|
||||
# Go through each role the user has and see if they want mail in
|
||||
# that role.
|
||||
|
@ -525,7 +533,7 @@ sub Send {
|
|||
$dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?',
|
||||
undef, ($end, $id));
|
||||
|
||||
return {'sent' => \@sent, 'excluded' => \@excluded};
|
||||
return {'sent' => \@sent};
|
||||
}
|
||||
|
||||
sub sendMail
|
||||
|
@ -604,13 +612,17 @@ sub sendMail
|
|||
push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
|
||||
|
||||
my @changedfields = uniq map { $_->{field_name} } @$diffs;
|
||||
|
||||
|
||||
# Add attachments.created to changedfields if one or more
|
||||
# comments contain information about a new attachment
|
||||
if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) {
|
||||
push(@changedfields, 'attachments.created');
|
||||
}
|
||||
|
||||
my $bugmailtype = "changed";
|
||||
$bugmailtype = "new" if !$bug->lastdiffed;
|
||||
$bugmailtype = "dep_changed" if $dep_only;
|
||||
|
||||
my $vars = {
|
||||
isnew => $isnew,
|
||||
showfieldvalues => \@showfieldvalues,
|
||||
|
@ -660,11 +672,53 @@ sub sendMail
|
|||
return 1;
|
||||
}
|
||||
|
||||
sub enqueue {
|
||||
my ($vars) = @_;
|
||||
# we need to flatten all objects to a hash before pushing to the job queue.
|
||||
# the hashes need to be inflated in the dequeue method.
|
||||
$vars->{bug} = _flatten_object($vars->{bug});
|
||||
$vars->{to_user} = $vars->{to_user}->flatten_to_hash;
|
||||
$vars->{changer} = _flatten_object($vars->{changer});
|
||||
$vars->{new_comments} = [ map { _flatten_object($_) } @{ $vars->{new_comments} } ];
|
||||
foreach my $diff (@{ $vars->{diffs} }) {
|
||||
$diff->{who} = _flatten_object($diff->{who});
|
||||
}
|
||||
Bugzilla->job_queue->insert('bug_mail', { vars => $vars });
|
||||
}
|
||||
|
||||
sub dequeue {
|
||||
my ($payload) = @_;
|
||||
# clone the payload so we can modify it without impacting TheSchwartz's
|
||||
# ability to process the job when we've finished
|
||||
my $vars = dclone($payload);
|
||||
# inflate objects
|
||||
$vars->{bug} = Bugzilla::Bug->new_from_hash($vars->{bug});
|
||||
$vars->{to_user} = Bugzilla::User->new_from_hash($vars->{to_user});
|
||||
$vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer});
|
||||
$vars->{new_comments} = [ map { Bugzilla::Comment->new_from_hash($_) } @{ $vars->{new_comments} } ];
|
||||
foreach my $diff (@{ $vars->{diffs} }) {
|
||||
$diff->{who} = Bugzilla::User->new_from_hash($diff->{who});
|
||||
}
|
||||
# generate bugmail and send
|
||||
MessageToMTA(_generate_bugmail($vars), 1);
|
||||
}
|
||||
|
||||
sub _flatten_object {
|
||||
my ($object) = @_;
|
||||
# nothing to do if it's already flattened
|
||||
return $object unless blessed($object);
|
||||
# the same objects are used for each recipient, so cache the flattened hash
|
||||
my $cache = Bugzilla->request_cache->{bugmail_flat_objects} ||= {};
|
||||
my $key = blessed($object) . '-' . $object->id;
|
||||
return $cache->{$key} ||= $object->flatten_to_hash;
|
||||
}
|
||||
|
||||
sub _generate_bugmail {
|
||||
my ($user, $vars) = @_;
|
||||
my ($vars) = @_;
|
||||
my $user = $vars->{to_user};
|
||||
my $template = Bugzilla->template_inner($user->setting('lang'));
|
||||
my ($msg_text, $msg_html, $msg_header);
|
||||
|
||||
|
||||
$template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header)
|
||||
|| ThrowTemplateError($template->error());
|
||||
$template->process("email/bugmail.txt.tmpl", $vars, \$msg_text)
|
||||
|
@ -724,7 +778,8 @@ sub _get_diffs {
|
|||
ON fielddefs.id = bugs_activity.fieldid
|
||||
WHERE bugs_activity.bug_id = ?
|
||||
$when_restriction
|
||||
ORDER BY bugs_activity.bug_when", {Slice=>{}}, @args);
|
||||
ORDER BY bugs_activity.bug_when, bugs_activity.id",
|
||||
{Slice=>{}}, @args);
|
||||
|
||||
foreach my $diff (@$diffs) {
|
||||
$user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who});
|
||||
|
@ -741,18 +796,43 @@ sub _get_diffs {
|
|||
}
|
||||
}
|
||||
|
||||
return @$diffs;
|
||||
my @changes = ();
|
||||
foreach my $diff (@$diffs) {
|
||||
# If this is the same field as the previous item, then concatenate
|
||||
# the data into the same change.
|
||||
if (scalar(@changes)
|
||||
&& $diff->{field_name} eq $changes[-1]->{field_name}
|
||||
&& $diff->{bug_when} eq $changes[-1]->{bug_when}
|
||||
&& $diff->{who} eq $changes[-1]->{who}
|
||||
&& ($diff->{attach_id} // 0) == ($changes[-1]->{attach_id} // 0)
|
||||
&& ($diff->{comment_id} // 0) == ($changes[-1]->{comment_id} // 0)
|
||||
) {
|
||||
my $old_change = pop @changes;
|
||||
$diff->{old} = join_activity_entries($diff->{field_name}, $old_change->{old}, $diff->{old});
|
||||
$diff->{new} = join_activity_entries($diff->{field_name}, $old_change->{new}, $diff->{new});
|
||||
}
|
||||
push @changes, $diff;
|
||||
}
|
||||
|
||||
return @changes;
|
||||
}
|
||||
|
||||
sub _get_new_bugmail_fields {
|
||||
my $bug = shift;
|
||||
my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) };
|
||||
my @diffs;
|
||||
my $params = Bugzilla->params;
|
||||
|
||||
foreach my $field (@fields) {
|
||||
my $name = $field->name;
|
||||
my $value = $bug->$name;
|
||||
|
||||
next if !$field->is_visible_on_bug($bug)
|
||||
|| ($name eq 'classification' && !$params->{'useclassification'})
|
||||
|| ($name eq 'status_whiteboard' && !$params->{'usestatuswhiteboard'})
|
||||
|| ($name eq 'qa_contact' && !$params->{'useqacontact'})
|
||||
|| ($name eq 'target_milestone' && !$params->{'usetargetmilestone'});
|
||||
|
||||
if (ref $value eq 'ARRAY') {
|
||||
$value = join(', ', @$value);
|
||||
}
|
||||
|
@ -782,6 +862,27 @@ sub _get_new_bugmail_fields {
|
|||
|
||||
1;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
BugMail - Routines to generate email notifications when a bug is created or
|
||||
modified.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<enqueue>
|
||||
|
||||
Serialises the variables required to generate bugmail and pushes the result to
|
||||
the job-queue for processing by TheSchwartz.
|
||||
|
||||
=item C<dequeue>
|
||||
|
||||
When given serialised variables from the job-queue, recreates the objects from
|
||||
the flattened hashes, generates the bugmail, and sends it.
|
||||
|
||||
=back
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
|
|
@ -22,7 +22,7 @@ sub should_handle {
|
|||
# Debian BTS URLs can look like various things:
|
||||
# http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
|
||||
# http://bugs.debian.org/1234
|
||||
return ($uri->authority =~ /^bugs.debian.org$/i
|
||||
return (lc($uri->authority) eq 'bugs.debian.org'
|
||||
and (($uri->path =~ /bugreport\.cgi$/
|
||||
and $uri->query_param('bug') =~ m|^\d+$|)
|
||||
or $uri->path =~ m|^/\d+$|)) ? 1 : 0;
|
||||
|
|
|
@ -21,7 +21,7 @@ sub should_handle {
|
|||
|
||||
# GitHub issue URLs have only one form:
|
||||
# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111
|
||||
return ($uri->authority =~ /^github.com$/i
|
||||
return (lc($uri->authority) eq 'github.com'
|
||||
and $uri->path =~ m|^/[^/]+/[^/]+/issues/\d+$|) ? 1 : 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ sub should_handle {
|
|||
|
||||
# Google Code URLs only have one form:
|
||||
# http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234
|
||||
return ($uri->authority =~ /^code.google.com$/i
|
||||
return (lc($uri->authority) eq 'code.google.com'
|
||||
and $uri->path =~ m|^/p/[^/]+/issues/detail$|
|
||||
and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ sub should_handle {
|
|||
# https://bugs.launchpad.net/ubuntu/+bug/1234
|
||||
# https://launchpad.net/bugs/1234
|
||||
# All variations end with either "/bugs/1234" or "/+bug/1234"
|
||||
return ($uri->authority =~ /launchpad.net$/
|
||||
return ($uri->authority =~ /launchpad\.net$/
|
||||
and $uri->path =~ m|bugs?/\d+$|) ? 1 : 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ sub should_handle {
|
|||
|
||||
# SourceForge tracker URLs have only one form:
|
||||
# http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111
|
||||
return ($uri->authority =~ /^sourceforge.net$/i
|
||||
return (lc($uri->authority) eq 'sourceforge.net'
|
||||
and $uri->path =~ m|/tracker/|
|
||||
and $uri->query_param('func') eq 'detail'
|
||||
and $uri->query_param('aid')
|
||||
|
|
|
@ -56,7 +56,7 @@ sub new {
|
|||
# the rendering of pages.
|
||||
my $script = basename($0);
|
||||
if (my $path_info = $self->path_info) {
|
||||
my @whitelist;
|
||||
my @whitelist = ("rest.cgi");
|
||||
Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist });
|
||||
if (!grep($_ eq $script, @whitelist)) {
|
||||
# IIS includes the full path to the script in PATH_INFO,
|
||||
|
@ -240,11 +240,11 @@ sub check_etag {
|
|||
$possible_etag =~ s/^\"//g;
|
||||
$possible_etag =~ s/\"$//g;
|
||||
if ($possible_etag eq $valid_etag or $possible_etag eq '*') {
|
||||
print $self->header(-ETag => $possible_etag,
|
||||
-status => '304 Not Modified');
|
||||
exit;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
# Have to add the cookies in.
|
||||
|
@ -286,6 +286,10 @@ sub header {
|
|||
unshift(@_, '-type' => shift(@_));
|
||||
}
|
||||
|
||||
if ($self->{'_content_disp'}) {
|
||||
unshift(@_, '-content_disposition' => $self->{'_content_disp'});
|
||||
}
|
||||
|
||||
# Add the cookies in if we have any
|
||||
if (scalar(@{$self->{Bugzilla_cookie_list}})) {
|
||||
unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list});
|
||||
|
@ -518,9 +522,9 @@ sub redirect_search_url {
|
|||
|
||||
# GET requests that lacked a list_id are always redirected. POST requests
|
||||
# are only redirected if they're under the CGI_URI_LIMIT though.
|
||||
my $uri_length = length($self->self_url());
|
||||
if ($self->request_method() ne 'POST' or $uri_length < CGI_URI_LIMIT) {
|
||||
print $self->redirect(-url => $self->self_url());
|
||||
my $self_url = $self->self_url();
|
||||
if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) {
|
||||
print $self->redirect(-url => $self_url);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
@ -574,7 +578,23 @@ sub url_is_attachment_base {
|
|||
$regex =~ s/\\\%bugid\\\%/\\d+/;
|
||||
}
|
||||
$regex = "^$regex";
|
||||
return ($self->self_url =~ $regex) ? 1 : 0;
|
||||
return ($self->url =~ $regex) ? 1 : 0;
|
||||
}
|
||||
|
||||
sub set_dated_content_disp {
|
||||
my ($self, $type, $prefix, $ext) = @_;
|
||||
|
||||
my @time = localtime(time());
|
||||
my $date = sprintf "%04d-%02d-%02d", 1900+$time[5], $time[4]+1, $time[3];
|
||||
my $filename = "$prefix-$date.$ext";
|
||||
|
||||
$filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering
|
||||
$filename =~ s/\\/_/g; # Remove backslashes as well
|
||||
$filename =~ s/"/\\"/g; # escape quotes
|
||||
|
||||
my $disposition = "$type; filename=\"$filename\"";
|
||||
|
||||
$self->{'_content_disp'} = $disposition;
|
||||
}
|
||||
|
||||
##########################
|
||||
|
@ -737,6 +757,11 @@ instead of calling this directly.
|
|||
|
||||
Redirects from the current URL to one prefixed by the urlbase parameter.
|
||||
|
||||
=item C<set_dated_content_disp>
|
||||
|
||||
Sets an appropriate date-dependent value for the Content Disposition header
|
||||
for a downloadable resource.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
|
|
@ -81,7 +81,7 @@ use constant VALIDATOR_DEPENDENCIES => {
|
|||
sub update {
|
||||
my $self = shift;
|
||||
my $changes = $self->SUPER::update(@_);
|
||||
$self->bug->_sync_fulltext();
|
||||
$self->bug->_sync_fulltext( update_comments => 1);
|
||||
return $changes;
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ sub get_param_list {
|
|||
{
|
||||
name => 'emailregexp',
|
||||
type => 't',
|
||||
default => q:^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
|
||||
default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
|
||||
checker => \&check_regexp
|
||||
},
|
||||
|
||||
|
|
|
@ -57,6 +57,14 @@ sub get_param_list {
|
|||
checker => \&check_group
|
||||
},
|
||||
|
||||
{
|
||||
name => 'debug_group',
|
||||
type => 's',
|
||||
choices => \&_get_all_group_names,
|
||||
default => 'admin',
|
||||
checker => \&check_group
|
||||
},
|
||||
|
||||
{
|
||||
name => 'usevisibilitygroups',
|
||||
type => 'b',
|
||||
|
|
|
@ -11,6 +11,13 @@ use 5.10.1;
|
|||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
# Return::Value 1.666002 pollutes the error log with warnings about this
|
||||
# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
|
||||
# to disable these warnings.
|
||||
BEGIN {
|
||||
$Return::Value::NO_CLUCK = 1;
|
||||
}
|
||||
use Email::Send;
|
||||
|
||||
our $sortkey = 1200;
|
||||
|
||||
|
|
|
@ -111,13 +111,14 @@ use Memoize;
|
|||
FIELD_TYPE_MULTI_SELECT
|
||||
FIELD_TYPE_TEXTAREA
|
||||
FIELD_TYPE_DATETIME
|
||||
FIELD_TYPE_DATE
|
||||
FIELD_TYPE_BUG_ID
|
||||
FIELD_TYPE_BUG_URLS
|
||||
FIELD_TYPE_EXTURL
|
||||
FIELD_TYPE_KEYWORDS
|
||||
|
||||
|
||||
FIELD_TYPE__BOUNDARY
|
||||
FIELD_TYPE_HIGHEST_PLUS_ONE
|
||||
EMPTY_DATETIME_REGEX
|
||||
|
||||
ABNORMAL_SELECTS
|
||||
|
@ -130,12 +131,14 @@ use Memoize;
|
|||
USAGE_MODE_EMAIL
|
||||
USAGE_MODE_JSON
|
||||
USAGE_MODE_TEST
|
||||
USAGE_MODE_REST
|
||||
|
||||
ERROR_MODE_WEBPAGE
|
||||
ERROR_MODE_DIE
|
||||
ERROR_MODE_DIE_SOAP_FAULT
|
||||
ERROR_MODE_JSON_RPC
|
||||
ERROR_MODE_TEST
|
||||
ERROR_MODE_REST
|
||||
|
||||
COLOR_ERROR
|
||||
COLOR_SUCCESS
|
||||
|
@ -176,6 +179,7 @@ use Memoize;
|
|||
MAX_POSSIBLE_DUPLICATES
|
||||
MAX_ATTACH_FILENAME_LENGTH
|
||||
MAX_QUIP_LENGTH
|
||||
MAX_WEBDOT_BUGS
|
||||
|
||||
PASSWORD_DIGEST_ALGORITHM
|
||||
PASSWORD_SALT_LENGTH
|
||||
|
@ -416,6 +420,10 @@ use constant FIELD_TYPE_BUG_ID => 6;
|
|||
use constant FIELD_TYPE_BUG_URLS => 7;
|
||||
use constant FIELD_TYPE_KEYWORDS => 8;
|
||||
use constant FIELD_TYPE_EXTURL => 9;
|
||||
use constant FIELD_TYPE_DATE => 9;
|
||||
# Add new field types above this line, and change the below value in the
|
||||
# obvious fashion
|
||||
use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 10;
|
||||
|
||||
# Upper boundary for FIELD_TYPE_* values
|
||||
use constant FIELD_TYPE__BOUNDARY => 9;
|
||||
|
@ -489,6 +497,7 @@ use constant USAGE_MODE_XMLRPC => 2;
|
|||
use constant USAGE_MODE_EMAIL => 3;
|
||||
use constant USAGE_MODE_JSON => 4;
|
||||
use constant USAGE_MODE_TEST => 5;
|
||||
use constant USAGE_MODE_REST => 6;
|
||||
|
||||
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
|
||||
# usually). Use with Bugzilla->error_mode.
|
||||
|
@ -497,6 +506,7 @@ use constant ERROR_MODE_DIE => 1;
|
|||
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
|
||||
use constant ERROR_MODE_JSON_RPC => 3;
|
||||
use constant ERROR_MODE_TEST => 4;
|
||||
use constant ERROR_MODE_REST => 5;
|
||||
|
||||
# The ANSI colors of messages that command-line scripts use
|
||||
use constant COLOR_ERROR => 'red';
|
||||
|
@ -604,6 +614,9 @@ use constant MAX_ATTACH_FILENAME_LENGTH => 255;
|
|||
# Maximum length of a quip.
|
||||
use constant MAX_QUIP_LENGTH => 512;
|
||||
|
||||
# Maximum number of bugs to display in a dependency graph
|
||||
use constant MAX_WEBDOT_BUGS => 2000;
|
||||
|
||||
# This is the name of the algorithm used to hash passwords before storing
|
||||
# them in the database. This can be any string that is valid to pass to
|
||||
# Perl's "Digest" module. Note that if you change this, it won't take
|
||||
|
|
|
@ -331,9 +331,8 @@ sub bz_setup_database {
|
|||
# hard to fix later. We do this up here because none of the code below
|
||||
# works if InnoDB is off. (Particularly if we've already converted the
|
||||
# tables to InnoDB.)
|
||||
my ($innodb_on) = @{$self->selectcol_arrayref(
|
||||
q{SHOW VARIABLES LIKE '%have_innodb%'}, {Columns=>[2]})};
|
||||
if ($innodb_on ne 'YES') {
|
||||
my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1,2]})};
|
||||
if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) {
|
||||
die install_string('mysql_innodb_disabled');
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ sub new {
|
|||
my $dsn = "dbi:Oracle:host=$host;sid=$dbname";
|
||||
$dsn .= ";port=$port" if $port;
|
||||
my $attrs = { FetchHashKeyName => 'NAME_lc',
|
||||
LongReadLen => max(Bugzilla->params->{'maxattachmentsize'},
|
||||
LongReadLen => max(Bugzilla->params->{'maxattachmentsize'} || 0,
|
||||
MIN_LONG_READ_LEN) * 1024,
|
||||
};
|
||||
my $self = $class->db_new({ dsn => $dsn, user => $user,
|
||||
|
@ -541,7 +541,9 @@ sub bz_setup_database {
|
|||
. " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;");
|
||||
|
||||
# Create types for group_concat
|
||||
$self->do("DROP TYPE T_GROUP_CONCAT");
|
||||
my $type_exists = $self->selectrow_array("SELECT 1 FROM user_types
|
||||
WHERE type_name = 'T_GROUP_CONCAT'");
|
||||
$self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists;
|
||||
$self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT "
|
||||
. "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)"
|
||||
. ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2"
|
||||
|
@ -795,3 +797,69 @@ sub fetch {
|
|||
return $row;
|
||||
}
|
||||
1;
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
||||
=item adjust_statement
|
||||
|
||||
=item bz_check_regexp
|
||||
|
||||
=item bz_drop_table
|
||||
|
||||
=item bz_explain
|
||||
|
||||
=item bz_last_key
|
||||
|
||||
=item bz_setup_database
|
||||
|
||||
=item bz_table_columns_real
|
||||
|
||||
=item bz_table_list_real
|
||||
|
||||
=item do
|
||||
|
||||
=item prepare
|
||||
|
||||
=item prepare_cached
|
||||
|
||||
=item quote_identifier
|
||||
|
||||
=item selectall_arrayref
|
||||
|
||||
=item selectall_hashref
|
||||
|
||||
=item selectcol_arrayref
|
||||
|
||||
=item selectrow_array
|
||||
|
||||
=item selectrow_arrayref
|
||||
|
||||
=item selectrow_hashref
|
||||
|
||||
=item sql_date_format
|
||||
|
||||
=item sql_date_math
|
||||
|
||||
=item sql_from_days
|
||||
|
||||
=item sql_fulltext_search
|
||||
|
||||
=item sql_group_concat
|
||||
|
||||
=item sql_in
|
||||
|
||||
=item sql_limit
|
||||
|
||||
=item sql_not_regexp
|
||||
|
||||
=item sql_position
|
||||
|
||||
=item sql_regexp
|
||||
|
||||
=item sql_string_concat
|
||||
|
||||
=item sql_to_days
|
||||
|
||||
=back
|
||||
|
|
|
@ -254,7 +254,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid'}},
|
||||
version => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
component_id => {TYPE => 'INT2', NOTNULL => 1,
|
||||
component_id => {TYPE => 'INT3', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'components',
|
||||
COLUMN => 'id'}},
|
||||
resolution => {TYPE => 'varchar(64)',
|
||||
|
@ -407,7 +407,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
extra_data => {TYPE => 'varchar(255)'}
|
||||
],
|
||||
INDEXES => [
|
||||
longdescs_bug_id_idx => ['bug_id'],
|
||||
longdescs_bug_id_idx => [qw(bug_id work_time)],
|
||||
longdescs_who_idx => [qw(who bug_id)],
|
||||
longdescs_bug_when_idx => ['bug_when'],
|
||||
],
|
||||
|
@ -641,14 +641,14 @@ use constant ABSTRACT_SCHEMA => {
|
|||
REFERENCES => {TABLE => 'products',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
component_id => {TYPE => 'INT2',
|
||||
component_id => {TYPE => 'INT3',
|
||||
REFERENCES => {TABLE => 'components',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
],
|
||||
INDEXES => [
|
||||
flaginclusions_type_id_idx =>
|
||||
[qw(type_id product_id component_id)],
|
||||
flaginclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)],
|
||||
TYPE => 'UNIQUE' },
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -662,14 +662,14 @@ use constant ABSTRACT_SCHEMA => {
|
|||
REFERENCES => {TABLE => 'products',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
component_id => {TYPE => 'INT2',
|
||||
component_id => {TYPE => 'INT3',
|
||||
REFERENCES => {TABLE => 'components',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
],
|
||||
INDEXES => [
|
||||
flagexclusions_type_id_idx =>
|
||||
[qw(type_id product_id component_id)],
|
||||
flagexclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)],
|
||||
TYPE => 'UNIQUE' },
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -948,6 +948,23 @@ use constant ABSTRACT_SCHEMA => {
|
|||
],
|
||||
},
|
||||
|
||||
email_bug_ignore => {
|
||||
FIELDS => [
|
||||
user_id => {TYPE => 'INT3', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid',
|
||||
DELETE => 'CASCADE'}},
|
||||
bug_id => {TYPE => 'INT3', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'bugs',
|
||||
COLUMN => 'bug_id',
|
||||
DELETE => 'CASCADE'}},
|
||||
],
|
||||
INDEXES => [
|
||||
email_bug_ignore_user_id_idx => {FIELDS => [qw(user_id bug_id)],
|
||||
TYPE => 'UNIQUE'},
|
||||
],
|
||||
},
|
||||
|
||||
watch => {
|
||||
FIELDS => [
|
||||
watcher => {TYPE => 'INT3', NOTNULL => 1,
|
||||
|
@ -1055,7 +1072,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid',
|
||||
DELETE => 'CASCADE'}},
|
||||
component_id => {TYPE => 'INT2', NOTNULL => 1,
|
||||
component_id => {TYPE => 'INT3', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'components',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
|
@ -1333,7 +1350,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
|
||||
components => {
|
||||
FIELDS => [
|
||||
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
|
||||
id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
|
||||
PRIMARYKEY => 1},
|
||||
name => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
product_id => {TYPE => 'INT2', NOTNULL => 1,
|
||||
|
|
|
@ -106,7 +106,7 @@ sub _initialize {
|
|||
LONGBLOB => 'longblob',
|
||||
|
||||
DATETIME => 'datetime',
|
||||
|
||||
DATE => 'date',
|
||||
};
|
||||
|
||||
$self->_adjust_schema;
|
||||
|
|
|
@ -56,7 +56,7 @@ sub _initialize {
|
|||
LONGBLOB => 'blob',
|
||||
|
||||
DATETIME => 'date',
|
||||
|
||||
DATE => 'date',
|
||||
};
|
||||
|
||||
$self->_adjust_schema;
|
||||
|
@ -205,6 +205,10 @@ sub get_add_column_ddl {
|
|||
}
|
||||
else {
|
||||
@sql = $self->SUPER::get_add_column_ddl(@_);
|
||||
# Create triggers to deal with empty string.
|
||||
if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) {
|
||||
push(@sql, _get_notnull_trigger_ddl($table, $column));
|
||||
}
|
||||
}
|
||||
|
||||
return @sql;
|
||||
|
|
|
@ -48,7 +48,7 @@ sub _initialize {
|
|||
LONGBLOB => 'bytea',
|
||||
|
||||
DATETIME => 'timestamp(0) without time zone',
|
||||
|
||||
DATE => 'date',
|
||||
};
|
||||
|
||||
$self->_adjust_schema;
|
||||
|
|
|
@ -46,6 +46,7 @@ sub _initialize {
|
|||
LONGBLOB => 'blob',
|
||||
|
||||
DATETIME => 'DATETIME',
|
||||
DATE => 'DATETIME',
|
||||
};
|
||||
|
||||
$self->_adjust_schema;
|
||||
|
|
|
@ -221,7 +221,7 @@ sub _throw_error
|
|||
}
|
||||
print $message;
|
||||
}
|
||||
elsif ($mode == ERROR_MODE_DIE_SOAP_FAULT || $mode == ERROR_MODE_JSON_RPC)
|
||||
elsif ($mode == ERROR_MODE_DIE_SOAP_FAULT || $mode == ERROR_MODE_JSON_RPC || Bugzilla->error_mode == ERROR_MODE_REST)
|
||||
{
|
||||
# FIXME FIXME FIXME: Numeric error codes are UGLY!!!
|
||||
# But we can't change them without breaking the compatibility...
|
||||
|
@ -245,13 +245,20 @@ sub _throw_error
|
|||
}
|
||||
else {
|
||||
my $server = Bugzilla->_json_server;
|
||||
|
||||
my $status_code = 0;
|
||||
if (Bugzilla->error_mode == ERROR_MODE_REST) {
|
||||
my %status_code_map = %{ REST_STATUS_CODE_MAP() };
|
||||
$status_code = $status_code_map{$code} || $status_code_map{'_default'};
|
||||
}
|
||||
# Technically JSON-RPC isn't allowed to have error numbers
|
||||
# higher than 999, but we do this to avoid conflicts with
|
||||
# the internal JSON::RPC error codes.
|
||||
$server->raise_error(code => 100000 + $code,
|
||||
message => $message,
|
||||
id => $server->{_bz_request_id},
|
||||
version => $server->version);
|
||||
$server->raise_error(code => 100000 + $code,
|
||||
status_code => $status_code,
|
||||
message => $message,
|
||||
id => $server->{_bz_request_id},
|
||||
version => $server->version);
|
||||
# Most JSON-RPC Throw*Error calls happen within an eval inside
|
||||
# of JSON::RPC. So, in that circumstance, instead of exiting,
|
||||
# we die with no message. JSON::RPC checks raise_error before
|
||||
|
|
|
@ -163,6 +163,7 @@ use constant SQL_DEFINITIONS => {
|
|||
FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT',
|
||||
NOTNULL => 1, DEFAULT => "''"},
|
||||
FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' },
|
||||
FIELD_TYPE_DATE, { TYPE => 'DATE' },
|
||||
FIELD_TYPE_BUG_ID, { TYPE => 'INT3' },
|
||||
};
|
||||
|
||||
|
@ -212,7 +213,7 @@ use constant DEFAULT_FIELDS => (
|
|||
buglist => 1},
|
||||
{name => 'cc', desc => 'CC', in_new_bugmail => 1},
|
||||
{name => 'dependson', desc => 'Depends on', in_new_bugmail => 1,
|
||||
is_numeric => 1},
|
||||
is_numeric => 1, buglist => 1},
|
||||
{name => 'blocked', desc => 'Blocks', in_new_bugmail => 1,
|
||||
is_numeric => 1},
|
||||
{name => 'dup_id', desc => 'Duplicate of', buglist => 1, in_new_bugmail => 1, type => FIELD_TYPE_BUG_ID},
|
||||
|
@ -357,9 +358,7 @@ sub _check_sortkey {
|
|||
sub _check_type {
|
||||
my ($invocant, $type, undef, $params) = @_;
|
||||
my $saved_type = $type;
|
||||
# The constant here should be updated every time a new,
|
||||
# higher field type is added.
|
||||
(detaint_natural($type) && $type <= FIELD_TYPE__BOUNDARY)
|
||||
(detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE)
|
||||
|| ThrowCodeError('invalid_customfield_type', { type => $saved_type });
|
||||
|
||||
my $custom = blessed($invocant) ? $invocant->custom : $params->{custom};
|
||||
|
@ -1066,7 +1065,10 @@ sub remove_from_db {
|
|||
}
|
||||
else {
|
||||
$bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
|
||||
if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) {
|
||||
if ($self->type != FIELD_TYPE_BUG_ID
|
||||
&& $self->type != FIELD_TYPE_DATE
|
||||
&& $self->type != FIELD_TYPE_DATETIME)
|
||||
{
|
||||
$bugs_query .= " AND $name != ''";
|
||||
}
|
||||
# Ignore the default single select value
|
||||
|
@ -1457,7 +1459,7 @@ sub check_field {
|
|||
Description: Returns the ID of the specified field name and throws
|
||||
an error if this field does not exist.
|
||||
|
||||
Params: $name - a field name
|
||||
Params: $fieldname - a field name
|
||||
|
||||
Returns: the corresponding field ID or an error if the field name
|
||||
does not exist.
|
||||
|
@ -1466,12 +1468,10 @@ Returns: the corresponding field ID or an error if the field name
|
|||
|
||||
=cut
|
||||
|
||||
sub get_field_id
|
||||
{
|
||||
my ($name) = @_;
|
||||
trick_taint($name);
|
||||
my $field = Bugzilla->get_field($name);
|
||||
ThrowCodeError('invalid_field_name', {field => $name}) unless $field;
|
||||
sub get_field_id {
|
||||
my $field = Bugzilla->fields({ by_name => 1 })->{$_[0]}
|
||||
or ThrowCodeError('invalid_field_name', {field => $_[0]});
|
||||
|
||||
return $field->id;
|
||||
}
|
||||
|
||||
|
|
|
@ -171,6 +171,7 @@ sub is_set_on_bug {
|
|||
# This allows bug/create/create.html.tmpl to pass in a hashref that
|
||||
# looks like a bug object.
|
||||
my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name};
|
||||
$value = $value->name if blessed($value);
|
||||
return 0 if !defined $value;
|
||||
|
||||
if ($self->field->type == FIELD_TYPE_BUG_URLS
|
||||
|
|
|
@ -294,6 +294,12 @@ sub set_flag {
|
|||
ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj });
|
||||
}
|
||||
|
||||
# Make sure the user can change flags
|
||||
my $privs;
|
||||
$bug->check_can_change_field('flagtypes.name', 0, 1, \$privs)
|
||||
|| ThrowUserError('illegal_change',
|
||||
{ field => 'flagtypes.name', privs => $privs });
|
||||
|
||||
# Update (or delete) an existing flag.
|
||||
if ($params->{id}) {
|
||||
my $flag = $class->check({ id => $params->{id} });
|
||||
|
@ -392,7 +398,7 @@ sub _validate {
|
|||
my $old_requestee_id = $obj_flag->requestee_id;
|
||||
|
||||
$obj_flag->_set_status($params->{status});
|
||||
$obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe});
|
||||
$obj_flag->_set_requestee($params->{requestee}, $bug, $attachment, $params->{skip_roe});
|
||||
|
||||
# The requestee ID can be undefined.
|
||||
my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);
|
||||
|
@ -622,10 +628,10 @@ sub force_retarget {
|
|||
###############################
|
||||
|
||||
sub _set_requestee {
|
||||
my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
|
||||
my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
|
||||
|
||||
$self->{requestee} =
|
||||
$self->_check_requestee($requestee, $attachment, $skip_requestee_on_error);
|
||||
$self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error);
|
||||
|
||||
$self->{requestee_id} =
|
||||
$self->{requestee} ? $self->{requestee}->id : undef;
|
||||
|
@ -647,7 +653,7 @@ sub _set_status {
|
|||
}
|
||||
|
||||
sub _check_requestee {
|
||||
my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
|
||||
my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
|
||||
|
||||
# If the flag status is not "?", then no requestee can be defined.
|
||||
return undef if ($self->status ne '?');
|
||||
|
@ -667,21 +673,29 @@ sub _check_requestee {
|
|||
# is specifically requestable. For existing flags, if the requestee
|
||||
# was set before the flag became specifically unrequestable, the
|
||||
# user can either remove him or leave him alone.
|
||||
ThrowUserError('flag_requestee_disabled', { type => $self->type })
|
||||
ThrowUserError('flag_type_requestee_disabled', { type => $self->type })
|
||||
if !$self->type->is_requesteeble;
|
||||
|
||||
# You can't ask a disabled account, as they don't have the ability to
|
||||
# set the flag.
|
||||
ThrowUserError('flag_requestee_disabled', { requestee => $requestee })
|
||||
if !$requestee->is_enabled;
|
||||
|
||||
# Make sure the requestee can see the bug.
|
||||
# Note that can_see_bug() will query the DB, so if the bug
|
||||
# is being added/removed from some groups and these changes
|
||||
# haven't been committed to the DB yet, they won't be taken
|
||||
# into account here. In this case, old restrictions matters.
|
||||
if (!$requestee->can_see_bug($self->bug_id)) {
|
||||
if (Bugzilla->params->{auto_add_flag_requestees_to_cc})
|
||||
{
|
||||
# CustIS Bug 55712 - Add flag requestees to CC list
|
||||
Bugzilla->cgi->param(-name => 'newcc', -value => [ Bugzilla->cgi->param('newcc'), $requestee->login ]);
|
||||
}
|
||||
elsif ($skip_requestee_on_error) {
|
||||
# into account here. In this case, old group restrictions matter.
|
||||
# However, if the user has just been changed to the assignee,
|
||||
# qa_contact, or added to the cc list of the bug and the bug
|
||||
# is cclist_accessible, the requestee is allowed.
|
||||
if (!$requestee->can_see_bug($self->bug_id)
|
||||
&& (!$bug->cclist_accessible
|
||||
|| !grep($_->id == $requestee->id, @{ $bug->cc_users })
|
||||
&& $requestee->id != $bug->assigned_to->id
|
||||
&& (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id)))
|
||||
{
|
||||
if ($skip_requestee_on_error) {
|
||||
undef $requestee;
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -95,6 +95,8 @@ sub SETTINGS {
|
|||
requestee_cc => { options => ['on', 'off'], default => 'on' },
|
||||
# 2012-04-30 glob@mozilla.com -- Bug 663747
|
||||
bugmail_new_prefix => { options => ['on', 'off'], default => 'on' },
|
||||
# 2013-07-26 joshi_sunil@in.com -- Bug 669535
|
||||
possible_duplicates => { options => ['on', 'off'], default => 'on' },
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ use Config;
|
|||
use CPAN;
|
||||
use Cwd qw(abs_path);
|
||||
use File::Path qw(rmtree);
|
||||
use List::Util qw(shuffle);
|
||||
|
||||
# These are required for install-module.pl to be able to install
|
||||
# all modules properly.
|
||||
|
@ -89,12 +88,7 @@ use constant CPAN_DEFAULTS => {
|
|||
unzip => bin_loc('unzip'),
|
||||
wget => bin_loc('wget'),
|
||||
|
||||
urllist => [shuffle qw(
|
||||
http://cpan.pair.com/
|
||||
http://mirror.hiwaay.net/CPAN/
|
||||
ftp://ftp.dc.aleron.net/pub/CPAN/
|
||||
http://mirrors.kernel.org/cpan/
|
||||
http://mirrors2.kernel.org/cpan/)],
|
||||
urllist => ['http://www.cpan.org/'],
|
||||
};
|
||||
|
||||
sub check_cpan_requirements {
|
||||
|
|
|
@ -725,6 +725,15 @@ sub update_table_definitions {
|
|||
# 2012-12-29 reed@reedloden.com - Bug 785283
|
||||
_add_password_salt_separator();
|
||||
|
||||
# 2013-01-02 LpSolit@gmail.com - Bug 824361
|
||||
_fix_longdescs_indexes();
|
||||
|
||||
# 2013-02-04 dkl@mozilla.com - Bug 824346
|
||||
_fix_flagclusions_indexes();
|
||||
|
||||
# 2013-08-26 sgreen@redhat.com - Bug 903895
|
||||
_fix_components_primary_key();
|
||||
|
||||
################################################################
|
||||
# New --TABLE-- changes should go *** A B O V E *** this point #
|
||||
################################################################
|
||||
|
@ -1443,9 +1452,9 @@ sub _use_ids_for_products_and_components {
|
|||
print "Updating the database to use component IDs.\n";
|
||||
|
||||
$dbh->bz_add_column("components", "id",
|
||||
{TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
|
||||
{TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
|
||||
$dbh->bz_add_column("bugs", "component_id",
|
||||
{TYPE => 'INT2', NOTNULL => 1}, 0);
|
||||
{TYPE => 'INT3', NOTNULL => 1}, 0);
|
||||
|
||||
my %components;
|
||||
$sth = $dbh->prepare("SELECT id, value, product_id FROM components");
|
||||
|
@ -3731,9 +3740,6 @@ sub _migrate_user_tags {
|
|||
VALUES (?, ?)');
|
||||
my $sth_nq = $dbh->prepare('UPDATE namedqueries SET query = ?
|
||||
WHERE id = ?');
|
||||
my $sth_nq_footer = $dbh->prepare(
|
||||
'DELETE FROM namedqueries_link_in_footer
|
||||
WHERE user_id = ? AND namedquery_id = ?');
|
||||
|
||||
if (scalar @$tags) {
|
||||
print install_string('update_queries_to_tags'), "\n";
|
||||
|
@ -3773,13 +3779,11 @@ sub _migrate_user_tags {
|
|||
next if !$bug_id;
|
||||
$sth_bug_tag->execute($bug_id, $tag_id);
|
||||
}
|
||||
|
||||
|
||||
# Existing tags may be used in whines, or shared with
|
||||
# other users. So we convert them rather than delete them.
|
||||
$uri->query_param('tag', $tag_name);
|
||||
$sth_nq->execute($uri->query, $query_id);
|
||||
# But we don't keep showing them in the footer.
|
||||
$sth_nq_footer->execute($user_id, $query_id);
|
||||
}
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
@ -3890,6 +3894,15 @@ sub _fix_longdescs_primary_key {
|
|||
}
|
||||
}
|
||||
|
||||
sub _fix_longdescs_indexes {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $bug_id_idx = $dbh->bz_index_info('longdescs', 'longdescs_bug_id_idx');
|
||||
if ($bug_id_idx && scalar @{$bug_id_idx->{'FIELDS'}} < 2) {
|
||||
$dbh->bz_drop_index('longdescs', 'longdescs_bug_id_idx');
|
||||
$dbh->bz_add_index('longdescs', 'longdescs_bug_id_idx', [qw(bug_id work_time)]);
|
||||
}
|
||||
}
|
||||
|
||||
sub _fix_dependencies_dupes {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $blocked_idx = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx');
|
||||
|
@ -3969,6 +3982,52 @@ sub _add_password_salt_separator {
|
|||
$dbh->bz_commit_transaction();
|
||||
}
|
||||
|
||||
sub _fix_flagclusions_indexes {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
foreach my $table ('flaginclusions', 'flagexclusions') {
|
||||
my $index = $table . '_type_id_idx';
|
||||
my $idx_info = $dbh->bz_index_info($table, $index);
|
||||
if ($idx_info && $idx_info->{'TYPE'} ne 'UNIQUE') {
|
||||
# Remove duplicated entries
|
||||
my $dupes = $dbh->selectall_arrayref("
|
||||
SELECT type_id, product_id, component_id, COUNT(*) AS count
|
||||
FROM $table " .
|
||||
$dbh->sql_group_by('type_id, product_id, component_id') . "
|
||||
HAVING COUNT(*) > 1",
|
||||
{ Slice => {} });
|
||||
say "Removing duplicated entries from the '$table' table..." if @$dupes;
|
||||
foreach my $dupe (@$dupes) {
|
||||
$dbh->do("DELETE FROM $table
|
||||
WHERE type_id = ? AND product_id = ? AND component_id = ?",
|
||||
undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id});
|
||||
$dbh->do("INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)",
|
||||
undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id});
|
||||
}
|
||||
$dbh->bz_drop_index($table, $index);
|
||||
$dbh->bz_add_index($table, $index,
|
||||
{ FIELDS => [qw(type_id product_id component_id)],
|
||||
TYPE => 'UNIQUE' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _fix_components_primary_key {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
if ($dbh->bz_column_info('components', 'id')->{TYPE} ne 'MEDIUMSERIAL') {
|
||||
$dbh->bz_drop_related_fks('components', 'id');
|
||||
$dbh->bz_alter_column("components", "id",
|
||||
{TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
|
||||
$dbh->bz_alter_column("flaginclusions", "component_id",
|
||||
{TYPE => 'INT3'});
|
||||
$dbh->bz_alter_column("flagexclusions", "component_id",
|
||||
{TYPE => 'INT3'});
|
||||
$dbh->bz_alter_column("bugs", "component_id",
|
||||
{TYPE => 'INT3', NOTNULL => 1});
|
||||
$dbh->bz_alter_column("component_cc", "component_id",
|
||||
{TYPE => 'INT3', NOTNULL => 1});
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
|
|
@ -19,9 +19,15 @@ use strict;
|
|||
use Bugzilla::Constants;
|
||||
use Bugzilla::Install::Util qw(vers_cmp install_string bin_loc);
|
||||
use List::Util qw(max);
|
||||
use Safe;
|
||||
use Term::ANSIColor;
|
||||
|
||||
# Return::Value 1.666002 pollutes the error log with warnings about this
|
||||
# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
|
||||
# in have_vers() to disable these warnings.
|
||||
BEGIN {
|
||||
$Return::Value::NO_CLUCK = 1;
|
||||
}
|
||||
|
||||
use parent qw(Exporter);
|
||||
our @EXPORT = qw(
|
||||
REQUIRED_MODULES
|
||||
|
@ -145,6 +151,7 @@ sub REQUIRED_MODULES {
|
|||
# in a URL query string.
|
||||
version => '1.37',
|
||||
},
|
||||
# 0.32 fixes several memory leaks in the XS version of some functions.
|
||||
{
|
||||
package => 'Lingua-Translit',
|
||||
module => 'Lingua::Translit',
|
||||
|
@ -158,7 +165,7 @@ sub REQUIRED_MODULES {
|
|||
{
|
||||
package => 'List-MoreUtils',
|
||||
module => 'List::MoreUtils',
|
||||
version => 0.22,
|
||||
version => 0.32,
|
||||
},
|
||||
{
|
||||
package => 'Math-Random-ISAAC',
|
||||
|
@ -273,6 +280,8 @@ sub OPTIONAL_MODULES {
|
|||
version => 0,
|
||||
feature => ['auth_radius'],
|
||||
},
|
||||
# XXX - Once we require XMLRPC::Lite 0.717 or higher, we can
|
||||
# remove SOAP::Lite from the list.
|
||||
{
|
||||
package => 'SOAP-Lite',
|
||||
module => 'SOAP::Lite',
|
||||
|
@ -281,11 +290,19 @@ sub OPTIONAL_MODULES {
|
|||
version => '0.712',
|
||||
feature => ['xmlrpc'],
|
||||
},
|
||||
# Since SOAP::Lite 1.0, XMLRPC::Lite is no longer included
|
||||
# and so it must be checked separately.
|
||||
{
|
||||
package => 'XMLRPC-Lite',
|
||||
module => 'XMLRPC::Lite',
|
||||
version => '0.712',
|
||||
feature => ['xmlrpc'],
|
||||
},
|
||||
{
|
||||
package => 'JSON-RPC',
|
||||
module => 'JSON::RPC',
|
||||
version => 0,
|
||||
feature => ['jsonrpc'],
|
||||
feature => ['jsonrpc', 'rest'],
|
||||
},
|
||||
{
|
||||
package => 'JSON-XS',
|
||||
|
@ -299,7 +316,7 @@ sub OPTIONAL_MODULES {
|
|||
module => 'Test::Taint',
|
||||
# 1.06 no longer throws warnings with Perl 5.10+.
|
||||
version => 1.06,
|
||||
feature => ['jsonrpc', 'xmlrpc'],
|
||||
feature => ['jsonrpc', 'xmlrpc', 'rest'],
|
||||
},
|
||||
{
|
||||
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
|
||||
|
@ -347,7 +364,8 @@ sub OPTIONAL_MODULES {
|
|||
{
|
||||
package => 'TheSchwartz',
|
||||
module => 'TheSchwartz',
|
||||
version => 0,
|
||||
# 1.07 supports the prioritization of jobs.
|
||||
version => 1.07,
|
||||
feature => ['jobqueue'],
|
||||
},
|
||||
{
|
||||
|
@ -413,6 +431,7 @@ use constant FEATURE_FILES => (
|
|||
jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'],
|
||||
xmlrpc => ['Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi',
|
||||
'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'],
|
||||
rest => ['Bugzilla/WebService/Server/REST.pm', 'rest.cgi'],
|
||||
moving => ['importxml.pl'],
|
||||
auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'],
|
||||
auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'],
|
||||
|
@ -592,16 +611,6 @@ sub print_module_instructions {
|
|||
( (!$output and @{$check_results->{missing}})
|
||||
or ($output and $check_results->{any_missing}) ) ? 1 : 0;
|
||||
|
||||
# We only print the PPM repository note if we have to.
|
||||
my $perl_ver = sprintf('%vd', $^V);
|
||||
if ($need_module_instructions && ON_ACTIVESTATE && vers_cmp($perl_ver, '5.12') < 0) {
|
||||
my $url_to_theory58S = 'http://cpan.uwinnipeg.ca/PPMPackages/10xx/';
|
||||
print colored(
|
||||
install_string('ppm_repo_add',
|
||||
{ theory_url => $url_to_theory58S }),
|
||||
COLOR_ERROR);
|
||||
}
|
||||
|
||||
if ($need_module_instructions or @{ $check_results->{apache} }) {
|
||||
# If any output was required, we want to close the "table"
|
||||
print "*" x TABLE_WIDTH . "\n";
|
||||
|
@ -708,16 +717,17 @@ sub have_vers {
|
|||
Bugzilla::Install::Util::set_output_encoding();
|
||||
|
||||
# VERSION is provided by UNIVERSAL::, and can be called even if
|
||||
# the module isn't loaded.
|
||||
my $vnum = $module->VERSION || -1;
|
||||
|
||||
# CGI's versioning scheme went 2.75, 2.751, 2.752, 2.753, 2.76
|
||||
# That breaks the standard version tests, so we need to manually correct
|
||||
# the version
|
||||
if ($module eq 'CGI' && $vnum =~ /(2\.7\d)(\d+)/) {
|
||||
$vnum = $1 . "." . $2;
|
||||
# the module isn't loaded. We eval'uate ->VERSION because it can die
|
||||
# when the version is not valid (yes, this happens from time to time).
|
||||
# In that case, we use an uglier method to get the version.
|
||||
my $vnum = eval { $module->VERSION };
|
||||
if ($@) {
|
||||
no strict 'refs';
|
||||
$vnum = ${"${module}::VERSION"};
|
||||
}
|
||||
# CPAN did a similar thing, where it has versions like 1.9304.
|
||||
$vnum ||= -1;
|
||||
|
||||
# Fix CPAN versions like 1.9304.
|
||||
if ($module eq 'CPAN' and $vnum =~ /^(\d\.\d{2})\d{2}$/) {
|
||||
$vnum = $1;
|
||||
}
|
||||
|
|
|
@ -378,7 +378,10 @@ sub include_languages {
|
|||
|
||||
# Basically, the way this works is that we have a list of languages
|
||||
# that we *want*, and a list of languages that Bugzilla actually
|
||||
# supports.
|
||||
# supports. If there is only one language installed, we take it.
|
||||
my $supported = supported_languages();
|
||||
return @$supported if @$supported == 1;
|
||||
|
||||
my $wanted;
|
||||
if ($params->{language}) {
|
||||
# We can pass several languages at once as an arrayref
|
||||
|
@ -389,7 +392,6 @@ sub include_languages {
|
|||
else {
|
||||
$wanted = _wanted_languages();
|
||||
}
|
||||
my $supported = supported_languages();
|
||||
my $actual = _wanted_to_actual_languages($wanted, $supported);
|
||||
return @$actual;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# 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::Job::BugMail;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use Bugzilla::BugMail;
|
||||
BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; }
|
||||
|
||||
sub work {
|
||||
my ($class, $job) = @_;
|
||||
my $success = eval {
|
||||
Bugzilla::BugMail::dequeue($job->arg->{vars});
|
||||
1;
|
||||
};
|
||||
if (!$success) {
|
||||
$job->failed($@);
|
||||
undef $@;
|
||||
}
|
||||
else {
|
||||
$job->completed;
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
|
@ -43,13 +43,3 @@ sub work {
|
|||
}
|
||||
|
||||
1;
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
||||
=item retry_delay
|
||||
|
||||
=item work
|
||||
|
||||
=back
|
||||
|
|
|
@ -13,7 +13,10 @@ use strict;
|
|||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Install::Util qw(install_string);
|
||||
use parent qw(TheSchwartz);
|
||||
use File::Basename;
|
||||
use File::Slurp;
|
||||
use base qw(TheSchwartz);
|
||||
use fields qw(_worker_pidfile);
|
||||
|
||||
# This maps job names for Bugzilla::JobQueue to the appropriate modules.
|
||||
# If you add new types of jobs, you should add a mapping here.
|
||||
|
@ -21,6 +24,7 @@ use parent qw(TheSchwartz);
|
|||
use constant JOB_MAP => {
|
||||
send_mail => 'Bugzilla::Job::Mailer',
|
||||
sm_sync => 'Bugzilla::Job::SM',
|
||||
bug_mail => 'Bugzilla::Job::BugMail',
|
||||
};
|
||||
|
||||
# Without a driver cache TheSchwartz opens a new database connection
|
||||
|
@ -95,6 +99,64 @@ sub insert {
|
|||
return $retval;
|
||||
}
|
||||
|
||||
# To avoid memory leaks/fragmentation which tends to happen for long running
|
||||
# perl processes; check for jobs, and spawn a new process to empty the queue.
|
||||
sub subprocess_worker {
|
||||
my $self = shift;
|
||||
|
||||
my $command = "$0 -d -p '" . $self->{_worker_pidfile} . "' onepass";
|
||||
|
||||
while (1) {
|
||||
my $time = (time);
|
||||
my @jobs = $self->list_jobs({
|
||||
funcname => $self->{all_abilities},
|
||||
run_after => $time,
|
||||
grabbed_until => $time,
|
||||
limit => 1,
|
||||
});
|
||||
if (@jobs) {
|
||||
$self->debug("Spawning queue worker process");
|
||||
# Run the worker as a daemon
|
||||
system $command;
|
||||
# And poll the PID to detect when the working has finished.
|
||||
# We do this instead of system() to allow for the INT signal to
|
||||
# interrup us and trigger kill_worker().
|
||||
my $pid = read_file($self->{_worker_pidfile}, err_mode => 'quiet');
|
||||
if ($pid) {
|
||||
sleep(3) while(kill(0, $pid));
|
||||
}
|
||||
$self->debug("Queue worker process completed");
|
||||
} else {
|
||||
$self->debug("No jobs found");
|
||||
}
|
||||
sleep(5);
|
||||
}
|
||||
}
|
||||
|
||||
sub kill_worker {
|
||||
my $self = Bugzilla->job_queue();
|
||||
if ($self->{_worker_pidfile} && -e $self->{_worker_pidfile}) {
|
||||
my $worker_pid = read_file($self->{_worker_pidfile});
|
||||
if ($worker_pid && kill(0, $worker_pid)) {
|
||||
$self->debug("Stopping worker process");
|
||||
system "$0 -f -p '" . $self->{_worker_pidfile} . "' stop";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub set_pidfile {
|
||||
my ($self, $pidfile) = @_;
|
||||
$self->{_worker_pidfile} = bz_locations->{'datadir'} .
|
||||
'/worker-' . basename($pidfile);
|
||||
}
|
||||
|
||||
# Clear the request cache at the start of each run.
|
||||
sub work_once {
|
||||
my $self = shift;
|
||||
Bugzilla->clear_request_cache();
|
||||
return $self->SUPER::work_once(@_);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -131,4 +193,8 @@ be sent away to be done later.
|
|||
|
||||
=item job_map
|
||||
|
||||
=item set_pidfile
|
||||
|
||||
=item kill_worker
|
||||
|
||||
=back
|
||||
|
|
|
@ -38,6 +38,7 @@ our $initscript = "bugzilla-queue";
|
|||
sub gd_preconfig {
|
||||
my $self = shift;
|
||||
|
||||
$self->{_run_command} = 'subprocess_worker';
|
||||
my $pidfile = $self->{gd_args}{pidfile};
|
||||
if (!$pidfile) {
|
||||
$pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname}
|
||||
|
@ -136,6 +137,7 @@ sub gd_can_install {
|
|||
print $config_fh <<END;
|
||||
#!/bin/sh
|
||||
BUGZILLA="$directory"
|
||||
# This user must have write access to Bugzilla's data/ directory.
|
||||
USER=$owner
|
||||
END
|
||||
close($config_fh);
|
||||
|
@ -183,21 +185,25 @@ sub gd_setup_signals {
|
|||
$SIG{TERM} = sub { $self->gd_quit_event(); }
|
||||
}
|
||||
|
||||
sub gd_other_cmd {
|
||||
my ($self) = shift;
|
||||
if ($ARGV[0] eq "once") {
|
||||
$self->_do_work("work_once");
|
||||
sub gd_quit_event {
|
||||
Bugzilla->job_queue->kill_worker();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
sub gd_other_cmd {
|
||||
my ($self, $do, $locked) = @_;
|
||||
if ($do eq "once") {
|
||||
$self->{_run_command} = 'work_once';
|
||||
} elsif ($do eq "onepass") {
|
||||
$self->{_run_command} = 'work_until_done';
|
||||
} else {
|
||||
$self->SUPER::gd_other_cmd($do, $locked);
|
||||
}
|
||||
|
||||
$self->SUPER::gd_other_cmd();
|
||||
}
|
||||
|
||||
sub gd_run {
|
||||
my $self = shift;
|
||||
|
||||
$self->_do_work("work");
|
||||
$self->_do_work($self->{_run_command});
|
||||
}
|
||||
|
||||
sub _do_work {
|
||||
|
@ -205,11 +211,11 @@ sub _do_work {
|
|||
|
||||
my $jq = Bugzilla->job_queue();
|
||||
$jq->set_verbose($self->{debug});
|
||||
$jq->set_pidfile($self->{gd_pidfile});
|
||||
foreach my $module (values %{ Bugzilla::JobQueue->job_map() }) {
|
||||
eval "use $module";
|
||||
$jq->can_do($module);
|
||||
}
|
||||
|
||||
$jq->$fn;
|
||||
}
|
||||
|
||||
|
@ -242,6 +248,8 @@ to run the Bugzilla job queue.
|
|||
|
||||
=item gd_can_install
|
||||
|
||||
=item gd_quit_event
|
||||
|
||||
=item gd_other_cmd
|
||||
|
||||
=item gd_more_opt
|
||||
|
|
|
@ -128,7 +128,7 @@ sub _check_name {
|
|||
|
||||
# We only want to validate the non-existence of the name if
|
||||
# we're creating a new Keyword or actually renaming the keyword.
|
||||
if (!ref($self) || $self->name ne $name) {
|
||||
if (!ref($self) || lc($self->name) ne lc($name)) {
|
||||
my $keyword = new Bugzilla::Keyword({ name => $name });
|
||||
ThrowUserError("keyword_already_exists", { name => $name }) if $keyword;
|
||||
}
|
||||
|
|
|
@ -153,6 +153,7 @@ sub do_migration {
|
|||
}
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
$self->before_read();
|
||||
# Read Other Database
|
||||
my $users = $self->users;
|
||||
my $products = $self->products;
|
||||
|
@ -447,8 +448,11 @@ sub translate_value {
|
|||
}
|
||||
|
||||
my $field_obj = $self->bug_fields->{$field};
|
||||
if ($field eq 'creation_ts' or $field eq 'delta_ts'
|
||||
or ($field_obj and $field_obj->type == FIELD_TYPE_DATETIME))
|
||||
if ($field eq 'creation_ts'
|
||||
or $field eq 'delta_ts'
|
||||
or ($field_obj and
|
||||
($field_obj->type == FIELD_TYPE_DATETIME
|
||||
or $field_obj->type == FIELD_TYPE_DATE)))
|
||||
{
|
||||
$value = trim($value);
|
||||
return undef if !$value;
|
||||
|
@ -541,6 +545,7 @@ sub write_config {
|
|||
sub after_insert {}
|
||||
sub before_insert {}
|
||||
sub after_read {}
|
||||
sub before_read {}
|
||||
|
||||
#############
|
||||
# Inserters #
|
||||
|
@ -815,7 +820,7 @@ sub _insert_comments {
|
|||
$self->_do_table_insert('longdescs', \%copy);
|
||||
$self->debug(" Inserted comment from " . $who->login, 2);
|
||||
}
|
||||
$bug->_sync_fulltext();
|
||||
$bug->_sync_fulltext( update_comments => 1 );
|
||||
}
|
||||
|
||||
sub _insert_history {
|
||||
|
@ -1144,6 +1149,11 @@ and yet shouldn't be added to the initial description of the bug when
|
|||
translating bugs, then they should be listed here. See L</translate_bug> for
|
||||
more detail.
|
||||
|
||||
=head2 before_read
|
||||
|
||||
This is called before any data is read from the "other bug-tracker".
|
||||
The default implementation does nothing.
|
||||
|
||||
=head2 after_read
|
||||
|
||||
This is run after all data is read from the other bug-tracker, but
|
||||
|
|
|
@ -46,8 +46,14 @@ sub TO_JSON { return { %{ $_[0] } }; }
|
|||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $object = $class->_init(@_);
|
||||
bless($object, $class) if $object;
|
||||
my $param = shift;
|
||||
|
||||
my $object = $class->_cache_get($param);
|
||||
return $object if $object;
|
||||
|
||||
$object = $class->new_from_hash($class->_load_from_db($param));
|
||||
$class->_cache_set($param, $object);
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
|
@ -55,11 +61,9 @@ sub new {
|
|||
# Bugzilla::Object, make sure that you modify bz_setup_database
|
||||
# in Bugzilla::DB::Pg appropriately, to add the right LOWER
|
||||
# index. You can see examples already there.
|
||||
sub _init {
|
||||
sub _load_from_db {
|
||||
my $class = shift;
|
||||
my ($param) = @_;
|
||||
my $object = $class->_cache_get($param);
|
||||
return $object if $object;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $columns = join(',', $class->_get_db_columns);
|
||||
my $table = $class->DB_TABLE;
|
||||
|
@ -72,18 +76,20 @@ sub _init {
|
|||
}
|
||||
my ($sql, @values);
|
||||
|
||||
my $object_data;
|
||||
if (defined $id) {
|
||||
# We special-case if somebody specifies an ID, so that we can
|
||||
# validate it as numeric.
|
||||
detaint_natural($id)
|
||||
|| ThrowCodeError('param_must_be_numeric',
|
||||
{function => $class . '::_init'});
|
||||
{function => $class . '::_load_from_db'});
|
||||
|
||||
# Too large integers make PostgreSQL crash.
|
||||
return if $id > MAX_INT_32;
|
||||
|
||||
$sql = "$id_field = ?";
|
||||
@values = ($id);
|
||||
$object_data = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM $table
|
||||
WHERE $id_field = ?}, undef, $id);
|
||||
} else {
|
||||
unless (defined $param->{name} || (defined $param->{condition}
|
||||
&& defined $param->{values}))
|
||||
|
@ -108,17 +114,47 @@ sub _init {
|
|||
}
|
||||
|
||||
map { trick_taint($_) } @values;
|
||||
$object_data = $dbh->selectrow_hashref(
|
||||
"SELECT $columns FROM $table WHERE $condition", undef, @values);
|
||||
}
|
||||
return $object_data;
|
||||
}
|
||||
|
||||
sub new_from_list {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my ($id_list) = @_;
|
||||
my $id_field = $class->ID_FIELD;
|
||||
|
||||
my @detainted_ids;
|
||||
foreach my $id (@$id_list) {
|
||||
detaint_natural($id) ||
|
||||
ThrowCodeError('param_must_be_numeric',
|
||||
{function => $class . '::new_from_list'});
|
||||
# Too large integers make PostgreSQL crash.
|
||||
next if $id > MAX_INT_32;
|
||||
push(@detainted_ids, $id);
|
||||
}
|
||||
|
||||
# It is recommended to use FOR UPDATE when updating!
|
||||
$sql = "SELECT $columns FROM $table WHERE $sql";
|
||||
if (ref $param eq 'HASH' && $param->{for_update})
|
||||
{
|
||||
$sql .= " FOR UPDATE";
|
||||
}
|
||||
# We don't do $invocant->match because some classes have
|
||||
# their own implementation of match which is not compatible
|
||||
# with this one. However, match() still needs to have the right $invocant
|
||||
# in order to do $class->DB_TABLE and so on.
|
||||
return match($invocant, { $id_field => \@detainted_ids });
|
||||
}
|
||||
|
||||
$object = $dbh->selectrow_hashref($sql, undef, @values);
|
||||
return $object;
|
||||
sub new_from_hash {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $object_data = shift || return;
|
||||
$class->_serialisation_keys($object_data);
|
||||
bless($object_data, $class);
|
||||
$object_data->initialize();
|
||||
return $object_data;
|
||||
}
|
||||
|
||||
sub initialize {
|
||||
# abstract
|
||||
}
|
||||
|
||||
# Provides a mechanism for objects to be cached in the request_cache
|
||||
|
@ -148,6 +184,15 @@ sub cache_key {
|
|||
}
|
||||
}
|
||||
|
||||
# To support serialisation, we need to capture the keys in an object's default
|
||||
# hashref.
|
||||
sub _serialisation_keys {
|
||||
my ($class, $object) = @_;
|
||||
my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {};
|
||||
$cache->{$class} = [ keys %$object ] if $object && !exists $cache->{$class};
|
||||
return @{ $cache->{$class} };
|
||||
}
|
||||
|
||||
sub check {
|
||||
my ($invocant, $param) = @_;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
|
@ -181,28 +226,6 @@ sub check {
|
|||
return $obj;
|
||||
}
|
||||
|
||||
sub new_from_list {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my ($id_list) = @_;
|
||||
my $id_field = $class->ID_FIELD;
|
||||
|
||||
my @detainted_ids;
|
||||
foreach my $id (@$id_list) {
|
||||
detaint_natural($id) ||
|
||||
ThrowCodeError('param_must_be_numeric',
|
||||
{function => $class . '::new_from_list'});
|
||||
# Too large integers make PostgreSQL crash.
|
||||
next if $id > MAX_INT_32;
|
||||
push(@detainted_ids, $id);
|
||||
}
|
||||
# We don't do $invocant->match because some classes have
|
||||
# their own implementation of match which is not compatible
|
||||
# with this one. However, match() still needs to have the right $invocant
|
||||
# in order to do $class->DB_TABLE and so on.
|
||||
return match($invocant, { $id_field => \@detainted_ids });
|
||||
}
|
||||
|
||||
# Note: Future extensions to this could be:
|
||||
# * Add a MATCH_JOIN constant so that we can join against
|
||||
# certain other tables for the WHERE criteria.
|
||||
|
@ -301,8 +324,11 @@ sub _do_list_select {
|
|||
my @untainted = @{ $values || [] };
|
||||
trick_taint($_) foreach @untainted;
|
||||
my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
|
||||
bless ($_, $class) foreach @$objects;
|
||||
return $objects
|
||||
$class->_serialisation_keys($objects->[0]) if @$objects;
|
||||
foreach my $object (@$objects) {
|
||||
$object = $class->new_from_hash($object);
|
||||
}
|
||||
return $objects;
|
||||
}
|
||||
|
||||
###############################
|
||||
|
@ -478,6 +504,13 @@ sub audit_log {
|
|||
}
|
||||
}
|
||||
|
||||
sub flatten_to_hash {
|
||||
my $self = shift;
|
||||
my $class = blessed($self);
|
||||
my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys;
|
||||
return \%hash;
|
||||
}
|
||||
|
||||
###############################
|
||||
#### Subroutines ######
|
||||
###############################
|
||||
|
@ -996,6 +1029,17 @@ database matching the parameters you passed in.
|
|||
|
||||
=back
|
||||
|
||||
=item C<initialize>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Abstract method to allow subclasses to perform initialization tasks after an
|
||||
object has been created.
|
||||
|
||||
=back
|
||||
|
||||
=item C<check>
|
||||
|
||||
=over
|
||||
|
@ -1035,6 +1079,13 @@ template.
|
|||
|
||||
Returns: A reference to an array of objects.
|
||||
|
||||
=item C<new_from_hash($hashref)>
|
||||
|
||||
Description: Create an object from the given hash.
|
||||
|
||||
Params: $hashref - A reference to a hash which was created by
|
||||
flatten_to_hash.
|
||||
|
||||
=item C<match>
|
||||
|
||||
=over
|
||||
|
@ -1272,6 +1323,17 @@ that should be passed to the C<set_> function that is called.
|
|||
|
||||
=back
|
||||
|
||||
=head2 Simple Methods
|
||||
|
||||
=over
|
||||
|
||||
=item C<flatten_to_hash>
|
||||
|
||||
Returns a hashref suitable for serialisation and re-inflation with C<new_from_hash>.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 Simple Validators
|
||||
|
||||
You can use these in your subclass L</VALIDATORS> or L</UPDATE_VALIDATORS>.
|
||||
|
|
|
@ -983,6 +983,7 @@ sub init
|
|||
delete $H->{resolution};
|
||||
}
|
||||
}
|
||||
$clause->add($join_clause);
|
||||
|
||||
# All fields that don't have a . in their name should be specifyable
|
||||
# in the URL directly.
|
||||
|
@ -1575,7 +1576,8 @@ sub pronoun
|
|||
{
|
||||
return "bugs.qa_contact";
|
||||
}
|
||||
return 0;
|
||||
|
||||
ThrowUserError('illegal_pronoun', { pronoun => $noun });
|
||||
}
|
||||
|
||||
# BuildOrderBy - Private Subroutine
|
||||
|
@ -3372,4 +3374,141 @@ use overload '""' => sub
|
|||
};
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Search - Provides methods to run queries against bugs.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Search;
|
||||
|
||||
my $search = new Bugzilla::Search({'fields' => \@fields,
|
||||
'params' => \%search_criteria,
|
||||
'sharer' => $sharer_id,
|
||||
'user' => $user_obj,
|
||||
'allow_unlimited' => 1});
|
||||
|
||||
my $data = $search->data;
|
||||
my ($data, $extra_data) = $search->data;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Search.pm represents a search object. It's the single way to collect
|
||||
data about bugs in a secure way. The list of bugs matching criteria
|
||||
defined by the caller are filtered based on the user privileges.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 new
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Create a Bugzilla::Search object.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
||||
=item C<fields>
|
||||
|
||||
An arrayref representing the bug attributes for which data is desired.
|
||||
Legal attributes are listed in the fielddefs DB table. At least one field
|
||||
must be defined, typically the 'bug_id' field.
|
||||
|
||||
=item C<params>
|
||||
|
||||
A hashref representing search criteria. Each key => value pair represents
|
||||
a search criteria, where the key is the search field and the value is the
|
||||
value for this field. At least one search criteria must be defined if the
|
||||
'search_allow_no_criteria' parameter is turned off, else an error is thrown.
|
||||
|
||||
=item C<sharer>
|
||||
|
||||
When a saved search is shared by a user, this is his user ID.
|
||||
|
||||
=item C<user>
|
||||
|
||||
A L<Bugzilla::User> object representing the user to whom the data is addressed.
|
||||
All security checks are done based on this user object, so it's not safe
|
||||
to share results of the query with other users as not all users have the
|
||||
same privileges or have the same role for all bugs in the list. If this
|
||||
parameter is not defined, then the currently logged in user is taken into
|
||||
account. If no user is logged in, then only public bugs will be returned.
|
||||
|
||||
=item C<allow_unlimited>
|
||||
|
||||
If set to a true value, the number of bugs retrieved by the query is not
|
||||
limited.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A L<Bugzilla::Search> object.
|
||||
|
||||
=back
|
||||
|
||||
=head2 data
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Returns bugs matching search criteria passed to C<new()>.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
None
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
In scalar context, this method returns a reference to a list of bugs.
|
||||
Each item of the list represents a bug, which is itself a reference to
|
||||
a list where each item represents a bug attribute, in the same order as
|
||||
specified in the C<fields> parameter of C<new()>.
|
||||
|
||||
In list context, this methods also returns a reference to a list containing
|
||||
references to hashes. For each hash, two keys are defined: C<sql> contains
|
||||
the SQL query which has been executed, and C<time> contains the time spent
|
||||
to execute the SQL query, in seconds. There can be either a single hash, or
|
||||
two hashes if two SQL queries have been executed sequentially to get all the
|
||||
required data.
|
||||
|
||||
=back
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
||||
=item invalid_order_columns
|
||||
|
||||
=item COLUMN_JOINS
|
||||
|
||||
=item split_order_term
|
||||
|
||||
=item SqlifyDate
|
||||
|
||||
=item REPORT_COLUMNS
|
||||
|
||||
=item pronoun
|
||||
|
||||
=item COLUMNS
|
||||
|
||||
=item order
|
||||
|
||||
=item search_description
|
||||
|
||||
=item IsValidQueryType
|
||||
|
||||
=item build_subselect
|
||||
|
||||
=item do_search_function
|
||||
|
||||
=item boolean_charts_to_custom_search
|
||||
|
||||
=back
|
||||
|
|
|
@ -86,25 +86,29 @@ sub walk_conditions {
|
|||
|
||||
sub as_string {
|
||||
my ($self) = @_;
|
||||
my @strings;
|
||||
foreach my $child (@{ $self->children }) {
|
||||
next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions;
|
||||
next if $child->isa('Bugzilla::Search::Condition')
|
||||
&& !$child->translated;
|
||||
if (!$self->{sql}) {
|
||||
my @strings;
|
||||
foreach my $child (@{ $self->children }) {
|
||||
next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions;
|
||||
next if $child->isa('Bugzilla::Search::Condition')
|
||||
&& !$child->translated;
|
||||
|
||||
my $string = $child->as_string;
|
||||
if ($self->joiner eq 'AND') {
|
||||
$string = "( $string )" if $string =~ /OR/;
|
||||
my $string = $child->as_string;
|
||||
next unless $string;
|
||||
if ($self->joiner eq 'AND') {
|
||||
$string = "( $string )" if $string =~ /OR/;
|
||||
}
|
||||
else {
|
||||
$string = "( $string )" if $string =~ /AND/;
|
||||
}
|
||||
push(@strings, $string);
|
||||
}
|
||||
else {
|
||||
$string = "( $string )" if $string =~ /AND/;
|
||||
}
|
||||
push(@strings, $string);
|
||||
|
||||
my $sql = join(' ' . $self->joiner . ' ', @strings);
|
||||
$sql = "NOT( $sql )" if $sql && $self->negate;
|
||||
$self->{sql} = $sql;
|
||||
}
|
||||
|
||||
my $sql = join(' ' . $self->joiner . ' ', @strings);
|
||||
$sql = "NOT( $sql )" if $sql && $self->negate;
|
||||
return $sql;
|
||||
return $self->{sql};
|
||||
}
|
||||
|
||||
# Search.pm converts URL parameters to Clause objects. This helps do the
|
||||
|
|
|
@ -10,7 +10,7 @@ package Bugzilla::Search::ClauseGroup;
|
|||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use base qw(Bugzilla::Search::Clause);
|
||||
use parent qw(Bugzilla::Search::Clause);
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Search::Condition qw(condition);
|
||||
|
@ -66,7 +66,10 @@ sub add {
|
|||
|
||||
# Unsupported fields
|
||||
if (grep { $_ eq $field } UNSUPPORTED_FIELDS ) {
|
||||
ThrowUserError('search_grouped_field_invalid', { field => $field });
|
||||
# XXX - Hack till bug 916882 is fixed.
|
||||
my $operator = scalar(@args) == 3 ? $args[1] : $args[0]->{operator};
|
||||
ThrowUserError('search_grouped_field_invalid', { field => $field })
|
||||
unless (($field eq 'product' || $field eq 'component') && $operator =~ /^changed/);
|
||||
}
|
||||
|
||||
$self->SUPER::add(@args);
|
||||
|
|
|
@ -21,9 +21,16 @@ sub new {
|
|||
}
|
||||
|
||||
sub field { return $_[0]->{field} }
|
||||
sub operator { return $_[0]->{operator} }
|
||||
sub value { return $_[0]->{value} }
|
||||
|
||||
sub operator {
|
||||
my ($self, $value) = @_;
|
||||
if (@_ == 2) {
|
||||
$self->{operator} = $value;
|
||||
}
|
||||
return $self->{operator};
|
||||
}
|
||||
|
||||
sub fov {
|
||||
my ($self) = @_;
|
||||
return ($self->field, $self->operator, $self->value);
|
||||
|
|
|
@ -149,7 +149,7 @@ sub quicksearch {
|
|||
|
||||
# Retain backslashes and quotes, to know which strings are quoted,
|
||||
# and which ones are not.
|
||||
my @words = parse_line('\s+', 1, $searchstring);
|
||||
my @words = _parse_line('\s+', 1, $searchstring);
|
||||
# If parse_line() returns no data, this means strings are badly quoted.
|
||||
# Rather than trying to guess what the user wanted to do, we throw an error.
|
||||
scalar(@words)
|
||||
|
@ -245,13 +245,36 @@ sub quicksearch {
|
|||
# Parts of quicksearch() #
|
||||
##########################
|
||||
|
||||
sub _parse_line {
|
||||
my ($delim, $keep, $line) = @_;
|
||||
return () unless defined $line;
|
||||
|
||||
# parse_line always treats ' as a quote character, making it impossible
|
||||
# to sanely search for contractions. As this behavour isn't
|
||||
# configurable, we replace ' with a placeholder to hide it from the
|
||||
# parser.
|
||||
|
||||
# only treat ' at the start or end of words as quotes
|
||||
# it's easier to do this in reverse with regexes
|
||||
$line =~ s/(^|\s|:)'/$1\001/g;
|
||||
$line =~ s/'($|\s)/\001$1/g;
|
||||
$line =~ s/\\?'/\000/g;
|
||||
$line =~ tr/\001/'/;
|
||||
|
||||
my @words = parse_line($delim, $keep, $line);
|
||||
foreach my $word (@words) {
|
||||
$word =~ tr/\000/'/ if defined $word;
|
||||
}
|
||||
return @words;
|
||||
}
|
||||
|
||||
sub _bug_numbers_only {
|
||||
my $searchstring = shift;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
# Allow separation by comma or whitespace.
|
||||
$searchstring =~ s/[,\s]+/,/g;
|
||||
|
||||
if ($searchstring !~ /,/) {
|
||||
if ($searchstring !~ /,/ && !i_am_webservice()) {
|
||||
# Single bug number; shortcut to show_bug.cgi.
|
||||
print $cgi->redirect(
|
||||
-uri => correct_urlbase() . "show_bug.cgi?id=$searchstring");
|
||||
|
@ -272,8 +295,9 @@ sub _handle_alias {
|
|||
# We use this direct SQL because we want quicksearch to be VERY fast.
|
||||
my $bug_id = Bugzilla->dbh->selectrow_array(
|
||||
q{SELECT bug_id FROM bugs WHERE alias = ?}, undef, $alias);
|
||||
# If the user cannot see the bug, do not resolve its alias.
|
||||
if ($bug_id && Bugzilla->user->can_see_bug($bug_id)) {
|
||||
# If the user cannot see the bug or if we are using a webservice,
|
||||
# do not resolve its alias.
|
||||
if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) {
|
||||
$alias = url_quote($alias);
|
||||
print Bugzilla->cgi->redirect(
|
||||
-uri => correct_urlbase() . "show_bug.cgi?id=$alias");
|
||||
|
@ -301,6 +325,7 @@ sub _handle_status_and_resolution
|
|||
sub _handle_special_first_chars {
|
||||
my $self = shift;
|
||||
my ($qsword, $negate) = @_;
|
||||
return 0 if !defined $qsword || length($qsword) <= 1;
|
||||
|
||||
my $firstChar = substr($qsword, 0, 1);
|
||||
my $baseWord = substr($qsword, 1);
|
||||
|
@ -426,9 +451,37 @@ sub _handle_field_names {
|
|||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
# Do not look inside quoted strings.
|
||||
return 0 if ($or_operand =~ /^(["']).*\1$/);
|
||||
|
||||
# Flag and requestee shortcut.
|
||||
if ($or_operand =~ /^([^\?]+\?)([^\?]*)$/) {
|
||||
_handle_flags($1, $2, $negate);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub _handle_flags {
|
||||
my ($flag, $requestee, $negate) = @_;
|
||||
|
||||
addChart('flagtypes.name', 'substring', $flag, $negate);
|
||||
if ($requestee) {
|
||||
# FIXME - Every time a requestee is involved and you use OR somewhere
|
||||
# in your quick search, the logic will be wrong because boolean charts
|
||||
# are unable to run queries of the form (a AND b) OR c. In our case:
|
||||
# (flag name is foo AND requestee is bar) OR (any other criteria).
|
||||
# But this has never been possible, so this is not a regression. If one
|
||||
# needs to run such queries, he must use the Custom Search section of
|
||||
# the Advanced Search page.
|
||||
$chart++;
|
||||
$and = $or = 0;
|
||||
addChart('requestees.login_name', 'substring', $requestee, $negate);
|
||||
}
|
||||
}
|
||||
|
||||
sub _translate_field_name {
|
||||
my $field = shift;
|
||||
$field = lc($field);
|
||||
|
|
|
@ -99,12 +99,15 @@ sub get_format {
|
|||
my $self = shift;
|
||||
my ($template, $format, $ctype) = @_;
|
||||
|
||||
$ctype ||= 'html';
|
||||
$format ||= '';
|
||||
$ctype //= 'html';
|
||||
$format //= '';
|
||||
|
||||
# Security - allow letters and a hyphen only
|
||||
$ctype =~ s/[^a-zA-Z\-]//g;
|
||||
$format =~ s/[^a-zA-Z\-]//g;
|
||||
# ctype and format can have letters and a hyphen only.
|
||||
if ($ctype =~ /[^a-zA-Z\-]/ || $format =~ /[^a-zA-Z\-]/) {
|
||||
ThrowUserError('format_not_found', {'format' => $format,
|
||||
'ctype' => $ctype,
|
||||
'invalid' => 1});
|
||||
}
|
||||
trick_taint($ctype);
|
||||
trick_taint($format);
|
||||
|
||||
|
@ -130,6 +133,7 @@ sub get_format {
|
|||
return
|
||||
{
|
||||
'template' => $template,
|
||||
'format' => $format,
|
||||
'extension' => $ctype,
|
||||
'ctype' => Bugzilla::Constants::contenttypes->{$ctype}
|
||||
};
|
||||
|
@ -223,6 +227,10 @@ sub quoteUrls {
|
|||
my $chr1 = chr(1);
|
||||
$text =~ s/\0/$chr1\0/g;
|
||||
|
||||
# If the comment is already wrapped, we should ignore newlines when
|
||||
# looking for matching regexps. Else we should take them into account.
|
||||
my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/;
|
||||
|
||||
# However, note that adding the title (for buglinks) can affect things
|
||||
# In particular, attachment matches go before bug titles, so that titles
|
||||
# with 'attachment 1' don't double match.
|
||||
|
@ -334,7 +342,7 @@ sub quoteUrls {
|
|||
~<a href=\"mailto:$2\">$1$2</a>~igx;
|
||||
|
||||
# attachment links
|
||||
$text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[details\])?)
|
||||
$text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[details\])?)
|
||||
~($things[$count++] = get_attachment_link($2, $1, $user)) &&
|
||||
("\0\0" . ($count-1) . "\0\0")
|
||||
~egsxi;
|
||||
|
@ -348,43 +356,43 @@ sub quoteUrls {
|
|||
# Also, we can't use $bug_re?$comment_re? because that will match the
|
||||
# empty string
|
||||
my $bug_word = template_var('terms')->{bug};
|
||||
my $bug_re = qr/\Q$bug_word\E\s*\#?\s*(\d+)/i;
|
||||
my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
|
||||
my $comment_word = template_var('terms')->{comment};
|
||||
my $comment_re = qr/(?:\Q$comment_word\E|comment)\s*\#?\s*(\d+)/i;
|
||||
$text =~ s~\b($bug_re(?:\s*,?\s*$comment_re)?|$comment_re)
|
||||
my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*(\d+)/i;
|
||||
$text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re)
|
||||
~ # We have several choices. $1 here is the link, and $2-4 are set
|
||||
# depending on which part matched
|
||||
(defined($2) ? get_bug_link($2, $1, { comment_num => $3, user => $user }) :
|
||||
"<a href=\"$current_bugurl#c$4\">$1</a>")
|
||||
~egsox;
|
||||
~egx;
|
||||
|
||||
# Handle a list of bug ids: bugs 1, #2, 3, 4
|
||||
# Currently, the only delimiter supported is comma.
|
||||
# Concluding "and" and "or" are not supported.
|
||||
my $bugs_word = template_var('terms')->{bugs};
|
||||
|
||||
my $bugs_re = qr/\Q$bugs_word\E\s*\#?\s*
|
||||
\d+(?:\s*,\s*\#?\s*\d+)+/ix;
|
||||
while ($text =~ m/($bugs_re)/go) {
|
||||
my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*
|
||||
\d+(?:$s*,$s*\#?$s*\d+)+/ix;
|
||||
while ($text =~ m/($bugs_re)/g) {
|
||||
my $offset = $-[0];
|
||||
my $length = $+[0] - $-[0];
|
||||
my $match = $1;
|
||||
|
||||
$match =~ s/((?:#\s*)?(\d+))/get_bug_link($2, $1);/eg;
|
||||
$match =~ s/((?:#$s*)?(\d+))/get_bug_link($2, $1);/eg;
|
||||
# Replace the old string with the linkified one.
|
||||
substr($text, $offset, $length) = $match;
|
||||
}
|
||||
|
||||
my $comments_word = template_var('terms')->{comments};
|
||||
|
||||
my $comments_re = qr/(?:comments|\Q$comments_word\E)\s*\#?\s*
|
||||
\d+(?:\s*,\s*\#?\s*\d+)+/ix;
|
||||
while ($text =~ m/($comments_re)/go) {
|
||||
my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s*
|
||||
\d+(?:$s*,$s*\#?$s*\d+)+/ix;
|
||||
while ($text =~ m/($comments_re)/g) {
|
||||
my $offset = $-[0];
|
||||
my $length = $+[0] - $-[0];
|
||||
my $match = $1;
|
||||
|
||||
$match =~ s|((?:#\s*)?(\d+))|<a href="$current_bugurl#c$2">$1</a>|g;
|
||||
$match =~ s|((?:#$s*)?(\d+))|<a href="$current_bugurl#c$2">$1</a>|g;
|
||||
substr($text, $offset, $length) = $match;
|
||||
}
|
||||
|
||||
|
@ -544,13 +552,10 @@ sub mtime_filter {
|
|||
#
|
||||
# 1. YUI CSS
|
||||
# 2. Standard Bugzilla stylesheet set (persistent)
|
||||
# 3. Standard Bugzilla stylesheet set (selectable)
|
||||
# 4. All third-party "skin" stylesheet sets (selectable)
|
||||
# 5. Page-specific styles
|
||||
# 6. Custom Bugzilla stylesheet set (persistent)
|
||||
#
|
||||
# "Selectable" skin file sets may be either preferred or alternate.
|
||||
# Exactly one is preferred, determined by the "skin" user preference.
|
||||
# 3. Third-party "skin" stylesheet set, per user prefs (persistent)
|
||||
# 4. Page-specific styles
|
||||
# 5. Custom Bugzilla stylesheet set (persistent)
|
||||
|
||||
sub css_files {
|
||||
my ($style_urls, $yui, $yui_css) = @_;
|
||||
|
||||
|
@ -567,18 +572,10 @@ sub css_files {
|
|||
|
||||
my @css_sets = map { _css_link_set($_) } @requested_css;
|
||||
|
||||
my %by_type = (standard => [], alternate => {}, skin => [], custom => []);
|
||||
my %by_type = (standard => [], skin => [], custom => []);
|
||||
foreach my $set (@css_sets) {
|
||||
foreach my $key (keys %$set) {
|
||||
if ($key eq 'alternate') {
|
||||
foreach my $alternate_skin (keys %{ $set->{alternate} }) {
|
||||
my $files = $by_type{alternate}->{$alternate_skin} ||= [];
|
||||
push(@$files, $set->{alternate}->{$alternate_skin});
|
||||
}
|
||||
}
|
||||
else {
|
||||
push(@{ $by_type{$key} }, $set->{$key});
|
||||
}
|
||||
push(@{ $by_type{$key} }, $set->{$key});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -595,27 +592,15 @@ sub _css_link_set {
|
|||
if ($file_name !~ m{(^|/)skins/standard/}) {
|
||||
return \%set;
|
||||
}
|
||||
|
||||
my $skin_user_prefs = Bugzilla->user->settings->{skin};
|
||||
|
||||
my $skin = Bugzilla->user->settings->{skin}->{value};
|
||||
my $cgi_path = bz_locations()->{'cgi_path'};
|
||||
# If the DB is not accessible, user settings are not available.
|
||||
my $all_skins = $skin_user_prefs ? $skin_user_prefs->legal_values : [];
|
||||
my %skin_urls;
|
||||
foreach my $option (@$all_skins) {
|
||||
next if $option eq 'standard';
|
||||
my $skin_file_name = $file_name;
|
||||
$skin_file_name =~ s{(^|/)skins/standard/}{skins/contrib/$option/};
|
||||
if (my $mtime = _mtime("$cgi_path/$skin_file_name")) {
|
||||
$skin_urls{$option} = mtime_filter($skin_file_name, $mtime);
|
||||
}
|
||||
my $skin_file_name = $file_name;
|
||||
$skin_file_name =~ s{(^|/)skins/standard/}{skins/contrib/$skin/};
|
||||
if (my $mtime = _mtime("$cgi_path/$skin_file_name")) {
|
||||
$set{skin} = mtime_filter($skin_file_name, $mtime);
|
||||
}
|
||||
$set{alternate} = \%skin_urls;
|
||||
|
||||
my $skin = $skin_user_prefs->{'value'};
|
||||
if ($skin ne 'standard' and defined $set{alternate}->{$skin}) {
|
||||
$set{skin} = delete $set{alternate}->{$skin};
|
||||
}
|
||||
|
||||
|
||||
my $custom_file_name = $file_name;
|
||||
$custom_file_name =~ s{(^|/)skins/standard/}{skins/custom/};
|
||||
if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) {
|
||||
|
@ -1044,9 +1029,7 @@ sub create {
|
|||
# (Wrapping the message in the WebService is unnecessary
|
||||
# and causes awkward things like \n's appearing in error
|
||||
# messages in JSON-RPC.)
|
||||
unless (Bugzilla->usage_mode == USAGE_MODE_JSON
|
||||
or Bugzilla->usage_mode == USAGE_MODE_XMLRPC)
|
||||
{
|
||||
unless (i_am_webservice()) {
|
||||
$var = wrap_comment($var, 72);
|
||||
}
|
||||
$var =~ s/\ / /g;
|
||||
|
@ -1168,12 +1151,7 @@ sub create {
|
|||
'user' => sub { return Bugzilla->user; },
|
||||
|
||||
# Currenly active language
|
||||
# XXX Eventually this should probably be replaced with something
|
||||
# like Bugzilla->language.
|
||||
'current_language' => sub {
|
||||
my ($language) = include_languages();
|
||||
return $language;
|
||||
},
|
||||
'current_language' => sub { return Bugzilla->current_language; },
|
||||
|
||||
# If an sudo session is in progress, this is the user who
|
||||
# started the session.
|
||||
|
@ -1183,8 +1161,8 @@ sub create {
|
|||
'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); },
|
||||
|
||||
# Allow templates to access docs url with users' preferred language
|
||||
'docs_urlbase' => sub {
|
||||
my ($language) = include_languages();
|
||||
'docs_urlbase' => sub {
|
||||
my $language = Bugzilla->current_language;
|
||||
my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
|
||||
$docs_urlbase =~ s/\%lang\%/$language/;
|
||||
return $docs_urlbase;
|
||||
|
@ -1276,7 +1254,7 @@ sub create {
|
|||
}
|
||||
return \@optional;
|
||||
},
|
||||
'default_authorizer' => new Bugzilla::Auth(),
|
||||
'default_authorizer' => sub { return Bugzilla::Auth->new() },
|
||||
},
|
||||
};
|
||||
# Use a per-process provider to cache compiled templates in memory across
|
||||
|
|
|
@ -122,13 +122,15 @@ sub IssuePasswordToken {
|
|||
|
||||
ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon;
|
||||
|
||||
my ($token, $token_ts) = _create_token($user->id, 'password', remote_ip());
|
||||
my $ip_addr = remote_ip();
|
||||
my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr);
|
||||
|
||||
# Mail the user the token along with instructions for using it.
|
||||
my $template = Bugzilla->template_inner($user->setting('lang'));
|
||||
my $vars = {};
|
||||
|
||||
$vars->{'token'} = $token;
|
||||
$vars->{'ip_addr'} = $ip_addr;
|
||||
$vars->{'emailaddress'} = $user->email;
|
||||
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
|
||||
# The user is not logged in (else he wouldn't request a new password).
|
||||
|
|
|
@ -440,6 +440,31 @@ sub tags {
|
|||
return $self->{tags};
|
||||
}
|
||||
|
||||
sub bugs_ignored {
|
||||
my ($self) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
if (!defined $self->{'bugs_ignored'}) {
|
||||
$self->{'bugs_ignored'} = $dbh->selectall_arrayref(
|
||||
'SELECT bugs.bug_id AS id,
|
||||
bugs.bug_status AS status,
|
||||
bugs.short_desc AS summary
|
||||
FROM bugs
|
||||
INNER JOIN email_bug_ignore
|
||||
ON bugs.bug_id = email_bug_ignore.bug_id
|
||||
WHERE user_id = ?',
|
||||
{ Slice => {} }, $self->id);
|
||||
# Go ahead and load these into the visible bugs cache
|
||||
# to speed up can_see_bug checks later
|
||||
$self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]);
|
||||
}
|
||||
return $self->{'bugs_ignored'};
|
||||
}
|
||||
|
||||
sub is_bug_ignored {
|
||||
my ($self, $bug_id) = @_;
|
||||
return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0;
|
||||
}
|
||||
|
||||
##########################
|
||||
# Saved Recent Bug Lists #
|
||||
##########################
|
||||
|
@ -811,8 +836,7 @@ sub in_group_id {
|
|||
sub groups_with_icon {
|
||||
my $self = shift;
|
||||
|
||||
my @groups = grep { $_->icon_url } @{ $self->groups };
|
||||
return \@groups;
|
||||
return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }];
|
||||
}
|
||||
|
||||
sub get_products_by_permission {
|
||||
|
@ -2255,7 +2279,7 @@ sub validate_password {
|
|||
my $complexity_level = Bugzilla->params->{password_complexity};
|
||||
if ($complexity_level eq 'letters_numbers_specialchars') {
|
||||
ThrowUserError('password_not_complex')
|
||||
if ($password !~ /\w/ || $password !~ /\d/ || $password !~ /[[:punct:]]/);
|
||||
if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/);
|
||||
} elsif ($complexity_level eq 'letters_numbers') {
|
||||
ThrowUserError('password_not_complex')
|
||||
if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/);
|
||||
|
@ -2384,6 +2408,34 @@ groups.
|
|||
Returns a hashref with tag IDs as key, and a hashref with tag 'id',
|
||||
'name' and 'bug_count' as value.
|
||||
|
||||
=item C<bugs_ignored>
|
||||
|
||||
Returns an array of hashrefs containing information about bugs currently
|
||||
being ignored by the user.
|
||||
|
||||
Each hashref contains the following information:
|
||||
|
||||
=over
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> The id of the bug.
|
||||
|
||||
=item C<status>
|
||||
|
||||
C<string> The current status of the bug.
|
||||
|
||||
=item C<summary>
|
||||
|
||||
C<string> The current summary of the bug.
|
||||
|
||||
=back
|
||||
|
||||
=item C<is_bug_ignored>
|
||||
|
||||
Returns true if the user does not want email notifications for the
|
||||
specified bug ID, else returns false.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Saved Recent Bug Lists
|
||||
|
@ -2624,7 +2676,8 @@ the database again. Used mostly by L<Bugzilla::Product>.
|
|||
|
||||
=item C<can_enter_product($product_name, $warn)>
|
||||
|
||||
Description: Returns 1 if the user can enter bugs into the specified product.
|
||||
Description: Returns a product object if the user can enter bugs into the
|
||||
specified product.
|
||||
If the user cannot enter bugs into the product, the behavior of
|
||||
this method depends on the value of $warn:
|
||||
- if $warn is false (or not given), a 'false' value is returned;
|
||||
|
@ -2635,7 +2688,7 @@ the database again. Used mostly by L<Bugzilla::Product>.
|
|||
must be thrown if the user cannot enter bugs
|
||||
into the specified product.
|
||||
|
||||
Returns: 1 if the user can enter bugs into the product,
|
||||
Returns: A product object if the user can enter bugs into the product,
|
||||
0 if the user cannot enter bugs into the product and if $warn
|
||||
is false (an error is thrown if $warn is true).
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ use constant PLATFORMS_MAP => (
|
|||
# HP
|
||||
qr/\(.*9000.*\)/ => ["PA-RISC", "HP"],
|
||||
# ARM
|
||||
qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["ARM"],
|
||||
qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"],
|
||||
# PocketPC intentionally before PowerPC
|
||||
qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"],
|
||||
|
@ -119,6 +120,12 @@ use constant OS_MAP => (
|
|||
qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"],
|
||||
qr/\(.*Windows.*NT.*\)/ => ["Windows NT"],
|
||||
# OS X
|
||||
qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"],
|
||||
qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"],
|
||||
qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"],
|
||||
qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"],
|
||||
qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"],
|
||||
qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["iOS"],
|
||||
qr/\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ => ["Mac OS X 10.8"],
|
||||
qr/\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ => ["Mac OS X 10.7"],
|
||||
qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"],
|
||||
|
|
|
@ -15,7 +15,7 @@ use parent qw(Exporter);
|
|||
detaint_signed
|
||||
html_quote url_quote url_quote_noslash xml_quote
|
||||
css_class_quote html_light_quote url_decode
|
||||
i_am_cgi correct_urlbase remote_ip validate_ip
|
||||
i_am_cgi i_am_webservice correct_urlbase remote_ip validate_ip
|
||||
lsearch do_ssl_redirect_if_required use_attachbase
|
||||
diff_arrays list on_main_db say
|
||||
trim wrap_hard wrap_comment find_wrap_point makeCitations
|
||||
|
@ -25,8 +25,8 @@ use parent qw(Exporter);
|
|||
bz_crypt generate_random_password check_email_syntax
|
||||
validate_email_syntax clean_text stem_text bz_encode_json
|
||||
xml_element xml_element_quote xml_dump_simple xml_simple
|
||||
get_text template_var disable_utf8
|
||||
detect_encoding email_filter union);
|
||||
get_text template_var disable_utf8 display_value
|
||||
detect_encoding email_filter union join_activity_entries);
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::RNG qw(irand);
|
||||
|
@ -84,7 +84,10 @@ sub html_quote {
|
|||
$var =~ s/"/"/g;
|
||||
# Obscure '@'.
|
||||
$var =~ s/\@/\@/g;
|
||||
if (Bugzilla->params->{'utf8'}) {
|
||||
|
||||
state $use_utf8 = Bugzilla->params->{'utf8'};
|
||||
|
||||
if ($use_utf8) {
|
||||
# Remove the following characters because they're
|
||||
# influencing BiDi:
|
||||
# --------------------------------------------------------
|
||||
|
@ -106,7 +109,7 @@ sub html_quote {
|
|||
# |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e |
|
||||
# |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f |
|
||||
# --------------------------------------------------------
|
||||
$var =~ s/[\x{202a}-\x{202e}]//g;
|
||||
$var =~ tr/\x{202a}-\x{202e}//d;
|
||||
}
|
||||
return $var;
|
||||
}
|
||||
|
@ -257,6 +260,13 @@ sub i_am_cgi {
|
|||
return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
|
||||
}
|
||||
|
||||
sub i_am_webservice {
|
||||
my $usage_mode = Bugzilla->usage_mode;
|
||||
return $usage_mode == USAGE_MODE_XMLRPC
|
||||
|| $usage_mode == USAGE_MODE_JSON
|
||||
|| $usage_mode == USAGE_MODE_REST;
|
||||
}
|
||||
|
||||
# This exists as a separate function from Bugzilla::CGI::redirect_to_https
|
||||
# because we don't want to create a CGI object during XML-RPC calls
|
||||
# (doing so can mess up XML-RPC).
|
||||
|
@ -535,6 +545,35 @@ sub find_wrap_point {
|
|||
return $wrappoint;
|
||||
}
|
||||
|
||||
sub join_activity_entries {
|
||||
my ($field, $current_change, $new_change) = @_;
|
||||
# We need to insert characters as these were removed by old
|
||||
# LogActivityEntry code.
|
||||
|
||||
return $new_change if $current_change eq '';
|
||||
|
||||
# Buglists and see_also need the comma restored
|
||||
if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
|
||||
if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
|
||||
return $current_change . $new_change;
|
||||
} else {
|
||||
return $current_change . ', ' . $new_change;
|
||||
}
|
||||
}
|
||||
|
||||
# Assume bug_file_loc contain a single url, don't insert a delimiter
|
||||
if ($field eq 'bug_file_loc') {
|
||||
return $current_change . $new_change;
|
||||
}
|
||||
|
||||
# All other fields get a space
|
||||
if (substr($new_change, 0, 1) eq ' ') {
|
||||
return $current_change . $new_change;
|
||||
} else {
|
||||
return $current_change . ' ' . $new_change;
|
||||
}
|
||||
}
|
||||
|
||||
sub wrap_hard {
|
||||
my ($string, $columns) = @_;
|
||||
local $Text::Wrap::columns = $columns;
|
||||
|
@ -1103,6 +1142,7 @@ Bugzilla::Util - Generic utility functions for bugzilla
|
|||
|
||||
# Functions that tell you about your environment
|
||||
my $is_cgi = i_am_cgi();
|
||||
my $is_webservice = i_am_webservice();
|
||||
my $urlbase = correct_urlbase();
|
||||
|
||||
# Data manipulation
|
||||
|
@ -1230,6 +1270,11 @@ Tells you whether or not you are being run as a CGI script in a web
|
|||
server. For example, it would return false if the caller is running
|
||||
in a command-line script.
|
||||
|
||||
=item C<i_am_webservice()>
|
||||
|
||||
Tells you whether or not the current usage mode is WebServices related
|
||||
such as JSONRPC, XMLRPC, or REST.
|
||||
|
||||
=item C<correct_urlbase()>
|
||||
|
||||
Returns either the C<sslbase> or C<urlbase> parameter, depending on the
|
||||
|
@ -1301,6 +1346,12 @@ Search for a comma, a whitespace or a hyphen to split $string, within the first
|
|||
$maxpos characters. If none of them is found, just split $string at $maxpos.
|
||||
The search starts at $maxpos and goes back to the beginning of the string.
|
||||
|
||||
=item C<join_activity_entries($field, $current_change, $new_change)>
|
||||
|
||||
Joins two strings together so they appear as one. The field name is specified
|
||||
as the method of joining the two strings depends on this. Returns the
|
||||
combined string.
|
||||
|
||||
=item C<is_7bit_clean($str)>
|
||||
|
||||
Returns true is the string contains only 7-bit characters (ASCII 32 through 126,
|
||||
|
|
|
@ -211,8 +211,8 @@ sub product {
|
|||
# Validators
|
||||
################################
|
||||
|
||||
sub set_name { $_[0]->set('value', $_[1]); }
|
||||
sub set_is_active { $_[0]->set('isactive', $_[1]); }
|
||||
sub set_value { $_[0]->set('value', $_[1]); }
|
||||
sub set_isactive { $_[0]->set('isactive', $_[1]); }
|
||||
|
||||
sub _check_value {
|
||||
my ($invocant, $name, undef, $params) = @_;
|
||||
|
@ -261,7 +261,7 @@ Bugzilla::Version - Bugzilla product version class.
|
|||
my $version = Bugzilla::Version->create(
|
||||
{ value => $name, product => $product_obj });
|
||||
|
||||
$version->set_name($new_name);
|
||||
$version->set_value($new_name);
|
||||
$version->update();
|
||||
|
||||
$version->remove_from_db;
|
||||
|
@ -297,9 +297,9 @@ below.
|
|||
|
||||
=item DEFAULT_VERSION
|
||||
|
||||
=item set_is_active
|
||||
=item set_isactive
|
||||
|
||||
=item set_name
|
||||
=item set_value
|
||||
|
||||
=item product_id
|
||||
|
||||
|
|
|
@ -45,15 +45,20 @@ This is the standard API for external programs that want to interact
|
|||
with Bugzilla. It provides various methods in various modules.
|
||||
|
||||
You can interact with this API via
|
||||
L<XML-RPC|Bugzilla::WebService::Server::XMLRPC> or
|
||||
L<JSON-RPC|Bugzilla::WebService::Server::JSONRPC>.
|
||||
L<XML-RPC|Bugzilla::WebService::Server::XMLRPC>,
|
||||
L<JSON-RPC|Bugzilla::WebService::Server::JSONRPC> or
|
||||
L<REST|Bugzilla::WebService::Server::REST>.
|
||||
|
||||
=head1 CALLING METHODS
|
||||
|
||||
Methods are grouped into "packages", like C<Bug> for
|
||||
Methods are grouped into "packages", like C<Bug> for
|
||||
L<Bugzilla::WebService::Bug>. So, for example,
|
||||
L<Bugzilla::WebService::Bug/get>, is called as C<Bug.get>.
|
||||
|
||||
For REST, the "package" is more determined by the path
|
||||
used to access the resource. See each relevant method
|
||||
for specific details on how to access via REST.
|
||||
|
||||
=head1 PARAMETERS
|
||||
|
||||
The Bugzilla API takes the following various types of parameters:
|
||||
|
@ -135,7 +140,7 @@ There are various ways to log in:
|
|||
|
||||
=item C<User.login>
|
||||
|
||||
You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla
|
||||
You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla
|
||||
user. This issues standard HTTP cookies that you must then use in future
|
||||
calls, so your client must be capable of receiving and transmitting
|
||||
cookies.
|
||||
|
@ -165,13 +170,24 @@ not expire.
|
|||
=back
|
||||
|
||||
The C<Bugzilla_restrictlogin> and C<Bugzilla_rememberlogin> options
|
||||
are only used when you have also specified C<Bugzilla_login> and
|
||||
are only used when you have also specified C<Bugzilla_login> and
|
||||
C<Bugzilla_password>.
|
||||
|
||||
Note that Bugzilla will return HTTP cookies along with the method
|
||||
response when you use these arguments (just like the C<User.login> method
|
||||
above).
|
||||
|
||||
For REST, you may also use the C<username> and C<password> variable
|
||||
names instead of C<Bugzilla_login> and C<Bugzilla_password> as a
|
||||
convenience.
|
||||
|
||||
=item B<Added in Bugzilla 5.0>
|
||||
|
||||
An error is now thrown if you pass invalid cookies or an invalid token.
|
||||
You will need to log in again to get new cookies or a new token. Previous
|
||||
releases simply ignored invalid cookies and token support was added in
|
||||
Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=head1 STABLE, EXPERIMENTAL, and UNSTABLE
|
||||
|
@ -266,6 +282,9 @@ would return something like:
|
|||
|
||||
{ users => [{ id => 1, name => 'user@domain.com' }] }
|
||||
|
||||
Note for REST, C<include_fields> may instead be a comma delimited string
|
||||
for GET type requests.
|
||||
|
||||
=item C<exclude_fields>
|
||||
|
||||
C<array> An array of strings, representing the (case-sensitive) names of
|
||||
|
@ -275,6 +294,13 @@ the returned hashes.
|
|||
If you specify all the fields, then this function will return empty
|
||||
hashes.
|
||||
|
||||
Some RPC calls support specifying sub fields. If an RPC call states that
|
||||
it support sub field restrictions, you can restrict what information is
|
||||
returned within the first field. For example, if you call Products.get
|
||||
with an include_fields of components.name, then only the component name
|
||||
would be returned (and nothing else). You can include the main field,
|
||||
and exclude a sub field.
|
||||
|
||||
Invalid field names are ignored.
|
||||
|
||||
Specifying fields here overrides C<include_fields>, so if you specify a
|
||||
|
@ -288,6 +314,9 @@ would return something like:
|
|||
|
||||
{ users => [{ id => 1, real_name => 'John Smith' }] }
|
||||
|
||||
Note for REST, C<exclude_fields> may instead be a comma delimited string
|
||||
for GET type requests.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
|
|
@ -17,7 +17,7 @@ use Bugzilla::Constants;
|
|||
use Bugzilla::Error;
|
||||
use Bugzilla::Field;
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Util qw(filter filter_wants validate);
|
||||
use Bugzilla::WebService::Util qw(filter filter_wants validate translate);
|
||||
use Bugzilla::Bug;
|
||||
use Bugzilla::BugMail;
|
||||
use Bugzilla::Util qw(trick_taint trim diff_arrays);
|
||||
|
@ -25,6 +25,12 @@ use Bugzilla::Version;
|
|||
use Bugzilla::Milestone;
|
||||
use Bugzilla::Status;
|
||||
use Bugzilla::Token qw(issue_hash_token);
|
||||
use Bugzilla::Search;
|
||||
use Bugzilla::Search::Quicksearch;
|
||||
|
||||
use List::Util qw(max);
|
||||
use List::MoreUtils qw(uniq);
|
||||
use Storable qw(dclone);
|
||||
|
||||
#############
|
||||
# Constants #
|
||||
|
@ -51,6 +57,20 @@ use constant READ_ONLY => qw(
|
|||
search
|
||||
);
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
######################################################
|
||||
# Add aliases here for old method name compatibility #
|
||||
######################################################
|
||||
|
@ -324,6 +344,12 @@ sub get {
|
|||
|
||||
my @bugs;
|
||||
my @faults;
|
||||
|
||||
# Cache permissions for bugs. This highly reduces the number of calls to the DB.
|
||||
# visible_bugs() is only able to handle bug IDs, so we have to skip aliases.
|
||||
my @int = grep { $_ =~ /^\d+$/ } @$ids;
|
||||
Bugzilla->user->visible_bugs(\@int);
|
||||
|
||||
foreach my $bug_id (@$ids) {
|
||||
my $bug;
|
||||
if ($params->{permissive}) {
|
||||
|
@ -344,6 +370,18 @@ sub get {
|
|||
push(@bugs, $self->_bug_to_hash($bug, $params));
|
||||
}
|
||||
|
||||
# Set the ETag before inserting the update tokens
|
||||
# since the tokens will always be unique even if
|
||||
# the data has not changed.
|
||||
$self->bz_etag(\@bugs);
|
||||
|
||||
if (Bugzilla->user->id) {
|
||||
foreach my $bug (@bugs) {
|
||||
my $token = issue_hash_token([$bug->{'id'}, $bug->{'last_change_time'}]);
|
||||
$bug->{'update_token'} = $self->type('string', $token);
|
||||
}
|
||||
}
|
||||
|
||||
return { bugs => \@bugs, faults => \@faults };
|
||||
}
|
||||
|
||||
|
@ -407,52 +445,106 @@ sub history {
|
|||
|
||||
sub search {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
my $user = Bugzilla->user;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
|
||||
if ( defined($params->{offset}) and !defined($params->{limit}) ) {
|
||||
ThrowCodeError('param_required',
|
||||
my $match_params = dclone($params);
|
||||
delete $match_params->{include_fields};
|
||||
delete $match_params->{exclude_fields};
|
||||
|
||||
# Determine whether this is a quicksearch query
|
||||
if (exists $match_params->{quicksearch}) {
|
||||
my $quicksearch = quicksearch($match_params->{'quicksearch'});
|
||||
my $cgi = Bugzilla::CGI->new($quicksearch);
|
||||
$match_params = $cgi->Vars;
|
||||
}
|
||||
|
||||
if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) {
|
||||
ThrowCodeError('param_required',
|
||||
{ param => 'limit', function => 'Bug.search()' });
|
||||
}
|
||||
|
||||
$params = Bugzilla::Bug::map_fields($params);
|
||||
delete $params->{WHERE};
|
||||
|
||||
unless (Bugzilla->user->is_timetracker) {
|
||||
delete $params->{$_} foreach TIMETRACKING_FIELDS;
|
||||
my $max_results = Bugzilla->params->{max_search_results};
|
||||
unless (defined $match_params->{limit} && $match_params->{limit} == 0) {
|
||||
if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) {
|
||||
$match_params->{limit} = $max_results;
|
||||
}
|
||||
}
|
||||
else {
|
||||
delete $match_params->{limit};
|
||||
delete $match_params->{offset};
|
||||
}
|
||||
|
||||
$match_params = Bugzilla::Bug::map_fields($match_params);
|
||||
|
||||
my %options = ( fields => ['bug_id'] );
|
||||
|
||||
# Find the highest custom field id
|
||||
my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
|
||||
my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
|
||||
|
||||
# Do special search types for certain fields.
|
||||
if ( my $bug_when = delete $params->{delta_ts} ) {
|
||||
$params->{WHERE}->{'delta_ts >= ?'} = $bug_when;
|
||||
if (my $change_when = delete $match_params->{'delta_ts'}) {
|
||||
$match_params->{"f${last_field_id}"} = 'delta_ts';
|
||||
$match_params->{"o${last_field_id}"} = 'greaterthaneq';
|
||||
$match_params->{"v${last_field_id}"} = $change_when;
|
||||
$last_field_id++;
|
||||
}
|
||||
if (my $when = delete $params->{creation_ts}) {
|
||||
$params->{WHERE}->{'creation_ts >= ?'} = $when;
|
||||
if (my $creation_when = delete $match_params->{'creation_ts'}) {
|
||||
$match_params->{"f${last_field_id}"} = 'creation_ts';
|
||||
$match_params->{"o${last_field_id}"} = 'greaterthaneq';
|
||||
$match_params->{"v${last_field_id}"} = $creation_when;
|
||||
$last_field_id++;
|
||||
}
|
||||
if (my $summary = delete $params->{short_desc}) {
|
||||
my @strings = ref $summary ? @$summary : ($summary);
|
||||
my @likes = ("short_desc LIKE ?") x @strings;
|
||||
my $clause = join(' OR ', @likes);
|
||||
$params->{WHERE}->{"($clause)"} = [map { "\%$_\%" } @strings];
|
||||
}
|
||||
if (my $whiteboard = delete $params->{status_whiteboard}) {
|
||||
my @strings = ref $whiteboard ? @$whiteboard : ($whiteboard);
|
||||
my @likes = ("status_whiteboard LIKE ?") x @strings;
|
||||
my $clause = join(' OR ', @likes);
|
||||
$params->{WHERE}->{"($clause)"} = [map { "\%$_\%" } @strings];
|
||||
}
|
||||
|
||||
# We want include_fields and exclude_fields to be passed to
|
||||
# _bug_to_hash but not to Bugzilla::Bug->match so we copy the
|
||||
# params and delete those before passing to Bugzilla::Bug->match.
|
||||
my %match_params = %{ $params };
|
||||
delete $match_params{'include_fields'};
|
||||
delete $match_params{'exclude_fields'};
|
||||
|
||||
my $bugs = Bugzilla::Bug->match(\%match_params);
|
||||
my $visible = Bugzilla->user->visible_bugs($bugs);
|
||||
my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible;
|
||||
return { bugs => \@hashes };
|
||||
# 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 {
|
||||
|
@ -667,6 +759,86 @@ sub add_attachment {
|
|||
return { ids => \@created_ids };
|
||||
}
|
||||
|
||||
sub update_attachment {
|
||||
my ($self, $params) = validate(@_, 'ids');
|
||||
|
||||
my $user = Bugzilla->login(LOGIN_REQUIRED);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $ids = delete $params->{ids};
|
||||
defined $ids || ThrowCodeError('param_required', { param => 'ids' });
|
||||
|
||||
# Some fields cannot be sent to set_all
|
||||
foreach my $key (qw(login password token)) {
|
||||
delete $params->{$key};
|
||||
}
|
||||
|
||||
# We can't update flags, and summary is really description
|
||||
delete $params->{flags};
|
||||
|
||||
$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($bug->product_id)
|
||||
|| ThrowUserError("illegal_attachment_edit", { attach_id => $id });
|
||||
|
||||
push @attachments, $attachment;
|
||||
$bugs{$bug->id} = $bug;
|
||||
}
|
||||
|
||||
# Update the values
|
||||
foreach my $attachment (@attachments) {
|
||||
$attachment->set_all($params);
|
||||
}
|
||||
|
||||
$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();
|
||||
|
||||
$changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
|
||||
|
||||
my %hash = (
|
||||
id => $self->type('int', $attachment->id),
|
||||
last_change_time => $self->type('dateTime', $attachment->modification_time),
|
||||
changes => {},
|
||||
);
|
||||
|
||||
foreach my $field (keys %$changes) {
|
||||
my $change = $changes->{$field};
|
||||
|
||||
# We normalize undef to an empty string, so that the API
|
||||
# stays consistent for things like Deadline that can become
|
||||
# empty.
|
||||
$hash{changes}->{$field} = {
|
||||
removed => $self->type('string', $change->[0] // ''),
|
||||
added => $self->type('string', $change->[1] // '')
|
||||
};
|
||||
}
|
||||
|
||||
push(@result, \%hash);
|
||||
}
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
# Email users about the change
|
||||
foreach my $bug (values %bugs) {
|
||||
Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
|
||||
}
|
||||
|
||||
# Return the information to the user
|
||||
return { attachments => \@result };
|
||||
}
|
||||
|
||||
sub add_comment {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
|
@ -848,8 +1020,6 @@ sub _bug_to_hash {
|
|||
# database call to get the info.
|
||||
my %item = (
|
||||
alias => $self->type('string', $bug->alias),
|
||||
classification => $self->type('string', $bug->classification),
|
||||
component => $self->type('string', $bug->component),
|
||||
creation_time => $self->type('dateTime', $bug->creation_ts),
|
||||
# No need to format $bug->deadline specially, because Bugzilla::Bug
|
||||
# already does it for us.
|
||||
|
@ -860,7 +1030,6 @@ sub _bug_to_hash {
|
|||
op_sys => $self->type('string', $bug->op_sys),
|
||||
platform => $self->type('string', $bug->rep_platform),
|
||||
priority => $self->type('string', $bug->priority),
|
||||
product => $self->type('string', $bug->product),
|
||||
resolution => $self->type('string', $bug->resolution),
|
||||
severity => $self->type('string', $bug->bug_severity),
|
||||
status => $self->type('string', $bug->bug_status),
|
||||
|
@ -882,6 +1051,12 @@ sub _bug_to_hash {
|
|||
my @blocks = map { $self->type('int', $_) } @{ $bug->blocked };
|
||||
$item{'blocks'} = \@blocks;
|
||||
}
|
||||
if (filter_wants $params, 'classification') {
|
||||
$item{classification} = $self->type('string', $bug->classification);
|
||||
}
|
||||
if (filter_wants $params, 'component') {
|
||||
$item{component} = $self->type('string', $bug->component);
|
||||
}
|
||||
if (filter_wants $params, 'cc') {
|
||||
my @cc = map { $self->type('email', $_) } @{ $bug->cc };
|
||||
$item{'cc'} = \@cc;
|
||||
|
@ -909,6 +1084,9 @@ sub _bug_to_hash {
|
|||
@{ $bug->keyword_objects };
|
||||
$item{'keywords'} = \@keywords;
|
||||
}
|
||||
if (filter_wants $params, 'product') {
|
||||
$item{product} = $self->type('string', $bug->product);
|
||||
}
|
||||
if (filter_wants $params, 'qa_contact') {
|
||||
my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
|
||||
$item{'qa_contact'} = $self->type('email', $qa_login);
|
||||
|
@ -930,7 +1108,9 @@ sub _bug_to_hash {
|
|||
if ($field->type == FIELD_TYPE_BUG_ID) {
|
||||
$item{$name} = $self->type('int', $bug->$name);
|
||||
}
|
||||
elsif ($field->type == FIELD_TYPE_DATETIME) {
|
||||
elsif ($field->type == FIELD_TYPE_DATETIME
|
||||
|| $field->type == FIELD_TYPE_DATE)
|
||||
{
|
||||
$item{$name} = $self->type('dateTime', $bug->$name);
|
||||
}
|
||||
elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
|
||||
|
@ -946,12 +1126,10 @@ sub _bug_to_hash {
|
|||
if (Bugzilla->user->is_timetracker) {
|
||||
$item{'estimated_time'} = $self->type('double', $bug->estimated_time);
|
||||
$item{'remaining_time'} = $self->type('double', $bug->remaining_time);
|
||||
$item{'actual_time'} = $self->type('double', $bug->actual_time);
|
||||
}
|
||||
|
||||
if (Bugzilla->user->id) {
|
||||
my $token = issue_hash_token([$bug->id, $bug->delta_ts]);
|
||||
$item{'update_token'} = $self->type('string', $token);
|
||||
if (filter_wants $params, 'actual_time') {
|
||||
$item{'actual_time'} = $self->type('double', $bug->actual_time);
|
||||
}
|
||||
}
|
||||
|
||||
# The "accessible" bits go here because they have long names and it
|
||||
|
@ -1044,6 +1222,10 @@ or get information about bugs that have already been filed.
|
|||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=head1 Utility Functions
|
||||
|
||||
=head2 fields
|
||||
|
@ -1057,11 +1239,26 @@ B<UNSTABLE>
|
|||
Get information about valid bug fields, including the lists of legal values
|
||||
for each field.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
You have several options for retreiving information about fields. The first
|
||||
part is the request method and the rest is the related path needed.
|
||||
|
||||
To get information about all fields:
|
||||
|
||||
GET /field/bug
|
||||
|
||||
To get information related to a single field:
|
||||
|
||||
GET /field/bug/<id_or_name>
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
You can pass either field ids or field names.
|
||||
|
||||
B<Note>: If neither C<ids> nor C<names> is specified, then all
|
||||
B<Note>: If neither C<ids> nor C<names> is specified, then all
|
||||
non-obsolete fields will be returned.
|
||||
|
||||
In addition to the parameters below, this method also accepts the
|
||||
|
@ -1257,6 +1454,8 @@ You specified an invalid field name or id.
|
|||
|
||||
=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
|
||||
|
@ -1272,6 +1471,18 @@ B<DEPRECATED> - Use L</fields> instead.
|
|||
|
||||
Tells you what values are allowed for a particular field.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To get information on the values for a field based on field name:
|
||||
|
||||
GET /field/bug/<field_name>/values
|
||||
|
||||
To get information based on field name and a specific product:
|
||||
|
||||
GET /field/bug/<field_name>/<product_id>/values
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
@ -1304,6 +1515,14 @@ You specified a field that doesn't exist or isn't a drop-down field.
|
|||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head1 Bug Information
|
||||
|
@ -1322,6 +1541,18 @@ and/or attachment ids.
|
|||
B<Note>: Private attachments will only be returned if you are in the
|
||||
insidergroup or if you are the submitter of the attachment.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To get all current attachments for a bug:
|
||||
|
||||
GET /bug/<bug_id>/attachment
|
||||
|
||||
To get a specific attachment based on attachment ID:
|
||||
|
||||
GET /bug/attachment/<attachment_id>
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
B<Note>: At least one of C<ids> or C<attachment_ids> is required.
|
||||
|
@ -1519,6 +1750,8 @@ C<summary>.
|
|||
|
||||
=item The C<flags> array was added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -1535,6 +1768,18 @@ B<STABLE>
|
|||
This allows you to get data about comments, given a list of bugs
|
||||
and/or comment ids.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To get all comments for a particular bug using the bug ID or alias:
|
||||
|
||||
GET /bug/<id_or_alias>/comment
|
||||
|
||||
To get a specific comment based on the comment ID:
|
||||
|
||||
GET /bug/comment/<comment_id>
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
B<Note>: At least one of C<ids> or C<comment_ids> is required.
|
||||
|
@ -1680,6 +1925,8 @@ C<creator>.
|
|||
|
||||
=item C<creation_time> was added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -1697,6 +1944,14 @@ Gets information about particular bugs in the database.
|
|||
|
||||
Note: Can also be called as "get_bugs" for compatibilty with Bugzilla 3.0 API.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To get information about a particular bug using its ID or alias:
|
||||
|
||||
GET /bug/<id_or_alias>
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
In addition to the parameters below, this method also accepts the
|
||||
|
@ -2024,7 +2279,7 @@ You do not have access to the bug_id you specified.
|
|||
|
||||
=over
|
||||
|
||||
=item C<permissive> argument added to this method's params in Bugzilla B<3.4>.
|
||||
=item C<permissive> argument added to this method's params in Bugzilla B<3.4>.
|
||||
|
||||
=item The following properties were added to this method's return values
|
||||
in Bugzilla B<3.4>:
|
||||
|
@ -2072,6 +2327,8 @@ and all custom fields.
|
|||
=item The C<actual_time> item was added to the C<bugs> return value
|
||||
in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -2086,6 +2343,14 @@ B<EXPERIMENTAL>
|
|||
|
||||
Gets the history of changes for particular bugs in the database.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To get the history for a specific bug ID:
|
||||
|
||||
GET /bug/<bug_id>/history
|
||||
|
||||
The returned data format will be the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
@ -2177,10 +2442,65 @@ The same as L</get>.
|
|||
consistent with other methods. Since Bugzilla B<4.4>, they now match
|
||||
names used by L<Bug.update|/"update"> for consistency.
|
||||
|
||||
=item REST API call added Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 possible_duplicates
|
||||
|
||||
B<UNSTABLE>
|
||||
|
||||
=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>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 search
|
||||
|
||||
|
@ -2192,6 +2512,14 @@ B<UNSTABLE>
|
|||
|
||||
Allows you to search for bugs based on particular criteria.
|
||||
|
||||
=item <REST>
|
||||
|
||||
To search for bugs:
|
||||
|
||||
GET /bug
|
||||
|
||||
The URL parameters and the returned data format are the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
Unless otherwise specified in the description of a parameter, bugs are
|
||||
|
@ -2212,10 +2540,19 @@ the "Foo" or "Bar" products, you'd pass:
|
|||
product => ['Foo', 'Bar']
|
||||
|
||||
Some Bugzillas may treat your arguments case-sensitively, depending
|
||||
on what database system they are using. Most commonly, though, Bugzilla is
|
||||
not case-sensitive with the arguments passed (because MySQL is the
|
||||
on what database system they are using. Most commonly, though, Bugzilla is
|
||||
not case-sensitive with the arguments passed (because MySQL is the
|
||||
most-common database to use with Bugzilla, and MySQL is not case sensitive).
|
||||
|
||||
In addition to the fields listed below, you may also use criteria that
|
||||
is similar to what is used in the Advanced Search screen of the Bugzilla
|
||||
UI. This includes fields specified by C<Search by Change History> and
|
||||
C<Custom Search>. The easiest way to determine what the field names are and what
|
||||
format Bugzilla expects, is to first construct your query using the
|
||||
Advanced Search UI, execute it and use the query parameters in they URL
|
||||
as your key/value pairs for the WebService call. With REST, you can
|
||||
just reuse the query parameter portion in the REST call itself.
|
||||
|
||||
=over
|
||||
|
||||
=item C<alias>
|
||||
|
@ -2256,13 +2593,16 @@ May not be an array.
|
|||
|
||||
=item C<limit>
|
||||
|
||||
C<int> Limit the number of results returned to C<int> records.
|
||||
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
|
||||
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>
|
||||
|
@ -2335,6 +2675,10 @@ C<string> Search the "Status Whiteboard" field on bugs for a substring.
|
|||
Works the same as the C<summary> field described above, but searches the
|
||||
Status Whiteboard field.
|
||||
|
||||
=item C<quicksearch>
|
||||
|
||||
C<string> Search for bugs using quicksearch syntax.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -2348,10 +2692,16 @@ log in and I<then> call this method.
|
|||
|
||||
=item B<Errors>
|
||||
|
||||
Currently, this function doesn't throw any special errors (other than
|
||||
the ones that all webservice functions can throw). If you specify
|
||||
an invalid value for a particular field, you just won't get any results
|
||||
for that value.
|
||||
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>
|
||||
|
||||
|
@ -2364,6 +2714,17 @@ for that value.
|
|||
=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
|
||||
|
@ -2390,10 +2751,19 @@ The WebService interface may allow you to set things other than those listed
|
|||
here, but realize that anything undocumented is B<UNSTABLE> and will very
|
||||
likely change in the future.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To create a new bug in Bugzilla:
|
||||
|
||||
POST /bug
|
||||
|
||||
The params to include in the POST body as well as the returned data format,
|
||||
are the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
Some params must be set, or an error will be thrown. These params are
|
||||
marked B<Required>.
|
||||
marked B<Required>.
|
||||
|
||||
Some parameters can have defaults set in Bugzilla, by the administrator.
|
||||
If these parameters have defaults set, you can omit them. These parameters
|
||||
|
@ -2554,6 +2924,8 @@ loop errors had a generic code of C<32000>.
|
|||
=item The ability to file new bugs with a C<resolution> was added in
|
||||
Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -2569,6 +2941,16 @@ B<STABLE>
|
|||
|
||||
This allows you to add an attachment to a bug in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To create attachment on a current bug:
|
||||
|
||||
POST /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
|
||||
|
@ -2666,6 +3048,158 @@ You set the "data" field to an empty string.
|
|||
|
||||
=item The return value has changed in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 update_attachment
|
||||
|
||||
B<UNSTABLE>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
This allows you to update attachment metadata in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To update attachment metadata on a current attachment:
|
||||
|
||||
PUT /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<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.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A C<hash> with a single field, "attachment". 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 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
|
||||
|
@ -2681,6 +3215,15 @@ B<STABLE>
|
|||
|
||||
This allows you to add a comment to a bug in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To create a comment on a current bug:
|
||||
|
||||
POST /bug/<bug_id>/comment
|
||||
|
||||
The params to include in the POST body as well as the returned data format,
|
||||
are the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
@ -2756,6 +3299,8 @@ purposes if you wish.
|
|||
=item Before Bugzilla B<3.6>, error 54 and error 114 had a generic error
|
||||
code of 32000.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -2772,6 +3317,16 @@ B<UNSTABLE>
|
|||
Allows you to update the fields of a bug. Automatically sends emails
|
||||
out about the changes.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To update the fields of a current bug:
|
||||
|
||||
PUT /bug/<bug_id>
|
||||
|
||||
The params to include in the PUT body as well as the returned data format,
|
||||
are the same as below. The C<ids> param will be overridden as it is
|
||||
pulled from the URL path.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
@ -3216,6 +3771,8 @@ rules don't allow that change.
|
|||
|
||||
=item Added in Bugzilla B<4.0>.
|
||||
|
||||
=item REST API call added Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -3403,8 +3960,6 @@ This method can throw the same errors as L</get>.
|
|||
|
||||
=item get_bugs
|
||||
|
||||
=item possible_duplicates
|
||||
|
||||
=item get_history
|
||||
|
||||
=back
|
||||
|
|
|
@ -114,12 +114,12 @@ sub time {
|
|||
sub last_audit_time {
|
||||
my ($self, $params) = validate(@_, 'class');
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
|
||||
my $sql_statement = "SELECT MAX(at_time) FROM audit_log";
|
||||
my $class_values = $params->{class};
|
||||
my @class_values_quoted;
|
||||
foreach my $class_value (@$class_values) {
|
||||
push (@class_values_quoted, $dbh->quote($class_value))
|
||||
push (@class_values_quoted, $dbh->quote($class_value))
|
||||
if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/;
|
||||
}
|
||||
|
||||
|
@ -128,11 +128,11 @@ sub last_audit_time {
|
|||
}
|
||||
|
||||
my $last_audit_time = $dbh->selectrow_array("$sql_statement");
|
||||
|
||||
|
||||
# All Webservices return times in UTC; Use UTC here for backwards compat.
|
||||
# Hardcode values where appropriate
|
||||
$last_audit_time = datetime_from($last_audit_time, 'UTC');
|
||||
|
||||
|
||||
return {
|
||||
last_audit_time => $self->type('dateTime', $last_audit_time)
|
||||
};
|
||||
|
@ -174,6 +174,10 @@ This provides functions that tell you about Bugzilla in general.
|
|||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=head2 version
|
||||
|
||||
B<STABLE>
|
||||
|
@ -184,6 +188,12 @@ B<STABLE>
|
|||
|
||||
Returns the current version of Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /version
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -193,6 +203,14 @@ string.
|
|||
|
||||
=item B<Errors> (none)
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 extensions
|
||||
|
@ -206,6 +224,12 @@ B<EXPERIMENTAL>
|
|||
Gets information about the extensions that are currently installed and enabled
|
||||
in this Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /extensions
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -236,6 +260,8 @@ The return value looks something like this:
|
|||
that the extensions define themselves. Before 3.6, the names of the
|
||||
extensions depended on the directory they were in on the Bugzilla server.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -251,6 +277,12 @@ Use L</time> instead.
|
|||
|
||||
Returns the timezone that Bugzilla expects dates and times in.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /timezone
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -265,6 +297,8 @@ string in (+/-)XXXX (RFC 2822) format.
|
|||
=item As of Bugzilla B<3.6>, the timezone returned is always C<+0000>
|
||||
(the UTC timezone).
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -281,6 +315,12 @@ B<STABLE>
|
|||
Gets information about what time the Bugzilla server thinks it is, and
|
||||
what timezone it's running in.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /time
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -291,7 +331,7 @@ A struct with the following items:
|
|||
|
||||
=item C<db_time>
|
||||
|
||||
C<dateTime> The current time in UTC, according to the Bugzilla
|
||||
C<dateTime> The current time in UTC, according to the Bugzilla
|
||||
I<database server>.
|
||||
|
||||
Note that Bugzilla assumes that the database and the webserver are running
|
||||
|
@ -301,7 +341,7 @@ rely on for doing searches and other input to the WebService.
|
|||
|
||||
=item C<web_time>
|
||||
|
||||
C<dateTime> This is the current time in UTC, according to Bugzilla's
|
||||
C<dateTime> This is the current time in UTC, according to Bugzilla's
|
||||
I<web server>.
|
||||
|
||||
This might be different by a second from C<db_time> since this comes from
|
||||
|
@ -317,7 +357,7 @@ versions of Bugzilla before 3.6.)
|
|||
=item C<tz_name>
|
||||
|
||||
C<string> The literal string C<UTC>. (Exists only for backwards-compatibility
|
||||
with versions of Bugzilla before 3.6.)
|
||||
with versions of Bugzilla before 3.6.)
|
||||
|
||||
=item C<tz_short_name>
|
||||
|
||||
|
@ -341,6 +381,8 @@ with versions of Bugzilla before 3.6.)
|
|||
were in the UTC timezone, instead of returning information in the server's
|
||||
local timezone.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -355,6 +397,12 @@ B<UNSTABLE>
|
|||
|
||||
Returns parameter values currently used in this Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /parameters
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -412,6 +460,8 @@ never be stable.
|
|||
|
||||
=item Added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -426,9 +476,15 @@ B<EXPERIMENTAL>
|
|||
|
||||
Gets the latest time of the audit_log table.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /last_audit_time
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
You can pass the optional parameter C<class> to get the maximum for only
|
||||
You can pass the optional parameter C<class> to get the maximum for only
|
||||
the listed classes.
|
||||
|
||||
=over
|
||||
|
@ -453,6 +509,8 @@ at_time from the audit_log.
|
|||
|
||||
=item Added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
|
@ -86,7 +86,7 @@ Bugzilla::Webservice::Classification - The Classification API
|
|||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla API allows you to deal with the available Classifications.
|
||||
This part of the Bugzilla API allows you to deal with the available Classifications.
|
||||
You will be able to get information about them as well as manipulate them.
|
||||
|
||||
=head1 METHODS
|
||||
|
@ -94,6 +94,10 @@ You will be able to get information about them as well as manipulate them.
|
|||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=head1 Classification Retrieval
|
||||
|
||||
=head2 get
|
||||
|
@ -106,13 +110,21 @@ B<EXPERIMENTAL>
|
|||
|
||||
Returns a hash containing information about a set of classifications.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To return information on a single classification:
|
||||
|
||||
GET /classification/<classification_id_or_name>
|
||||
|
||||
The returned data format will be the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
In addition to the parameters below, this method also accepts the
|
||||
standard L<include_fields|Bugzilla::WebService/include_fields> and
|
||||
L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments.
|
||||
|
||||
You could get classifications info by supplying their names and/or ids.
|
||||
You could get classifications info by supplying their names and/or ids.
|
||||
So, this method accepts the following parameters:
|
||||
|
||||
=over
|
||||
|
@ -127,10 +139,10 @@ An array of classification names.
|
|||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
=item B<Returns>
|
||||
|
||||
A hash with the key C<classifications> and an array of hashes as the corresponding value.
|
||||
Each element of the array represents a classification that the user is authorized to see
|
||||
Each element of the array represents a classification that the user is authorized to see
|
||||
and has the following keys:
|
||||
|
||||
=over
|
||||
|
@ -190,6 +202,8 @@ Classification is not enabled on this installation.
|
|||
|
||||
=item Added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
|
@ -14,9 +14,22 @@ use parent qw(Exporter);
|
|||
|
||||
our @EXPORT = qw(
|
||||
WS_ERROR_CODE
|
||||
|
||||
STATUS_OK
|
||||
STATUS_CREATED
|
||||
STATUS_ACCEPTED
|
||||
STATUS_NO_CONTENT
|
||||
STATUS_MULTIPLE_CHOICES
|
||||
STATUS_BAD_REQUEST
|
||||
STATUS_NOT_FOUND
|
||||
STATUS_GONE
|
||||
REST_STATUS_CODE_MAP
|
||||
|
||||
ERROR_UNKNOWN_FATAL
|
||||
ERROR_UNKNOWN_TRANSIENT
|
||||
|
||||
XMLRPC_CONTENT_TYPE_WHITELIST
|
||||
REST_CONTENT_TYPE_WHITELIST
|
||||
|
||||
WS_DISPATCH
|
||||
);
|
||||
|
@ -163,14 +176,56 @@ use constant WS_ERROR_CODE => {
|
|||
# Classification errors are 900-1000
|
||||
auth_classification_not_enabled => 900,
|
||||
|
||||
# Search errors are 1000-1100
|
||||
buglist_parameters_required => 1000,
|
||||
|
||||
# Errors thrown by the WebService itself. The ones that are negative
|
||||
# conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
|
||||
xmlrpc_invalid_value => -32600,
|
||||
unknown_method => -32601,
|
||||
json_rpc_post_only => 32610,
|
||||
json_rpc_invalid_callback => 32611,
|
||||
xmlrpc_illegal_content_type => 32612,
|
||||
json_rpc_illegal_content_type => 32613,
|
||||
xmlrpc_illegal_content_type => 32612,
|
||||
json_rpc_illegal_content_type => 32613,
|
||||
rest_invalid_resource => 32614,
|
||||
};
|
||||
|
||||
# RESTful webservices use the http status code
|
||||
# to describe whether a call was successful or
|
||||
# to describe the type of error that occurred.
|
||||
use constant STATUS_OK => 200;
|
||||
use constant STATUS_CREATED => 201;
|
||||
use constant STATUS_ACCEPTED => 202;
|
||||
use constant STATUS_NO_CONTENT => 204;
|
||||
use constant STATUS_MULTIPLE_CHOICES => 300;
|
||||
use constant STATUS_BAD_REQUEST => 400;
|
||||
use constant STATUS_NOT_AUTHORIZED => 401;
|
||||
use constant STATUS_NOT_FOUND => 404;
|
||||
use constant STATUS_GONE => 410;
|
||||
|
||||
# The integer value is the error code above returned by
|
||||
# the related webvservice call. We choose the appropriate
|
||||
# http status code based on the error code or use the
|
||||
# default STATUS_BAD_REQUEST.
|
||||
use constant REST_STATUS_CODE_MAP => {
|
||||
51 => STATUS_NOT_FOUND,
|
||||
101 => STATUS_NOT_FOUND,
|
||||
102 => STATUS_NOT_AUTHORIZED,
|
||||
106 => STATUS_NOT_AUTHORIZED,
|
||||
109 => STATUS_NOT_AUTHORIZED,
|
||||
110 => STATUS_NOT_AUTHORIZED,
|
||||
113 => STATUS_NOT_AUTHORIZED,
|
||||
115 => STATUS_NOT_AUTHORIZED,
|
||||
120 => STATUS_NOT_AUTHORIZED,
|
||||
300 => STATUS_NOT_AUTHORIZED,
|
||||
301 => STATUS_NOT_AUTHORIZED,
|
||||
302 => STATUS_NOT_AUTHORIZED,
|
||||
303 => STATUS_NOT_AUTHORIZED,
|
||||
304 => STATUS_NOT_AUTHORIZED,
|
||||
410 => STATUS_NOT_AUTHORIZED,
|
||||
504 => STATUS_NOT_AUTHORIZED,
|
||||
505 => STATUS_NOT_AUTHORIZED,
|
||||
_default => STATUS_BAD_REQUEST
|
||||
};
|
||||
|
||||
# These are the fallback defaults for errors not in ERROR_CODE.
|
||||
|
@ -184,6 +239,13 @@ use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw(
|
|||
application/xml
|
||||
);
|
||||
|
||||
use constant REST_CONTENT_TYPE_WHITELIST => qw(
|
||||
text/html
|
||||
application/javascript
|
||||
application/json
|
||||
text/javascript
|
||||
);
|
||||
|
||||
sub WS_DISPATCH {
|
||||
# We "require" here instead of "use" above to avoid a dependency loop.
|
||||
require Bugzilla::Hook;
|
||||
|
|
|
@ -113,6 +113,10 @@ get information about them.
|
|||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=head1 Group Creation and Modification
|
||||
|
||||
=head2 create
|
||||
|
@ -125,9 +129,16 @@ B<UNSTABLE>
|
|||
|
||||
This allows you to create a new group in Bugzilla.
|
||||
|
||||
=item B<Params>
|
||||
=item B<REST>
|
||||
|
||||
Some params must be set, or an error will be thrown. These params are
|
||||
POST /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
|
||||
|
@ -148,7 +159,7 @@ name of the group.
|
|||
C<string> A regular expression. Any user whose Bugzilla username matches
|
||||
this regular expression will automatically be granted membership in this group.
|
||||
|
||||
=item C<is_active>
|
||||
=item C<is_active>
|
||||
|
||||
C<boolean> C<True> if new group can be used for bugs, C<False> if this
|
||||
is a group that will only contain users and no bugs will be restricted
|
||||
|
@ -162,7 +173,7 @@ if they are in this group.
|
|||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
=item B<Returns>
|
||||
|
||||
A hash with one element, C<id>. This is the id of the newly-created group.
|
||||
|
||||
|
@ -188,7 +199,15 @@ You specified an invalid regular expression in the C<user_regexp> field.
|
|||
|
||||
=back
|
||||
|
||||
=back
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 update
|
||||
|
||||
|
@ -200,6 +219,14 @@ B<UNSTABLE>
|
|||
|
||||
This allows you to update a group in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
PUT /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.
|
||||
|
@ -278,6 +305,14 @@ comma-and-space-separated list if multiple values were removed.
|
|||
|
||||
The same as L</create>.
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
|
|
@ -50,64 +50,92 @@ BEGIN { *get_products = \&get }
|
|||
# Get the ids of the products the user can search
|
||||
sub get_selectable_products {
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
|
||||
return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
|
||||
}
|
||||
|
||||
# Get the ids of the products the user can enter bugs against
|
||||
sub get_enterable_products {
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
|
||||
return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
|
||||
}
|
||||
|
||||
# Get the union of the products the user can search and enter bugs against.
|
||||
sub get_accessible_products {
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
|
||||
return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
|
||||
}
|
||||
|
||||
# Get a list of actual products, based on list of ids or names
|
||||
sub get {
|
||||
my ($self, $params) = validate(@_, 'ids', 'names');
|
||||
my ($self, $params) = validate(@_, 'ids', 'names', 'type');
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
defined $params->{ids} || defined $params->{names}
|
||||
defined $params->{ids} || defined $params->{names} || defined $params->{type}
|
||||
|| ThrowCodeError("params_required", { function => "Product.get",
|
||||
params => ['ids', 'names'] });
|
||||
params => ['ids', 'names', 'type'] });
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
|
||||
# Only products that are in the users accessible products,
|
||||
# can be allowed to be returned
|
||||
my $accessible_products = Bugzilla->user->get_enterable_products;
|
||||
my $products = [];
|
||||
if (defined $params->{type}) {
|
||||
my %product_hash;
|
||||
foreach my $type (@{ $params->{type} }) {
|
||||
my $result = [];
|
||||
if ($type eq 'accessible') {
|
||||
$result = $user->get_accessible_products();
|
||||
}
|
||||
elsif ($type eq 'enterable') {
|
||||
$result = $user->get_enterable_products();
|
||||
}
|
||||
elsif ($type eq 'selectable') {
|
||||
$result = $user->get_selectable_products();
|
||||
}
|
||||
else {
|
||||
ThrowUserError('get_products_invalid_type',
|
||||
{ type => $type });
|
||||
}
|
||||
map { $product_hash{$_->id} = $_ } @$result;
|
||||
}
|
||||
$products = [ values %product_hash ];
|
||||
}
|
||||
else {
|
||||
$products = $user->get_accessible_products;
|
||||
}
|
||||
|
||||
my @requested_accessible;
|
||||
my @requested_products;
|
||||
|
||||
if (defined $params->{ids}) {
|
||||
# Create a hash with the ids the user wants
|
||||
my %ids = map { $_ => 1 } @{$params->{ids}};
|
||||
|
||||
# Return the intersection of this, by grepping the ids from
|
||||
# accessible products.
|
||||
push(@requested_accessible,
|
||||
grep { $ids{$_->id} } @$accessible_products);
|
||||
# Return the intersection of this, by grepping the ids from $products.
|
||||
push(@requested_products,
|
||||
grep { $ids{$_->id} } @$products);
|
||||
}
|
||||
|
||||
if (defined $params->{names}) {
|
||||
# Create a hash with the names the user wants
|
||||
my %names = map { lc($_) => 1 } @{$params->{names}};
|
||||
|
||||
# Return the intersection of this, by grepping the names from
|
||||
# accessible products, union'ed with products found by ID to
|
||||
# Return the intersection of this, by grepping the names
|
||||
# from $products, union'ed with products found by ID to
|
||||
# avoid duplicates
|
||||
foreach my $product (grep { $names{lc $_->name} }
|
||||
@$accessible_products) {
|
||||
@$products) {
|
||||
next if grep { $_->id == $product->id }
|
||||
@requested_accessible;
|
||||
push @requested_accessible, $product;
|
||||
@requested_products;
|
||||
push @requested_products, $product;
|
||||
}
|
||||
}
|
||||
|
||||
# If we just requested a specific type of products without
|
||||
# specifying ids or names, then return the entire list.
|
||||
if (!defined $params->{ids} && !defined $params->{names}) {
|
||||
@requested_products = @$products;
|
||||
}
|
||||
|
||||
# Now create a result entry for each.
|
||||
my @products = map { $self->_product_to_hash($params, $_) }
|
||||
@requested_accessible;
|
||||
@requested_products;
|
||||
return { products => \@products };
|
||||
}
|
||||
|
||||
|
@ -115,7 +143,7 @@ sub create {
|
|||
my ($self, $params) = @_;
|
||||
|
||||
Bugzilla->login(LOGIN_REQUIRED);
|
||||
Bugzilla->user->in_group('editcomponents')
|
||||
Bugzilla->user->in_group('editcomponents')
|
||||
|| ThrowUserError("auth_failure", { group => "editcomponents",
|
||||
action => "add",
|
||||
object => "products"});
|
||||
|
@ -151,7 +179,7 @@ sub update {
|
|||
object => "products" });
|
||||
|
||||
defined($params->{names}) || defined($params->{ids})
|
||||
|| ThrowCodeError('params_required',
|
||||
|| ThrowCodeError('params_required',
|
||||
{ function => 'Product.update', params => ['ids', 'names'] });
|
||||
|
||||
my $product_objects = params_to_objects($params, 'Bugzilla::Product');
|
||||
|
@ -170,10 +198,10 @@ sub update {
|
|||
my %changes;
|
||||
foreach my $product (@$product_objects) {
|
||||
my $returned_changes = $product->update();
|
||||
$changes{$product->id} = translate($returned_changes, MAPPED_RETURNS);
|
||||
$changes{$product->id} = translate($returned_changes, MAPPED_RETURNS);
|
||||
}
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
|
||||
my @result;
|
||||
foreach my $product (@$product_objects) {
|
||||
my %hash = (
|
||||
|
@ -185,7 +213,7 @@ sub update {
|
|||
my $change = $changes{$product->id}->{$field};
|
||||
$hash{changes}{$field} = {
|
||||
removed => $self->type('string', $change->[0]),
|
||||
added => $self->type('string', $change->[1])
|
||||
added => $self->type('string', $change->[1])
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -214,12 +242,12 @@ sub _product_to_hash {
|
|||
}
|
||||
if (filter_wants($params, 'versions')) {
|
||||
$field_data->{versions} = [map {
|
||||
$self->_version_to_hash($_)
|
||||
$self->_version_to_hash($_, $params)
|
||||
} @{$product->versions}];
|
||||
}
|
||||
if (filter_wants($params, 'milestones')) {
|
||||
$field_data->{milestones} = [map {
|
||||
$self->_milestone_to_hash($_)
|
||||
$self->_milestone_to_hash($_, $params)
|
||||
} @{$product->milestones}];
|
||||
}
|
||||
return filter($params, $field_data);
|
||||
|
@ -243,23 +271,26 @@ sub _component_to_hash {
|
|||
0,
|
||||
is_active =>
|
||||
$self->type('boolean', $component->is_active),
|
||||
flag_types => {
|
||||
};
|
||||
|
||||
if (filter_wants($params, 'flag_types', 'components')) {
|
||||
$field_data->{flag_types} = {
|
||||
bug =>
|
||||
[map {
|
||||
$self->_flag_type_to_hash($_, $params)
|
||||
$self->_flag_type_to_hash($_)
|
||||
} @{$component->flag_types->{'bug'}}],
|
||||
attachment =>
|
||||
[map {
|
||||
$self->_flag_type_to_hash($_, $params)
|
||||
$self->_flag_type_to_hash($_)
|
||||
} @{$component->flag_types->{'attachment'}}],
|
||||
}
|
||||
};
|
||||
return filter($params, $field_data, 'component');
|
||||
};
|
||||
}
|
||||
return filter($params, $field_data, 'components');
|
||||
}
|
||||
|
||||
sub _flag_type_to_hash {
|
||||
my ($self, $flag_type, $params) = @_;
|
||||
my $field_data = {
|
||||
my ($self, $flag_type) = @_;
|
||||
return {
|
||||
id =>
|
||||
$self->type('int', $flag_type->id),
|
||||
name =>
|
||||
|
@ -283,12 +314,11 @@ sub _flag_type_to_hash {
|
|||
request_group =>
|
||||
$self->type('int', $flag_type->request_group_id),
|
||||
};
|
||||
return filter($params, $field_data, 'flag_type');
|
||||
}
|
||||
|
||||
sub _version_to_hash {
|
||||
my ($self, $version) = @_;
|
||||
return {
|
||||
my ($self, $version, $params) = @_;
|
||||
my $field_data = {
|
||||
id =>
|
||||
$self->type('int', $version->id),
|
||||
name =>
|
||||
|
@ -298,11 +328,12 @@ sub _version_to_hash {
|
|||
is_active =>
|
||||
$self->type('boolean', $version->is_active),
|
||||
};
|
||||
return filter($params, $field_data, 'versions');
|
||||
}
|
||||
|
||||
sub _milestone_to_hash {
|
||||
my ($self, $milestone) = @_;
|
||||
return {
|
||||
my ($self, $milestone, $params) = @_;
|
||||
my $field_data = {
|
||||
id =>
|
||||
$self->type('int', $milestone->id),
|
||||
name =>
|
||||
|
@ -312,6 +343,7 @@ sub _milestone_to_hash {
|
|||
is_active =>
|
||||
$self->type('boolean', $milestone->is_active),
|
||||
};
|
||||
return filter($params, $field_data, 'milestones');
|
||||
}
|
||||
|
||||
1;
|
||||
|
@ -332,6 +364,10 @@ get information about them.
|
|||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=head1 List Products
|
||||
|
||||
=head2 get_selectable_products
|
||||
|
@ -344,15 +380,29 @@ B<EXPERIMENTAL>
|
|||
|
||||
Returns a list of the ids of the products the user can search on.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /product_selectable
|
||||
|
||||
the returned data format is same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
=item B<Returns>
|
||||
|
||||
A hash containing one item, C<ids>, that contains an array of product
|
||||
ids.
|
||||
|
||||
=item B<Errors> (none)
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 get_enterable_products
|
||||
|
@ -366,6 +416,12 @@ B<EXPERIMENTAL>
|
|||
Returns a list of the ids of the products the user can enter bugs
|
||||
against.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /product_enterable
|
||||
|
||||
the returned data format is same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -375,6 +431,14 @@ ids.
|
|||
|
||||
=item B<Errors> (none)
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 get_accessible_products
|
||||
|
@ -388,6 +452,12 @@ B<UNSTABLE>
|
|||
Returns a list of the ids of the products the user can search or enter
|
||||
bugs against.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /product_accessible
|
||||
|
||||
the returned data format is same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -397,6 +467,14 @@ ids.
|
|||
|
||||
=item B<Errors> (none)
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 get
|
||||
|
@ -413,12 +491,32 @@ 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 /product?type=accessible
|
||||
|
||||
To return information about a specific product by C<id> or C<name>:
|
||||
|
||||
GET /product/<product_id_or_name>
|
||||
|
||||
You can also return information about more than one specific product
|
||||
by using the following in your query string:
|
||||
|
||||
GET /product?ids=1&ids=2&ids=3 or GET /product?names=ProductOne&names=Product2
|
||||
|
||||
the returned data format is same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
In addition to the parameters below, this method also accepts the
|
||||
standard L<include_fields|Bugzilla::WebService/include_fields> and
|
||||
L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments.
|
||||
|
||||
This RPC call supports sub field restrictions.
|
||||
|
||||
=over
|
||||
|
||||
=item C<ids>
|
||||
|
@ -429,9 +527,15 @@ An array of product ids
|
|||
|
||||
An array of product names
|
||||
|
||||
=item C<type>
|
||||
|
||||
The group of products to return. Valid values are: C<accessible> (default),
|
||||
C<selectable>, and C<enterable>. C<type> can be a single value or an array
|
||||
of values if more than one group is needed with duplicates removed.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
=item B<Returns>
|
||||
|
||||
A hash containing one item, C<products>, that is an array of
|
||||
hashes. Each hash describes a product, and has the following items:
|
||||
|
@ -511,7 +615,7 @@ components are not enabled for new bugs.
|
|||
|
||||
=item C<flag_types>
|
||||
|
||||
A hash containing the two items C<bug> and C<attachment> that each contains an
|
||||
A hash containing the two items C<bug> and C<attachment> that each contains an
|
||||
array of hashes, where each hash describes a flagtype, and has the
|
||||
following items:
|
||||
|
||||
|
@ -565,8 +669,8 @@ flagtype.
|
|||
|
||||
=item C<request_group>
|
||||
|
||||
C<int> the group id that is allowed to request the flag if the flag
|
||||
is of the type requestable. If the item is not included all users
|
||||
C<int> the group id that is allowed to request the flag if the flag
|
||||
is of the type requestable. If the item is not included all users
|
||||
are allowed request this flagtype.
|
||||
|
||||
=back
|
||||
|
@ -606,6 +710,8 @@ been removed.
|
|||
=item In Bugzilla B<4.4>, C<flag_types> was added to the fields returned
|
||||
by C<get>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -622,9 +728,16 @@ B<EXPERIMENTAL>
|
|||
|
||||
This allows you to create a new product in Bugzilla.
|
||||
|
||||
=item B<Params>
|
||||
=item B<REST>
|
||||
|
||||
Some params must be set, or an error will be thrown. These params are
|
||||
POST /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
|
||||
|
@ -638,11 +751,11 @@ within Bugzilla.
|
|||
|
||||
B<Required> C<string> A description for this product. Allows some simple HTML.
|
||||
|
||||
=item C<version>
|
||||
=item C<version>
|
||||
|
||||
B<Required> C<string> The default version for this product.
|
||||
|
||||
=item C<has_unconfirmed>
|
||||
=item C<has_unconfirmed>
|
||||
|
||||
C<boolean> Allow the UNCONFIRMED status to be set on bugs in this product.
|
||||
Default: true.
|
||||
|
@ -651,11 +764,11 @@ Default: true.
|
|||
|
||||
C<string> The name of the Classification which contains this product.
|
||||
|
||||
=item C<default_milestone>
|
||||
=item C<default_milestone>
|
||||
|
||||
C<string> The default milestone for this product. Default '---'.
|
||||
|
||||
=item C<is_open>
|
||||
=item C<is_open>
|
||||
|
||||
C<boolean> True if the product is currently allowing bugs to be entered
|
||||
into it. Default: true.
|
||||
|
@ -667,7 +780,7 @@ new product. Default: true.
|
|||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
=item B<Returns>
|
||||
|
||||
A hash with one element, id. This is the id of the newly-filed product.
|
||||
|
||||
|
@ -703,6 +816,14 @@ You must specify a version for this product.
|
|||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 update
|
||||
|
@ -715,6 +836,14 @@ B<EXPERIMENTAL>
|
|||
|
||||
This allows you to update a product in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
PUT /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.
|
||||
|
@ -795,7 +924,7 @@ Note that booleans will be represented with the strings '1' and '0'.
|
|||
|
||||
Here's an example of what a return value might look like:
|
||||
|
||||
{
|
||||
{
|
||||
products => [
|
||||
{
|
||||
id => 123,
|
||||
|
@ -835,10 +964,6 @@ You specified the name of a product that already exists.
|
|||
|
||||
You must specify a description for this product.
|
||||
|
||||
=item 704 (Product must have version)
|
||||
|
||||
You must specify a version for this product.
|
||||
|
||||
=item 705 (Product must define a default milestone)
|
||||
|
||||
You must define a default milestone.
|
||||
|
@ -853,6 +978,8 @@ You must define a default milestone.
|
|||
|
||||
=item Added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
|
@ -14,6 +14,9 @@ use Bugzilla::Error;
|
|||
use Bugzilla::Util qw(datetime_from);
|
||||
|
||||
use Scalar::Util qw(blessed);
|
||||
use Digest::MD5 qw(md5_base64);
|
||||
|
||||
use Storable qw(freeze);
|
||||
|
||||
sub handle_login {
|
||||
my ($self, $class, $method, $full_method) = @_;
|
||||
|
@ -29,7 +32,7 @@ sub handle_login {
|
|||
|
||||
sub datetime_format_inbound {
|
||||
my ($self, $time) = @_;
|
||||
|
||||
|
||||
my $converted = datetime_from($time, Bugzilla->local_timezone);
|
||||
if (!defined $converted) {
|
||||
ThrowUserError('illegal_date', { date => $time });
|
||||
|
@ -55,8 +58,63 @@ sub datetime_format_outbound {
|
|||
return $time->iso8601();
|
||||
}
|
||||
|
||||
# ETag support
|
||||
sub bz_etag {
|
||||
my ($self, $data) = @_;
|
||||
my $cache = Bugzilla->request_cache;
|
||||
if (defined $data) {
|
||||
# Serialize the data if passed a reference
|
||||
local $Storable::canonical = 1;
|
||||
$data = freeze($data) if ref $data;
|
||||
|
||||
# Wide characters cause md5_base64() to die.
|
||||
utf8::encode($data) if utf8::is_utf8($data);
|
||||
|
||||
# Append content_type to the end of the data
|
||||
# string as we want the etag to be unique to
|
||||
# the content_type. We do not need this for
|
||||
# XMLRPC as text/xml is always returned.
|
||||
if (blessed($self) && $self->can('content_type')) {
|
||||
$data .= $self->content_type if $self->content_type;
|
||||
}
|
||||
|
||||
$cache->{'bz_etag'} = md5_base64($data);
|
||||
}
|
||||
return $cache->{'bz_etag'};
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::WebService::Server - Base server class for the WebService API
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Bugzilla::WebService::Server is the base class for the individual WebService API
|
||||
servers such as XMLRPC, JSONRPC, and REST. You never actually create a
|
||||
Bugzilla::WebService::Server directly, you only make subclasses of it.
|
||||
|
||||
=head1 FUNCTIONS
|
||||
|
||||
=over
|
||||
|
||||
=item C<bz_etag>
|
||||
|
||||
This function is used to store an ETag value that will be used when returning
|
||||
the data by the different API server modules such as XMLRPC, or REST. The individual
|
||||
webservice methods can also set the value earlier in the process if needed such as
|
||||
before a unique update token is added. If a value is not set earlier, an etag will
|
||||
automatically be created using the returned data except in some cases when an error
|
||||
has occurred.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::WebService::Server::XMLRPC|XMLRPC>, L<Bugzilla::WebService::Server::JSONRPC|JSONRPC>,
|
||||
and L<Bugzilla::WebService::Server::REST|REST>.
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
|
|
@ -25,7 +25,7 @@ BEGIN {
|
|||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Util qw(taint_data);
|
||||
use Bugzilla::WebService::Util qw(taint_data fix_credentials);
|
||||
use Bugzilla::Util;
|
||||
|
||||
use HTTP::Message;
|
||||
|
@ -75,12 +75,12 @@ sub response_header {
|
|||
|
||||
sub response {
|
||||
my ($self, $response) = @_;
|
||||
my $cgi = $self->cgi;
|
||||
|
||||
# Implement JSONP.
|
||||
if (my $callback = $self->_bz_callback) {
|
||||
my $content = $response->content;
|
||||
$response->content("$callback($content)");
|
||||
|
||||
}
|
||||
|
||||
# Use $cgi->header properly instead of just printing text directly.
|
||||
|
@ -95,9 +95,18 @@ sub response {
|
|||
push(@header_args, "-$name", $value);
|
||||
}
|
||||
}
|
||||
my $cgi = $self->cgi;
|
||||
print $cgi->header(-status => $response->code, @header_args);
|
||||
print $response->content;
|
||||
|
||||
# ETag support
|
||||
my $etag = $self->bz_etag;
|
||||
if ($etag && $cgi->check_etag($etag)) {
|
||||
push(@header_args, "-ETag", $etag);
|
||||
print $cgi->header(-status => '304 Not Modified', @header_args);
|
||||
}
|
||||
else {
|
||||
push(@header_args, "-ETag", $etag) if $etag;
|
||||
print $cgi->header(-status => $response->code, @header_args);
|
||||
print $response->content;
|
||||
}
|
||||
}
|
||||
|
||||
# The JSON-RPC 1.1 GET specification is not so great--you can't specify
|
||||
|
@ -257,7 +266,17 @@ sub _handle {
|
|||
my $self = shift;
|
||||
my ($obj) = @_;
|
||||
$self->{_bz_request_id} = $obj->{id};
|
||||
return $self->SUPER::_handle(@_);
|
||||
|
||||
my $result = $self->SUPER::_handle(@_);
|
||||
|
||||
# Set the ETag if not already set in the webservice methods.
|
||||
my $etag = $self->bz_etag;
|
||||
if (!$etag && ref $result) {
|
||||
my $data = $self->json->decode($result)->{'result'};
|
||||
$self->bz_etag($data);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
# Make all error messages returned by JSON::RPC go into the 100000
|
||||
|
@ -354,6 +373,10 @@ sub _argument_type_check {
|
|||
}
|
||||
}
|
||||
|
||||
# Update the params to allow for several convenience key/values
|
||||
# use for authentication
|
||||
fix_credentials($params);
|
||||
|
||||
Bugzilla->input_params($params);
|
||||
|
||||
if ($self->request->method eq 'POST') {
|
||||
|
|
|
@ -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::WebService::Server::REST;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use parent qw(Bugzilla::WebService::Server::JSONRPC);
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Util qw(taint_data fix_credentials);
|
||||
use Bugzilla::Util qw(correct_urlbase html_quote);
|
||||
|
||||
# Load resource modules
|
||||
use Bugzilla::WebService::Server::REST::Resources::Bug;
|
||||
use Bugzilla::WebService::Server::REST::Resources::Bugzilla;
|
||||
use Bugzilla::WebService::Server::REST::Resources::Classification;
|
||||
use Bugzilla::WebService::Server::REST::Resources::Group;
|
||||
use Bugzilla::WebService::Server::REST::Resources::Product;
|
||||
use Bugzilla::WebService::Server::REST::Resources::User;
|
||||
|
||||
use Scalar::Util qw(blessed reftype);
|
||||
use MIME::Base64 qw(decode_base64);
|
||||
|
||||
###########################
|
||||
# Public Method Overrides #
|
||||
###########################
|
||||
|
||||
sub handle {
|
||||
my ($self) = @_;
|
||||
|
||||
# Determine how the data should be represented. We do this early so
|
||||
# errors will also be returned with the proper content type.
|
||||
$self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST()));
|
||||
|
||||
# Using current path information, decide which class/method to
|
||||
# use to serve the request. Throw error if no resource was found
|
||||
# unless we were looking for OPTIONS
|
||||
if (!$self->_find_resource($self->cgi->path_info)) {
|
||||
if ($self->request->method eq 'OPTIONS'
|
||||
&& $self->bz_rest_options)
|
||||
{
|
||||
my $response = $self->response_header(STATUS_OK, "");
|
||||
my $options_string = join(', ', @{ $self->bz_rest_options });
|
||||
$response->header('Allow' => $options_string,
|
||||
'Access-Control-Allow-Methods' => $options_string);
|
||||
return $self->response($response);
|
||||
}
|
||||
|
||||
ThrowUserError("rest_invalid_resource",
|
||||
{ path => $self->cgi->path_info,
|
||||
method => $self->request->method });
|
||||
}
|
||||
|
||||
# Dispatch to the proper module
|
||||
my $class = $self->bz_class_name;
|
||||
my ($path) = $class =~ /::([^:]+)$/;
|
||||
$self->path_info($path);
|
||||
delete $self->{dispatch_path};
|
||||
$self->dispatch({ $path => $class });
|
||||
|
||||
my $params = $self->_retrieve_json_params;
|
||||
|
||||
fix_credentials($params);
|
||||
|
||||
# Fix includes/excludes for each call
|
||||
rest_include_exclude($params);
|
||||
|
||||
# Set callback name if exists
|
||||
$self->_bz_callback($params->{'callback'}) if $params->{'callback'};
|
||||
|
||||
Bugzilla->input_params($params);
|
||||
|
||||
# Set the JSON version to 1.1 and the id to the current urlbase
|
||||
# also set up the correct handler method
|
||||
my $obj = {
|
||||
version => '1.1',
|
||||
id => correct_urlbase(),
|
||||
method => $self->bz_method_name,
|
||||
params => $params
|
||||
};
|
||||
|
||||
# Execute the handler
|
||||
my $result = $self->_handle($obj);
|
||||
|
||||
if (!$self->error_response_header) {
|
||||
return $self->response(
|
||||
$self->response_header($self->bz_success_code || STATUS_OK, $result));
|
||||
}
|
||||
|
||||
$self->response($self->error_response_header);
|
||||
}
|
||||
|
||||
sub response {
|
||||
my ($self, $response) = @_;
|
||||
|
||||
# If we have thrown an error, the 'error' key will exist
|
||||
# otherwise we use 'result'. JSONRPC returns other data
|
||||
# along with the result/error such as version and id which
|
||||
# we will strip off for REST calls.
|
||||
my $content = $response->content;
|
||||
my $json_data = {};
|
||||
if ($content) {
|
||||
$json_data = $self->json->decode($content);
|
||||
}
|
||||
|
||||
my $result = {};
|
||||
if (exists $json_data->{error}) {
|
||||
$result = $json_data->{error};
|
||||
$result->{error} = $self->type('boolean', 1);
|
||||
delete $result->{'name'}; # Remove JSONRPCError
|
||||
}
|
||||
elsif (exists $json_data->{result}) {
|
||||
$result = $json_data->{result};
|
||||
}
|
||||
|
||||
# Access Control
|
||||
$response->header("Access-Control-Allow-Origin", "*");
|
||||
$response->header("Access-Control-Allow-Headers", "origin, content-type, accept");
|
||||
|
||||
# ETag support
|
||||
my $etag = $self->bz_etag;
|
||||
$self->bz_etag($result) if !$etag;
|
||||
|
||||
# If accessing through web browser, then display in readable format
|
||||
if ($self->content_type eq 'text/html') {
|
||||
$result = $self->json->pretty->canonical->allow_nonref->encode($result);
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
$content = "";
|
||||
$template->process("rest.html.tmpl", { result => $result }, \$content)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
||||
$response->content_type('text/html');
|
||||
}
|
||||
else {
|
||||
$content = $self->json->encode($result);
|
||||
}
|
||||
|
||||
$response->content($content);
|
||||
|
||||
$self->SUPER::response($response);
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Bugzilla::WebService Implementation #
|
||||
#######################################
|
||||
|
||||
sub handle_login {
|
||||
my $self = shift;
|
||||
|
||||
# If we're being called using GET, we don't allow cookie-based or Env
|
||||
# login, because GET requests can be done cross-domain, and we don't
|
||||
# want private data showing up on another site unless the user
|
||||
# explicitly gives that site their username and password. (This is
|
||||
# particularly important for JSONP, which would allow a remote site
|
||||
# to use private data without the user's knowledge, unless we had this
|
||||
# protection in place.) We do allow this for GET /login as we need to
|
||||
# for Bugzilla::Auth::Persist::Cookie to create a login cookie that we
|
||||
# can also use for Bugzilla_token support. This is OK as it requires
|
||||
# a login and password to be supplied and will fail if they are not
|
||||
# valid for the user.
|
||||
if (!grep($_ eq $self->request->method, ('POST', 'PUT'))
|
||||
&& !($self->bz_class_name eq 'Bugzilla::WebService::User'
|
||||
&& $self->bz_method_name eq 'login'))
|
||||
{
|
||||
# XXX There's no particularly good way for us to get a parameter
|
||||
# to Bugzilla->login at this point, so we pass this information
|
||||
# around using request_cache, which is a bit of a hack. The
|
||||
# implementation of it is in Bugzilla::Auth::Login::Stack.
|
||||
Bugzilla->request_cache->{'auth_no_automatic_login'} = 1;
|
||||
}
|
||||
|
||||
my $class = $self->bz_class_name;
|
||||
my $method = $self->bz_method_name;
|
||||
my $full_method = $class . "." . $method;
|
||||
|
||||
# Bypass JSONRPC::handle_login
|
||||
Bugzilla::WebService::Server->handle_login($class, $method, $full_method);
|
||||
}
|
||||
|
||||
############################
|
||||
# Private Method Overrides #
|
||||
############################
|
||||
|
||||
# We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure
|
||||
# as it determines the method name differently.
|
||||
sub _find_procedure {
|
||||
my $self = shift;
|
||||
if ($self->isa('JSON::RPC::Server::CGI')) {
|
||||
return JSON::RPC::Server::_find_procedure($self, @_);
|
||||
}
|
||||
else {
|
||||
return JSON::RPC::Legacy::Server::_find_procedure($self, @_);
|
||||
}
|
||||
}
|
||||
|
||||
sub _argument_type_check {
|
||||
my $self = shift;
|
||||
my $params;
|
||||
|
||||
if ($self->isa('JSON::RPC::Server::CGI')) {
|
||||
$params = JSON::RPC::Server::_argument_type_check($self, @_);
|
||||
}
|
||||
else {
|
||||
$params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_);
|
||||
}
|
||||
|
||||
# JSON-RPC 1.0 requires all parameters to be passed as an array, so
|
||||
# we just pull out the first item and assume it's an object.
|
||||
my $params_is_array;
|
||||
if (ref $params eq 'ARRAY') {
|
||||
$params = $params->[0];
|
||||
$params_is_array = 1;
|
||||
}
|
||||
|
||||
taint_data($params);
|
||||
|
||||
Bugzilla->input_params($params);
|
||||
|
||||
# Now, convert dateTime fields on input.
|
||||
my $method = $self->bz_method_name;
|
||||
my $pkg = $self->{dispatch_path}->{$self->path_info};
|
||||
my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
|
||||
foreach my $field (@date_fields) {
|
||||
if (defined $params->{$field}) {
|
||||
my $value = $params->{$field};
|
||||
if (ref $value eq 'ARRAY') {
|
||||
$params->{$field} =
|
||||
[ map { $self->datetime_format_inbound($_) } @$value ];
|
||||
}
|
||||
else {
|
||||
$params->{$field} = $self->datetime_format_inbound($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] };
|
||||
foreach my $field (@base64_fields) {
|
||||
if (defined $params->{$field}) {
|
||||
$params->{$field} = decode_base64($params->{$field});
|
||||
}
|
||||
}
|
||||
|
||||
# This is the best time to do login checks.
|
||||
$self->handle_login();
|
||||
|
||||
# Bugzilla::WebService packages call internal methods like
|
||||
# $self->_some_private_method. So we have to inherit from
|
||||
# that class as well as this Server class.
|
||||
my $new_class = ref($self) . '::' . $pkg;
|
||||
my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
|
||||
eval "package $new_class;$isa_string;";
|
||||
bless $self, $new_class;
|
||||
|
||||
if ($params_is_array) {
|
||||
$params = [$params];
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
###################
|
||||
# Utility Methods #
|
||||
###################
|
||||
|
||||
sub bz_method_name {
|
||||
my ($self, $method) = @_;
|
||||
$self->{_bz_method_name} = $method if $method;
|
||||
return $self->{_bz_method_name};
|
||||
}
|
||||
|
||||
sub bz_class_name {
|
||||
my ($self, $class) = @_;
|
||||
$self->{_bz_class_name} = $class if $class;
|
||||
return $self->{_bz_class_name};
|
||||
}
|
||||
|
||||
sub bz_success_code {
|
||||
my ($self, $value) = @_;
|
||||
$self->{_bz_success_code} = $value if $value;
|
||||
return $self->{_bz_success_code};
|
||||
}
|
||||
|
||||
sub bz_rest_params {
|
||||
my ($self, $params) = @_;
|
||||
$self->{_bz_rest_params} = $params if $params;
|
||||
return $self->{_bz_rest_params};
|
||||
}
|
||||
|
||||
sub bz_rest_options {
|
||||
my ($self, $options) = @_;
|
||||
$self->{_bz_rest_options} = $options if $options;
|
||||
return $self->{_bz_rest_options};
|
||||
}
|
||||
|
||||
sub rest_include_exclude {
|
||||
my ($params) = @_;
|
||||
|
||||
# _all is same as default columns
|
||||
if ($params->{'include_fields'}
|
||||
&& ($params->{'include_fields'} eq '_all'
|
||||
|| $params->{'include_fields'} eq '_default'))
|
||||
{
|
||||
delete $params->{'include_fields'};
|
||||
delete $params->{'exclude_fields'} if $params->{'exclude_fields'};
|
||||
}
|
||||
|
||||
if ($params->{'include_fields'} && !ref $params->{'include_fields'}) {
|
||||
$params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ];
|
||||
}
|
||||
if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
|
||||
$params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ];
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
##########################
|
||||
# Private Custom Methods #
|
||||
##########################
|
||||
|
||||
sub _retrieve_json_params {
|
||||
my $self = shift;
|
||||
|
||||
# Make a copy of the current input_params rather than edit directly
|
||||
my $params = {};
|
||||
%{$params} = %{ Bugzilla->input_params };
|
||||
|
||||
# First add any params we were able to pull out of the path
|
||||
# based on the resource regexp
|
||||
%{$params} = (%{$params}, %{$self->bz_rest_params}) if $self->bz_rest_params;
|
||||
|
||||
# Merge any additional query key/values with $obj->{params} if not a GET request
|
||||
# We do this manually cause CGI.pm doesn't understand JSON strings.
|
||||
if ($self->request->method ne 'GET') {
|
||||
my $extra_params = {};
|
||||
my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
|
||||
if ($json) {
|
||||
eval { $extra_params = $self->json->decode($json); };
|
||||
if ($@) {
|
||||
ThrowUserError('json_rpc_invalid_params', { err_msg => $@ });
|
||||
}
|
||||
}
|
||||
|
||||
# Allow parameters in the query string if request was not GET.
|
||||
# Note: query string parameters will override any matching params
|
||||
# also specified in the request body.
|
||||
foreach my $param ($self->cgi->url_param()) {
|
||||
$extra_params->{$param} = $self->cgi->url_param($param);
|
||||
}
|
||||
|
||||
%{$params} = (%{$params}, %{$extra_params}) if %{$extra_params};
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
sub _find_resource {
|
||||
my ($self, $path) = @_;
|
||||
|
||||
# Load in the WebService module from the dispatch map and then call
|
||||
# $module->rest_resources to get the resources array ref.
|
||||
my $resources = {};
|
||||
foreach my $module (values %{ $self->{dispatch_path} }) {
|
||||
eval("require $module") || die $@;
|
||||
next if !$module->can('rest_resources');
|
||||
$resources->{$module} = $module->rest_resources;
|
||||
}
|
||||
|
||||
# Use the resources hash from each module loaded earlier to determine
|
||||
# which handler to use based on a regex match of the CGI path.
|
||||
# Also any matches found in the regex will be passed in later to the
|
||||
# handler for possible use.
|
||||
my $request_method = $self->request->method;
|
||||
|
||||
my (@matches, $handler_found, $handler_method, $handler_class);
|
||||
foreach my $class (keys %{ $resources }) {
|
||||
# The resource data for each module needs to be
|
||||
# an array ref with an even number of elements
|
||||
# to work correctly.
|
||||
next if (ref $resources->{$class} ne 'ARRAY'
|
||||
|| scalar @{ $resources->{$class} } % 2 != 0);
|
||||
|
||||
while (my $regex = shift @{ $resources->{$class} }) {
|
||||
my $options_data = shift @{ $resources->{$class} };
|
||||
next if ref $options_data ne 'HASH';
|
||||
|
||||
if (@matches = ($path =~ $regex)) {
|
||||
# If a specific path is accompanied by a OPTIONS request
|
||||
# method, the user is asking for a list of possible request
|
||||
# methods for a specific path.
|
||||
$self->bz_rest_options([ keys %{ $options_data } ]);
|
||||
|
||||
if ($options_data->{$request_method}) {
|
||||
my $resource_data = $options_data->{$request_method};
|
||||
$self->bz_class_name($class);
|
||||
|
||||
# The method key/value can be a simple scalar method name
|
||||
# or a anonymous subroutine so we execute it here.
|
||||
my $method = ref $resource_data->{method} eq 'CODE'
|
||||
? $resource_data->{method}->($self)
|
||||
: $resource_data->{method};
|
||||
$self->bz_method_name($method);
|
||||
|
||||
# Pull out any parameters parsed from the URL path
|
||||
# and store them for use by the method.
|
||||
if ($resource_data->{params}) {
|
||||
$self->bz_rest_params($resource_data->{params}->(@matches));
|
||||
}
|
||||
|
||||
# If a special success code is needed for this particular
|
||||
# method, then store it for later when generating response.
|
||||
if ($resource_data->{success_code}) {
|
||||
$self->bz_success_code($resource_data->{success_code});
|
||||
}
|
||||
$handler_found = 1;
|
||||
}
|
||||
}
|
||||
last if $handler_found;
|
||||
}
|
||||
last if $handler_found;
|
||||
}
|
||||
|
||||
return $handler_found;
|
||||
}
|
||||
|
||||
sub _best_content_type {
|
||||
my ($self, @types) = @_;
|
||||
return ($self->_simple_content_negotiation(@types))[0] || '*/*';
|
||||
}
|
||||
|
||||
sub _simple_content_negotiation {
|
||||
my ($self, @types) = @_;
|
||||
my @accept_types = $self->_get_content_prefs();
|
||||
my $score = sub { $self->_score_type(shift, @accept_types) };
|
||||
return sort {$score->($b) <=> $score->($a)} @types;
|
||||
}
|
||||
|
||||
sub _score_type {
|
||||
my ($self, $type, @accept_types) = @_;
|
||||
my $score = scalar(@accept_types);
|
||||
for my $accept_type (@accept_types) {
|
||||
return $score if $type eq $accept_type;
|
||||
$score--;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub _get_content_prefs {
|
||||
my $self = shift;
|
||||
my $default_weight = 1;
|
||||
my @prefs;
|
||||
|
||||
# Parse the Accept header, and save type name, score, and position.
|
||||
my @accept_types = split /,/, $self->cgi->http('accept') || '';
|
||||
my $order = 0;
|
||||
for my $accept_type (@accept_types) {
|
||||
my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
|
||||
my ($name) = ($accept_type =~ m#(\S+/[^;]+)#);
|
||||
next unless $name;
|
||||
push @prefs, { name => $name, order => $order++};
|
||||
if (defined $weight) {
|
||||
$prefs[-1]->{score} = $weight;
|
||||
} else {
|
||||
$prefs[-1]->{score} = $default_weight;
|
||||
$default_weight -= 0.001;
|
||||
}
|
||||
}
|
||||
|
||||
# Sort the types by score, subscore by order, and pull out just the name
|
||||
@prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} ||
|
||||
$a->{order} <=> $b->{order}} @prefs;
|
||||
return @prefs, '*/*'; # Allows allow for */*
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::WebService::Server::REST - The REST Interface to Bugzilla
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This documentation describes things about the Bugzilla WebService that
|
||||
are specific to REST. For a general overview of the Bugzilla WebServices,
|
||||
see L<Bugzilla::WebService>. The L<Bugzilla::WebService::Server::REST>
|
||||
module is a sub-class of L<Bugzilla::WebService::Server::JSONRPC> so any
|
||||
method documentation not found here can be viewed in it's POD.
|
||||
|
||||
Please note that I<everything> about this REST interface is
|
||||
B<EXPERIMENTAL>. If you want a fully stable API, please use the
|
||||
C<Bugzilla::WebService::Server::XMLRPC|XML-RPC> interface.
|
||||
|
||||
=head1 CONNECTING
|
||||
|
||||
The endpoint for the REST interface is the C<rest.cgi> script in
|
||||
your Bugzilla installation. If using Apache and mod_rewrite is installed
|
||||
and enabled, you can also use /rest/ as your endpoint. For example, if your
|
||||
Bugzilla is at C<bugzilla.yourdomain.com>, then your REST client would
|
||||
access the API via: C<http://bugzilla.yourdomain.com/rest/bug/35> which
|
||||
looks cleaner.
|
||||
|
||||
=head1 BROWSING
|
||||
|
||||
If the Accept: header of a request is set to text/html (as it is by an
|
||||
ordinary web browser) then the API will return the JSON data as a HTML
|
||||
page which the browser can display. In other words, you can play with the
|
||||
API using just your browser and see results in a human-readable form.
|
||||
This is a good way to try out the various GET calls, even if you can't use
|
||||
it for POST or PUT.
|
||||
|
||||
=head1 DATA FORMAT
|
||||
|
||||
The REST API only supports JSON input, and either JSON and JSONP output.
|
||||
So objects sent and received must be in JSON format. Basically since
|
||||
the REST API is a sub class of the JSONRPC API, you can refer to
|
||||
L<JSONRPC|Bugzilla::WebService::Server::JSONRPC> for more information
|
||||
on data types that are valid for REST.
|
||||
|
||||
On every request, you must set both the "Accept" and "Content-Type" HTTP
|
||||
headers to the MIME type of the data format you are using to communicate with
|
||||
the API. Content-Type tells the API how to interpret your request, and Accept
|
||||
tells it how you want your data back. "Content-Type" must be "application/json".
|
||||
"Accept" can be either that, or "application/javascript" for JSONP - add a "callback"
|
||||
parameter to name your callback.
|
||||
|
||||
Parameters may also be passed in as part of the query string for non-GET requests
|
||||
and will override any matching parameters in the request body.
|
||||
|
||||
=head1 AUTHENTICATION
|
||||
|
||||
Along with viewing data as an anonymous user, you may also see private information
|
||||
if you have a Bugzilla account by providing your login credentials.
|
||||
|
||||
=over
|
||||
|
||||
=item Login name and password
|
||||
|
||||
Pass in as query parameters of any request:
|
||||
|
||||
login=fred@example.com&password=ilovecheese
|
||||
|
||||
Remember to URL encode any special characters, which are often seen in passwords and to
|
||||
also enable SSL support.
|
||||
|
||||
=item Login token
|
||||
|
||||
By calling GET /login?login=fred@example.com&password=ilovecheese, you get back
|
||||
a C<token> value which can then be passed to each subsequent call as
|
||||
authentication. This is useful for third party clients that cannot use cookies
|
||||
and do not want to store a user's login and password in the client. You can also
|
||||
pass in "token" as a convenience.
|
||||
|
||||
=back
|
||||
|
||||
=head1 ERRORS
|
||||
|
||||
When an error occurs over REST, a hash structure is returned with the key C<error>
|
||||
set to C<true>.
|
||||
|
||||
The error contents look similar to:
|
||||
|
||||
{ "error": true, "message": "Some message here", "code": 123 }
|
||||
|
||||
Every error has a "code", as described in L<Bugzilla::WebService/ERRORS>.
|
||||
Errors with a numeric C<code> higher than 100000 are errors thrown by
|
||||
the JSON-RPC library that Bugzilla uses, not by Bugzilla.
|
||||
|
||||
=head1 UTILITY FUNCTIONS
|
||||
|
||||
=over
|
||||
|
||||
=item B<handle>
|
||||
|
||||
This method overrides the handle method provided by JSONRPC so that certain
|
||||
actions related to REST such as determining the proper resource to use,
|
||||
loading query parameters, etc. can be done before the proper WebService
|
||||
method is executed.
|
||||
|
||||
=item B<response>
|
||||
|
||||
This method overrides the response method provided by JSONRPC so that
|
||||
the response content can be altered for REST before being returned to
|
||||
the client.
|
||||
|
||||
=item B<handle_login>
|
||||
|
||||
This method determines the proper WebService all to make based on class
|
||||
and method name determined earlier. Then calls L<Bugzilla::WebService::Server::handle_login>
|
||||
which will attempt to authenticate the client.
|
||||
|
||||
=item B<bz_method_name>
|
||||
|
||||
The WebService method name that matches the path used by the client.
|
||||
|
||||
=item B<bz_class_name>
|
||||
|
||||
The WebService class containing the method that matches the path used by the client.
|
||||
|
||||
=item B<bz_rest_params>
|
||||
|
||||
Each REST resource contains a hash key called C<params> that is a subroutine reference.
|
||||
This subroutine will return a hash structure based on matched values from the path
|
||||
information that is formatted properly for the WebService method that will be called.
|
||||
|
||||
=item B<bz_rest_options>
|
||||
|
||||
When a client uses the OPTIONS request method along with a specific path, they are
|
||||
requesting the list of request methods that are valid for the path. Such as for the
|
||||
path /bug, the valid request methods are GET (search) and POST (create). So the
|
||||
client would receive in the response header, C<Access-Control-Allow-Methods: GET, POST>.
|
||||
|
||||
=item B<bz_success_code>
|
||||
|
||||
Each resource can specify a specific SUCCESS CODE if the operation completes successfully.
|
||||
OTherwise STATUS OK (200) is the default returned.
|
||||
|
||||
=item B<rest_include_exclude>
|
||||
|
||||
Normally the WebService methods required C<include_fields> and C<exclude_fields> to be an
|
||||
array of field names. REST allows for the values for these to be instead comma delimited
|
||||
string of field names. This method converts the latter into the former so the WebService
|
||||
methods will not complain.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::WebService>
|
|
@ -0,0 +1,158 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Server::REST::Resources::Bug;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Bug;
|
||||
|
||||
BEGIN {
|
||||
*Bugzilla::WebService::Bug::rest_resources = \&_rest_resources;
|
||||
};
|
||||
|
||||
sub _rest_resources {
|
||||
my $rest_resources = [
|
||||
qr{^/bug$}, {
|
||||
GET => {
|
||||
method => 'search',
|
||||
},
|
||||
POST => {
|
||||
method => 'create',
|
||||
status_code => STATUS_CREATED
|
||||
}
|
||||
},
|
||||
qr{^/bug/([^/]+)$}, {
|
||||
GET => {
|
||||
method => 'get',
|
||||
params => sub {
|
||||
return { ids => [ $_[0] ] };
|
||||
}
|
||||
},
|
||||
PUT => {
|
||||
method => 'update',
|
||||
params => sub {
|
||||
return { ids => [ $_[0] ] };
|
||||
}
|
||||
}
|
||||
},
|
||||
qr{^/bug/([^/]+)/comment$}, {
|
||||
GET => {
|
||||
method => 'comments',
|
||||
params => sub {
|
||||
return { ids => [ $_[0] ] };
|
||||
}
|
||||
},
|
||||
POST => {
|
||||
method => 'add_comment',
|
||||
params => sub {
|
||||
return { id => $_[0] };
|
||||
},
|
||||
success_code => STATUS_CREATED
|
||||
}
|
||||
},
|
||||
qr{^/bug/comment/([^/]+)$}, {
|
||||
GET => {
|
||||
method => 'comments',
|
||||
params => sub {
|
||||
return { comment_ids => [ $_[0] ] };
|
||||
}
|
||||
}
|
||||
},
|
||||
qr{^/bug/([^/]+)/history$}, {
|
||||
GET => {
|
||||
method => 'history',
|
||||
params => sub {
|
||||
return { ids => [ $_[0] ] };
|
||||
},
|
||||
}
|
||||
},
|
||||
qr{^/bug/([^/]+)/attachment$}, {
|
||||
GET => {
|
||||
method => 'attachments',
|
||||
params => sub {
|
||||
return { ids => [ $_[0] ] };
|
||||
}
|
||||
},
|
||||
POST => {
|
||||
method => 'add_attachment',
|
||||
params => sub {
|
||||
return { ids => [ $_[0] ] };
|
||||
},
|
||||
success_code => STATUS_CREATED
|
||||
}
|
||||
},
|
||||
qr{^/bug/attachment/([^/]+)$}, {
|
||||
GET => {
|
||||
method => 'attachments',
|
||||
params => sub {
|
||||
return { attachment_ids => [ $_[0] ] };
|
||||
}
|
||||
},
|
||||
PUT => {
|
||||
method => 'update_attachment',
|
||||
params => sub {
|
||||
return { ids => [ $_[0] ] };
|
||||
}
|
||||
}
|
||||
},
|
||||
qr{^/field/bug$}, {
|
||||
GET => {
|
||||
method => 'fields',
|
||||
}
|
||||
},
|
||||
qr{^/field/bug/([^/]+)$}, {
|
||||
GET => {
|
||||
method => 'fields',
|
||||
params => sub {
|
||||
my $value = $_[0];
|
||||
my $param = 'names';
|
||||
$param = 'ids' if $value =~ /^\d+$/;
|
||||
return { $param => [ $_[0] ] };
|
||||
}
|
||||
}
|
||||
},
|
||||
qr{^/field/bug/([^/]+)/values$}, {
|
||||
GET => {
|
||||
method => 'legal_values',
|
||||
params => sub {
|
||||
return { field => $_[0] };
|
||||
}
|
||||
}
|
||||
},
|
||||
qr{^/field/bug/([^/]+)/([^/]+)/values$}, {
|
||||
GET => {
|
||||
method => 'legal_values',
|
||||
params => sub {
|
||||
return { field => $_[0],
|
||||
product_id => $_[1] };
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
];
|
||||
return $rest_resources;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Webservice::Server::REST::Resources::Bug - The REST API for creating,
|
||||
changing, and getting the details of bugs.
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla REST API allows you to file a new bug in Bugzilla,
|
||||
or get information about bugs that have already been filed.
|
||||
|
||||
See L<Bugzilla::WebService::Bug> for more details on how to use this part of
|
||||
the REST API.
|
|
@ -0,0 +1,69 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Server::REST::Resources::Bugzilla;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Bugzilla;
|
||||
|
||||
BEGIN {
|
||||
*Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources;
|
||||
};
|
||||
|
||||
sub _rest_resources {
|
||||
my $rest_resources = [
|
||||
qr{^/version$}, {
|
||||
GET => {
|
||||
method => 'version'
|
||||
}
|
||||
},
|
||||
qr{^/extensions$}, {
|
||||
GET => {
|
||||
method => 'extensions'
|
||||
}
|
||||
},
|
||||
qr{^/timezone$}, {
|
||||
GET => {
|
||||
method => 'timezone'
|
||||
}
|
||||
},
|
||||
qr{^/time$}, {
|
||||
GET => {
|
||||
method => 'time'
|
||||
}
|
||||
},
|
||||
qr{^/last_audit_time$}, {
|
||||
GET => {
|
||||
method => 'last_audit_time'
|
||||
}
|
||||
},
|
||||
qr{^/parameters$}, {
|
||||
GET => {
|
||||
method => 'parameters'
|
||||
}
|
||||
}
|
||||
];
|
||||
return $rest_resources;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::WebService::Bugzilla - Global functions for the webservice interface.
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This provides functions that tell you about Bugzilla in general.
|
||||
|
||||
See L<Bugzilla::WebService::Bugzilla> for more details on how to use this part
|
||||
of the REST API.
|
|
@ -0,0 +1,49 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Server::REST::Resources::Classification;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Classification;
|
||||
|
||||
BEGIN {
|
||||
*Bugzilla::WebService::Classification::rest_resources = \&_rest_resources;
|
||||
};
|
||||
|
||||
sub _rest_resources {
|
||||
my $rest_resources = [
|
||||
qr{^/classification/([^/]+)$}, {
|
||||
GET => {
|
||||
method => 'get',
|
||||
params => sub {
|
||||
my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
|
||||
return { $param => [ $_[0] ] };
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
return $rest_resources;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Webservice::Server::REST::Resources::Classification - The Classification REST API
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla REST API allows you to deal with the available Classifications.
|
||||
You will be able to get information about them as well as manipulate them.
|
||||
|
||||
See L<Bugzilla::WebService::Classification> for more details on how to use this part
|
||||
of the REST API.
|
|
@ -0,0 +1,56 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Server::REST::Resources::Group;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Group;
|
||||
|
||||
BEGIN {
|
||||
*Bugzilla::WebService::Group::rest_resources = \&_rest_resources;
|
||||
};
|
||||
|
||||
sub _rest_resources {
|
||||
my $rest_resources = [
|
||||
qr{^/group$}, {
|
||||
POST => {
|
||||
method => 'create',
|
||||
success_code => STATUS_CREATED
|
||||
}
|
||||
},
|
||||
qr{^/group/([^/]+)$}, {
|
||||
PUT => {
|
||||
method => 'update',
|
||||
params => sub {
|
||||
my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
|
||||
return { $param => [ $_[0] ] };
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
return $rest_resources;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Webservice::Server::REST::Resources::Group - The REST API for
|
||||
creating, changing, and getting information about Groups.
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla REST API allows you to create Groups and
|
||||
get information about them.
|
||||
|
||||
See L<Bugzilla::WebService::Group> for more details on how to use this part
|
||||
of the REST API.
|
|
@ -0,0 +1,82 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Server::REST::Resources::Product;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Product;
|
||||
|
||||
use Bugzilla::Error;
|
||||
|
||||
BEGIN {
|
||||
*Bugzilla::WebService::Product::rest_resources = \&_rest_resources;
|
||||
};
|
||||
|
||||
sub _rest_resources {
|
||||
my $rest_resources = [
|
||||
qr{^/product_accessible$}, {
|
||||
GET => {
|
||||
method => 'get_accessible_products'
|
||||
}
|
||||
},
|
||||
qr{^/product_enterable$}, {
|
||||
GET => {
|
||||
method => 'get_enterable_products'
|
||||
}
|
||||
},
|
||||
qr{^/product_selectable$}, {
|
||||
GET => {
|
||||
method => 'get_selectable_products'
|
||||
}
|
||||
},
|
||||
qr{^/product$}, {
|
||||
GET => {
|
||||
method => 'get'
|
||||
},
|
||||
POST => {
|
||||
method => 'create',
|
||||
success_code => STATUS_CREATED
|
||||
}
|
||||
},
|
||||
qr{^/product/([^/]+)$}, {
|
||||
GET => {
|
||||
method => 'get',
|
||||
params => sub {
|
||||
my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
|
||||
return { $param => [ $_[0] ] };
|
||||
}
|
||||
},
|
||||
PUT => {
|
||||
method => 'update',
|
||||
params => sub {
|
||||
my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
|
||||
return { $param => [ $_[0] ] };
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
return $rest_resources;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Webservice::Server::REST::Resources::Product - The Product REST API
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla REST API allows you to list the available Products and
|
||||
get information about them.
|
||||
|
||||
See L<Bugzilla::WebService::Product> for more details on how to use this part of
|
||||
the REST API.
|
|
@ -0,0 +1,80 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Server::REST::Resources::User;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::User;
|
||||
|
||||
BEGIN {
|
||||
*Bugzilla::WebService::User::rest_resources = \&_rest_resources;
|
||||
};
|
||||
|
||||
sub _rest_resources {
|
||||
my $rest_resources = [
|
||||
qr{^/login$}, {
|
||||
GET => {
|
||||
method => 'login'
|
||||
}
|
||||
},
|
||||
qr{^/logout$}, {
|
||||
GET => {
|
||||
method => 'logout'
|
||||
}
|
||||
},
|
||||
qr{^/valid_login$}, {
|
||||
GET => {
|
||||
method => 'valid_login'
|
||||
}
|
||||
},
|
||||
qr{^/user$}, {
|
||||
GET => {
|
||||
method => 'get'
|
||||
},
|
||||
POST => {
|
||||
method => 'create',
|
||||
success_code => STATUS_CREATED
|
||||
}
|
||||
},
|
||||
qr{^/user/([^/]+)$}, {
|
||||
GET => {
|
||||
method => 'get',
|
||||
params => sub {
|
||||
my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
|
||||
return { $param => [ $_[0] ] };
|
||||
}
|
||||
},
|
||||
PUT => {
|
||||
method => 'update',
|
||||
params => sub {
|
||||
my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
|
||||
return { $param => [ $_[0] ] };
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
return $rest_resources;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Webservice::Server::REST::Resources::User - The User Account REST API
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla REST API allows you to get User information as well
|
||||
as create User Accounts.
|
||||
|
||||
See L<Bugzilla::WebService::User> for more details on how to use this part of
|
||||
the REST API.
|
|
@ -21,8 +21,8 @@ if ($ENV{MOD_PERL}) {
|
|||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::Util;
|
||||
|
||||
# Allow WebService methods to call XMLRPC::Lite's type method directly
|
||||
BEGIN {
|
||||
# Allow WebService methods to call XMLRPC::Lite's type method directly
|
||||
*Bugzilla::WebService::type = sub {
|
||||
my ($self, $type, $value) = @_;
|
||||
if ($type eq 'dateTime') {
|
||||
|
@ -39,6 +39,11 @@ BEGIN {
|
|||
}
|
||||
return XMLRPC::Data->type($type)->value($value);
|
||||
};
|
||||
|
||||
# Add support for ETags into XMLRPC WebServices
|
||||
*Bugzilla::WebService::bz_etag = sub {
|
||||
return Bugzilla::WebService::Server->bz_etag($_[1]);
|
||||
};
|
||||
}
|
||||
|
||||
sub initialize {
|
||||
|
@ -52,13 +57,37 @@ sub initialize {
|
|||
|
||||
sub make_response {
|
||||
my $self = shift;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
$self->SUPER::make_response(@_);
|
||||
|
||||
# XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
|
||||
# its cookies in Bugzilla::CGI, so we need to copy them over.
|
||||
foreach (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
|
||||
$self->response->headers->push_header('Set-Cookie', $_);
|
||||
foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) {
|
||||
$self->response->headers->push_header('Set-Cookie', $cookie);
|
||||
}
|
||||
|
||||
# Copy across security related headers from Bugzilla::CGI
|
||||
foreach my $header (split(/[\r\n]+/, $cgi->header)) {
|
||||
my ($name, $value) = $header =~ /^([^:]+): (.*)/;
|
||||
if (!$self->response->headers->header($name)) {
|
||||
$self->response->headers->header($name => $value);
|
||||
}
|
||||
}
|
||||
|
||||
# ETag support
|
||||
my $etag = $self->bz_etag;
|
||||
if (!$etag) {
|
||||
my $data = $self->response->as_string;
|
||||
$etag = $self->bz_etag($data);
|
||||
}
|
||||
|
||||
if ($etag && $cgi->check_etag($etag)) {
|
||||
$self->response->headers->push_header('ETag', $etag);
|
||||
$self->response->headers->push_header('status', '304 Not Modified');
|
||||
}
|
||||
elsif ($etag) {
|
||||
$self->response->headers->push_header('ETag', $etag);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -291,7 +320,7 @@ package Bugzilla::XMLRPC::Serializer;
|
|||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use Scalar::Util qw(blessed);
|
||||
use Scalar::Util qw(blessed reftype);
|
||||
# We can't use "use parent" because XMLRPC::Serializer doesn't return
|
||||
# a true value.
|
||||
use XMLRPC::Lite;
|
||||
|
@ -325,8 +354,8 @@ sub envelope {
|
|||
my $self = shift;
|
||||
my ($type, $method, $data) = @_;
|
||||
# If the type isn't a successful response we don't want to change the values.
|
||||
if ($type eq 'response'){
|
||||
$data = _strip_undefs($data);
|
||||
if ($type eq 'response') {
|
||||
_strip_undefs($data);
|
||||
}
|
||||
return $self->SUPER::envelope($type, $method, $data);
|
||||
}
|
||||
|
@ -337,7 +366,9 @@ sub envelope {
|
|||
# so it cannot be recursed like the other hash type objects.
|
||||
sub _strip_undefs {
|
||||
my ($initial) = @_;
|
||||
if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) {
|
||||
my $type = reftype($initial) or return;
|
||||
|
||||
if ($type eq "HASH") {
|
||||
while (my ($key, $value) = each(%$initial)) {
|
||||
if ( !defined $value
|
||||
|| (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
|
||||
|
@ -346,11 +377,11 @@ sub _strip_undefs {
|
|||
delete $initial->{$key};
|
||||
}
|
||||
else {
|
||||
$initial->{$key} = _strip_undefs($value);
|
||||
_strip_undefs($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) {
|
||||
elsif ($type eq "ARRAY") {
|
||||
for (my $count = 0; $count < scalar @{$initial}; $count++) {
|
||||
my $value = $initial->[$count];
|
||||
if ( !defined $value
|
||||
|
@ -361,11 +392,10 @@ sub _strip_undefs {
|
|||
$count--;
|
||||
}
|
||||
else {
|
||||
$initial->[$count] = _strip_undefs($value);
|
||||
_strip_undefs($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $initial;
|
||||
}
|
||||
|
||||
sub BEGIN {
|
||||
|
|
|
@ -19,6 +19,8 @@ use Bugzilla::User;
|
|||
use Bugzilla::Util qw(trim);
|
||||
use Bugzilla::WebService::Util qw(filter validate translate params_to_objects);
|
||||
|
||||
use List::Util qw(first);
|
||||
|
||||
# Don't need auth to login
|
||||
use constant LOGIN_EXEMPT => {
|
||||
login => 1,
|
||||
|
@ -73,14 +75,36 @@ sub login {
|
|||
$input_params->{'Bugzilla_password'} = $params->{password};
|
||||
$input_params->{'Bugzilla_remember'} = $remember;
|
||||
|
||||
Bugzilla->login();
|
||||
return { id => $self->type('int', Bugzilla->user->id) };
|
||||
my $user = Bugzilla->login();
|
||||
|
||||
my $result = { id => $self->type('int', $user->id) };
|
||||
|
||||
# We will use the stored cookie value combined with the user id
|
||||
# to create a token that can be used with future requests in the
|
||||
# query parameters
|
||||
my $login_cookie = first { $_->name eq 'Bugzilla_logincookie' }
|
||||
@{ Bugzilla->cgi->{'Bugzilla_cookie_list'} };
|
||||
if ($login_cookie) {
|
||||
$result->{'token'} = $user->id . "-" . $login_cookie->value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
sub logout {
|
||||
my $self = shift;
|
||||
Bugzilla->logout;
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub valid_login {
|
||||
my ($self, $params) = @_;
|
||||
defined $params->{login}
|
||||
|| ThrowCodeError('param_required', { param => 'login' });
|
||||
Bugzilla->login();
|
||||
if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
|
||||
return $self->type('boolean', 1);
|
||||
}
|
||||
return $self->type('boolean', 0);
|
||||
}
|
||||
|
||||
#################
|
||||
|
@ -130,7 +154,7 @@ sub create {
|
|||
# $call = $rpc->call( 'User.get', { match => [ 'testusera', 'testuserb' ],
|
||||
# maxusermatches => 20, excludedisabled => 1 });
|
||||
sub get {
|
||||
my ($self, $params) = validate(@_, 'names', 'ids');
|
||||
my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
|
||||
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
|
||||
|
@ -408,6 +432,10 @@ log in/out using an existing account.
|
|||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=head1 Logging In and Out
|
||||
|
||||
=head2 login
|
||||
|
@ -426,7 +454,7 @@ etc. This method logs in an user.
|
|||
|
||||
=over
|
||||
|
||||
=item C<login> (string) - The user's login name.
|
||||
=item C<login> (string) - The user's login name.
|
||||
|
||||
=item C<password> (string) - The user's password.
|
||||
|
||||
|
@ -442,10 +470,14 @@ management of cookies across sessions.
|
|||
|
||||
=item B<Returns>
|
||||
|
||||
On success, a hash containing one item, C<id>, the numeric id of the
|
||||
user that was logged in. A set of http cookies is also sent with the
|
||||
response. These cookies must be sent along with any future requests
|
||||
to the webservice, for the duration of the session.
|
||||
On success, a hash containing two items, C<id>, the numeric id of the
|
||||
user that was logged in, and a C<token> which can be passed in
|
||||
the parameters as authentication in other calls. A set of http cookies
|
||||
is also sent with the response. These cookies *or* the token can be sent
|
||||
along with any future requests to the webservice, for the duration of the
|
||||
session. Note that cookies are not accepted for GET requests for JSONRPC
|
||||
and REST for security reasons. You may, however, use the token or valid
|
||||
login parameters for those requests.
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
|
@ -491,6 +523,50 @@ Log out the user. Does nothing if there is no user logged in.
|
|||
|
||||
=back
|
||||
|
||||
=head2 valid_login
|
||||
|
||||
B<UNSTABLE>
|
||||
|
||||
=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>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head1 Account Creation and Modification
|
||||
|
||||
=head2 offer_account_by_email
|
||||
|
@ -550,6 +626,13 @@ actually receive an email. This function does not check that.
|
|||
You must be logged in and have the C<editusers> privilege in order to
|
||||
call this function.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
POST /user
|
||||
|
||||
The params to include in the POST body as well as the returned data format,
|
||||
are the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
@ -593,6 +676,8 @@ password is under three characters.)
|
|||
|
||||
=item Error 503 (Password Too Long) removed in Bugzilla B<3.6>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -607,6 +692,14 @@ B<EXPERIMENTAL>
|
|||
|
||||
Updates user accounts in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
PUT /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
|
||||
|
@ -693,6 +786,14 @@ Logged-in users are not authorized to edit other users.
|
|||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head1 User Info
|
||||
|
@ -707,6 +808,18 @@ B<STABLE>
|
|||
|
||||
Gets information about user accounts in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To get information about a single user:
|
||||
|
||||
GET /user/<user_id_or_name>
|
||||
|
||||
To search for users by name, group using URL params same as below:
|
||||
|
||||
GET /user
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified.
|
||||
|
@ -929,6 +1042,8 @@ illegal to pass a group name you don't belong to.
|
|||
=item C<groups>, C<saved_searches>, and C<saved_reports> were added
|
||||
in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
|
@ -23,6 +23,7 @@ our @EXPORT_OK = qw(
|
|||
validate
|
||||
translate
|
||||
params_to_objects
|
||||
fix_credentials
|
||||
);
|
||||
|
||||
sub filter ($$;$) {
|
||||
|
@ -38,19 +39,38 @@ sub filter ($$;$) {
|
|||
|
||||
sub filter_wants ($$;$) {
|
||||
my ($params, $field, $prefix) = @_;
|
||||
|
||||
# Since this is operation is resource intensive, we will cache the results
|
||||
# This assumes that $params->{*_fields} doesn't change between calls
|
||||
my $cache = Bugzilla->request_cache->{filter_wants} ||= {};
|
||||
$field = "${prefix}.${field}" if $prefix;
|
||||
|
||||
if (exists $cache->{$field}) {
|
||||
return $cache->{$field};
|
||||
}
|
||||
|
||||
my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
|
||||
my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
|
||||
|
||||
$field = "${prefix}.${field}" if $prefix;
|
||||
|
||||
if (defined $params->{include_fields}) {
|
||||
return 0 if !$include{$field};
|
||||
my $wants = 1;
|
||||
if (defined $params->{exclude_fields} && $exclude{$field}) {
|
||||
$wants = 0;
|
||||
}
|
||||
if (defined $params->{exclude_fields}) {
|
||||
return 0 if $exclude{$field};
|
||||
elsif (defined $params->{include_fields} && !$include{$field}) {
|
||||
if ($prefix) {
|
||||
# Include the field if the parent is include (and this one is not excluded)
|
||||
$wants = 0 if !$include{$prefix};
|
||||
}
|
||||
else {
|
||||
# We want to include this if one of the sub keys is included
|
||||
my $key = $field . '.';
|
||||
my $len = length($key);
|
||||
$wants = 0 if ! grep { substr($_, 0, $len) eq $key } keys %include;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
$cache->{$field} = $wants;
|
||||
return $wants;
|
||||
}
|
||||
|
||||
sub taint_data {
|
||||
|
@ -127,6 +147,22 @@ sub params_to_objects {
|
|||
return \@objects;
|
||||
}
|
||||
|
||||
sub fix_credentials {
|
||||
my ($params) = @_;
|
||||
# Allow user to pass in login=foo&password=bar as a convenience
|
||||
# even if not calling GET /login. We also do not delete them as
|
||||
# GET /login requires "login" and "password".
|
||||
if (exists $params->{'login'} && exists $params->{'password'}) {
|
||||
$params->{'Bugzilla_login'} = $params->{'login'};
|
||||
$params->{'Bugzilla_password'} = $params->{'password'};
|
||||
}
|
||||
# Allow user to pass token=12345678 as a convenience which becomes
|
||||
# "Bugzilla_token" which is what the auth code looks for.
|
||||
if (exists $params->{'token'}) {
|
||||
$params->{'Bugzilla_token'} = $params->{'token'};
|
||||
}
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
@ -190,6 +226,12 @@ Helps make life simpler for WebService methods that internally create objects
|
|||
via both "ids" and "names" fields. Also de-duplicates objects that were loaded
|
||||
by both "ids" and "names". Returns an arrayref of objects.
|
||||
|
||||
=head2 fix_credentials
|
||||
|
||||
Allows for certain parameters related to authentication such as Bugzilla_login,
|
||||
Bugzilla_password, and Bugzilla_token to have shorter named equivalents passed in.
|
||||
This function converts the shorter versions to their respective internal names.
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
|
79
buglist.cgi
79
buglist.cgi
|
@ -27,11 +27,6 @@ use Bugzilla::Token;
|
|||
|
||||
use Time::HiRes qw(gettimeofday);
|
||||
use Date::Parse;
|
||||
use POSIX;
|
||||
|
||||
# FIXME TRASHCODE!!! MUST BE REFACTORED!!!
|
||||
# For example: buglist.cgi?dotweak=1&format=superworktime => $vars->{token} will be incorrect
|
||||
use Time::HiRes qw(gettimeofday tv_interval);
|
||||
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
@ -151,7 +146,7 @@ my $serverpush =
|
|||
&& exists $ENV{'HTTP_USER_AGENT'}
|
||||
&& $ENV{'HTTP_USER_AGENT'} =~ /(Mozilla.[3-9]|Opera)/
|
||||
&& $ENV{'HTTP_USER_AGENT'} !~ /compatible/i
|
||||
&& $ENV{'HTTP_USER_AGENT'} !~ /WebKit/
|
||||
&& $ENV{'HTTP_USER_AGENT'} !~ /(?:WebKit|Trident|KHTML)/
|
||||
&& !defined($cgi->param('serverpush'))
|
||||
|| $cgi->param('serverpush');
|
||||
|
||||
|
@ -326,9 +321,10 @@ sub GetGroups {
|
|||
}
|
||||
|
||||
sub _close_standby_message {
|
||||
my ($contenttype, $disposition, $serverpush) = @_;
|
||||
my ($contenttype, $disp, $disp_prefix, $extension, $serverpush) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
$cgi->set_dated_content_disp($disp, $disp_prefix, $extension);
|
||||
|
||||
# Close the "please wait" page, then open the buglist page
|
||||
if ($serverpush) {
|
||||
$cgi->send_multipart_end();
|
||||
|
@ -369,17 +365,10 @@ $params ||= new Bugzilla::CGI($cgi);
|
|||
# if available. We have to do this now, even though we return HTTP headers
|
||||
# at the end, because the fact that there is a remembered query gets
|
||||
# forgotten in the process of retrieving it.
|
||||
my @time = localtime(time());
|
||||
my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
|
||||
my $filename = "bugs-$date.$format->{extension}";
|
||||
my $disp_prefix = "bugs";
|
||||
if ($cmdtype eq "dorem" && $remaction =~ /^run/) {
|
||||
$filename = $cgi->param('namedcmd') . "-$date.$format->{extension}";
|
||||
# Remove white-space from the filename so the user cannot tamper
|
||||
# with the HTTP headers.
|
||||
$filename =~ s/\s/_/g;
|
||||
$disp_prefix = $cgi->param('namedcmd');
|
||||
}
|
||||
$filename =~ s/\\/\\\\/g; # escape backslashes
|
||||
$filename =~ s/"/\\"/g; # escape quotes
|
||||
|
||||
# Take appropriate action based on user's request.
|
||||
if ($cmdtype eq "dorem") {
|
||||
|
@ -809,18 +798,6 @@ if ($superworktime)
|
|||
# Query Execution
|
||||
################################################################################
|
||||
|
||||
if ($cgi->param('debug')) {
|
||||
$vars->{'debug'} = 1;
|
||||
$vars->{'query'} = $query;
|
||||
# Explains are limited to admins because you could use them to figure
|
||||
# out how many hidden bugs are in a particular product (by doing
|
||||
# searches and looking at the number of rows the explain says it's
|
||||
# examining).
|
||||
if ($user->in_group('admin')) {
|
||||
$vars->{'query_explain'} = $dbh->bz_explain($query);
|
||||
}
|
||||
}
|
||||
|
||||
# Time to use server push to display an interim message to the user until
|
||||
# the query completes and we can display the bug list.
|
||||
if ($serverpush) {
|
||||
|
@ -856,10 +833,28 @@ $::SIG{PIPE} = 'DEFAULT';
|
|||
my $query_sql_time = gettimeofday();
|
||||
|
||||
# Execute the query.
|
||||
my $start_time = [gettimeofday()];
|
||||
my $buglist_sth = $dbh->prepare($query);
|
||||
$buglist_sth->execute();
|
||||
$vars->{query_time} = tv_interval($start_time);
|
||||
my ($data, $extra_data) = $search->data;
|
||||
$vars->{'search_description'} = $search->search_description;
|
||||
|
||||
if ($cgi->param('debug')
|
||||
&& Bugzilla->params->{debug_group}
|
||||
&& $user->in_group(Bugzilla->params->{debug_group})
|
||||
) {
|
||||
$vars->{'debug'} = 1;
|
||||
$vars->{'queries'} = $extra_data;
|
||||
my $query_time = 0;
|
||||
$query_time += $_->{'time'} foreach @$extra_data;
|
||||
$vars->{'query_time'} = $query_time;
|
||||
# Explains are limited to admins because you could use them to figure
|
||||
# out how many hidden bugs are in a particular product (by doing
|
||||
# searches and looking at the number of rows the explain says it's
|
||||
# examining).
|
||||
if ($user->in_group('admin')) {
|
||||
foreach my $query (@$extra_data) {
|
||||
$query->{explain} = $dbh->bz_explain($query->{sql});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Results Retrieval
|
||||
|
@ -892,14 +887,14 @@ my @bugidlist;
|
|||
|
||||
my @bugs; # the list of records
|
||||
|
||||
while (my @row = $buglist_sth->fetchrow_array()) {
|
||||
foreach my $row (@$data) {
|
||||
my $bug = {}; # a record
|
||||
|
||||
# Slurp the row of data into the record.
|
||||
# The second from last column in the record is the number of groups
|
||||
# to which the bug is restricted.
|
||||
foreach my $column (@selectcolumns) {
|
||||
$bug->{$column} = shift @row;
|
||||
$bug->{$column} = shift @$row;
|
||||
}
|
||||
|
||||
# Process certain values further (i.e. date format conversion).
|
||||
|
@ -1030,7 +1025,10 @@ if (scalar(@bugowners) > 1 && $user->in_group('editbugs')) {
|
|||
# the list more compact.
|
||||
$vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0;
|
||||
|
||||
$vars->{'quip'} = GetQuip();
|
||||
if ($user->settings->{'display_quips'}->{'value'} eq 'on') {
|
||||
$vars->{'quip'} = GetQuip();
|
||||
}
|
||||
|
||||
$vars->{'currenttime'} = localtime(time());
|
||||
|
||||
# See if there's only one product in all the results (or only one product
|
||||
|
@ -1055,7 +1053,8 @@ if ($one_product && $user->can_enter_product($one_product)) {
|
|||
# The following variables are used when the user is making changes to multiple bugs.
|
||||
if ($dotweak && scalar @bugs) {
|
||||
if (!$vars->{'caneditbugs'}) {
|
||||
_close_standby_message('text/html', 'inline', $serverpush);
|
||||
_close_standby_message('text/html',
|
||||
'inline', "error", "html", $serverpush);
|
||||
ThrowUserError('auth_failure', {group => 'editbugs',
|
||||
action => 'modify',
|
||||
object => 'multiple_bugs'});
|
||||
|
@ -1217,10 +1216,8 @@ if ($format->{'extension'} eq "csv") {
|
|||
$vars->{'human'} = $cgi->param('human');
|
||||
}
|
||||
|
||||
# Suggest a name for the bug list if the user wants to save it as a file.
|
||||
$disposition .= "; filename=\"$filename\"";
|
||||
|
||||
_close_standby_message($contenttype, $disposition, $serverpush);
|
||||
_close_standby_message($contenttype, $disposition, $disp_prefix,
|
||||
$format->{'extension'}, $serverpush);
|
||||
|
||||
################################################################################
|
||||
# Content Generation
|
||||
|
|
|
@ -204,13 +204,14 @@ sub collect_stats {
|
|||
|
||||
if (!$exists || scalar(@data)) {
|
||||
my $fields = join('|', ('DATE', @statuses, @resolutions));
|
||||
my $product_name = $product->name;
|
||||
print DATA <<FIN;
|
||||
# Bugzilla Daily Bug Stats
|
||||
#
|
||||
# Do not edit me! This file is generated.
|
||||
#
|
||||
# fields: $fields
|
||||
# Product: $product->name
|
||||
# Product: $product_name
|
||||
# Created: $when
|
||||
FIN
|
||||
}
|
||||
|
@ -308,13 +309,14 @@ sub regenerate_stats {
|
|||
|
||||
if (open DATA, ">$file") {
|
||||
my $fields = join('|', ('DATE', @statuses, @resolutions));
|
||||
my $product_name = $product->name;
|
||||
print DATA <<FIN;
|
||||
# Bugzilla Daily Bug Stats
|
||||
#
|
||||
# Do not edit me! This file is generated.
|
||||
#
|
||||
# fields: $fields
|
||||
# Product: $product->name
|
||||
# Product: $product_name
|
||||
# Created: $when
|
||||
FIN
|
||||
# For each day, generate a line of statistics.
|
||||
|
@ -323,7 +325,7 @@ FIN
|
|||
for (my $day = $start + 1; $day <= $end; $day++) {
|
||||
# Some output feedback
|
||||
my $percent_done = ($day - $start - 1) * 100 / $total_days;
|
||||
printf "\rRegenerating %s \[\%.1f\%\%]", $product->name,
|
||||
printf "\rRegenerating %s \[\%.1f\%\%]", $product_name,
|
||||
$percent_done;
|
||||
|
||||
# Get a list of bugs that were created the previous day, and
|
||||
|
@ -372,7 +374,7 @@ FIN
|
|||
|
||||
# Finish up output feedback for this product.
|
||||
my $tend = time;
|
||||
say "\rRegenerating " . $product->name . ' [100.0%] - ' .
|
||||
say "\rRegenerating " . $product_name . ' [100.0%] - ' .
|
||||
delta_time($tstart, $tend);
|
||||
|
||||
close DATA;
|
||||
|
@ -472,8 +474,7 @@ sub CollectSeriesData {
|
|||
'fields' => ["bug_id"],
|
||||
'allow_unlimited' => 1,
|
||||
'user' => $user);
|
||||
my $sql = $search->sql;
|
||||
$data = $shadow_dbh->selectall_arrayref($sql);
|
||||
$data = $search->data;
|
||||
};
|
||||
|
||||
if (!$@) {
|
||||
|
|
|
@ -142,7 +142,11 @@ sub display_data {
|
|||
utf8::encode($digest_data) if utf8::is_utf8($digest_data);
|
||||
my $digest = md5_base64($digest_data);
|
||||
|
||||
$cgi->check_etag($digest);
|
||||
if ($cgi->check_etag($digest)) {
|
||||
print $cgi->header(-ETag => $digest,
|
||||
-status => '304 Not Modified');
|
||||
exit;
|
||||
}
|
||||
|
||||
print $cgi->header (-ETag => $digest,
|
||||
-type => $format->{'ctype'});
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::BmpConvert;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
use lib qw(. lib);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/perl -w
|
||||
#!/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/.
|
||||
|
@ -6,6 +6,8 @@
|
|||
# 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;
|
||||
|
@ -16,7 +18,7 @@ use Bugzilla::User;
|
|||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
sub usage {
|
||||
print STDERR "Usage: $0 bug_id user_email\n";
|
||||
say STDERR "Usage: $0 bug_id user_email";
|
||||
exit;
|
||||
}
|
||||
|
||||
|
@ -30,7 +32,7 @@ my $changer = $ARGV[1];
|
|||
|
||||
# Validate the bug number.
|
||||
if (!($bugnum =~ /^(\d+)$/)) {
|
||||
print STDERR "Bug number \"$bugnum\" not numeric.\n";
|
||||
say STDERR "Bug number \"$bugnum\" not numeric.";
|
||||
usage();
|
||||
}
|
||||
|
||||
|
@ -40,19 +42,19 @@ my ($id) = $dbh->selectrow_array("SELECT bug_id FROM bugs WHERE bug_id = ?",
|
|||
undef, $bugnum);
|
||||
|
||||
if (!$id) {
|
||||
print STDERR "Bug number $bugnum does not exist.\n";
|
||||
say STDERR "Bug number $bugnum does not exist.";
|
||||
usage();
|
||||
}
|
||||
|
||||
# Validate the changer address.
|
||||
my $match = Bugzilla->params->{'emailregexp'};
|
||||
if ($changer !~ /$match/) {
|
||||
print STDERR "Changer \"$changer\" doesn't match email regular expression.\n";
|
||||
say STDERR "Changer \"$changer\" doesn't match email regular expression.";
|
||||
usage();
|
||||
}
|
||||
my $changer_user = new Bugzilla::User({ name => $changer });
|
||||
unless ($changer_user) {
|
||||
print STDERR "\"$changer\" is not a valid user.\n";
|
||||
say STDERR "\"$changer\" is not a valid user.";
|
||||
usage();
|
||||
}
|
||||
|
||||
|
@ -61,26 +63,15 @@ my $outputref = Bugzilla::BugMail::Send($bugnum, {'changer' => $changer_user });
|
|||
|
||||
# Report the results.
|
||||
my $sent = scalar(@{$outputref->{sent}});
|
||||
my $excluded = scalar(@{$outputref->{excluded}});
|
||||
|
||||
if ($sent) {
|
||||
print "email sent to $sent recipients:\n";
|
||||
say "email sent to $sent recipients:";
|
||||
} else {
|
||||
print "No email sent.\n";
|
||||
say "No email sent.";
|
||||
}
|
||||
|
||||
foreach my $sent (@{$outputref->{sent}}) {
|
||||
print " $sent\n";
|
||||
}
|
||||
|
||||
if ($excluded) {
|
||||
print "$excluded recipients excluded:\n";
|
||||
} else {
|
||||
print "No recipients excluded.\n";
|
||||
}
|
||||
|
||||
foreach my $excluded (@{$outputref->{excluded}}) {
|
||||
print " $excluded\n";
|
||||
say " $sent";
|
||||
}
|
||||
|
||||
# This document is copyright (C) 2004 Perforce Software, Inc. All rights
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
# 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;
|
||||
|
@ -25,28 +25,21 @@ my $list = $dbh->selectcol_arrayref(
|
|||
' ORDER BY bug_id');
|
||||
|
||||
if (scalar(@$list) > 0) {
|
||||
print "OK, now attempting to send unsent mail\n";
|
||||
print scalar(@$list) . " bugs found with possibly unsent mail.\n\n";
|
||||
say "OK, now attempting to send unsent mail";
|
||||
say scalar(@$list) . " bugs found with possibly unsent mail.\n";
|
||||
foreach my $bugid (@$list) {
|
||||
my $start_time = time;
|
||||
print "Sending mail for bug $bugid...\n";
|
||||
say "Sending mail for bug $bugid...";
|
||||
my $outputref = Bugzilla::BugMail::Send($bugid);
|
||||
if ($ARGV[0] && $ARGV[0] eq "--report") {
|
||||
print "Mail sent to:\n";
|
||||
foreach (sort @{$outputref->{sent}}) {
|
||||
print $_ . "\n";
|
||||
}
|
||||
|
||||
print "Excluded:\n";
|
||||
foreach (sort @{$outputref->{excluded}}) {
|
||||
print $_ . "\n";
|
||||
}
|
||||
say "Mail sent to:";
|
||||
say $_ foreach (sort @{$outputref->{sent}});
|
||||
}
|
||||
else {
|
||||
my ($sent, $excluded) = (scalar(@{$outputref->{sent}}),scalar(@{$outputref->{excluded}}));
|
||||
print "$sent mails sent, $excluded people excluded.\n";
|
||||
print "Took " . (time - $start_time) . " seconds.\n\n";
|
||||
}
|
||||
my $sent = scalar @{$outputref->{sent}};
|
||||
say "$sent mails sent.";
|
||||
say "Took " . (time - $start_time) . " seconds.\n";
|
||||
}
|
||||
}
|
||||
print "Unsent mail has been sent.\n";
|
||||
say "Unsent mail has been sent.";
|
||||
}
|
||||
|
|
|
@ -240,22 +240,15 @@ if($readonly == 0) {
|
|||
|
||||
print "Phase 2: updating existing users... " unless $quiet;
|
||||
|
||||
my $sth_update_login = $dbh->prepare(
|
||||
'UPDATE profiles
|
||||
SET login_name = ?
|
||||
WHERE ' . $dbh->sql_istrcmp('login_name', '?'));
|
||||
my $sth_update_realname = $dbh->prepare(
|
||||
'UPDATE profiles
|
||||
SET realname = ?
|
||||
WHERE ' . $dbh->sql_istrcmp('login_name', '?'));
|
||||
|
||||
if($noupdate == 0) {
|
||||
while( my ($key, $value) = each(%update_users) ) {
|
||||
my $user = Bugzilla::User->check($key);
|
||||
if(defined $value->{'new_login_name'}) {
|
||||
$sth_update_login->execute($value->{'new_login_name'}, $key);
|
||||
$user->set_login($value->{'new_login_name'});
|
||||
} else {
|
||||
$sth_update_realname->execute($value->{'realname'}, $key);
|
||||
$user->set_name($value->{'realname'});
|
||||
}
|
||||
$user->update();
|
||||
}
|
||||
print "done!\n" unless $quiet;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,9 @@ my $vars = {};
|
|||
Bugzilla->switch_to_shadow_db;
|
||||
|
||||
$vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
|
||||
if (!@{$vars->{keywords}}) {
|
||||
ThrowUserError("no_keywords");
|
||||
}
|
||||
$vars->{'caneditkeywords'} = $user->in_group("editkeywords");
|
||||
|
||||
$template->process("reports/keywords.html.tmpl", $vars)
|
||||
|
|
|
@ -1065,7 +1065,7 @@
|
|||
<para>
|
||||
<emphasis>Login Name</emphasis>:
|
||||
This is generally the user's full email address. However, if you
|
||||
have are using the <quote>emailsuffix</quote> parameter, this may
|
||||
are using the <quote>emailsuffix</quote> parameter, this may
|
||||
just be the user's login name. Note that users can now change their
|
||||
login names themselves (to any valid email address).
|
||||
</para>
|
||||
|
|
|
@ -1602,21 +1602,6 @@ AddType application/rdf+xml .rdf</screen>
|
|||
C:\perl> <command>ppm install <module name></command>
|
||||
</programlisting>
|
||||
|
||||
<para>
|
||||
If you are using Perl 5.10.1, the best source for the Windows PPM modules
|
||||
needed for Bugzilla is probably the theory58S website, which you can add
|
||||
to your list of repositories as follows:
|
||||
</para>
|
||||
|
||||
<programlisting>
|
||||
<command>ppm repo add theory58S http://cpan.uwinnipeg.ca/PPMPackages/10xx/</command>
|
||||
</programlisting>
|
||||
|
||||
<para>
|
||||
If you are using Perl 5.12 or newer, you no longer need to add
|
||||
this repository. All modules you need are already available from
|
||||
the ActiveState repository.
|
||||
</para>
|
||||
<note>
|
||||
<para>
|
||||
The PPM repository stores modules in 'packages' that may have
|
||||
|
@ -1765,20 +1750,20 @@ C:\perl> <command>ppm install <module name></command>
|
|||
</section>
|
||||
|
||||
<section id="os-linux">
|
||||
<title>Linux Distributions</title>
|
||||
<para>Many Linux distributions include Bugzilla and its
|
||||
<title>Linux/BSD Distributions</title>
|
||||
<para>Many Linux/BSD distributions include Bugzilla and its
|
||||
dependencies in their native package management systems.
|
||||
Installing Bugzilla with root access on any Linux system
|
||||
Installing Bugzilla with root access on any Linux/BSD system
|
||||
should be as simple as finding the Bugzilla package in the
|
||||
package management application and installing it using the
|
||||
normal command syntax. Several distributions also perform
|
||||
the proper web server configuration automatically on installation.
|
||||
</para>
|
||||
<para>Please consult the documentation of your Linux
|
||||
<para>Please consult the documentation of your Linux/BSD
|
||||
distribution for instructions on how to install packages,
|
||||
or for specific instructions on installing Bugzilla with
|
||||
native package management tools. There is also a
|
||||
<ulink url="http://wiki.mozilla.org/Bugzilla:Linux_Distro_Installation">
|
||||
<ulink url="https://wiki.mozilla.org/Bugzilla:Prerequisites">
|
||||
Bugzilla Wiki Page</ulink> for distro-specific installation
|
||||
notes.
|
||||
</para>
|
||||
|
|
|
@ -54,10 +54,8 @@
|
|||
<note>
|
||||
<para>
|
||||
Running Bugzilla on Windows requires the use of ActiveState
|
||||
Perl &min-perl-ver; or higher. Many modules already exist in the core
|
||||
distribution of ActiveState Perl. Additional modules can be downloaded
|
||||
from <ulink url="http://cpan.uwinnipeg.ca/PPMPackages/10xx/" />
|
||||
if you use Perl 5.10.1.
|
||||
Perl &min-perl-ver; or higher. Most modules already exist in the core
|
||||
distribution of ActiveState Perl.
|
||||
</para>
|
||||
</note>
|
||||
|
||||
|
|
|
@ -550,7 +550,7 @@
|
|||
Sometimes, a query needs to compare a user-related field
|
||||
(such as ReportedBy) with a role-specific user (such as the
|
||||
user running the query or the user to whom each bug is assigned).
|
||||
When the operator is either "equals" or "notequals", the value
|
||||
When the operator is either "is equal to" or "is not equal to", the value
|
||||
can be "%reporter%", "%assignee%", "%qacontact%", or "%user%".
|
||||
The user pronoun
|
||||
refers to the user who is executing the query or, in the case
|
||||
|
@ -560,12 +560,12 @@
|
|||
</para>
|
||||
<para>
|
||||
Boolean charts also let you type a group name in any user-related
|
||||
field if the operator is either "equals", "notequals" or "anyexact".
|
||||
This will let you query for any member belonging (or not) to the
|
||||
specified group. The group name must be entered following the
|
||||
"%group.foo%" syntax, where "foo" is the group name.
|
||||
So if you are looking for bugs reported by any user being in the
|
||||
"editbugs" group, then you can type "%group.editbugs%".
|
||||
field if the operator is either "is equal to", "is not equal to" or
|
||||
"contains the string (exact case)". This will let you query for
|
||||
any member belonging (or not) to the specified group. The group name
|
||||
must be entered following the "%group.foo%" syntax, where "foo" is
|
||||
the group name. So if you are looking for bugs reported by any user
|
||||
being in the "editbugs" group, then you can type "%group.editbugs%".
|
||||
</para>
|
||||
</section>
|
||||
<section id="negation">
|
||||
|
@ -603,16 +603,16 @@
|
|||
negated. Negation permits queries such as
|
||||
<blockquote>
|
||||
<para>
|
||||
NOT(("product" "equals" "update") OR
|
||||
("component" "equals" "Documentation"))
|
||||
NOT(("product" "is equal to" "update") OR
|
||||
("component" "is equal to" "Documentation"))
|
||||
</para>
|
||||
</blockquote>
|
||||
to find bugs that are neither
|
||||
in the update product or in the documentation component or
|
||||
<blockquote>
|
||||
<para>
|
||||
NOT(("commenter" "equals" "%assignee%") OR
|
||||
("component" "equals" "Documentation"))
|
||||
NOT(("commenter" "is equal to" "%assignee%") OR
|
||||
("component" "is equal to" "Documentation"))
|
||||
</para>
|
||||
</blockquote>
|
||||
to find non-documentation
|
||||
|
@ -1420,6 +1420,15 @@
|
|||
their <quote>Field/recipient specific options</quote> setting.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
The <quote>Ignore Bugs</quote> section lets you specify a
|
||||
comma-separated list of bugs from which you never want to get any
|
||||
email notification of any kind. Removing a bug from this list will
|
||||
re-enable email notification for this bug. This is especially useful
|
||||
e.g. if you are the reporter of a very noisy bug which you are not
|
||||
interested in anymore or if you are watching someone who is in such
|
||||
a noisy bug.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section id="savedsearches" xreflabel="Saved Searches">
|
||||
|
|
|
@ -22,6 +22,7 @@ use Bugzilla::Flag;
|
|||
use Bugzilla::Field;
|
||||
use Bugzilla::Group;
|
||||
use Bugzilla::Token;
|
||||
use Bugzilla::Mailer;
|
||||
|
||||
my $user = Bugzilla->login(LOGIN_REQUIRED);
|
||||
|
||||
|
@ -66,7 +67,7 @@ if ($action eq 'search') {
|
|||
my $matchstr = trim($cgi->param('matchstr'));
|
||||
my $matchtype = $cgi->param('matchtype');
|
||||
my $grouprestrict = $cgi->param('grouprestrict') || '0';
|
||||
my $enabled_only = $cgi->param('enabled_only') || '0';
|
||||
my $is_enabled = scalar $cgi->param('is_enabled');
|
||||
my $query = 'SELECT DISTINCT userid, login_name, realname, is_enabled, ' .
|
||||
$dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date ' .
|
||||
'FROM profiles';
|
||||
|
@ -158,11 +159,12 @@ if ($action eq 'search') {
|
|||
$query .= " $nextCondition ugm.group_id IN($grouplist) ";
|
||||
}
|
||||
|
||||
if ($enabled_only eq '1') {
|
||||
$query .= " $nextCondition profiles.is_enabled = 1 ";
|
||||
detaint_natural($is_enabled);
|
||||
if ($is_enabled == 0 || $is_enabled == 1) {
|
||||
$query .= " $nextCondition profiles.is_enabled = ?";
|
||||
$nextCondition = 'AND';
|
||||
push(@bindValues, $is_enabled);
|
||||
}
|
||||
|
||||
$query .= ' ORDER BY profiles.login_name';
|
||||
|
||||
$vars->{'users'} = $dbh->selectall_arrayref($query,
|
||||
|
@ -217,6 +219,15 @@ if ($action eq 'search') {
|
|||
|
||||
delete_token($token);
|
||||
|
||||
if ($cgi->param('notify_user')) {
|
||||
$vars->{'new_user'} = $new_user;
|
||||
my $message;
|
||||
|
||||
$template->process('email/new-user-details.txt.tmpl', $vars, \$message)
|
||||
|| ThrowTemplateError($template->error());
|
||||
MessageToMTA($message);
|
||||
}
|
||||
|
||||
# We already display the updated page. We have to recreate a token now.
|
||||
$vars->{'token'} = issue_session_token('edit_user');
|
||||
$vars->{'message'} = 'account_created';
|
||||
|
|
|
@ -180,8 +180,10 @@ if ($action eq 'update') {
|
|||
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
$version->set_name($version_name);
|
||||
$version->set_is_active($isactive);
|
||||
$version->set_all({
|
||||
value => $version_name,
|
||||
isactive => $isactive
|
||||
});
|
||||
my $changes = $version->update();
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
|
|
@ -25,6 +25,8 @@ use Bugzilla::Whine::Schedule;
|
|||
use Bugzilla::Whine::Query;
|
||||
use Bugzilla::Whine;
|
||||
|
||||
use DateTime;
|
||||
|
||||
# require the user to have logged in
|
||||
my $user = Bugzilla->login(LOGIN_REQUIRED);
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::BmpConvert;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use parent qw(Bugzilla::Extension);
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# 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::Example;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use constant NAME => 'Example';
|
||||
use constant REQUIRED_MODULES => [
|
||||
{
|
||||
package => 'Data-Dumper',
|
||||
module => 'Data::Dumper',
|
||||
version => 0,
|
||||
},
|
||||
];
|
||||
|
||||
use constant OPTIONAL_MODULES => [
|
||||
{
|
||||
package => 'Acme',
|
||||
module => 'Acme',
|
||||
version => 1.11,
|
||||
feature => ['example_acme'],
|
||||
},
|
||||
];
|
||||
|
||||
__PACKAGE__->NAME;
|
|
@ -6,6 +6,8 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::Example;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use parent qw(Bugzilla::Extension);
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::Example::Auth::Login;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use parent qw(Bugzilla::Auth::Login);
|
||||
use constant user_can_create_account => 0;
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::Example::Auth::Verify;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use parent qw(Bugzilla::Auth::Verify);
|
||||
use Bugzilla::Constants;
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::Example::Config;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::Example::Util;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::Example::WebService;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
use parent qw(Bugzilla::WebService);
|
||||
|
|
|
@ -6,15 +6,11 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
#%]
|
||||
|
||||
[% USE Bugzilla %]
|
||||
[% cgi = Bugzilla.cgi %]
|
||||
[% USE date %]
|
||||
|
||||
[% IF cgi.param("help") %]
|
||||
<script type="text/javascript"> <!--
|
||||
[% FOREACH help_name = help_html.keys %]
|
||||
g_helpTexts["[% help_name FILTER js %]"] =
|
||||
"[%- help_html.$help_name FILTER js -%]";
|
||||
[% END %]
|
||||
// -->
|
||||
</script>
|
||||
[% END %]
|
||||
<p align="center">
|
||||
<em>[% component.callers.first FILTER html %]</em> processed
|
||||
on [% date.format(date.now, '%b %d, %Y at %H:%M:%S') FILTER html %].
|
||||
<br>
|
||||
(provided by the Example extension).
|
||||
</p>
|
|
@ -0,0 +1,13 @@
|
|||
[%# 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.
|
||||
#%]
|
||||
|
||||
[%
|
||||
tabs.push ({ name => 'newsearch', label => "New Search",
|
||||
link => "query.cgi?format=newsearch" })
|
||||
%]
|
||||
|
|
@ -6,6 +6,8 @@
|
|||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::Extension::MoreBugUrl;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
|
||||
use constant NAME => 'MoreBugUrl';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue