Last merge with trunk

custis-merge
Krasilnikov Andrey 2014-01-28 12:50:26 +04:00
commit b271aaa842
225 changed files with 5601 additions and 1518 deletions

View File

@ -26,3 +26,8 @@ Options -Indexes
</IfModule>
</IfModule>
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE]
</IfModule>

View File

@ -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>

View File

@ -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;

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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()");
}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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')

View File

@ -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

View File

@ -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;
}

View File

@ -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
},

View File

@ -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',

View File

@ -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;

View File

@ -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

View File

@ -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');
}

View File

@ -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

View File

@ -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,

View File

@ -106,7 +106,7 @@ sub _initialize {
LONGBLOB => 'longblob',
DATETIME => 'datetime',
DATE => 'date',
};
$self->_adjust_schema;

View File

@ -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;

View File

@ -48,7 +48,7 @@ sub _initialize {
LONGBLOB => 'bytea',
DATETIME => 'timestamp(0) without time zone',
DATE => 'date',
};
$self->_adjust_schema;

View File

@ -46,6 +46,7 @@ sub _initialize {
LONGBLOB => 'blob',
DATETIME => 'DATETIME',
DATE => 'DATETIME',
};
$self->_adjust_schema;

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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 {

View File

@ -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' },
}
};

View File

@ -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 {

View File

@ -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__

View File

@ -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;
}

View File

@ -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;
}

31
Bugzilla/Job/BugMail.pm Normal file
View File

@ -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;

View File

@ -43,13 +43,3 @@ sub work {
}
1;
=head1 B<Methods in need of POD>
=over
=item retry_delay
=item work
=back

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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>.

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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/\&nbsp;/ /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

View File

@ -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).

View File

@ -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).

View File

@ -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"],

View File

@ -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/"/&quot;/g;
# Obscure '@'.
$var =~ s/\@/\&#64;/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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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') {

View File

@ -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>

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 (!$@) {

View File

@ -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'});

View File

@ -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);

View File

@ -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

View File

@ -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.";
}

View File

@ -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;
}

View File

@ -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)

View File

@ -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>

View File

@ -1602,21 +1602,6 @@ AddType application/rdf+xml .rdf</screen>
C:\perl&gt; <command>ppm install &lt;module name&gt;</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&gt; <command>ppm install &lt;module name&gt;</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>

View File

@ -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>

View File

@ -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">

View File

@ -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';

View File

@ -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();

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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" })
%]

View File

@ -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