diff --git a/Bugzilla.pm b/Bugzilla.pm
index fbe6f6e48..cc8d04d8c 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -32,9 +32,11 @@ use Bugzilla::Constants;
use Bugzilla::Auth;
use Bugzilla::Auth::Persist::Cookie;
use Bugzilla::CGI;
+use Bugzilla::Extension;
use Bugzilla::DB;
use Bugzilla::Install::Localconfig qw(read_localconfig);
-use Bugzilla::JobQueue;
+use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
+use Bugzilla::Install::Util;
use Bugzilla::Template;
use Bugzilla::User;
use Bugzilla::Error;
@@ -78,9 +80,6 @@ BEGIN
};
}
-# This creates the request cache for non-mod_perl installations.
-our $_request_cache = {};
-
#####################################################################
# Constants
#####################################################################
@@ -89,6 +88,7 @@ our $_request_cache = {};
use constant SHUTDOWNHTML_EXEMPT => [
'editparams.cgi',
'checksetup.pl',
+ 'migrate.pl',
'recode.pl',
];
@@ -119,7 +119,11 @@ my $re_encoded_word = qr{
my $re_especials = qr{$re_encoded_word}xo;
# >>>
-*Encode::MIME::Header::encode = sub($$;$) {
+undef &Encode::MIME::Header::encode;
+
+*Encode::MIME::Header::encode = *encode_mime_header;
+
+sub encode_mime_header($$;$) {
my ( $obj, $str, $chk ) = @_;
my @line = ();
for my $line ( split /\r\n|[\r\n]/o, $str ) {
@@ -151,6 +155,7 @@ my $re_especials = qr{$re_encoded_word}xo;
$_[1] = '' if $chk;
return join( "\n", @line );
}
+
}
#####################################################################
@@ -163,15 +168,20 @@ my $re_especials = qr{$re_encoded_word}xo;
sub init_page {
(binmode STDOUT, ':utf8') if Bugzilla->params->{'utf8'};
-
if (${^TAINT}) {
- # Some environment variables are not taint safe
- delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
- # Some modules throw undefined errors (notably File::Spec::Win32) if
- # PATH is undefined.
- $ENV{'PATH'} = '';
+ # Some environment variables are not taint safe
+ delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+ # Some modules throw undefined errors (notably File::Spec::Win32) if
+ # PATH is undefined.
+ $ENV{'PATH'} = '';
}
+ # Because this function is run live from perl "use" commands of
+ # other scripts, we're skipping the rest of this function if we get here
+ # during a perl syntax check (perl -c, like we do during the
+ # 001compile.t test).
+ return if $^C;
+
# IIS prints out warnings to the webpage, so ignore them, or log them
# to a file if the file exists.
if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) {
@@ -186,18 +196,18 @@ sub init_page {
};
}
+ # Because of attachment_base, attachment.cgi handles this itself.
+ if (basename($0) ne 'attachment.cgi') {
+ do_ssl_redirect_if_required();
+ }
+
# If Bugzilla is shut down, do not allow anything to run, just display a
# message to the user about the downtime and log out. Scripts listed in
# SHUTDOWNHTML_EXEMPT are exempt from this message.
#
- # Because this is code which is run live from perl "use" commands of other
- # scripts, we're skipping this part if we get here during a perl syntax
- # check -- runtests.pl compiles scripts without running them, so we
- # need to make sure that this check doesn't apply to 'perl -c' calls.
- #
# This code must go here. It cannot go anywhere in Bugzilla::CGI, because
# it uses Template, and that causes various dependency loops.
- if (!$^C && Bugzilla->params->{"shutdownhtml"}
+ if (Bugzilla->params->{"shutdownhtml"}
&& lsearch(SHUTDOWNHTML_EXEMPT, basename($0)) == -1)
{
# Allow non-cgi scripts to exit silently (without displaying any
@@ -244,8 +254,6 @@ sub init_page {
}
}
-init_page() if !$ENV{MOD_PERL};
-
#####################################################################
# Subroutines and Methods
#####################################################################
@@ -266,12 +274,83 @@ sub template_inner {
return $class->request_cache->{"template_inner_$lang"};
}
+our $extension_packages;
+sub extensions {
+ my ($class) = @_;
+ my $cache = $class->request_cache;
+ if (!$cache->{extensions}) {
+ # Under mod_perl, mod_perl.pl populates $extension_packages for us.
+ if (!$extension_packages) {
+ $extension_packages = Bugzilla::Extension->load_all();
+ }
+ my @extensions;
+ foreach my $package (@$extension_packages) {
+ my $extension = $package->new();
+ if ($extension->enabled) {
+ push(@extensions, $extension);
+ }
+ }
+ $cache->{extensions} = \@extensions;
+ }
+ return $cache->{extensions};
+}
+
+sub feature {
+ my ($class, $feature) = @_;
+ my $cache = $class->request_cache;
+ return $cache->{feature}->{$feature}
+ if exists $cache->{feature}->{$feature};
+
+ my $feature_map = $cache->{feature_map};
+ if (!$feature_map) {
+ foreach my $package (@{ OPTIONAL_MODULES() }) {
+ foreach my $f (@{ $package->{feature} }) {
+ $feature_map->{$f} ||= [];
+ push(@{ $feature_map->{$f} }, $package->{module});
+ }
+ }
+ $cache->{feature_map} = $feature_map;
+ }
+
+ if (!$feature_map->{$feature}) {
+ ThrowCodeError('invalid_feature', { feature => $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;
+ }
+ $cache->{feature}->{$feature} = $success;
+ return $success;
+}
+
sub cgi {
my $class = shift;
$class->request_cache->{cgi} ||= new Bugzilla::CGI();
return $class->request_cache->{cgi};
}
+sub input_params {
+ my ($class, $params) = @_;
+ my $cache = $class->request_cache;
+ # This is how the WebService and other places set input_params.
+ if (defined $params) {
+ $cache->{input_params} = $params;
+ }
+ return $cache->{input_params} if defined $cache->{input_params};
+
+ # Making this scalar makes it a tied hash to the internals of $cgi,
+ # so if a variable is changed, then it actually changes the $cgi object
+ # as well.
+ $cache->{input_params} = $class->cgi->Vars;
+ return $cache->{input_params};
+}
+
sub localconfig {
my $class = shift;
$class->request_cache->{localconfig} ||= read_localconfig();
@@ -318,6 +397,7 @@ sub login {
my $authorizer = new Bugzilla::Auth();
$type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn');
+
if (!defined $type || $type == LOGIN_NORMAL) {
$type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL;
}
@@ -361,14 +441,6 @@ sub login {
$class->set_user($authenticated_user);
}
- # We run after the login has completed since
- # some of the checks in ssl_require_redirect
- # look for Bugzilla->user->id to determine
- # if redirection is required.
- if (i_am_cgi() && ssl_require_redirect()) {
- $class->cgi->require_https($class->params->{'sslbase'});
- }
-
return $class->user;
}
@@ -408,6 +480,7 @@ sub logout_request {
sub job_queue {
my $class = shift;
+ require Bugzilla::JobQueue;
$class->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
return $class->request_cache->{job_queue};
}
@@ -455,6 +528,15 @@ sub error_mode {
|| (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE);
}
+# This is used only by Bugzilla::Error to throw errors.
+sub _json_server {
+ my ($class, $newval) = @_;
+ if (defined $newval) {
+ $class->request_cache->{_json_server} = $newval;
+ }
+ return $class->request_cache->{_json_server};
+}
+
sub usage_mode {
my ($class, $newval) = @_;
if (defined $newval) {
@@ -464,9 +546,12 @@ sub usage_mode {
elsif ($newval == USAGE_MODE_CMDLINE) {
$class->error_mode(ERROR_MODE_DIE);
}
- elsif ($newval == USAGE_MODE_WEBSERVICE) {
+ elsif ($newval == USAGE_MODE_XMLRPC) {
$class->error_mode(ERROR_MODE_DIE_SOAP_FAULT);
}
+ elsif ($newval == USAGE_MODE_JSON) {
+ $class->error_mode(ERROR_MODE_JSON_RPC);
+ }
elsif ($newval == USAGE_MODE_EMAIL) {
$class->error_mode(ERROR_MODE_DIE);
}
@@ -541,7 +626,7 @@ sub active_custom_fields {
my $class = shift;
if (!exists $class->request_cache->{active_custom_fields}) {
$class->request_cache->{active_custom_fields} =
- Bugzilla::Field->match({ custom => 1, obsolete => 0 });
+ Bugzilla::Field->match({ custom => 1, obsolete => 0 });
}
return @{$class->request_cache->{active_custom_fields}};
}
@@ -555,12 +640,6 @@ sub has_flags {
return $class->request_cache->{has_flags};
}
-sub hook_args {
- my ($class, $args) = @_;
- $class->request_cache->{hook_args} = $args if $args;
- return $class->request_cache->{hook_args};
-}
-
sub local_timezone {
my $class = shift;
@@ -571,10 +650,20 @@ sub local_timezone {
return $class->request_cache->{local_timezone};
}
+# This creates the request cache for non-mod_perl installations.
+# This is identical to Install::Util::_cache so that things loaded
+# into Install::Util::_cache during installation can be read out
+# of request_cache later in installation.
+our $_request_cache = $Bugzilla::Install::Util::_cache;
+
sub request_cache {
if ($ENV{MOD_PERL}) {
require Apache2::RequestUtil;
- return Apache2::RequestUtil->request->pnotes();
+ # Sometimes (for example, during mod_perl.pl), the request
+ # object isn't available, and we should use $_request_cache instead.
+ my $request = eval { Apache2::RequestUtil->request };
+ return $_request_cache if !$request;
+ return $request->pnotes();
}
return $_request_cache ||= {};
}
@@ -599,6 +688,8 @@ sub END {
_cleanup() unless $ENV{MOD_PERL};
}
+init_page() if !$ENV{MOD_PERL};
+
1;
__END__
@@ -682,6 +773,26 @@ The current C object. Note that modules should B be using this in
general. Not all Bugzilla actions are cgi requests. Its useful as a convenience
method for those scripts/templates which are only use via CGI, though.
+=item C
+
+When running under the WebService, this is a hashref containing the arguments
+passed to the WebService method that was called. When running in a normal
+script, this is a hashref containing the contents of the CGI parameters.
+
+Modifying this hashref will modify the CGI parameters or the WebService
+arguments (depending on what C currently represents).
+
+This should be used instead of L in situations where your code
+could be being called by either a normal CGI script or a WebService method,
+such as during a code hook.
+
+B When C represents the CGI parameters, any
+parameter specified more than once (like C) will appear
+as an arrayref in the hash, but any value specified only once will appear
+as a scalar. This means that even if a value I appear multiple times,
+if it only I appear once, then it will be a scalar in C,
+not an arrayref.
+
=item C
C if there is no currently logged in user or if the login code has not
@@ -766,10 +877,11 @@ usage mode changes.
=item C
Call either Cusage_mode(Bugzilla::Constants::USAGE_MODE_CMDLINE)>
-or Cusage_mode(Bugzilla::Constants::USAGE_MODE_WEBSERVICE)> near the
+or Cusage_mode(Bugzilla::Constants::USAGE_MODE_XMLRPC)> near the
beginning of your script to change this flag's default of
C and to indicate that Bugzilla is
being called in a non-interactive manner.
+
This influences error handling because on usage mode changes, C
calls Cerror_mode> to set an error mode which makes sense for the
usage mode.
@@ -813,11 +925,6 @@ The current Parameters of Bugzilla, as a hashref. If C
does not exist, then we return an empty hashref. If C
is unreadable or is not valid perl, we C.
-=item C
-
-If you are running inside a code hook (see L) this
-is how you get the arguments passed to the hook.
-
=item C
Returns the local timezone of the Bugzilla installation,
@@ -830,4 +937,9 @@ Returns a L that you can use for queueing jobs.
Will throw an error if job queueing is not correctly configured on
this Bugzilla installation.
+=item C
+
+Tells you whether or not a specific feature is enabled. For names
+of features, see C in C.
+
=back
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index c8022bc99..c10eb5e9e 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -57,12 +57,12 @@ use Bugzilla::Flag;
use Bugzilla::User;
use Bugzilla::Util;
use Bugzilla::Field;
+use Bugzilla::Hook;
+
use LWP::MediaTypes;
use base qw(Bugzilla::Object);
-use Encode;
-
###############################
#### Initialization ####
###############################
@@ -89,6 +89,38 @@ sub DB_COLUMNS {
$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts';
}
+use constant REQUIRED_CREATE_FIELDS => qw(
+ bug
+ data
+ description
+ filename
+ mimetype
+);
+
+use constant UPDATE_COLUMNS => qw(
+ description
+ filename
+ isobsolete
+ ispatch
+ isprivate
+ mimetype
+);
+
+use constant VALIDATORS => {
+ bug => \&_check_bug,
+ description => \&_check_description,
+ ispatch => \&Bugzilla::Object::check_boolean,
+ isprivate => \&_check_is_private,
+ isurl => \&_check_is_url,
+ mimetype => \&_check_content_type,
+ store_in_file => \&_check_store_in_file,
+};
+
+use constant UPDATE_VALIDATORS => {
+ filename => \&_check_filename,
+ isobsolete => \&Bugzilla::Object::check_boolean,
+};
+
###############################
#### Accessors ######
###############################
@@ -126,7 +158,7 @@ sub bug {
my $self = shift;
require Bugzilla::Bug;
- $self->{bug} = Bugzilla::Bug->new($self->bug_id);
+ $self->{bug} ||= Bugzilla::Bug->new($self->bug_id);
return $self->{bug};
}
@@ -396,6 +428,13 @@ sub datasize {
return $self->{datasize};
}
+sub _get_local_filename {
+ my $self = shift;
+ my $hash = ($self->id % 100) + 100;
+ $hash =~ s/.*(\d\d)$/group.$1/;
+ return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id;
+}
+
=over
=item C
@@ -408,9 +447,9 @@ flags that have been set on the attachment
sub flags {
my $self = shift;
- return $self->{flags} if exists $self->{flags};
- $self->{flags} = Bugzilla::Flag->match({ 'attach_id' => $self->id });
+ # Don't cache it as it must be in sync with ->flag_types.
+ $self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}];
return $self->{flags};
}
@@ -434,7 +473,7 @@ sub flag_types {
component_id => $self->bug->component_id,
attach_id => $self->id };
- $self->{flag_types} = Bugzilla::Flag::_flag_types($vars);
+ $self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
return $self->{flag_types};
}
@@ -442,33 +481,136 @@ sub flag_types {
#### Validators ######
###############################
-# Instance methods; no POD documentation here yet because the only ones so far
-# are private.
+sub set_content_type { $_[0]->set('mimetype', $_[1]); }
+sub set_description { $_[0]->set('description', $_[1]); }
+sub set_filename { $_[0]->set('filename', $_[1]); }
+sub set_is_patch { $_[0]->set('ispatch', $_[1]); }
+sub set_is_private { $_[0]->set('isprivate', $_[1]); }
-sub _get_local_filename {
- my $self = shift;
- my $hash = ($self->id % 100) + 100;
- $hash =~ s/.*(\d\d)$/group.$1/;
- return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id;
+sub set_is_obsolete {
+ my ($self, $obsolete) = @_;
+
+ my $old = $self->isobsolete;
+ $self->set('isobsolete', $obsolete);
+ my $new = $self->isobsolete;
+
+ # If the attachment is being marked as obsolete, cancel pending requests.
+ if ($new && $old != $new) {
+ my @requests = grep { $_->status eq '?' } @{$self->flags};
+ return unless scalar @requests;
+
+ my %flag_ids = map { $_->id => 1 } @requests;
+ foreach my $flagtype (@{$self->flag_types}) {
+ @{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}};
+ }
+ }
}
-sub _validate_filename {
- my ($throw_error) = @_;
- my $cgi = Bugzilla->cgi;
- defined $cgi->upload('data')
- || ($cgi->param('text_attachment') !~ /^\s*$/so)
- || ($throw_error ? ThrowUserError("file_not_specified") : return 0);
+sub set_flags {
+ my ($self, $flags, $new_flags) = @_;
- my $filename = $cgi->upload('data') || $cgi->param('filename');
- $filename = $cgi->param('description')
- if !$filename && $cgi->param('text_attachment') !~ /^\s*$/so;
- if (Bugzilla->params->{utf8})
- {
- # CGI::upload() will probably return non-UTF8 string, so set UTF8 flag on
- # utf8::decode() or Encode::_utf8_on() does not work on tainted values...
- $filename = trick_taint_copy($filename);
- Encode::_utf8_on($filename);
+ Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags);
+}
+
+sub _check_bug {
+ my ($invocant, $bug) = @_;
+ my $user = Bugzilla->user;
+
+ $bug = ref $invocant ? $invocant->bug : $bug;
+ ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id))
+ || ThrowUserError("illegal_attachment_edit_bug", { bug_id => $bug->id });
+
+ return $bug;
+}
+
+sub _legal_content_type {
+ my ($content_type) = @_;
+ my $legal_types = join('|', LEGAL_CONTENT_TYPES);
+ return $content_type !~ /^($legal_types)\/.+$/;
+}
+
+sub _check_content_type {
+ my ($invocant, $content_type) = @_;
+
+ $content_type = 'text/plain' if (ref $invocant && ($invocant->isurl || $invocant->ispatch));
+ if (!_legal_content_type($content_type)) {
+ ThrowUserError("invalid_content_type", { contenttype => $content_type });
}
+ trick_taint($content_type);
+
+ return $content_type;
+}
+
+sub _check_data {
+ my ($invocant, $params) = @_;
+
+ my $data;
+ if ($params->{isurl}) {
+ $data = $params->{data};
+ ($data && $data =~ m#^(http|https|ftp)://\S+#)
+ || ThrowUserError('attachment_illegal_url', { url => $data });
+
+ $params->{mimetype} = 'text/plain';
+ $params->{ispatch} = 0;
+ $params->{store_in_file} = 0;
+ }
+ else {
+ if ($params->{store_in_file} || !ref $params->{data}) {
+ # If it's a filehandle, just store it, not the content of the file
+ # itself as the file may be quite large. If it's not a filehandle,
+ # it already contains the content of the file.
+ $data = $params->{data};
+ }
+ else {
+ # The file will be stored in the DB. We need the content of the file.
+ local $/;
+ my $fh = $params->{data};
+ $data = <$fh>;
+ }
+ }
+ Bugzilla::Hook::process('attachment_process_data', { data => \$data,
+ attributes => $params });
+
+ # Do not validate the size if we have a filehandle. It will be checked later.
+ return $data if ref $data;
+
+ $data || ThrowUserError('zero_length_file');
+ # Make sure the attachment does not exceed the maximum permitted size.
+ my $len = length($data);
+ my $max_size = $params->{store_in_file} ? Bugzilla->params->{'maxlocalattachment'} * 1048576
+ : Bugzilla->params->{'maxattachmentsize'} * 1024;
+ if ($len > $max_size) {
+ my $vars = { filesize => sprintf("%.0f", $len/1024) };
+ if ($params->{ispatch}) {
+ ThrowUserError('patch_too_large', $vars);
+ }
+ elsif ($params->{store_in_file}) {
+ ThrowUserError('local_file_too_large');
+ }
+ else {
+ ThrowUserError('file_too_large', $vars);
+ }
+ }
+ return $data;
+}
+
+sub _check_description {
+ my ($invocant, $description) = @_;
+
+ $description = trim($description);
+ $description || ThrowUserError('missing_attachment_description');
+ return $description;
+}
+
+sub _check_filename {
+ my ($invocant, $filename, $is_url) = @_;
+
+ $is_url = $invocant->isurl if ref $invocant;
+ # No file is attached, so it has no name.
+ return '' if $is_url;
+
+ $filename = trim($filename);
+ $filename || ThrowUserError('file_not_specified');
# Remove path info (if any) from the file name. The browser should do this
# for us, but some are buggy. This may not work on Mac file names and could
@@ -480,70 +622,39 @@ sub _validate_filename {
# Truncate the filename to 100 characters, counting from the end of the
# string to make sure we keep the filename extension.
$filename = substr($filename, -100, 100);
+ trick_taint($filename);
return $filename;
}
-sub _validate_data {
- my ($throw_error, $hr_vars) = @_;
- my $cgi = Bugzilla->cgi;
+sub _check_is_private {
+ my ($invocant, $is_private) = @_;
- my $fh;
- # Skip uploading into a local variable if the user wants to upload huge
- # attachments into local files.
- if (!$cgi->param('bigfile')) {
- $fh = $cgi->upload('data');
+ $is_private = $is_private ? 1 : 0;
+ if (((!ref $invocant && $is_private)
+ || (ref $invocant && $invocant->isprivate != $is_private))
+ && !Bugzilla->user->is_insider) {
+ ThrowUserError('user_not_insider');
}
- my $data;
+ return $is_private;
+}
- # We could get away with reading only as much as required, except that then
- # we wouldn't have a size to print to the error handler below.
- if (!$cgi->param('bigfile')) {
- # enable 'slurp' mode
- local $/;
- $data = <$fh>;
+sub _check_is_url {
+ my ($invocant, $is_url) = @_;
+
+ if ($is_url && !Bugzilla->params->{'allow_attach_url'}) {
+ ThrowCodeError('attachment_url_disabled');
}
+ return $is_url ? 1 : 0;
+}
- $data
- || ($cgi->param('bigfile'))
- || ($cgi->param('text_attachment') !~ /^\s*$/so)
- || ($throw_error ? ThrowUserError("zero_length_file") : return 0);
+sub _check_store_in_file {
+ my ($invocant, $store_in_file) = @_;
- if (!$data && $cgi->param('text_attachment') !~ /^\s*$/so)
- {
- $data = $cgi->param('text_attachment');
+ if ($store_in_file && !Bugzilla->params->{'maxlocalattachment'}) {
+ ThrowCodeError('attachment_local_storage_disabled');
}
-
- # Windows screenshots are usually uncompressed BMP files which
- # makes for a quick way to eat up disk space. Let's compress them.
- # We do this before we check the size since the uncompressed version
- # could easily be greater than maxattachmentsize.
- if (Bugzilla->params->{'convert_uncompressed_images'}
- && $cgi->param('contenttype') eq 'image/bmp') {
- require Image::Magick;
- my $img = Image::Magick->new(magick=>'bmp');
- $img->BlobToImage($data);
- $img->set(magick=>'png');
- my $imgdata = $img->ImageToBlob();
- $data = $imgdata;
- $cgi->param('contenttype', 'image/png');
- $hr_vars->{'convertedbmp'} = 1;
- }
-
- # Make sure the attachment does not exceed the maximum permitted size
- my $maxsize = Bugzilla->params->{'maxattachmentsize'} * 1024; # Convert from K
- my $len = $data ? length($data) : 0;
- if ($maxsize && $len > $maxsize) {
- my $vars = { filesize => sprintf("%.0f", $len/1024) };
- if ($cgi->param('ispatch')) {
- $throw_error ? ThrowUserError("patch_too_large", $vars) : return 0;
- }
- else {
- $throw_error ? ThrowUserError("file_too_large", $vars) : return 0;
- }
- }
-
- return $data || '';
+ return $store_in_file ? 1 : 0;
}
=pod
@@ -606,128 +717,6 @@ sub get_attachments_by_bug {
=pod
-=item C
-
-Description: validates the "patch" flag passed in by CGI.
-
-Returns: 1 on success.
-
-=cut
-
-sub validate_is_patch {
- my ($class, $throw_error) = @_;
- my $cgi = Bugzilla->cgi;
-
- # Set the ispatch flag to zero if it is undefined, since the UI uses
- # an HTML checkbox to represent this flag, and unchecked HTML checkboxes
- # do not get sent in HTML requests.
- $cgi->param('ispatch', $cgi->param('ispatch') ? 1 : 0);
-
- # Set the content type to text/plain if the attachment is a patch.
- $cgi->param('contenttype', 'text/plain') if $cgi->param('ispatch');
-
- return 1;
-}
-
-=pod
-
-=item C
-
-Description: validates the description passed in by CGI.
-
-Returns: 1 on success.
-
-=cut
-
-sub validate_description {
- my ($class, $throw_error) = @_;
- my $cgi = Bugzilla->cgi;
-
- $cgi->param('description')
- || ($throw_error ? ThrowUserError("missing_attachment_description") : return 0);
-
- return 1;
-}
-
-=pod
-
-=item C
-
-Description: validates the content type passed in by CGI.
-
-Returns: 1 on success.
-
-=cut
-
-sub valid_content_type { $_[0] =~ /^(application|audio|image|message|model|multipart|text|video)\/.+$/ }
-
-my $lwp_read_mime_types;
-sub validate_content_type {
- my ($class, $throw_error) = @_;
- my $cgi = Bugzilla->cgi;
-
- if (!defined $cgi->param('contenttypemethod')) {
- $throw_error ? ThrowUserError("missing_content_type_method") : return 0;
- }
- elsif ($cgi->param('contenttypemethod') eq 'autodetect') {
- my $contenttype;
- if ($cgi->param('data'))
- {
- $contenttype = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
- if (!valid_content_type($contenttype) && Bugzilla->params->{mime_types_file})
- {
- if (!$lwp_read_mime_types)
- {
- LWP::MediaTypes::read_media_types(Bugzilla->params->{mime_types_file});
- $lwp_read_mime_types = 1;
- }
- my $file = $cgi->param('data');
- $contenttype = LWP::MediaTypes::guess_media_type("$file");
- }
- if (!valid_content_type($contenttype))
- {
- $contenttype = 'application/octet-stream';
- }
- }
- else
- {
- $contenttype = 'text/plain';
- }
- # The user asked us to auto-detect the content type, so use the type
- # specified in the HTTP request headers.
- if ( !$contenttype ) {
- $throw_error ? ThrowUserError("missing_content_type") : return 0;
- }
- $cgi->param('contenttype', $contenttype);
- }
- elsif ($cgi->param('contenttypemethod') eq 'list') {
- # The user selected a content type from the list, so use their
- # selection.
- $cgi->param('contenttype', $cgi->param('contenttypeselection'));
- }
- elsif ($cgi->param('contenttypemethod') eq 'manual') {
- # The user entered a content type manually, so use their entry.
- $cgi->param('contenttype', $cgi->param('contenttypeentry'));
- }
- else {
- $throw_error ?
- ThrowCodeError("illegal_content_type_method",
- { contenttypemethod => $cgi->param('contenttypemethod') }) :
- return 0;
- }
-
- if (!valid_content_type($cgi->param('contenttype'))) {
- $throw_error ?
- ThrowUserError("invalid_content_type",
- { contenttype => $cgi->param('contenttype') }) :
- return 0;
- }
-
- return 1;
-}
-
-=pod
-
=item C
Description: validates if the user is allowed to view and edit the attachment.
@@ -738,7 +727,7 @@ Description: validates if the user is allowed to view and edit the attachment.
Params: $attachment - the attachment object being edited.
$product_id - the product ID the attachment belongs to.
-Returns: 1 on success. Else an error is thrown.
+Returns: 1 on success, 0 otherwise.
=cut
@@ -747,12 +736,9 @@ sub validate_can_edit {
my $user = Bugzilla->user;
# The submitter can edit their attachments.
- return 1 if ($attachment->attacher->id == $user->id
- || ((!$attachment->isprivate || $user->is_insider)
- && $user->in_group('editbugs', $product_id)));
-
- # If we come here, then this attachment cannot be seen by the user.
- ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
+ return ($attachment->attacher->id == $user->id
+ || ((!$attachment->isprivate || $user->is_insider)
+ && $user->in_group('editbugs', $product_id))) ? 1 : 0;
}
=item C
@@ -769,14 +755,13 @@ Returns: 1 on success. Else an error is thrown.
=cut
sub validate_obsolete {
- my ($class, $bug) = @_;
- my $cgi = Bugzilla->cgi;
+ my ($class, $bug, $list) = @_;
# Make sure the attachment id is valid and the user has permissions to view
# the bug to which it is attached. Make sure also that the user can view
# the attachment itself.
my @obsolete_attachments;
- foreach my $attachid ($cgi->param('obsolete')) {
+ foreach my $attachid (@$list) {
my $vars = {};
$vars->{'attach_id'} = $attachid;
@@ -788,7 +773,8 @@ sub validate_obsolete {
|| ThrowUserError('invalid_attach_id', $vars);
# Check that the user can view and edit this attachment.
- $attachment->validate_can_edit($bug->product_id);
+ $attachment->validate_can_edit($bug->product_id)
+ || ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
$vars->{'description'} = $attachment->description;
@@ -813,144 +799,75 @@ sub validate_obsolete {
=pod
-=item C
+=item C
-Description: inserts an attachment from CGI input for the given bug.
+Description: inserts an attachment into the given bug.
-Params: C<$bug> - Bugzilla::Bug object - the bug for which to insert
+Params: takes a hashref with the following keys:
+ C - Bugzilla::Bug object - the bug for which to insert
the attachment.
- C<$user> - Bugzilla::User object - the user we're inserting an
- attachment for.
- C<$timestamp> - scalar - timestamp of the insert as returned
- by SELECT NOW().
- C<$hr_vars> - hash reference - reference to a hash of template
- variables.
+ C - Either a filehandle pointing to the content of the
+ attachment, or the content of the attachment itself.
+ C - string - describe what the attachment is about.
+ C - string - the name of the attachment (used by the
+ browser when downloading it). If the attachment is a URL, this
+ parameter has no effect.
+ C - string - a valid MIME type.
+ C - string (optional) - timestamp of the insert
+ as returned by SELECT LOCALTIMESTAMP(0).
+ C - boolean (optional, default false) - true if the
+ attachment is a patch.
+ C - boolean (optional, default false) - true if
+ the attachment is private.
+ C - boolean (optional, default false) - true if the
+ attachment is a URL pointing to some external ressource.
+ C - boolean (optional, default false) - true
+ if the attachment must be stored in data/attachments/ instead
+ of in the DB.
-Returns: the ID of the new attachment.
+Returns: The new attachment object.
=cut
-# FIXME: needs to follow the way Object->create() works.
sub create {
- my ($class, $throw_error, $bug, $user, $timestamp, $hr_vars) = @_;
-
- my $cgi = Bugzilla->cgi;
+ my $class = shift;
my $dbh = Bugzilla->dbh;
- my $attachurl = $cgi->param('attachurl') || '';
- my $data;
- my $filename;
- my $contenttype;
- my $isurl;
- $class->validate_is_patch($throw_error) || return;
- $class->validate_description($throw_error) || return;
- if (Bugzilla->params->{force_attach_bigfile})
- {
- # Force uploading into files instead of DB
- $cgi->param('bigfile', 1);
- }
- if (Bugzilla->params->{'allow_attach_url'}
- && ($attachurl =~ /^(http|https|ftp):\/\/\S+/)
- && !defined $cgi->upload('data'))
- {
- $filename = '';
- $data = $attachurl;
- $isurl = 1;
- $contenttype = 'text/plain';
- $cgi->param('ispatch', 0);
- $cgi->delete('bigfile');
- }
- else {
- $filename = _validate_filename($throw_error) || return;
- # need to validate content type before data as
- # we now check the content type for image/bmp in _validate_data()
- unless ($cgi->param('ispatch')) {
- $class->validate_content_type($throw_error) || return;
+ $class->check_required_create_fields(@_);
+ my $params = $class->run_create_validators(@_);
- # Set the ispatch flag to 1 if we're set to autodetect
- # and the content type is text/x-diff or text/x-patch
- if ($cgi->param('contenttypemethod') eq 'autodetect'
- && $cgi->param('contenttype') =~ m{text/x-(?:diff|patch)})
- {
- $cgi->param('ispatch', 1);
- $cgi->param('contenttype', 'text/plain');
- }
- }
- $data = _validate_data($throw_error, $hr_vars);
- # If the attachment is stored locally, $data eq ''.
- # If an error is thrown, $data eq '0'.
- ($data ne '0') || return;
- $contenttype = $cgi->param('contenttype');
+ # Extract everything which is not a valid column name.
+ my $bug = delete $params->{bug};
+ $params->{bug_id} = $bug->id;
+ my $fh = delete $params->{data};
+ my $store_in_file = delete $params->{store_in_file};
- # These are inserted using placeholders so no need to panic
- trick_taint($filename);
- trick_taint($contenttype);
- $isurl = 0;
- }
-
- # Check attachments the user tries to mark as obsolete.
- my @obsolete_attachments;
- if ($cgi->param('obsolete')) {
- @obsolete_attachments = $class->validate_obsolete($bug);
- }
-
- # The order of these function calls is important, as Flag::validate
- # assumes User::match_field has ensured that the
- # values in the requestee fields are legitimate user email addresses.
- my $match_status = Bugzilla::User::match_field($cgi, {
- '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
- }, MATCH_SKIP_CONFIRM);
-
- $hr_vars->{'match_field'} = 'requestee';
- if ($match_status == USER_MATCH_FAILED) {
- $hr_vars->{'message'} = 'user_match_failed';
- }
- elsif ($match_status == USER_MATCH_MULTIPLE) {
- $hr_vars->{'message'} = 'user_match_multiple';
- }
-
- # Escape characters in strings that will be used in SQL statements.
- my $description = $cgi->param('description');
- trick_taint($description);
- my $isprivate = $cgi->param('isprivate') ? 1 : 0;
-
- # Insert the attachment into the database.
- my $sth = $dbh->do(
- "INSERT INTO attachments
- (bug_id, creation_ts, modification_time, filename, description,
- mimetype, ispatch, isurl, isprivate, submitter_id)
- VALUES (?,?,?,?,?,?,?,?,?,?)", undef, ($bug->bug_id, $timestamp, $timestamp,
- $filename, $description, $contenttype, $cgi->param('ispatch'),
- $isurl, $isprivate, $user->id));
- # Retrieve the ID of the newly created attachment record.
- my $attachid = $dbh->bz_last_key('attachments', 'attach_id');
+ my $attachment = $class->insert_create_data($params);
+ my $attachid = $attachment->id;
# We only use $data here in this INSERT with a placeholder,
# so it's safe.
- $sth = $dbh->prepare("INSERT INTO attach_data
- (id, thedata) VALUES ($attachid, ?)");
- if (!$cgi->param('bigfile') && $data)
- {
- trick_taint($data);
- $sth->bind_param(1, $data, $dbh->BLOB_TYPE);
- $sth->execute();
- }
+ my $sth = $dbh->prepare("INSERT INTO attach_data
+ (id, thedata) VALUES ($attachid, ?)");
+
+ my $data = $store_in_file ? "" : $fh;
+ trick_taint($data);
+ $sth->bind_param(1, $data, $dbh->BLOB_TYPE);
+ $sth->execute();
# If the file is to be stored locally, stream the file from the web server
# to the local file without reading it into a local variable.
- if ($cgi->param('bigfile'))
- {
+ if ($store_in_file) {
my $attachdir = bz_locations()->{'attachdir'};
my $hash = ($attachid % 100) + 100;
$hash =~ s/.*(\d\d)$/group.$1/;
mkdir "$attachdir/$hash", 0770;
chmod 0770, "$attachdir/$hash";
- open AH, ">$attachdir/$hash/attachment.$attachid" or die "Could not write into $attachdir/$hash/attachment.$attachid: $!";
+ open(AH, '>', "$attachdir/$hash/attachment.$attachid") or die "Could not write into $attachdir/$hash/attachment.$attachid: $!";
binmode AH;
- if (my $fh = $cgi->upload('data'))
- {
+ if (ref $fh) {
+ my $limit = Bugzilla->params->{"maxlocalattachment"} * 1048576;
my $sizecount = 0;
- my $limit = (Bugzilla->params->{"maxlocalattachment"} * 1048576);
while (<$fh>) {
print AH $_;
$sizecount += length($_);
@@ -958,64 +875,73 @@ sub create {
close AH;
close $fh;
unlink "$attachdir/$hash/attachment.$attachid";
- $throw_error ? ThrowUserError("local_file_too_large") : return;
+ ThrowUserError("local_file_too_large");
}
}
close $fh;
}
- elsif ($data)
- {
- print AH $data;
+ else {
+ print AH $fh;
}
close AH;
}
- # Make existing attachments obsolete.
- my $fieldid = get_field_id('attachments.isobsolete');
-
- foreach my $obsolete_attachment (@obsolete_attachments) {
- # If the obsolete attachment has request flags, cancel them.
- # This call must be done before updating the 'attachments' table.
- Bugzilla::Flag->CancelRequests($bug, $obsolete_attachment, $timestamp);
-
- $dbh->do('UPDATE attachments SET isobsolete = 1, modification_time = ?
- WHERE attach_id = ?',
- undef, ($timestamp, $obsolete_attachment->id));
-
- $dbh->do('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
- fieldid, removed, added)
- VALUES (?,?,?,?,?,?,?)',
- undef, ($bug->bug_id, $obsolete_attachment->id, $user->id,
- $timestamp, $fieldid, 0, 1));
- }
-
- my $attachment = new Bugzilla::Attachment($attachid);
-
- # 1. Add flags, if any. To avoid dying if something goes wrong
- # while processing flags, we will eval() flag validation.
- # This requires errors to die().
- # XXX: this can go away as soon as flag validation is able to
- # fail without dying.
- #
- # 2. Flag::validate() should not detect any reference to existing flags
- # when creating a new attachment. Setting the third param to -1 will
- # force this function to check this point.
- my $error_mode_cache = Bugzilla->error_mode;
- Bugzilla->error_mode(ERROR_MODE_DIE);
- eval {
- Bugzilla::Flag::validate($bug->bug_id, -1, SKIP_REQUESTEE_ON_ERROR);
- Bugzilla::Flag->process($bug, $attachment, $timestamp, $hr_vars);
- };
- Bugzilla->error_mode($error_mode_cache);
- if ($@) {
- $hr_vars->{'message'} = 'flag_creation_failed';
- $hr_vars->{'flag_creation_error'} = $@;
- }
-
# Return the new attachment object.
return $attachment;
}
+sub run_create_validators {
+ my ($class, $params) = @_;
+
+ # Let's validate the attachment content first as it may
+ # alter some other attachment attributes.
+ $params->{data} = $class->_check_data($params);
+ $params = $class->SUPER::run_create_validators($params);
+
+ $params->{filename} = $class->_check_filename($params->{filename}, $params->{isurl});
+ $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $params->{modification_time} = $params->{creation_ts};
+ $params->{submitter_id} = Bugzilla->user->id || ThrowCodeError('invalid_user');
+
+ return $params;
+}
+
+sub update {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ my ($changes, $old_self) = $self->SUPER::update(@_);
+
+ my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_self, $timestamp);
+ if ($removed || $added) {
+ $changes->{'flagtypes.name'} = [$removed, $added];
+ }
+
+ # Record changes in the activity table.
+ my $sth = $dbh->prepare('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
+ fieldid, removed, added)
+ VALUES (?, ?, ?, ?, ?, ?, ?)');
+
+ foreach my $field (keys %$changes) {
+ my $change = $changes->{$field};
+ $field = "attachments.$field" unless $field eq "flagtypes.name";
+ my $fieldid = get_field_id($field);
+ $sth->execute($self->bug_id, $self->id, $user->id, $timestamp,
+ $fieldid, $change->[0], $change->[1]);
+ }
+
+ if (scalar(keys %$changes)) {
+ $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?',
+ undef, ($timestamp, $self->id));
+ $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
+ undef, ($timestamp, $self->bug_id));
+ }
+
+ return $changes;
+}
+
=pod
=item C
@@ -1042,4 +968,67 @@ sub remove_from_db {
$dbh->bz_commit_transaction();
}
+###############################
+#### Helpers #####
+###############################
+
+# Extract the content type from the attachment form.
+my $lwp_read_mime_types;
+sub get_content_type {
+ my $cgi = Bugzilla->cgi;
+
+ return 'text/plain' if ($cgi->param('ispatch') ||
+ $cgi->param('text_attachment') !~ /^\s*$/so ||
+ $cgi->param('attachurl'));
+
+ my $content_type;
+ if (!defined $cgi->param('contenttypemethod')) {
+ ThrowUserError("missing_content_type_method");
+ }
+ elsif ($cgi->param('contenttypemethod') eq 'autodetect') {
+ defined $cgi->upload('data') || ThrowUserError('file_not_specified');
+ # The user asked us to auto-detect the content type, so use the type
+ # specified in the HTTP request headers.
+ $content_type =
+ $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
+ if (!_valid_content_type($content_type) && Bugzilla->params->{mime_types_file})
+ {
+ if (!$lwp_read_mime_types)
+ {
+ LWP::MediaTypes::read_media_types(Bugzilla->params->{mime_types_file});
+ $lwp_read_mime_types = 1;
+ }
+ my $file = $cgi->param('data');
+ $content_type = LWP::MediaTypes::guess_media_type("$file");
+ }
+ if (!_valid_content_type($content_type))
+ {
+ $content_type = 'application/octet-stream';
+ }
+ $content_type || ThrowUserError("missing_content_type");
+
+ # Set the ispatch flag to 1 if the content type
+ # is text/x-diff or text/x-patch
+ if ($content_type =~ m{text/x-(?:diff|patch)}) {
+ $cgi->param('ispatch', 1);
+ $content_type = 'text/plain';
+ }
+ }
+ elsif ($cgi->param('contenttypemethod') eq 'list') {
+ # The user selected a content type from the list, so use their
+ # selection.
+ $content_type = $cgi->param('contenttypeselection');
+ }
+ elsif ($cgi->param('contenttypemethod') eq 'manual') {
+ # The user entered a content type manually, so use their entry.
+ $content_type = $cgi->param('contenttypeentry');
+ }
+ else {
+ ThrowCodeError("illegal_content_type_method",
+ { contenttypemethod => $cgi->param('contenttypemethod') });
+ }
+ return $content_type;
+}
+
+
1;
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index 4a8dbfbb3..469a0deaf 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -32,6 +32,9 @@ use fields qw(
use Bugzilla::Constants;
use Bugzilla::Error;
+use Bugzilla::Mailer;
+use Bugzilla::Util qw(datetime_from);
+use Bugzilla::User::Setting ();
use Bugzilla::Auth::Login::Stack;
use Bugzilla::Auth::Verify::Stack;
use Bugzilla::Auth::Persist::Cookie;
@@ -120,7 +123,7 @@ sub can_change_password {
my $verifier = $self->{_verifier}->{successful};
$verifier ||= $self->{_verifier};
my $getter = $self->{_info_getter}->{successful};
- $getter = $self->{_info_getter}
+ $getter = $self->{_info_getter}
if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
return $verifier->can_change_password &&
$getter->user_can_create_account;
@@ -185,7 +188,10 @@ sub _handle_login_result {
# the password was just wrong. (This makes it harder for a cracker
# to find account names by brute force)
elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) {
- ThrowUserError("invalid_username_or_password");
+ my $remaining_attempts = MAX_LOGIN_ATTEMPTS
+ - ($result->{failure_count} || 0);
+ ThrowUserError("invalid_username_or_password",
+ { remaining => $remaining_attempts });
}
# The account may be disabled
elsif ($fail_code == AUTH_DISABLED) {
@@ -196,6 +202,40 @@ sub _handle_login_result {
ThrowUserError("account_disabled",
{'disabled_reason' => $result->{user}->disabledtext});
}
+ elsif ($fail_code == AUTH_LOCKOUT) {
+ my $attempts = $user->account_ip_login_failures;
+
+ # We want to know when the account will be unlocked. This is
+ # determined by the 5th-from-last login failure (or more/less than
+ # 5th, if MAX_LOGIN_ATTEMPTS is not 5).
+ my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS];
+ my $unlock_at = datetime_from($determiner->{login_time},
+ Bugzilla->local_timezone);
+ $unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL);
+
+ # If we were *just* locked out, notify the maintainer about the
+ # lockout.
+ if ($result->{just_locked_out}) {
+ # We're sending to the maintainer, who may be not a Bugzilla
+ # account, but just an email address. So we use the
+ # installation's default language for sending the email.
+ my $default_settings = Bugzilla::User::Setting::get_defaults();
+ my $template = Bugzilla->template_inner($default_settings->{lang});
+ my $vars = {
+ locked_user => $user,
+ attempts => $attempts,
+ unlock_at => $unlock_at,
+ };
+ my $message;
+ $template->process('email/lockout.txt.tmpl', $vars, \$message)
+ || ThrowTemplateError($template->error);
+ MessageToMTA($message);
+ }
+
+ $unlock_at->set_time_zone($user->timezone);
+ ThrowUserError('account_locked',
+ { ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at });
+ }
# If we get here, then we've run out of options, which shouldn't happen.
else {
ThrowCodeError("authres_unhandled", { value => $fail_code });
@@ -257,6 +297,11 @@ various fields to be used in the error message.
An incorrect username or password was given.
+The hashref may also contain a C element, which specifies
+how many times the account has failed to log in within the lockout
+period (see L). This is used to warn the user when
+he is getting close to being locked out.
+
=head2 C
This is an optional more-specific version of C.
@@ -274,6 +319,15 @@ should never be communicated to the user, for security reasons.
The user successfully logged in, but their account has been disabled.
Usually this is throw only by C.
+=head2 C
+
+The user's account is locked out after having failed to log in too many
+times within a certain period of time (as specified by
+L).
+
+The hashref will also contain a C element, representing the
+L whose account is locked out.
+
=head1 LOGIN TYPES
The C function (below) can do different types of login, depending
diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm
index 5be98aa7a..8e877b951 100644
--- a/Bugzilla/Auth/Login/CGI.pm
+++ b/Bugzilla/Auth/Login/CGI.pm
@@ -40,12 +40,10 @@ use Bugzilla::Error;
sub get_login_info {
my ($self) = @_;
- my $cgi = Bugzilla->cgi;
+ my $params = Bugzilla->input_params;
- my $username = trim($cgi->param("Bugzilla_login"));
- my $password = $cgi->param("Bugzilla_password");
-
- $cgi->delete('Bugzilla_login', 'Bugzilla_password');
+ my $username = trim(delete $params->{"Bugzilla_login"});
+ my $password = delete $params->{"Bugzilla_password"};
if (!defined $username || !defined $password) {
return { failure => AUTH_NODATA };
@@ -59,21 +57,8 @@ sub fail_nodata {
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
- if (Bugzilla->error_mode == Bugzilla::Constants::ERROR_MODE_DIE_SOAP_FAULT) {
- die SOAP::Fault
- ->faultcode(ERROR_AUTH_NODATA)
- ->faultstring('Login Required');
- }
-
- # If system is not configured to never require SSL connections
- # we want to always redirect to SSL since passing usernames and
- # passwords over an unprotected connection is a bad idea. If we
- # get here then a login form will be provided to the user so we
- # want this to be protected if possible.
- if ($cgi->protocol ne 'https' && Bugzilla->params->{'sslbase'} ne ''
- && Bugzilla->params->{'ssl'} ne 'never')
- {
- $cgi->require_https(Bugzilla->params->{'sslbase'});
+ if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
+ ThrowUserError('login_required');
}
print $cgi->header();
diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm
index e2cd8f5ee..570988f7e 100644
--- a/Bugzilla/Auth/Login/Cookie.pm
+++ b/Bugzilla/Auth/Login/Cookie.pm
@@ -35,8 +35,7 @@ sub get_login_info {
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
- my $ip_addr = $cgi->remote_addr();
- my $net_addr = get_netaddr($ip_addr);
+ my $ip_addr = remote_ip();
my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
my $user_id = $cgi->cookie("Bugzilla_login");
@@ -60,24 +59,16 @@ sub get_login_info {
trick_taint($login_cookie);
detaint_natural($user_id);
- my $query = "SELECT userid
- FROM logincookies
- WHERE logincookies.cookie = ?
- AND logincookies.userid = ?
- AND (logincookies.ipaddr = ?";
-
- # If we have a network block that's allowed to use this cookie,
- # as opposed to just a single IP.
- my @params = ($login_cookie, $user_id, $ip_addr);
- if (defined $net_addr) {
- trick_taint($net_addr);
- $query .= " OR logincookies.ipaddr = ?";
- push(@params, $net_addr);
- }
- $query .= ")";
+ my $is_valid =
+ $dbh->selectrow_array('SELECT 1
+ FROM logincookies
+ WHERE cookie = ?
+ AND userid = ?
+ AND (ipaddr = ? OR ipaddr IS NULL)',
+ undef, ($login_cookie, $user_id, $ip_addr));
# If the cookie is valid, return a valid username.
- if ($dbh->selectrow_array($query, undef, @params)) {
+ if ($is_valid) {
# If we logged in successfully, then update the lastused
# time on the login cookie
$dbh->do("UPDATE logincookies SET lastused = NOW()
diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm
index a5752f22b..bef9171c9 100644
--- a/Bugzilla/Auth/Login/Stack.pm
+++ b/Bugzilla/Auth/Login/Stack.pm
@@ -35,7 +35,7 @@ sub new {
my $list = shift;
my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list);
lock_keys(%methods);
- Bugzilla::Hook::process('auth-login_methods', { modules => \%methods });
+ Bugzilla::Hook::process('auth_login_methods', { modules => \%methods });
$self->{_stack} = [];
foreach my $login_method (split(',', $list)) {
diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm
index c533252d3..232212075 100644
--- a/Bugzilla/Auth/Persist/Cookie.pm
+++ b/Bugzilla/Auth/Persist/Cookie.pm
@@ -48,18 +48,16 @@ sub persist_login {
my ($self, $user) = @_;
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
+ my $input_params = Bugzilla->input_params;
- my $ip_addr = $cgi->remote_addr;
- unless ($cgi->param('Bugzilla_restrictlogin') ||
- Bugzilla->params->{'loginnetmask'} == 32)
- {
- $ip_addr = get_netaddr($ip_addr);
+ my $ip_addr;
+ if ($input_params->{'Bugzilla_restrictlogin'}) {
+ $ip_addr = remote_ip();
+ # The IP address is valid, at least for comparing with itself in a
+ # subsequent login
+ trick_taint($ip_addr);
}
- # The IP address is valid, at least for comparing with itself in a
- # subsequent login
- trick_taint($ip_addr);
-
$dbh->bz_start_transaction();
my $login_cookie =
@@ -83,17 +81,15 @@ sub persist_login {
# or admin didn't forbid it and user told to remember.
if ( Bugzilla->params->{'rememberlogin'} eq 'on' ||
(Bugzilla->params->{'rememberlogin'} ne 'off' &&
- $cgi->param('Bugzilla_remember') &&
- $cgi->param('Bugzilla_remember') eq 'on') )
+ $input_params->{'Bugzilla_remember'} &&
+ $input_params->{'Bugzilla_remember'} eq 'on') )
{
# Not a session cookie, so set an infinite expiry
$cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT';
}
- if (Bugzilla->params->{'ssl'} ne 'never'
- && Bugzilla->params->{'sslbase'} ne '')
- {
- # Bugzilla->login will automatically redirect to https://,
- # so it's safe to turn on the 'secure' bit.
+ if (Bugzilla->params->{'ssl_redirect'}) {
+ # Make these cookies only be sent to us by the browser during
+ # HTTPS sessions, if we're using SSL.
$cookieargs{'-secure'} = 1;
}
diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm
index e3dffcd02..d8794472e 100644
--- a/Bugzilla/Auth/Verify/DB.pm
+++ b/Bugzilla/Auth/Verify/DB.pm
@@ -41,37 +41,51 @@ sub check_credentials {
my $dbh = Bugzilla->dbh;
my $username = $login_data->{username};
- my $user_id = login_to_id($username);
-
- return { failure => AUTH_NO_SUCH_USER } unless $user_id;
+ my $user = new Bugzilla::User({ name => $username });
+
+ return { failure => AUTH_NO_SUCH_USER } unless $user;
+
+ $login_data->{user} = $user;
+ $login_data->{bz_username} = $user->login;
+
+ if ($user->account_is_locked_out) {
+ return { failure => AUTH_LOCKOUT, user => $user };
+ }
- $login_data->{bz_username} = $username;
my $password = $login_data->{password};
-
- trick_taint($username);
- my ($real_password_crypted) = $dbh->selectrow_array(
- "SELECT cryptpassword FROM profiles WHERE userid = ?",
- undef, $user_id);
+ my $real_password_crypted = $user->cryptpassword;
# Using the internal crypted password as the salt,
# crypt the password the user entered.
my $entered_password_crypted = bz_crypt($password, $real_password_crypted);
-
- return { failure => AUTH_LOGINFAILED }
- if $entered_password_crypted ne $real_password_crypted;
+
+ if ($entered_password_crypted ne $real_password_crypted) {
+ # Record the login failure
+ $user->note_login_failure();
+
+ # Immediately check if we are locked out
+ if ($user->account_is_locked_out) {
+ return { failure => AUTH_LOCKOUT, user => $user,
+ just_locked_out => 1 };
+ }
+
+ return { failure => AUTH_LOGINFAILED,
+ failure_count => scalar(@{ $user->account_ip_login_failures }),
+ };
+ }
# The user's credentials are okay, so delete any outstanding
- # password tokens they may have generated.
- Bugzilla::Token::DeletePasswordTokens($user_id, "user_logged_in");
+ # password tokens or login failures they may have generated.
+ Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in");
+ $user->clear_login_failures();
# If their old password was using crypt() or some different hash
# than we're using now, convert the stored password to using
# whatever hashing system we're using now.
my $current_algorithm = PASSWORD_DIGEST_ALGORITHM;
if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
- my $new_crypted = bz_crypt($password);
- $dbh->do('UPDATE profiles SET cryptpassword = ? WHERE userid = ?',
- undef, $new_crypted, $user_id);
+ $user->set_password($password);
+ $user->update();
}
return $login_data;
diff --git a/Bugzilla/Auth/Verify/LDAP.pm b/Bugzilla/Auth/Verify/LDAP.pm
index b5904301d..cdc802ca0 100644
--- a/Bugzilla/Auth/Verify/LDAP.pm
+++ b/Bugzilla/Auth/Verify/LDAP.pm
@@ -56,7 +56,7 @@ sub check_credentials {
# just appending the Base DN to the uid isn't sufficient to get the
# user's DN. For servers which don't work this way, there will still
# be no harm done.
- $self->_bind_ldap_anonymously();
+ $self->_bind_ldap_for_search();
# Now, we verify that the user exists, and get a LDAP Distinguished
# Name for the user.
@@ -76,12 +76,35 @@ sub check_credentials {
return { failure => AUTH_LOGINFAILED } if $pw_result->code;
# And now we fill in the user's details.
- my $detail_result = $self->ldap->search(_bz_search_params($username));
- return { failure => AUTH_ERROR, error => "ldap_search_error",
- details => {errstr => $detail_result->error, username => $username}
- } if $detail_result->code;
- my $user_entry = $detail_result->shift_entry;
+ # First try the search as the (already bound) user in question.
+ my $user_entry;
+ my $error_string;
+ my $detail_result = $self->ldap->search(_bz_search_params($username));
+ if ($detail_result->code) {
+ # Stash away the original error, just in case
+ $error_string = $detail_result->error;
+ } else {
+ $user_entry = $detail_result->shift_entry;
+ }
+
+ # If that failed (either because the search failed, or returned no
+ # results) then try re-binding as the initial search user, but only
+ # if the LDAPbinddn parameter is set.
+ if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) {
+ $self->_bind_ldap_for_search();
+
+ $detail_result = $self->ldap->search(_bz_search_params($username));
+ if (!$detail_result->code) {
+ $user_entry = $detail_result->shift_entry;
+ }
+ }
+
+ # If we *still* don't have anything in $user_entry then give up.
+ return { failure => AUTH_ERROR, error => "ldap_search_error",
+ details => {errstr => $error_string, username => $username}
+ } if !$user_entry;
+
my $mail_attr = Bugzilla->params->{"LDAPmailattribute"};
if ($mail_attr) {
@@ -128,7 +151,7 @@ sub _bz_search_params {
. Bugzilla->params->{"LDAPfilter"} . ')');
}
-sub _bind_ldap_anonymously {
+sub _bind_ldap_for_search {
my ($self) = @_;
my $bind_result;
if (Bugzilla->params->{"LDAPbinddn"}) {
diff --git a/Bugzilla/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm
index c23b532fd..2df3fcd25 100644
--- a/Bugzilla/Auth/Verify/Stack.pm
+++ b/Bugzilla/Auth/Verify/Stack.pm
@@ -30,7 +30,7 @@ sub new {
my $self = $class->SUPER::new(@_);
my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list);
lock_keys(%methods);
- Bugzilla::Hook::process('auth-verify_methods', { modules => \%methods });
+ Bugzilla::Hook::process('auth_verify_methods', { modules => \%methods });
$self->{_stack} = [];
foreach my $verify_method (split(',', $list)) {
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index dfd18b0b5..de789bfd2 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -39,13 +39,16 @@ use Bugzilla::FlagType;
use Bugzilla::FlagType::UserList;
use Bugzilla::Hook;
use Bugzilla::Keyword;
+use Bugzilla::Milestone;
use Bugzilla::User;
use Bugzilla::Util;
+use Bugzilla::Version;
use Bugzilla::Error;
use Bugzilla::Product;
use Bugzilla::Component;
use Bugzilla::Group;
use Bugzilla::Status;
+use Bugzilla::Comment;
use List::Util qw(min);
use Storable qw(dclone);
@@ -108,9 +111,9 @@ sub DB_COLUMNS {
$dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts',
$dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
@custom_names);
-
- Bugzilla::Hook::process("bug-columns", { columns => \@columns });
-
+
+ Bugzilla::Hook::process("bug_columns", { columns => \@columns });
+
return @columns;
}
@@ -124,24 +127,27 @@ use constant REQUIRED_CREATE_FIELDS => qw(
# There are also other, more complex validators that are called
# from run_create_validators.
sub VALIDATORS {
+ my $cache = Bugzilla->request_cache;
+ return $cache->{bug_validators} if defined $cache->{bug_validators};
+
my $validators = {
alias => \&_check_alias,
bug_file_loc => \&_check_bug_file_loc,
- bug_severity => \&_check_bug_severity,
+ bug_severity => \&_check_select_field,
comment => \&_check_comment,
commentprivacy => \&_check_commentprivacy,
deadline => \&_check_deadline,
estimated_time => \&_check_estimated_time,
- op_sys => \&_check_op_sys,
+ op_sys => \&_check_select_field,
priority => \&_check_priority,
product => \&_check_product,
remaining_time => \&_check_remaining_time,
- rep_platform => \&_check_rep_platform,
+ rep_platform => \&_check_select_field,
short_desc => \&_check_short_desc,
status_whiteboard => \&_check_status_whiteboard,
};
- # Set up validators for custom fields.
+ # Set up validators for custom fields.
foreach my $field (Bugzilla->active_custom_fields) {
my $validator;
if ($field->type == FIELD_TYPE_SINGLE_SELECT) {
@@ -165,7 +171,8 @@ sub VALIDATORS {
$validators->{$field->name} = $validator;
}
- return $validators;
+ $cache->{bug_validators} = $validators;
+ return $cache->{bug_validators};
};
use constant UPDATE_VALIDATORS => {
@@ -239,6 +246,27 @@ use constant UPDATE_COMMENT_COLUMNS => qw(
# activity table.
use constant MAX_LINE_LENGTH => 254;
+# This maps the names of internal Bugzilla bug fields to things that would
+# make sense to somebody who's not intimately familiar with the inner workings
+# of Bugzilla. (These are the field names that the WebService and email_in.pl
+# use.)
+use constant FIELD_MAP => {
+ creation_time => 'creation_ts',
+ description => 'comment',
+ id => 'bug_id',
+ last_change_time => 'delta_ts',
+ platform => 'rep_platform',
+ severity => 'bug_severity',
+ status => 'bug_status',
+ summary => 'short_desc',
+ url => 'bug_file_loc',
+ whiteboard => 'status_whiteboard',
+
+ # These are special values for the WebService Bug.search method.
+ limit => 'LIMIT',
+ offset => 'OFFSET',
+};
+
#####################################################################
sub new {
@@ -383,12 +411,12 @@ sub match {
# code to deal with the different sets of fields here.
foreach my $field (qw(assigned_to qa_contact reporter)) {
delete $params->{"${field}_id"};
- $params->{$field} = $translated{$field}
+ $params->{$field} = $translated{$field}
if exists $translated{$field};
}
foreach my $field (qw(product component)) {
delete $params->{$field};
- $params->{"${field}_id"} = $translated{$field}
+ $params->{"${field}_id"} = $translated{$field}
if exists $translated{$field};
}
@@ -425,10 +453,10 @@ sub match {
#
# C - The full login name of the user who the bug is
# initially assigned to.
-# C - The full login name of the QA Contact for this bug.
+# C - The full login name of the QA Contact for this bug.
# Will be ignored if C is off.
#
-# C - For time-tracking. Will be ignored if
+# C - For time-tracking. Will be ignored if
# C is not set, or if the current
# user is not a member of the timetrackinggroup.
# C - For time-tracking. Will be ignored for the same
@@ -443,13 +471,13 @@ sub create {
# These fields have default values which we can use if they are undefined.
$params->{bug_severity} = Bugzilla->params->{defaultseverity}
- unless defined $params->{bug_severity};
+ unless defined $params->{bug_severity};
$params->{priority} = Bugzilla->params->{defaultpriority}
- unless defined $params->{priority};
+ unless defined $params->{priority};
$params->{op_sys} = Bugzilla->params->{defaultopsys}
- unless defined $params->{op_sys};
+ unless defined $params->{op_sys};
$params->{rep_platform} = Bugzilla->params->{defaultplatform}
- unless defined $params->{rep_platform};
+ unless defined $params->{rep_platform};
# Make sure a comment is always defined.
$params->{comment} = '' unless defined $params->{comment};
@@ -469,12 +497,12 @@ sub create {
# Set up the keyword cache for bug creation.
my $keywords = $params->{keywords};
- $params->{keywords} = join(', ', sort {lc($a) cmp lc($b)}
+ $params->{keywords} = join(', ', sort {lc($a) cmp lc($b)}
map($_->name, @$keywords));
# We don't want the bug to appear in the system until it's correctly
# protected by groups.
- my $timestamp = delete $params->{creation_ts};
+ my $timestamp = delete $params->{creation_ts};
my $ms_values = $class->_extract_multi_selects($params);
my $bug = $class->insert_create_data($params);
@@ -539,7 +567,7 @@ sub create {
# but sometimes it's blank.
my @columns = qw(bug_id who bug_when thetext work_time);
my @values = ($bug->bug_id, $bug->{reporter_id}, $timestamp, $comment, $work_time);
- # We don't include the "isprivate" column unless it was specified.
+ # We don't include the "isprivate" column unless it was specified.
# This allows it to fall back to its database default.
if (defined $privacy) {
push(@columns, 'isprivate');
@@ -550,7 +578,7 @@ sub create {
$dbh->do('INSERT INTO longdescs (' . join(',', @columns) . ")
VALUES ($qmarks)", undef, @values);
- Bugzilla::Hook::process('bug-end_of_create', { bug => $bug,
+ Bugzilla::Hook::process('bug_end_of_create', { bug => $bug,
timestamp => $timestamp,
});
@@ -564,7 +592,6 @@ sub create {
return $bug;
}
-
sub run_create_validators {
my $class = shift;
my $params = $class->SUPER::run_create_validators(@_);
@@ -600,16 +627,16 @@ sub run_create_validators {
$params->{component_id} = $component->id;
delete $params->{component};
- $params->{assigned_to} =
+ $params->{assigned_to} =
$class->_check_assigned_to($params->{assigned_to}, $component);
$params->{qa_contact} =
$class->_check_qa_contact($params->{qa_contact}, $component);
$params->{cc} = $class->_check_cc($component, $params->{cc});
- # Callers cannot set Reporter, currently.
+ # Callers cannot set reporter, creation_ts, or delta_ts.
$params->{reporter} = $class->_check_reporter();
-
- $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ $params->{creation_ts} =
+ Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
$params->{delta_ts} = $params->{creation_ts};
if ($params->{estimated_time}) {
@@ -621,7 +648,7 @@ sub run_create_validators {
$class->_check_strict_isolation($params->{cc}, $params->{assigned_to},
$params->{qa_contact}, $product);
- ($params->{dependson}, $params->{blocked}) =
+ ($params->{dependson}, $params->{blocked}) =
$class->_check_dependencies($params->{dependson}, $params->{blocked},
$product);
@@ -630,6 +657,9 @@ sub run_create_validators {
delete $params->{lastdiffed};
delete $params->{bug_id};
+ Bugzilla::Hook::process('bug_end_of_create_validators',
+ { params => $params });
+
return $params;
}
@@ -640,7 +670,7 @@ sub update {
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 NOW()");
+ my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
Bugzilla::Hook::process('bug-pre_update', { bug => $self, timestamp => $delta_ts });
@@ -648,14 +678,12 @@ sub update {
# Certain items in $changes have to be fixed so that they hold
# a name instead of an ID.
- foreach my $field (qw(product_id component_id))
- {
+ foreach my $field (qw(product_id component_id)) {
my $change = delete $changes->{$field};
- if ($change)
- {
+ if ($change) {
my $new_field = $field;
$new_field =~ s/_id$//;
- $changes->{$new_field} =
+ $changes->{$new_field} =
[$self->{"_old_${new_field}_name"}, $self->$new_field];
}
}
@@ -687,6 +715,7 @@ sub update {
my ($restricted_cc, undef) = diff_arrays($self->cc_users, \@new_cc);
if (scalar @$restricted_cc)
{
+ # CustIS Bug 38616 - we must print a warning about that we've removed somebody from CC
# нужно вывести предупреждение о том, что кое-кого сюда подписывать нельзя!
$self->{cc_restrict_group} = $ccg;
$self->{restricted_cc} = [ map { $_->login } @$restricted_cc ];
@@ -695,15 +724,14 @@ sub update {
{
delete $self->{restricted_cc};
}
-
@new_cc = map {$_->id} @new_cc;
my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc);
+
if (scalar @$removed_cc) {
- $dbh->do('DELETE FROM cc WHERE bug_id = ? AND '
+ $dbh->do('DELETE FROM cc WHERE bug_id = ? AND '
. $dbh->sql_in('who', $removed_cc), undef, $self->id);
}
- foreach my $user_id (@$added_cc)
- {
+ foreach my $user_id (@$added_cc) {
$dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)',
undef, $self->id, $user_id);
}
@@ -715,7 +743,7 @@ sub update {
my $added_names = join(', ', (map {$_->login} @$added_users));
$changes->{cc} = [$removed_names, $added_names];
}
-
+
# Keywords
my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects};
my @new_kw_ids = map { $_->id } @{$self->keyword_objects};
@@ -723,7 +751,7 @@ sub update {
my ($removed_kw, $added_kw) = diff_arrays(\@old_kw_ids, \@new_kw_ids);
if (scalar @$removed_kw) {
- $dbh->do('DELETE FROM keywords WHERE bug_id = ? AND '
+ $dbh->do('DELETE FROM keywords WHERE bug_id = ? AND '
. $dbh->sql_in('keywordid', $removed_kw), undef, $self->id);
}
foreach my $keyword_id (@$added_kw) {
@@ -746,12 +774,12 @@ sub update {
my ($type, $other) = @$pair;
my $old = $old_bug->$type;
my $new = $self->$type;
-
+
my ($removed, $added) = diff_arrays($old, $new);
foreach my $removed_id (@$removed) {
$dbh->do("DELETE FROM dependencies WHERE $type = ? AND $other = ?",
undef, $removed_id, $self->id);
-
+
# Add an activity entry for the other bug.
LogActivityEntry($removed_id, $other, $self->id, '',
$user->id, $delta_ts);
@@ -762,7 +790,7 @@ sub update {
foreach my $added_id (@$added) {
$dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)",
undef, $added_id, $self->id);
-
+
# Add an activity entry for the other bug.
LogActivityEntry($added_id, $other, '', $self->id,
$user->id, $delta_ts);
@@ -770,7 +798,7 @@ sub update {
$dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
undef, $delta_ts, $added_id);
}
-
+
if (scalar(@$removed) || scalar(@$added)) {
$changes->{$type} = [join(', ', @$removed), join(', ', @$added)];
}
@@ -799,6 +827,12 @@ sub update {
join(', ', @added_names)];
}
+ # Flags
+ my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts);
+ if ($removed || $added) {
+ $changes->{'flagtypes.name'} = [$removed, $added];
+ }
+
# Comments
foreach my $comment (@{$self->{added_comments} || []})
{
@@ -818,7 +852,7 @@ sub update {
Bugzilla->user->id, $delta_ts);
}
}
-
+
foreach my $comment_id (keys %{$self->{comment_isprivate} || {}}) {
$dbh->do("UPDATE longdescs SET isprivate = ? WHERE comment_id = ?",
undef, $self->{comment_isprivate}->{$comment_id}, $comment_id);
@@ -844,7 +878,7 @@ sub update {
}
# See Also
- my ($removed_see, $added_see) =
+ my ($removed_see, $added_see) =
diff_arrays($old_bug->see_also, $self->see_also);
if (scalar @$removed_see) {
@@ -884,14 +918,13 @@ sub update {
$update_dup->update();
}
}
-
+
$changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
}
- Bugzilla::Hook::process('bug-end_of_update', { bug => $self,
- timestamp => $delta_ts,
- changes => $changes,
- });
+ Bugzilla::Hook::process('bug_end_of_update',
+ { bug => $self, timestamp => $delta_ts, changes => $changes,
+ old_bug => $old_bug });
# If any change occurred, refresh the timestamp of the bug.
if (scalar(keys %$changes) || $self->{added_comments}) {
@@ -1019,7 +1052,7 @@ sub remove_from_db {
WHERE bug_id = ?", undef, $bug_id);
if (scalar(@$attach_ids)) {
- $dbh->do("DELETE FROM attach_data WHERE "
+ $dbh->do("DELETE FROM attach_data WHERE "
. $dbh->sql_in('id', $attach_ids));
}
@@ -1107,7 +1140,7 @@ sub _check_assigned_to {
sub _check_bug_file_loc {
my ($invocant, $url) = @_;
$url = '' if !defined($url);
- # On bug entry, if bug_file_loc is "http://", the default, use an
+ # On bug entry, if bug_file_loc is "http://", the default, use an
# empty value instead. However, on bug editing people can set that
# back if they *really* want to.
if (!ref $invocant && $url eq 'http://') {
@@ -1116,13 +1149,6 @@ sub _check_bug_file_loc {
return $url;
}
-sub _check_bug_severity {
- my ($invocant, $severity) = @_;
- $severity = trim($severity);
- check_field('bug_severity', $severity);
- return $severity;
-}
-
sub _check_bug_status {
my ($invocant, $new_status, $product, $comment, $assigned_to) = @_;
my $user = Bugzilla->user;
@@ -1130,7 +1156,7 @@ sub _check_bug_status {
my $old_status; # Note that this is undef for new bugs.
if (ref $invocant) {
- @valid_statuses = @{$invocant->status->can_change_to};
+ @valid_statuses = @{$invocant->statuses_available};
$product = $invocant->product_obj;
$old_status = $invocant->status;
my $comments = $invocant->{added_comments} || [];
@@ -1138,12 +1164,9 @@ sub _check_bug_status {
}
else {
@valid_statuses = @{Bugzilla::Status->can_change_to()};
- }
-
- if (!$product->votes_to_confirm) {
- # UNCONFIRMED becomes an invalid status if votes_to_confirm is 0,
- # even if you are in editbugs.
- @valid_statuses = grep {$_->name ne 'UNCONFIRMED'} @valid_statuses;
+ if (!$product->allows_unconfirmed) {
+ @valid_statuses = grep {$_->name ne 'UNCONFIRMED'} @valid_statuses;
+ }
}
if ($assigned_to && $user->email ne $assigned_to)
@@ -1180,9 +1203,13 @@ sub _check_bug_status {
}
}
}
+
# Time to validate the bug status.
$new_status = Bugzilla::Status->check($new_status) unless ref($new_status);
- if (!grep {$_->name eq $new_status->name} @valid_statuses) {
+ # We skip this check if we are changing from a status to itself.
+ if ( (!$old_status || $old_status->id != $new_status->id)
+ && !grep {$_->name eq $new_status->name} @valid_statuses)
+ {
ThrowUserError('illegal_bug_status_transition',
{ old => $old_status, new => $new_status });
}
@@ -1192,9 +1219,9 @@ sub _check_bug_status {
{
ThrowUserError('comment_required', { old => $old_status,
new => $new_status });
-
+
}
-
+
if (ref $invocant && $new_status->name eq 'ASSIGNED'
&& Bugzilla->params->{"usetargetmilestone"}
&& Bugzilla->params->{"musthavemilestoneonaccept"}
@@ -1214,6 +1241,9 @@ sub _check_cc {
my ($invocant, $component, $ccs) = @_;
return [map {$_->id} @{$component->initial_cc}] unless $ccs;
+ # Allow comma-separated input as well as arrayrefs.
+ $ccs = [split(/[\s,]+/, $ccs)] if !ref $ccs;
+
my %cc_ids;
my ($ccg) = $component->product->description =~ /\[[CС]{2}:\s*([^\]]+)\s*\]/iso;
foreach my $person (@$ccs) {
@@ -1256,7 +1286,7 @@ sub _check_commentprivacy {
sub _check_comment_type {
my ($invocant, $type) = @_;
detaint_natural($type)
- || ThrowCodeError('bad_arg', { argument => 'type',
+ || ThrowCodeError('bad_arg', { argument => 'type',
function => caller });
return $type;
}
@@ -1272,13 +1302,12 @@ sub _check_component {
sub _check_deadline {
my ($invocant, $date) = @_;
-
+
# Check time-tracking permissions.
- my $tt_group = Bugzilla->params->{"timetrackinggroup"};
# deadline() returns '' instead of undef if no deadline is set.
my $current = ref $invocant ? ($invocant->deadline || undef) : undef;
- return $current unless $tt_group && Bugzilla->user->in_group($tt_group);
-
+ return $current unless Bugzilla->user->is_timetracker;
+
# Validate entered deadline
$date = trim($date);
return undef if !$date;
@@ -1302,22 +1331,22 @@ sub _check_dependencies {
my %deps_in = (dependson => $depends_on || '', blocked => $blocks || '');
foreach my $type qw(dependson blocked) {
- my @bug_ids = ref($deps_in{$type})
- ? @{$deps_in{$type}}
+ my @bug_ids = ref($deps_in{$type})
+ ? @{$deps_in{$type}}
: split(/[\s,]+/, $deps_in{$type});
# Eliminate nulls.
@bug_ids = grep {$_} @bug_ids;
# We do this up here to make sure all aliases are converted to IDs.
@bug_ids = map { $invocant->check($_, $type)->id } @bug_ids;
-
+
my @check_access = @bug_ids;
- # When we're updating a bug, only added or removed bug_ids are
+ # When we're updating a bug, only added or removed bug_ids are
# checked for whether or not we can see/edit those bugs.
if (ref $invocant) {
my $old = $invocant->$type;
my ($removed, $added) = diff_arrays($old, \@bug_ids);
@check_access = (@$added, @$removed);
-
+
# Check field permissions if we've changed anything.
if (@check_access) {
my $privs;
@@ -1339,7 +1368,7 @@ sub _check_dependencies {
}
}
}
-
+
$deps_in{$type} = \@bug_ids;
}
@@ -1353,7 +1382,7 @@ sub _check_dependencies {
sub _check_dup_id {
my ($self, $dupe_of) = @_;
my $dbh = Bugzilla->dbh;
-
+
$dupe_of = trim($dupe_of);
$dupe_of || ThrowCodeError('undefined_field', { field => 'dup_id' });
# Validate the bug ID. The second argument will force check() to only
@@ -1403,8 +1432,8 @@ sub _check_dup_id {
$self->{_add_dup_cc} = 1
if $dupe_of_bug->reporter->id != $self->reporter->id;
}
- # What if the reporter currently can't see the new bug? In the browser
- # interface, we prompt the user. In other interfaces, we default to
+ # What if the reporter currently can't see the new bug? In the browser
+ # interface, we prompt the user. In other interfaces, we default to
# not adding the user, as the safest option.
elsif (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
# If we've already confirmed whether the user should be added...
@@ -1458,9 +1487,9 @@ sub _check_groups {
my $membercontrol = $controls->{$id}
&& $controls->{$id}->{membercontrol};
- my $othercontrol = $controls->{$id}
+ my $othercontrol = $controls->{$id}
&& $controls->{$id}->{othercontrol};
-
+
my $permit = ($membercontrol && $user->in_group($group->name))
|| $othercontrol;
@@ -1475,7 +1504,7 @@ sub _check_groups {
# Add groups required
if ($membercontrol == CONTROLMAPMANDATORY
|| ($othercontrol == CONTROLMAPMANDATORY
- && !$user->in_group_id($id)))
+ && !$user->in_group_id($id)))
{
# User had no option, bug needs to be in this group.
$add_groups{$id} = 1;
@@ -1490,12 +1519,12 @@ sub _check_keywords {
my ($invocant, $keyword_string, $product) = @_;
$keyword_string = trim($keyword_string);
return [] if !$keyword_string;
-
+
# On creation, only editbugs users can set keywords.
if (!ref $invocant) {
return [] if !Bugzilla->user->in_group('editbugs', $product->id);
}
-
+
my %keywords;
foreach my $keyword (split(/[\s,]+/, $keyword_string)) {
next unless $keyword;
@@ -1522,29 +1551,18 @@ sub _check_product {
return new Bugzilla::Product({ name => $name });
}
-sub _check_op_sys {
- my ($invocant, $op_sys) = @_;
- return Bugzilla->params->{defaultopsys} unless Bugzilla->params->{useopsys};
- $op_sys = trim($op_sys);
- check_field('op_sys', $op_sys);
- return $op_sys;
-}
-
sub _check_priority {
my ($invocant, $priority) = @_;
if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) {
$priority = Bugzilla->params->{'defaultpriority'};
}
- $priority = trim($priority);
- check_field('priority', $priority);
-
- return $priority;
+ return $invocant->_check_select_field($priority, 'priority');
}
sub _check_qa_contact {
my ($invocant, $qa_contact, $component) = @_;
$qa_contact = trim($qa_contact) if !ref $qa_contact;
-
+
my $id;
if (!ref $invocant) {
# Bugs get no QA Contact on creation if useqacontact is off.
@@ -1557,7 +1575,7 @@ sub _check_qa_contact {
$id = $component->default_qa_contact->id;
}
}
-
+
# If a QA Contact was specified or if we're updating, check
# the QA Contact for validity.
if (!defined $id && $qa_contact) {
@@ -1584,14 +1602,6 @@ sub _check_remaining_time {
return $_[0]->_check_time($_[1], 'remaining_time');
}
-sub _check_rep_platform {
- my ($invocant, $platform) = @_;
- return Bugzilla->params->{defaultplatform} unless Bugzilla->params->{useplatform};
- $platform = trim($platform);
- check_field('rep_platform', $platform);
- return $platform;
-}
-
sub _check_reporter {
my $invocant = shift;
my $reporter;
@@ -1623,19 +1633,19 @@ sub _check_reporter {
sub _check_resolution {
my ($self, $resolution) = @_;
$resolution = trim($resolution);
-
+
# Throw a special error for resolving bugs without a resolution
# (or trying to change the resolution to '' on a closed bug without
# using clear_resolution).
ThrowUserError('missing_resolution', { status => $self->status->name })
if !$resolution && !$self->status->is_open;
-
+
# Make sure this is a valid resolution.
- check_field('resolution', $resolution);
+ $resolution = $self->_check_select_field($resolution, 'resolution');
# Don't allow open bugs to have resolutions.
ThrowUserError('resolution_not_allowed') if $self->status->is_open;
-
+
# Check noresolveonopenblockers.
if (Bugzilla->params->{"noresolveonopenblockers"} && $resolution eq 'FIXED')
{
@@ -1648,13 +1658,13 @@ sub _check_resolution {
}
# Check if they're changing the resolution and need to comment.
- if (Bugzilla->params->{'commentonchange_resolution'}
- && $self->resolution && $resolution ne $self->resolution
+ if (Bugzilla->params->{'commentonchange_resolution'}
+ && $self->resolution && $resolution ne $self->resolution
&& !$self->{added_comments})
{
ThrowUserError('comment_required');
}
-
+
return $resolution;
}
@@ -1750,9 +1760,9 @@ sub _check_target_milestone {
$target = trim($target);
$target = $product->default_milestone if !defined $target;
- check_field('target_milestone', $target,
- [map($_->name, @{$product->milestones})], undef, { product => $product });
- return $target;
+ my $object = Bugzilla::Milestone->check(
+ { product => $product, name => $target });
+ return $object->name;
}
sub _check_time {
@@ -1762,9 +1772,8 @@ sub _check_time {
if (ref $invocant && $field ne 'work_time') {
$current = $invocant->$field;
}
- my $tt_group = Bugzilla->params->{"timetrackinggroup"};
- return $current unless $tt_group && Bugzilla->user->in_group($tt_group);
-
+ return $current unless Bugzilla->user->is_timetracker;
+
$time = ValidateTime($time, $field);
return $time;
}
@@ -1773,8 +1782,9 @@ sub _check_version {
my ($invocant, $version, $product) = @_;
$version = trim($version);
($product = $invocant->product_obj) if ref $invocant;
- check_field('version', $version, [map($_->name, @{$product->versions})], undef, { product => $product });
- return $version;
+ my $object =
+ Bugzilla::Version->check({ product => $product, name => $version });
+ return $object->name;
}
sub _check_work_time {
@@ -1819,20 +1829,29 @@ sub _check_freetext_field {
sub _check_multi_select_field {
my ($invocant, $values, $field) = @_;
- return [] if !$values;
- foreach my $value (@$values) {
- $value = trim($value);
- check_field($field, $value);
- trick_taint($value);
+
+ # Allow users (mostly email_in.pl) to specify multi-selects as
+ # comma-separated values.
+ if (defined $values and !ref $values) {
+ # We don't split on spaces because multi-select values can and often
+ # do have spaces in them. (Theoretically they can have commas in them
+ # too, but that's much less common and people should be able to work
+ # around it pretty cleanly, if they want to use email_in.pl.)
+ $values = [split(',', $values)];
}
- return $values;
+
+ return [] if !$values;
+ my @checked_values;
+ foreach my $value (@$values) {
+ push(@checked_values, $invocant->_check_select_field($value, $field));
+ }
+ return \@checked_values;
}
sub _check_select_field {
my ($invocant, $value, $field) = @_;
- $value = trim($value);
- check_field($field, $value);
- return $value;
+ my $object = Bugzilla::Field::Choice->type($field)->check($value);
+ return $object->name;
}
sub _check_bugid_field {
@@ -1848,8 +1867,8 @@ sub _check_bugid_field {
sub fields {
my $class = shift;
- my @fields =
- (
+ my @fields =
+ (
# Standard Fields
# Keep this ordering in sync with bugzilla.dtd.
qw(bug_id alias creation_ts short_desc delta_ts
@@ -1874,13 +1893,13 @@ sub fields {
# Custom Fields
map { $_->name } Bugzilla->active_custom_fields
);
- Bugzilla::Hook::process("bug-fields", {'fields' => \@fields} );
-
+ Bugzilla::Hook::process('bug_fields', {'fields' => \@fields} );
+
return @fields;
}
#####################################################################
-# Mutators
+# Mutators
#####################################################################
# To run check_can_change_field.
@@ -1932,12 +1951,12 @@ sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); }
sub set_comment_is_private {
my ($self, $comment_id, $isprivate) = @_;
return unless Bugzilla->user->is_insider;
- my ($comment) = grep($comment_id eq $_->{id}, @{$self->longdescs});
- ThrowUserError('comment_invalid_isprivate', { id => $comment_id })
+ my ($comment) = grep($comment_id == $_->id, @{ $self->comments });
+ ThrowUserError('comment_invalid_isprivate', { id => $comment_id })
if !$comment;
$isprivate = $isprivate ? 1 : 0;
- if ($isprivate != $comment->{isprivate}) {
+ if ($isprivate != $comment->is_private) {
$self->{comment_isprivate} ||= {};
$self->{comment_isprivate}->{$comment_id} = $isprivate;
}
@@ -1960,6 +1979,7 @@ sub set_component {
}
sub set_custom_field {
my ($self, $field, $value) = @_;
+
if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) {
$value = $value->[0];
}
@@ -1984,7 +2004,7 @@ sub set_dup_id {
$self->set('dup_id', $dup_id);
my $new = $self->dup_id;
return if $old == $new;
-
+
# Update the other bug.
my $dupe_of = new Bugzilla::Bug($self->dup_id);
if (delete $self->{_add_dup_cc}) {
@@ -1993,7 +2013,7 @@ sub set_dup_id {
$dupe_of->add_comment("", { type => CMT_HAS_DUPE,
extra_data => $self->id });
$self->{_dup_for_update} = $dupe_of;
-
+
# Now make sure that we add a duplicate comment on *this* bug.
# (Change an existing comment into a dup comment, if there is one,
# or add an empty dup comment.)
@@ -2011,6 +2031,11 @@ sub set_dup_id {
}
sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); }
sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); }
+sub set_flags {
+ my ($self, $flags, $new_flags) = @_;
+
+ Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags);
+}
sub set_op_sys { $_[0]->set('op_sys', $_[1]); }
sub set_platform { $_[0]->set('rep_platform', $_[1]); }
sub set_priority { $_[0]->set('priority', $_[1]); }
@@ -2018,7 +2043,7 @@ sub set_product {
my ($self, $name, $params) = @_;
my $old_product = $self->product_obj;
my $product = $self->_check_product($name);
-
+
my $product_changed = 0;
if ($old_product->id != $product->id) {
$self->{product_id} = $product->id;
@@ -2028,7 +2053,6 @@ sub set_product {
$self->{_old_product_name} = $old_product->name;
# Delete fields that depend upon the old Product value.
delete $self->{choices};
- delete $self->{milestoneurl};
$product_changed = 1;
}
@@ -2066,7 +2090,7 @@ sub set_product {
# other part of Bugzilla that checks $@.
undef $@;
Bugzilla->error_mode($old_error_mode);
-
+
my $verified = $params->{change_confirmed};
my %vars;
if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) {
@@ -2074,8 +2098,8 @@ sub set_product {
# Note that because of the eval { set } above, these are
# already set correctly if they're valid, otherwise they're
# set to some invalid value which the template will ignore.
- component => $self->component,
- version => $self->version,
+ component => $self->component,
+ version => $self->version,
milestone => $milestone_ok ? $self->target_milestone : $product->default_milestone
};
$vars{component_ok} = $component_ok;
@@ -2107,9 +2131,9 @@ sub set_product {
. Bugzilla->user->groups_as_string . '))
OR gcm.othercontrol != ?) )',
undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA));
- $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids);
+ $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids);
}
-
+
if (%vars)
{
$vars{product} = $product;
@@ -2119,8 +2143,8 @@ sub set_product {
$version_ok || push @ex, 'version';
$component_ok || push @ex, 'component';
$milestone_ok || push @ex, 'target_milestone';
- warn 'EX='.join ',', @ex;
- $vars{incorrect_fields} = [ map { get_fielddesc($_) } @ex ];
+ my $fd = template_var('field_descs');
+ $vars{incorrect_fields} = [ map { $fd->{$_} } @ex ];
$vars{verify_bug_groups} && push @ex, 'bit-\d+';
$vars{exclude_params_re} = '^' . join('|', @ex) . '$';
# Output "Verify new product details" page
@@ -2143,7 +2167,7 @@ sub set_product {
$self->set_target_milestone($tm_name);
}
}
-
+
if ($product_changed) {
# Remove groups that aren't valid in the new product. This will also
# have the side effect of removing the bug from groups that aren't
@@ -2157,13 +2181,13 @@ sub set_product {
$self->remove_group($group);
}
}
-
+
# Make sure the bug is in all the mandatory groups for the new product.
foreach my $group (@{$product->groups_mandatory_for(Bugzilla->user)}) {
$self->add_group($group);
}
}
-
+
# XXX This is temporary until all of process_bug uses update();
return $product_changed;
}
@@ -2189,9 +2213,10 @@ sub _zero_remaining_time { }
sub set_reporter_accessible { $_[0]->set('reporter_accessible', $_[1]); }
sub set_resolution {
my ($self, $value, $params) = @_;
-
+
my $old_res = $self->resolution;
$self->set('resolution', $value);
+ delete $self->{choices};
my $new_res = $self->resolution;
if ($new_res ne $old_res) {
@@ -2208,7 +2233,7 @@ sub set_resolution {
$self->_zero_remaining_time();
}
}
-
+
# We don't check if we're entering or leaving the dup resolution here,
# because we could be moving from being a dup of one bug to being a dup
# of another, theoretically. Note that this code block will also run
@@ -2227,8 +2252,8 @@ sub clear_resolution {
if (!$self->status->is_open) {
ThrowUserError('resolution_cant_clear', { bug_id => $self->id });
}
- $self->{'resolution'} = '';
- $self->_clear_dup_id;
+ $self->{'resolution'} = '';
+ $self->_clear_dup_id;
}
sub set_severity { $_[0]->set('bug_severity', $_[1]); }
sub set_status {
@@ -2236,8 +2261,10 @@ sub set_status {
my $old_status = $self->status;
$self->set('bug_status', $status);
delete $self->{'status'};
+ delete $self->{'statuses_available'};
+ delete $self->{'choices'};
my $new_status = $self->status;
-
+
if ($new_status->is_open) {
# Check for the everconfirmed transition
$self->_set_everconfirmed($new_status->name eq 'UNCONFIRMED' ? 0 : 1);
@@ -2326,7 +2353,7 @@ sub add_comment {
$params->{type} = $self->_check_comment_type($params->{type});
}
if (exists $params->{isprivate}) {
- $params->{isprivate} =
+ $params->{isprivate} =
$self->_check_commentprivacy($params->{isprivate});
}
# XXX We really should check extra_data, too.
@@ -2359,12 +2386,12 @@ sub add_comment {
# process_bug to use.
sub modify_keywords {
my ($self, $keywords, $action) = @_;
-
+
$action ||= "makeexact";
if (!grep($action eq $_, qw(add delete makeexact))) {
$action = "makeexact";
}
-
+
$keywords = $self->_check_keywords($keywords);
my (@result, $any_changes);
@@ -2390,7 +2417,7 @@ sub modify_keywords {
}
# Make sure we retain the sort order.
@result = sort {lc($a->name) cmp lc($b->name)} @result;
-
+
if ($any_changes) {
my $privs;
my $new = join(', ', (map {$_->name} @result));
@@ -2442,14 +2469,14 @@ sub remove_group {
my ($self, $group) = @_;
$group = new Bugzilla::Group($group) unless ref $group;
return unless $group;
-
+
# First, check if this is a valid group for this product.
# You can *always* remove a group that is not valid for this product, so
# we don't do any other checks if that's the case. (set_product does this.)
#
# This particularly happens when isbuggroup is no longer 1, and we're
# moving a bug to a new product.
- if (grep($_->id == $group->id, @{$self->product_obj->groups_valid})) {
+ if (grep($_->id == $group->id, @{$self->product_obj->groups_valid})) {
my $controls = $self->product_obj->group_controls->{$group->id};
# Nobody can ever remove a Mandatory group.
@@ -2471,7 +2498,7 @@ sub remove_group {
}
}
}
-
+
my $current_groups = $self->groups_in;
@$current_groups = grep { $_->id != $group->id } @$current_groups;
}
@@ -2480,7 +2507,7 @@ sub add_see_also {
my ($self, $input) = @_;
$input = trim($input);
- # We assume that the URL is an HTTP URL if there is no (something)://
+ # We assume that the URL is an HTTP URL if there is no (something)://
# in front.
my $uri = new URI($input);
if (!$uri->scheme) {
@@ -2510,10 +2537,53 @@ sub add_see_also {
{ url => $input, reason => 'id' });
}
}
+ # Google Code URLs
+ elsif ($uri->authority =~ /^code.google.com$/i) {
+ # Google Code URLs only have one form:
+ # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234
+ my $project_name;
+ if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) {
+ $project_name = $1;
+ } else {
+ ThrowUserError('bug_url_invalid',
+ { url => $input });
+ }
+ my $bug_id = $uri->query_param('id');
+ detaint_natural($bug_id);
+ if (!$bug_id) {
+ ThrowUserError('bug_url_invalid',
+ { url => $input, reason => 'id' });
+ }
+ # While Google Code URLs can be either HTTP or HTTPS,
+ # always go with the HTTP scheme, as that's the default.
+ $result = "http://code.google.com/p/" . $project_name .
+ "/issues/detail?id=" . $bug_id;
+ }
+ # Debian BTS URLs
+ elsif ($uri->authority =~ /^bugs.debian.org$/i) {
+ # Debian BTS URLs can look like various things:
+ # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
+ # http://bugs.debian.org/1234
+ my $bug_id;
+ if ($uri->path =~ m|^/(\d+)$|) {
+ $bug_id = $1;
+ }
+ elsif ($uri->path =~ /bugreport\.cgi$/) {
+ $bug_id = $uri->query_param('bug');
+ detaint_natural($bug_id);
+ }
+ if (!$bug_id) {
+ ThrowUserError('bug_url_invalid',
+ { url => $input, reason => 'id' });
+ }
+ # This is the shortest standard URL form for Debian BTS URLs,
+ # and so we reduce all URLs to this.
+ $result = "http://bugs.debian.org/" . $bug_id;
+ }
# Bugzilla URLs
else {
if ($uri->path !~ /show_bug\.cgi$/) {
- ThrowUserError('bug_url_invalid',
+ ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'show_bug' });
}
@@ -2524,7 +2594,7 @@ sub add_see_also {
# we can allow aliases.
detaint_natural($bug_id);
if (!$bug_id) {
- ThrowUserError('bug_url_invalid',
+ ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'id' });
}
@@ -2589,10 +2659,10 @@ sub dup_id {
$self->{'dup_id'} = undef;
return if $self->{'error'};
- if ($self->{'resolution'} eq 'DUPLICATE') {
+ if ($self->{'resolution'} eq 'DUPLICATE') {
my $dbh = Bugzilla->dbh;
$self->{'dup_id'} =
- $dbh->selectrow_array(q{SELECT dupe_of
+ $dbh->selectrow_array(q{SELECT dupe_of
FROM duplicates
WHERE dupe = ?},
undef,
@@ -2605,14 +2675,13 @@ sub actual_time {
my ($self) = @_;
return $self->{'actual_time'} if exists $self->{'actual_time'};
- if ( $self->{'error'} ||
- !Bugzilla->user->in_group(Bugzilla->params->{"timetrackinggroup"}) ) {
+ if ( $self->{'error'} || !Bugzilla->user->is_timetracker ) {
$self->{'actual_time'} = undef;
return $self->{'actual_time'};
}
my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time)
- FROM longdescs
+ FROM longdescs
WHERE longdescs.bug_id=?");
$sth->execute($self->{bug_id});
$self->{'actual_time'} = $sth->fetchrow_array();
@@ -2621,12 +2690,16 @@ sub actual_time {
sub any_flags_requesteeble {
my ($self) = @_;
- return $self->{'any_flags_requesteeble'}
+ return $self->{'any_flags_requesteeble'}
if exists $self->{'any_flags_requesteeble'};
return 0 if $self->{'error'};
- $self->{'any_flags_requesteeble'} =
- grep($_->{'is_requesteeble'}, @{$self->flag_types});
+ my $any_flags_requesteeble =
+ grep { $_->is_requestable && $_->is_requesteeble } @{$self->flag_types};
+ # Useful in case a flagtype is no longer requestable but a requestee
+ # has been set before we turned off that bit.
+ $any_flags_requesteeble ||= grep { $_->requestee_id } @{$self->flags};
+ $self->{'any_flags_requesteeble'} = $any_flags_requesteeble;
return $self->{'any_flags_requesteeble'};
}
@@ -2683,7 +2756,7 @@ sub cc_users {
my $self = shift;
return $self->{'cc_users'} if exists $self->{'cc_users'};
return [] if $self->{'error'};
-
+
my $dbh = Bugzilla->dbh;
my $cc_ids = $dbh->selectcol_arrayref(
'SELECT who FROM cc WHERE bug_id = ?', undef, $self->id);
@@ -2734,7 +2807,7 @@ sub dependson {
my ($self) = @_;
return $self->{'dependson'} if exists $self->{'dependson'};
return [] if $self->{'error'};
- $self->{'dependson'} =
+ $self->{'dependson'} =
EmitDependList("blocked", "dependson", $self->bug_id);
return $self->{'dependson'};
}
@@ -2749,7 +2822,7 @@ sub flag_types {
component_id => $self->{component_id},
bug_id => $self->bug_id };
- $self->{'flag_types'} = Bugzilla::Flag::_flag_types($vars);
+ $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars);
# Custom list for flag selection - moved from flag/list.html.tmpl
# Fucking templaty logic
@@ -2766,52 +2839,55 @@ sub flag_types {
$cl->add(Assignee => $_) for $self->assigned_to || ();
$type->{custom_list} = $cl;
$type->{allow_other} = 1;
- foreach (@{$type->{flags}})
+ foreach my $flag (@{$type->{flags}})
{
unless ($type->is_active && $type->is_requestable && $type->is_requesteeble)
{
# In case there was already a requestee, the only valid action
# is to remove the requestee or leave it alone.
- $_->{custom_list} = new Bugzilla::FlagType::UserList;
- $_->{custom_list}->add('', $_->requestee);
- $_->{allow_other} = 0;
+ $flag->{custom_list} = new Bugzilla::FlagType::UserList;
+ $flag->{custom_list}->add('', $flag->requestee);
+ $flag->{allow_other} = 0;
}
else
{
# Else take type's custom list
- $_->{custom_list} = $cl;
- $_->{allow_other} = 1;
+ $flag->{custom_list} = $cl;
+ $flag->{allow_other} = 1;
}
$st = [];
# TODO remove hardcoded status list
- push @$st, 'X' if $user->can_request_flag($type);
+ push @$st, 'X' if $user->can_request_flag($type) || $flag->setter_id == $user->id;
if ($type->is_active)
{
- push @$st, '?' if $type->is_requestable && $user->can_request_flag($type) || $_->status == '?';
- push @$st, '+' if $user->can_set_flag($type) || $_->status == '+';
- push @$st, '-' if $user->can_set_flag($type) || $_->status == '-';
+ push @$st, '?' if $type->is_requestable && $user->can_request_flag($type) || $flag->status == '?';
+ push @$st, '+' if $user->can_set_flag($type) || $flag->status == '+';
+ push @$st, '-' if $user->can_set_flag($type) || $flag->status == '-';
}
else
{
- push @$st, $_->status;
+ push @$st, $flag->status;
}
- $_->{statuses} = $st;
+ $flag->{statuses} = $st;
}
}
return $self->{'flag_types'};
}
+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};
+}
+
sub isopened {
my $self = shift;
return is_open_state($self->{bug_status}) ? 1 : 0;
}
-sub isunconfirmed {
- my $self = shift;
- return ($self->bug_status eq 'UNCONFIRMED') ? 1 : 0;
-}
-
sub keywords {
my ($self) = @_;
return join(', ', (map { $_->name } @{$self->keyword_objects}));
@@ -2830,21 +2906,40 @@ sub keyword_objects {
return $self->{'keyword_objects'};
}
-sub longdescs {
- my ($self) = @_;
- return $self->{'longdescs'} if exists $self->{'longdescs'};
+sub comments {
+ my ($self, $params) = @_;
return [] if $self->{'error'};
- $self->{'longdescs'} = GetComments($self->{bug_id});
- return $self->{'longdescs'};
-}
+ $params ||= {};
-sub milestoneurl {
- my ($self) = @_;
- return $self->{'milestoneurl'} if exists $self->{'milestoneurl'};
- return '' if $self->{'error'};
+ if (!defined $self->{'comments'}) {
+ $self->{'comments'} = Bugzilla::Comment->match({ bug_id => $self->id });
+ my $count = 0;
+ foreach my $comment (@{ $self->{'comments'} }) {
+ $comment->{count} = $count++;
+ $comment->{bug} = $self;
+ }
+ Bugzilla::Comment->preload($self->{'comments'});
+ }
+ my @comments = @{ $self->{'comments'} };
- $self->{'milestoneurl'} = $self->product_obj->milestone_url;
- return $self->{'milestoneurl'};
+ my $order = $params->{order}
+ || Bugzilla->user->settings->{'comment_sort_order'}->{'value'};
+ if ($order ne 'oldest_to_newest') {
+ @comments = reverse @comments;
+ if ($order eq 'newest_to_oldest_desc_first') {
+ unshift(@comments, pop @comments);
+ }
+ }
+
+ if ($params->{after}) {
+ my $from = datetime_from($params->{after});
+ @comments = grep { datetime_from($_->creation_ts) > $from } @comments;
+ }
+ if ($params->{to}) {
+ my $to = datetime_from($params->{to});
+ @comments = grep { datetime_from($_->creation_ts) <= $to } @comments;
+ }
+ return \@comments;
}
sub product {
@@ -2873,8 +2968,8 @@ sub qa_contact {
if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) {
$self->{'qa_contact_obj'} = new Bugzilla::User($self->{'qa_contact'});
} else {
- # XXX - This is somewhat inconsistent with the assignee/reporter
- # methods, which will return an empty User if they get a 0.
+ # XXX - This is somewhat inconsistent with the assignee/reporter
+ # methods, which will return an empty User if they get a 0.
# However, we're keeping it this way now, for backwards-compatibility.
$self->{'qa_contact_obj'} = undef;
}
@@ -2905,9 +3000,39 @@ sub status {
return $self->{'status'};
}
+sub statuses_available {
+ my $self = shift;
+ return [] if $self->{'error'};
+ return $self->{'statuses_available'}
+ if defined $self->{'statuses_available'};
+
+ my @statuses = @{ $self->status->can_change_to };
+
+ # UNCONFIRMED is only a valid status if it is enabled in this product.
+ if (!$self->product_obj->allows_unconfirmed) {
+ @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses;
+ }
+
+ my @available;
+ foreach my $status (@statuses) {
+ # Make sure this is a legal status transition
+ next if !$self->check_can_change_field(
+ 'bug_status', $self->status->name, $status->name);
+ push(@available, $status);
+ }
+
+ # If this bug has an inactive status set, it should still be in the list.
+ if (!grep($_->name eq $self->status->name, @available)) {
+ unshift(@available, $self->status);
+ }
+
+ $self->{'statuses_available'} = \@available;
+ return $self->{'statuses_available'};
+}
+
sub show_attachment_flags {
my ($self) = @_;
- return $self->{'show_attachment_flags'}
+ return $self->{'show_attachment_flags'}
if exists $self->{'show_attachment_flags'};
return 0 if $self->{'error'};
@@ -2933,7 +3058,7 @@ sub use_votes {
my ($self) = @_;
return 0 if $self->{'error'};
- return Bugzilla->params->{'usevotes'}
+ return Bugzilla->params->{'usevotes'}
&& $self->product_obj->votes_per_user > 0;
}
@@ -2959,7 +3084,7 @@ sub groups {
" THEN 1 ELSE 0 END," .
" CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," .
" isactive, membercontrol, othercontrol" .
- " FROM groups" .
+ " FROM groups" .
" LEFT JOIN bug_group_map" .
" ON bug_group_map.group_id = groups.id" .
" AND bug_id = ?" .
@@ -3042,38 +3167,40 @@ sub user {
return $self->{'user'};
}
+# This is intended to get values that can be selected by the user in the
+# UI. It should not be used for security or validation purposes.
sub choices {
my $self = shift;
return $self->{choices} if exists $self->{'choices'};
return {} if $self->{'error'};
+ my $user = Bugzilla->user;
- $self->{'choices'} = {};
-
- my @prodlist = map {$_->name} @{Bugzilla->user->get_enterable_products};
+ my @products = @{ $user->get_enterable_products };
# The current product is part of the popup, even if new bugs are no longer
# allowed for that product
- if (lsearch(\@prodlist, $self->product) < 0) {
- push(@prodlist, $self->product);
- @prodlist = sort @prodlist;
+ if (!grep($_->name eq $self->product_obj->name, @products)) {
+ unshift(@products, $self->product_obj);
}
- $self->{choices} = {
- product => \@prodlist,
- priority => get_legal_field_values('priority'),
- bug_severity => get_legal_field_values('bug_severity'),
- bug_status => get_legal_field_values('bug_status'),
- component => [map($_->name, @{$self->product_obj->components})],
- version => [map($_->name, @{$self->product_obj->versions})],
- target_milestone => [map($_->name, @{$self->product_obj->milestones})],
- };
+ my %choices = (
+ bug_status => $self->statuses_available,
+ product => \@products,
+ component => $self->product_obj->components,
+ version => $self->product_obj->versions,
+ target_milestone => $self->product_obj->milestones,
+ );
- # Hack - this array contains "". See bug 106589.
- $self->{choices}->{resolution} = [ grep ($_, @{get_legal_field_values('resolution')}) ];
+ my $resolution_field = new Bugzilla::Field({ name => 'resolution' });
+ # Don't include the empty resolution in drop-downs.
+ my @resolutions = grep($_->name, @{ $resolution_field->legal_values });
+ # And don't include MOVED in the list unless the bug is already MOVED.
+ if ($self->resolution ne 'MOVED') {
+ @resolutions= grep { $_->name ne 'MOVED' } @resolutions;
+ }
+ $choices{'resolution'} = \@resolutions;
- $self->{choices}->{op_sys} = get_legal_field_values('op_sys') if Bugzilla->params->{useopsys};
- $self->{choices}->{rep_platform} = get_legal_field_values('rep_platform') if Bugzilla->params->{useplatform};
-
- return $self->{choices};
+ $self->{'choices'} = \%choices;
+ return $self->{'choices'};
}
sub votes {
@@ -3116,37 +3243,6 @@ sub get_test_case_count {
# Subroutines
#####################################################################
-sub update_comment {
- my ($self, $comment_id, $new_comment) = @_;
-
- # Some validation checks.
- if ($self->{'error'}) {
- ThrowCodeError("bug_error", { bug => $self });
- }
- detaint_natural($comment_id)
- || ThrowCodeError('bad_arg', {argument => 'comment_id', function => 'update_comment'});
-
- # The comment ID must belong to this bug.
- my @current_comment_obj = grep {$_->{'id'} == $comment_id} @{$self->longdescs};
- scalar(@current_comment_obj)
- || ThrowCodeError('bad_arg', {argument => 'comment_id', function => 'update_comment'});
-
- # If the new comment is undefined, then there is nothing to update.
- # To delete a comment, an empty string should be passed.
- return unless defined $new_comment;
- $new_comment =~ s/\s*$//s; # Remove trailing whitespaces.
- $new_comment =~ s/\r\n?/\n/g; # Handle Windows and Mac-style line endings.
- trick_taint($new_comment);
-
- # We assume _check_comment() has already been called earlier.
- Bugzilla->dbh->do('UPDATE longdescs SET thetext = ? WHERE comment_id = ?',
- undef, ($new_comment, $comment_id));
- $self->_sync_fulltext();
-
- # Update the comment object with this new text.
- $current_comment_obj[0]->{'body'} = $new_comment;
-}
-
# FIXME // Vitaliy Filippov 2010-02-01 19:23
# editable_bug_fields() is one more example of incorrect and unused generalization.
# It does not represent which fields from the bugs table are handled by process_bug.cgi,
@@ -3213,74 +3309,6 @@ sub ValidateTime
return $time;
}
-sub GetComments {
- my ($id, $comment_sort_order, $start, $end, $raw) = @_;
- my $dbh = Bugzilla->dbh;
-
- $comment_sort_order = $comment_sort_order ||
- Bugzilla->user->settings->{'comment_sort_order'}->{'value'};
-
- my $sort_order = ($comment_sort_order eq "oldest_to_newest") ? 'asc' : 'desc';
-
- my @comments;
- my @args = ($id);
-
- my $query = 'SELECT longdescs.comment_id AS id, profiles.userid, ' .
- $dbh->sql_date_format('longdescs.bug_when', '%Y.%m.%d %H:%i:%s') .
- ' AS time, longdescs.thetext AS body, longdescs.work_time,
- isprivate, already_wrapped, type, extra_data
- FROM longdescs
- INNER JOIN profiles
- ON profiles.userid = longdescs.who
- WHERE longdescs.bug_id = ?';
-
- if ($start) {
- $query .= ' AND longdescs.bug_when > ?';
- push(@args, $start);
- }
- if ($end) {
- $query .= ' AND longdescs.bug_when <= ?';
- push(@args, $end);
- }
-
- $query .= " ORDER BY longdescs.bug_when $sort_order";
- my $sth = $dbh->prepare($query);
- $sth->execute(@args);
-
- # Cache the users we look up
- my %users;
-
- while (my $comment_ref = $sth->fetchrow_hashref()) {
- my %comment = %$comment_ref;
- $users{$comment{'userid'}} ||= new Bugzilla::User($comment{'userid'});
- $comment{'author'} = $users{$comment{'userid'}};
-
- # If raw data is requested, do not format 'special' comments.
- $comment{'body'} = format_comment(\%comment) unless $raw;
-
- push (@comments, \%comment);
- }
-
- if ($comment_sort_order eq "newest_to_oldest_desc_first") {
- unshift(@comments, pop @comments);
- }
-
- return \@comments;
-}
-
-# Format language specific comments.
-sub format_comment {
- my $comment = shift;
- my $template = Bugzilla->template_inner;
- my $vars = {comment => $comment};
- my $body;
-
- $template->process("bug/format_comment.txt.tmpl", $vars, \$body)
- || ThrowTemplateError($template->error());
- $body =~ s/^X//;
- return $body;
-}
-
# Get the activity of a bug, starting from $starttime (if given).
# This routine assumes Bugzilla::Bug->check has been previously called.
sub GetBugActivity {
@@ -3307,22 +3335,14 @@ sub GetBugActivity {
# Only includes attachments the user is allowed to see.
my $suppjoins = "";
my $suppwhere = "";
- if (Bugzilla->params->{"insidergroup"}
- && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'}))
+ if (!Bugzilla->user->is_insider)
{
- $suppjoins = "LEFT JOIN attachments
+ $suppjoins = "LEFT JOIN attachments
ON attachments.attach_id = bugs_activity.attach_id";
$suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0";
}
- my $query = "
- SELECT COALESCE(fielddefs.description, "
- # This is a hack - PostgreSQL requires both COALESCE
- # arguments to be of the same type, and this is the only
- # way supported by both MySQL 3 and PostgreSQL to convert
- # an integer to a string. MySQL 4 supports CAST.
- . $dbh->sql_string_concat('bugs_activity.fieldid', q{''}) .
- "), fielddefs.name, bugs_activity.attach_id, " .
+ my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " .
$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
", bugs_activity.removed, bugs_activity.added, profiles.login_name
FROM bugs_activity
@@ -3345,7 +3365,7 @@ sub GetBugActivity {
my $incomplete_data = 0;
foreach my $entry (@$list) {
- my ($field, $fieldname, $attachid, $when, $removed, $added, $who) = @$entry;
+ my ($fieldname, $attachid, $when, $removed, $added, $who) = @$entry;
my %change;
my $activity_visible = 1;
@@ -3355,16 +3375,12 @@ sub GetBugActivity {
|| $fieldname eq 'work_time'
|| $fieldname eq 'deadline')
{
- $activity_visible =
- Bugzilla->user->in_group(Bugzilla->params->{'timetrackinggroup'}) ? 1 : 0;
+ $activity_visible = Bugzilla->user->is_timetracker;
} else {
$activity_visible = 1;
}
if ($activity_visible) {
- # This gets replaced with a hyperlink in the template.
- $field =~ s/^Attachment\s*// if $attachid;
-
# Check for the results of an old Bugzilla data corruption bug
$incomplete_data = 1 if ($added =~ /^\?/ || $removed =~ /^\?/);
@@ -3387,7 +3403,6 @@ sub GetBugActivity {
$operation->{'who'} = $who;
$operation->{'when'} = $when;
- $change{'field'} = $field;
$change{'fieldname'} = $fieldname;
$change{'attachid'} = $attachid;
$change{'removed'} = $removed;
@@ -3462,6 +3477,25 @@ sub LogActivityEntry {
}
}
+# Convert WebService API and email_in.pl field names to internal DB field
+# names.
+sub map_fields {
+ my ($params) = @_;
+
+ my %field_values;
+ foreach my $field (keys %$params) {
+ my $field_name = FIELD_MAP->{$field} || $field;
+ $field_values{$field_name} = $params->{$field};
+ }
+
+ # This protects the WebService Bug.search method.
+ unless (Bugzilla->user->is_timetracker) {
+ delete @field_values{qw(estimated_time remaining_time deadline)};
+ }
+
+ return \%field_values;
+}
+
# CountOpenDependencies counts the number of open dependent bugs for a
# list of bugs and returns a list of bug_id's and their dependency count
# It takes one parameter:
@@ -3499,7 +3533,7 @@ sub RemoveVotes {
my $sth = $dbh->prepare("SELECT profiles.login_name, " .
"profiles.userid, votes.vote_count, " .
"products.votesperuser, products.maxvotesperbug " .
- "FROM profiles " .
+ "FROM profiles " .
"LEFT JOIN votes ON profiles.userid = votes.who " .
"LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " .
"LEFT JOIN products ON products.id = bugs.product_id " .
@@ -3582,7 +3616,10 @@ sub CheckIfVotedConfirmed {
my $bug = new Bugzilla::Bug($id);
my $ret = 0;
- if (!$bug->everconfirmed && $bug->votes >= $bug->product_obj->votes_to_confirm) {
+ if (!$bug->everconfirmed
+ and $bug->product_obj->votes_to_confirm
+ and $bug->votes >= $bug->product_obj->votes_to_confirm)
+ {
$bug->add_comment('', { type => CMT_POPULAR_VOTES });
if ($bug->bug_status eq 'UNCONFIRMED') {
@@ -3667,11 +3704,10 @@ sub check_can_change_field {
# $PrivilegesRequired = 1 : the reporter, assignee or an empowered user;
# $PrivilegesRequired = 2 : the assignee or an empowered user;
# $PrivilegesRequired = 3 : an empowered user.
-
+
# Only users in the time-tracking group can change time-tracking fields.
if ( grep($_ eq $field, qw(deadline estimated_time remaining_time)) ) {
- my $tt_group = Bugzilla->params->{timetrackinggroup};
- if (!$tt_group || !$user->in_group($tt_group)) {
+ if (!$user->is_timetracker) {
$$PrivilegesRequired = 3;
return 0;
}
@@ -3683,12 +3719,7 @@ sub check_can_change_field {
}
# *Only* users with (product-specific) "canconfirm" privs can confirm bugs.
- if ($field eq 'canconfirm'
- || ($field eq 'everconfirmed' && $newvalue)
- || ($field eq 'bug_status'
- && $oldvalue eq 'UNCONFIRMED'
- && is_open_state($newvalue)))
- {
+ if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) {
$$PrivilegesRequired = 3;
return $user->in_group('canconfirm', $self->{'product_id'});
}
@@ -3747,7 +3778,7 @@ sub check_can_change_field {
}
# - change the status from one open state to another
if ($field eq 'bug_status'
- && is_open_state($oldvalue) && is_open_state($newvalue))
+ && is_open_state($oldvalue) && is_open_state($newvalue))
{
$$PrivilegesRequired = 2;
return 0;
@@ -3764,6 +3795,24 @@ sub check_can_change_field {
return 0;
}
+# A helper for check_can_change_field
+sub _changes_everconfirmed {
+ my ($self, $field, $old, $new) = @_;
+ return 1 if $field eq 'everconfirmed';
+ if ($field eq 'bug_status') {
+ if ($self->everconfirmed) {
+ # Moving a confirmed bug to UNCONFIRMED will change everconfirmed.
+ return 1 if $new eq 'UNCONFIRMED';
+ }
+ else {
+ # Moving an unconfirmed bug to an open state that isn't
+ # UNCONFIRMED will confirm the bug.
+ return 1 if (is_open_state($new) and $new ne 'UNCONFIRMED');
+ }
+ }
+ return 0;
+}
+
#
# Field Validation
#
@@ -3858,8 +3907,7 @@ sub _validate_attribute {
my @valid_attributes = (
# Miscellaneous properties and methods.
qw(error groups product_id component_id
- longdescs milestoneurl attachments
- isopened isunconfirmed
+ comments milestoneurl attachments isopened
flag_types num_attachment_flag_types
show_attachment_flags any_flags_requesteeble),
@@ -3871,40 +3919,40 @@ sub _validate_attribute {
}
sub AUTOLOAD {
- use vars qw($AUTOLOAD);
- my $attr = $AUTOLOAD;
+ use vars qw($AUTOLOAD);
+ my $attr = $AUTOLOAD;
- $attr =~ s/.*:://;
- return unless $attr=~ /[^A-Z]/;
- if (!_validate_attribute($attr)) {
- require Carp;
- Carp::confess("invalid bug attribute $attr");
- }
+ $attr =~ s/.*:://;
+ return unless $attr=~ /[^A-Z]/;
+ if (!_validate_attribute($attr)) {
+ require Carp;
+ Carp::confess("invalid bug attribute $attr");
+ }
- no strict 'refs';
- *$AUTOLOAD = sub {
- my $self = shift;
+ no strict 'refs';
+ *$AUTOLOAD = sub {
+ my $self = shift;
- return $self->{$attr} if defined $self->{$attr};
+ return $self->{$attr} if defined $self->{$attr};
- $self->{_multi_selects} ||= [Bugzilla->get_fields(
- {custom => 1, type => FIELD_TYPE_MULTI_SELECT })];
- if ( grep($_->name eq $attr, @{$self->{_multi_selects}}) ) {
- # There is a bug in Perl 5.10.0, which is fixed in 5.10.1,
- # which taints $attr at this point. trick_taint() can go
- # away once we require 5.10.1 or newer.
- trick_taint($attr);
+ $self->{_multi_selects} ||= [Bugzilla->get_fields(
+ {custom => 1, type => FIELD_TYPE_MULTI_SELECT })];
+ if ( grep($_->name eq $attr, @{$self->{_multi_selects}}) ) {
+ # There is a bug in Perl 5.10.0, which is fixed in 5.10.1,
+ # which taints $attr at this point. trick_taint() can go
+ # away once we require 5.10.1 or newer.
+ trick_taint($attr);
- $self->{$attr} ||= Bugzilla->dbh->selectcol_arrayref(
- "SELECT value FROM bug_$attr WHERE bug_id = ? ORDER BY value",
- undef, $self->id);
- return $self->{$attr};
- }
+ $self->{$attr} ||= Bugzilla->dbh->selectcol_arrayref(
+ "SELECT value FROM bug_$attr WHERE bug_id = ? ORDER BY value",
+ undef, $self->id);
+ return $self->{$attr};
+ }
- return '';
- };
+ return '';
+ };
- goto &$AUTOLOAD;
+ goto &$AUTOLOAD;
}
1;
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 432c1a66b..31fb1ba54 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -110,7 +110,6 @@ sub three_columns {
# roles when the email is sent.
# All the names are email addresses, not userids
# values are scalars, except for cc, which is a list
-# This hash usually comes from the "mailrecipients" var in a template call.
sub Send {
my ($id, $forced) = (@_);
@@ -121,6 +120,7 @@ sub Send {
my $msg = "";
my $dbh = Bugzilla->dbh;
+ my $bug = new Bugzilla::Bug($id);
# XXX - These variables below are useless. We could use field object
# methods directly. But we first have to implement a cache in
@@ -327,7 +327,7 @@ sub Send {
}
}
- my ($comments, $anyprivate) = get_comments_by_bug($id, $start, $end);
+ my $comments = $bug->comments({ after => $start, to => $end });
###########################################################################
# Start of email filtering code
@@ -388,6 +388,9 @@ sub Send {
}
}
+ Bugzilla::Hook::process('bugmail_recipients',
+ { bug => $bug, recipients => \%recipients });
+
# Find all those user-watching anyone on the current list, who is not
# on it already themselves.
my $involved = join(",", keys %recipients);
@@ -449,11 +452,6 @@ sub Send {
# So the user exists, can see the bug, and wants mail in at least
# one role. But do we want to send it to them?
- # If we are using insiders, and the comment is private, only send
- # to insiders
- my $insider_ok = 1;
- $insider_ok = 0 if $anyprivate && !$user->is_insider;
-
# We shouldn't send mail if this is a dependency mail (i.e. there
# is something in @depbugs), and any of the depending bugs are not
# visible to the user. This is to avoid leaking the summaries of
@@ -468,10 +466,7 @@ sub Send {
# Make sure the user isn't in the nomail list, and the insider and
# dep checks passed.
- if ($user->email_enabled &&
- $insider_ok &&
- $dep_ok)
- {
+ if ($user->email_enabled && $dep_ok) {
# OK, OK, if we must. Email the user.
$sent_mail = sendMail(
user => $user,
@@ -482,7 +477,6 @@ sub Send {
fields => \%fielddescription,
diffs => $diffs,
newcomm => $comments,
- anypriv => $anyprivate,
isnew => !$start,
id => $id,
watch => exists $watching{$user_id} ? $watching{$user_id} : undef,
@@ -508,14 +502,15 @@ sub sendMail
{
my %arguments = @_;
my ($user, $hlRef, $relRef, $valueRef, $dmhRef, $fdRef,
- $diffs, $newcomments, $anyprivate, $isnew,
+ $diffs, $comments_in, $isnew,
$id, $watchingRef
) = @arguments{qw(
user headers rels values defhead fields
- diffs newcomm anypriv isnew
+ diffs newcomm isnew
id watch
)};
+ my @send_comments = @$comments_in;
my %values = %$valueRef;
my @headerlist = @$hlRef;
my %mailhead = %$dmhRef;
@@ -539,7 +534,11 @@ sub sendMail
$diffs = $new_diffs;
- if (!@$diffs && !scalar(@$newcomments) && !$isnew) {
+ if (!$user->is_insider) {
+ @send_comments = grep { !$_->is_private } @send_comments;
+ }
+
+ if (!@$diffs && !scalar(@send_comments) && !$isnew) {
# Whoops, no differences!
return 0;
}
@@ -598,7 +597,7 @@ sub sendMail
reporter => $values{'reporter'},
reportername => Bugzilla::User->new({name => $values{'reporter'}})->name,
diffs => $diffs,
- new_comments => $newcomments,
+ new_comments => \@send_comments,
threadingmarker => build_thread_marker($id, $user->id, $isnew),
three_columns => \&three_columns,
};
@@ -623,43 +622,4 @@ sub sendMail
return 1;
}
-# Get bug comments for the given period.
-sub get_comments_by_bug {
- my ($id, $start, $end) = @_;
- my $dbh = Bugzilla->dbh;
-
- my $result = "";
- my $count = 0;
- my $anyprivate = 0;
-
- # $start will be undef for new bugs, and defined for pre-existing bugs.
- if ($start) {
- # If $start is not NULL, obtain the count-index
- # of this comment for the leading "Comment #xxx" line.
- $count = $dbh->selectrow_array('SELECT COUNT(*) FROM longdescs
- WHERE bug_id = ? AND bug_when <= ?',
- undef, ($id, $start));
- }
-
- my $raw = 0; # Do not format comments which are not of type CMT_NORMAL.
- my $comments = Bugzilla::Bug::GetComments($id, "oldest_to_newest", $start, $end, $raw);
- my $attach_base = correct_urlbase() . 'attachment.cgi?id=';
-
- foreach my $comment (@$comments) {
- $comment->{count} = $count++;
- # If an attachment was created, then add an URL. (Note: the 'g'lobal
- # replace should work with comments with multiple attachments.)
- if ($comment->{body} =~ /Created an attachment \(/) {
- $comment->{body} =~ s/(Created an attachment \(id=([0-9]+)\))/$1\n --> \($attach_base$2\)/g;
- }
- $comment->{body} = $comment->{'already_wrapped'} ? $comment->{body} : wrap_comment($comment->{body});
- }
-
- if (Bugzilla->params->{'insidergroup'}) {
- $anyprivate = 1 if scalar(grep {$_->{'isprivate'} > 0} @$comments);
- }
-
- return ($comments, $anyprivate);
-}
-
1;
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 98e72a2ac..2830670b9 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -21,26 +21,27 @@
# Byron Jones
# Marc Schumann
+package Bugzilla::CGI;
use strict;
-package Bugzilla::CGI;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Util;
+
+use File::Basename;
BEGIN {
- if ($^O =~ /MSWin32/i) {
+ if (ON_WINDOWS) {
# Help CGI find the correct temp directory as the default list
# isn't Windows friendly (Bug 248988)
$ENV{'TMPDIR'} = $ENV{'TEMP'} || $ENV{'TMP'} || "$ENV{'WINDIR'}\\TEMP";
}
}
-use CGI qw(-no_xhtml -oldstyle_urls :private_tempfiles :unique_headers SERVER_PUSH);
-
+use CGI qw(-no_xhtml -oldstyle_urls :private_tempfiles
+ :unique_headers SERVER_PUSH);
use base qw(CGI);
-use Bugzilla::Constants;
-use Bugzilla::Error;
-use Bugzilla::Util;
-
# We need to disable output buffering - see bug 179174
$| = 1;
@@ -72,15 +73,9 @@ sub new {
$self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : '');
# Redirect to urlbase/sslbase if we are not viewing an attachment.
- if (use_attachbase() && i_am_cgi()) {
- my $cgi_file = $self->url('-path_info' => 0, '-query' => 0, '-relative' => 1);
- $cgi_file =~ s/\?$//;
- my $urlbase = Bugzilla->params->{'urlbase'};
- my $sslbase = Bugzilla->params->{'sslbase'};
- my $path_regexp = $sslbase ? qr/^(\Q$urlbase\E|\Q$sslbase\E)/ : qr/^\Q$urlbase\E/;
- if ($cgi_file ne 'attachment.cgi' && $self->self_url !~ /$path_regexp/) {
- $self->redirect_to_urlbase;
- }
+ my $script = basename($0);
+ if ($self->url_is_attachment_base and $script ne 'attachment.cgi') {
+ $self->redirect_to_urlbase();
}
# Check for errors
@@ -122,6 +117,7 @@ sub parse_params {
sub canonicalise_query {
my ($self, @exclude) = @_;
+ $self->convert_old_params();
# Reconstruct the URL by concatenating the sorted param=value pairs
my @parameters;
foreach my $key (sort($self->param())) {
@@ -146,6 +142,17 @@ sub canonicalise_query {
return join("&", @parameters);
}
+sub convert_old_params {
+ my $self = shift;
+
+ # bugidtype is now bug_id_type.
+ if ($self->param('bugidtype')) {
+ my $value = $self->param('bugidtype') eq 'exclude' ? 'nowords' : 'anyexact';
+ $self->param('bug_id_type', $value);
+ $self->delete('bugidtype');
+ }
+}
+
sub clean_search_url {
my $self = shift;
# Delete any empty URL parameter.
@@ -165,9 +172,6 @@ sub clean_search_url {
}
}
- # Delete certain parameters if the associated parameter is empty.
- $self->delete('bugidtype') if !$self->param('bug_id');
-
# Delete leftovers from the login form
$self->delete('Bugzilla_remember', 'GoAheadAndLogIn');
@@ -306,7 +310,7 @@ sub param {
}
return wantarray ? @result : $result[0];
- }
+ }
# And for various other functions in CGI.pm, we need to correctly
# return the URL parameters in addition to the POST parameters when
# asked for the list of parameters.
@@ -374,25 +378,26 @@ sub remove_cookie {
'-value' => 'X');
}
-# Redirect to https if required
-sub require_https {
- my ($self, $url) = @_;
- # Do not create query string if data submitted via XMLRPC
- # since we want the data to be resubmitted over POST method.
- my $query = Bugzilla->usage_mode == USAGE_MODE_WEBSERVICE ? 0 : 1;
- # XMLRPC clients (SOAP::Lite at least) requires 301 to redirect properly
- # and do not work with 302.
- my $status = Bugzilla->usage_mode == USAGE_MODE_WEBSERVICE ? 301 : 302;
- if (defined $url) {
- $url .= $self->url('-path_info' => 1, '-query' => $query, '-relative' => 1);
- } else {
- $url = $self->self_url;
- $url =~ s/^http:/https:/i;
- }
- print $self->redirect(-location => $url, -status => $status);
- # When using XML-RPC with mod_perl, we need the headers sent immediately.
- $self->r->rflush if $ENV{MOD_PERL};
- exit;
+sub redirect_to_https {
+ my $self = shift;
+ my $sslbase = Bugzilla->params->{'sslbase'};
+ # If this is a POST, we don't want ?POSTDATA in the query string.
+ # We expect the client to re-POST, which may be a violation of
+ # the HTTP spec, but the only time we're expecting it often is
+ # in the WebService, and WebService clients usually handle this
+ # correctly.
+ $self->delete('POSTDATA');
+ my $url = $sslbase . $self->url('-path_info' => 1, '-query' => 1,
+ '-relative' => 1);
+
+ # XML-RPC clients (SOAP::Lite at least) require a 301 to redirect properly
+ # and do not work with 302. Our redirect really is permanent anyhow, so
+ # it doesn't hurt to make it a 301.
+ print $self->redirect(-location => $url, -status => 301);
+
+ # When using XML-RPC with mod_perl, we need the headers sent immediately.
+ $self->r->rflush if $ENV{MOD_PERL};
+ exit;
}
# Redirect to the urlbase version of the current URL.
@@ -403,6 +408,61 @@ sub redirect_to_urlbase {
exit;
}
+sub url_is_attachment_base {
+ my ($self, $id) = @_;
+ return 0 if !use_attachbase() or !i_am_cgi();
+ my $attach_base = Bugzilla->params->{'attachment_base'};
+ # If we're passed an id, we only want one specific attachment base
+ # for a particular bug. If we're not passed an ID, we just want to
+ # know if our current URL matches the attachment_base *pattern*.
+ my $regex;
+ if ($id) {
+ $attach_base =~ s/\%bugid\%/$id/;
+ $regex = quotemeta($attach_base);
+ }
+ else {
+ # In this circumstance we run quotemeta first because we need to
+ # insert an active regex meta-character afterward.
+ $regex = quotemeta($attach_base);
+ $regex =~ s/\\\%bugid\\\%/\\d+/;
+ }
+ $regex = "^$regex";
+ return ($self->self_url =~ $regex) ? 1 : 0;
+}
+
+##########################
+# Vars TIEHASH Interface #
+##########################
+
+# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept
+# arrayrefs.
+sub STORE {
+ my $self = shift;
+ my ($param, $value) = @_;
+ if (defined $value and ref $value eq 'ARRAY') {
+ return $self->param(-name => $param, -value => $value);
+ }
+ return $self->SUPER::STORE(@_);
+}
+
+sub FETCH {
+ my ($self, $param) = @_;
+ return $self if $param eq 'CGI'; # CGI.pm did this, so we do too.
+ my @result = $self->param($param);
+ return undef if !scalar(@result);
+ return $result[0] if scalar(@result) == 1;
+ return \@result;
+}
+
+# For the Vars TIEHASH interface: the normal CGI.pm DELETE doesn't return
+# the value deleted, but Perl's "delete" expects that value.
+sub DELETE {
+ my ($self, $param) = @_;
+ my $value = $self->FETCH($param);
+ $self->delete($param);
+ return $value;
+}
+
# cookie() with UTF-8 support...
sub cookie
{
@@ -518,13 +578,13 @@ effectively removing the cookie.
As its only argument, it takes the name of the cookie to expire.
-=item C
+=item C
-This routine redirects the client to a different location using the https protocol.
-If the client is using XMLRPC, it will not retain the QUERY_STRING since XMLRPC uses POST.
+This routine redirects the client to the https version of the page that
+they're looking at, using the C parameter for the redirection.
-It takes an optional argument which will be used as the base URL. If $baseurl
-is not provided, the current URL is used.
+Generally you should use L
+instead of calling this directly.
=item C
diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm
index a7f59b4bb..322b5cf99 100644
--- a/Bugzilla/Classification.pm
+++ b/Bugzilla/Classification.pm
@@ -70,7 +70,7 @@ sub remove_from_db {
$dbh->do("UPDATE products SET classification_id = 1
WHERE classification_id = ?", undef, $self->id);
- $dbh->do("DELETE FROM classifications WHERE id = ?", undef, $self->id);
+ $self->SUPER::remove_from_db();
$dbh->bz_commit_transaction();
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
new file mode 100644
index 000000000..cbdddba3c
--- /dev/null
+++ b/Bugzilla/Comment.pm
@@ -0,0 +1,298 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is James Robson.
+# Portions created by James Robson are Copyright (c) 2009 James Robson.
+# All rights reserved.
+#
+# Contributor(s): James Robson
+
+use strict;
+
+package Bugzilla::Comment;
+
+use base qw(Bugzilla::Object);
+
+use Bugzilla::Attachment;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::User;
+use Bugzilla::Util;
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_COLUMNS => qw(
+ comment_id
+ bug_id
+ who
+ bug_when
+ work_time
+ thetext
+ isprivate
+ already_wrapped
+ type
+ extra_data
+);
+
+use constant UPDATE_COLUMNS => qw(
+ type
+ extra_data
+);
+
+use constant DB_TABLE => 'longdescs';
+use constant ID_FIELD => 'comment_id';
+use constant LIST_ORDER => 'bug_when';
+
+use constant VALIDATORS => {
+ type => \&_check_type,
+};
+
+use constant UPDATE_VALIDATORS => {
+ extra_data => \&_check_extra_data,
+};
+
+#########################
+# Database Manipulation #
+#########################
+
+sub update {
+ my $self = shift;
+ my $changes = $self->SUPER::update(@_);
+ $self->bug->_sync_fulltext();
+ return $changes;
+}
+
+# Speeds up displays of comment lists by loading all ->author objects
+# at once for a whole list.
+sub preload {
+ my ($class, $comments) = @_;
+ my %user_ids = map { $_->{who} => 1 } @$comments;
+ my $users = Bugzilla::User->new_from_list([keys %user_ids]);
+ my %user_map = map { $_->id => $_ } @$users;
+ foreach my $comment (@$comments) {
+ $comment->{author} = $user_map{$comment->{who}};
+ }
+}
+
+###############################
+#### Accessors ######
+###############################
+
+sub already_wrapped { return $_[0]->{'already_wrapped'}; }
+sub body { return $_[0]->{'thetext'}; }
+sub bug_id { return $_[0]->{'bug_id'}; }
+sub creation_ts { return $_[0]->{'bug_when'}; }
+sub is_private { return $_[0]->{'isprivate'}; }
+sub work_time { return $_[0]->{'work_time'}; }
+sub type { return $_[0]->{'type'}; }
+sub extra_data { return $_[0]->{'extra_data'} }
+
+sub bug {
+ my $self = shift;
+ require Bugzilla::Bug;
+ $self->{bug} ||= new Bugzilla::Bug($self->bug_id);
+ return $self->{bug};
+}
+
+sub is_about_attachment {
+ my ($self) = @_;
+ return 1 if ($self->type == CMT_ATTACHMENT_CREATED
+ or $self->type == CMT_ATTACHMENT_UPDATED);
+ return 0;
+}
+
+sub attachment {
+ my ($self) = @_;
+ return undef if not $self->is_about_attachment;
+ $self->{attachment} ||= new Bugzilla::Attachment($self->extra_data);
+ return $self->{attachment};
+}
+
+sub author {
+ my $self = shift;
+ $self->{'author'} ||= new Bugzilla::User($self->{'who'});
+ return $self->{'author'};
+}
+
+sub body_full {
+ my ($self, $params) = @_;
+ $params ||= {};
+ my $template = Bugzilla->template_inner;
+ my $body;
+ if ($self->type) {
+ $template->process("bug/format_comment.txt.tmpl",
+ { comment => $self, %$params }, \$body)
+ || ThrowTemplateError($template->error());
+ $body =~ s/^X//;
+ }
+ else {
+ $body = $self->body;
+ }
+ if ($params->{wrap} and !$self->already_wrapped) {
+ $body = wrap_comment($body);
+ }
+ return $body;
+}
+
+############
+# Mutators #
+############
+
+sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
+
+sub set_type {
+ my ($self, $type, $extra_data) = @_;
+ $self->set('type', $type);
+ $self->set_extra_data($extra_data);
+}
+
+##############
+# Validators #
+##############
+
+sub _check_extra_data {
+ my ($invocant, $extra_data, $type) = @_;
+ $type = $invocant->type if ref $invocant;
+ if ($type == CMT_NORMAL or $type == CMT_POPULAR_VOTES) {
+ if (defined $extra_data) {
+ ThrowCodeError('comment_extra_data_not_allowed',
+ { type => $type, extra_data => $extra_data });
+ }
+ }
+ else {
+ if (!defined $extra_data) {
+ ThrowCodeError('comment_extra_data_required', { type => $type });
+ }
+ if ($type == CMT_MOVED_TO) {
+ $extra_data = Bugzilla::User->check($extra_data)->login;
+ }
+ elsif ($type == CMT_ATTACHMENT_CREATED
+ or $type == CMT_ATTACHMENT_UPDATED)
+ {
+ my $attachment = Bugzilla::Attachment->check({
+ id => $extra_data });
+ $extra_data = $attachment->id;
+ }
+ else {
+ my $original = $extra_data;
+ detaint_natural($extra_data)
+ or ThrowCodeError('comment_extra_data_not_numeric',
+ { type => $type, extra_data => $original });
+ }
+ }
+
+ return $extra_data;
+}
+
+sub _check_type {
+ my ($invocant, $type) = @_;
+ $type ||= CMT_NORMAL;
+ my $original = $type;
+ detaint_natural($type)
+ or ThrowCodeError('comment_type_invalid', { type => $original });
+ return $type;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Comment - A Comment for a given bug
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Comment;
+
+ my $comment = Bugzilla::Comment->new($comment_id);
+ my $comments = Bugzilla::Comment->new_from_list($comment_ids);
+
+=head1 DESCRIPTION
+
+Bugzilla::Comment represents a comment attached to a bug.
+
+This implements all standard C methods. See
+L for more details.
+
+=head2 Accessors
+
+=over
+
+=item C
+
+C The ID of the bug to which the comment belongs.
+
+=item C
+
+C The comment creation timestamp.
+
+=item C
+
+C The body without any special additional text.
+
+=item C
+
+C Time spent as related to this comment.
+
+=item C
+
+C Comment is marked as private
+
+=item C
+
+If this comment is stored in the database word-wrapped, this will be C<1>.
+C<0> otherwise.
+
+=item C
+
+L who created the comment.
+
+=item C
+
+=over
+
+=item B
+
+C Body of the comment, including any special text (such as
+"this bug was marked as a duplicate of...").
+
+=item B
+
+=over
+
+=item C
+
+C. C<1> if this comment should be formatted specifically for
+bugmail.
+
+=item C
+
+C. C<1> if the comment should be returned word-wrapped.
+
+=back
+
+=item B
+
+A string, the full text of the comment as it would be displayed to an end-user.
+
+=back
+
+
+
+=back
+
+=cut
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index 0bd2b1291..7148e4cef 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -65,6 +65,7 @@ use constant UPDATE_COLUMNS => qw(
);
use constant VALIDATORS => {
+ create_series => \&Bugzilla::Object::check_boolean,
product => \&_check_product,
initialowner => \&_check_initialowner,
initialqacontact => \&_check_initialqacontact,
@@ -120,14 +121,15 @@ sub create {
$class->check_required_create_fields(@_);
my $params = $class->run_create_validators(@_);
my $cc_list = delete $params->{initial_cc};
+ my $create_series = delete $params->{create_series};
my $component = $class->insert_create_data($params);
# We still have to fill the component_cc table.
- $component->_update_cc_list($cc_list);
+ $component->_update_cc_list($cc_list) if $cc_list;
# Create series for the new component.
- $component->_create_series();
+ $component->_create_series() if $create_series;
$dbh->bz_commit_transaction();
return $component;
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 469241794..f399900c7 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -35,6 +35,7 @@ use strict;
use base qw(Exporter);
use Bugzilla::Constants;
use Bugzilla::Hook;
+use Bugzilla::Install::Filesystem qw(fix_file_permissions);
use Data::Dumper;
use File::Temp;
@@ -68,7 +69,7 @@ sub _load_params {
}
# This hook is also called in editparams.cgi. This call here is required
# to make SetParam work.
- Bugzilla::Hook::process('config-modify_panels',
+ Bugzilla::Hook::process('config_modify_panels',
{ panels => \%hook_panels });
}
# END INIT CODE
@@ -84,7 +85,7 @@ sub param_panels {
$param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
}
# Now check for any hooked params
- Bugzilla::Hook::process('config-add_panels',
+ Bugzilla::Hook::process('config_add_panels',
{ panel_modules => $param_panels });
return $param_panels;
}
@@ -151,10 +152,6 @@ sub update_params {
{
$param->{'makeproductgroups'} = $param->{'usebuggroups'};
}
- if (exists $param->{'usebuggroupsentry'}
- && !exists $param->{'useentrygroupdefault'}) {
- $param->{'useentrygroupdefault'} = $param->{'usebuggroupsentry'};
- }
# Modularise auth code
if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
@@ -196,6 +193,13 @@ sub update_params {
$param->{'mail_delivery_method'} = $translation{$method};
}
+ # Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
+ # Both "authenticated sessions" and "always" turn on "ssl_redirect"
+ # when upgrading.
+ if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
+ $param->{'ssl_redirect'} = 1;
+ }
+
# --- DEFAULTS FOR NEW PARAMS ---
_load_params unless %params;
@@ -203,7 +207,12 @@ sub update_params {
my $name = $item->{'name'};
unless (exists $param->{$name}) {
print "New parameter: $name\n" unless $new_install;
- $param->{$name} = $answer->{$name} || $item->{'default'};
+ if (exists $answer->{$name}) {
+ $param->{$name} = $answer->{$name};
+ }
+ else {
+ $param->{$name} = $item->{'default'};
+ }
}
}
@@ -293,29 +302,13 @@ sub write_params {
rename $tmpname, $param_file
or die "Can't rename $tmpname to $param_file: $!";
- ChmodDataFile($param_file, 0666);
+ fix_file_permissions($param_file);
# And now we have to reset the params cache so that Bugzilla will re-read
# them.
delete Bugzilla->request_cache->{params};
}
-# Some files in the data directory must be world readable if and only if
-# we don't have a webserver group. Call this function to do this.
-# This will become a private function once all the datafile handling stuff
-# moves into this package
-
-# This sub is not perldoc'd for that reason - noone should know about it
-sub ChmodDataFile {
- my ($file, $mask) = @_;
- my $perm = 0770;
- if ((stat(bz_locations()->{'datadir'}))[2] & 0002) {
- $perm = 0777;
- }
- $perm = $perm & $mask;
- chmod $perm,$file;
-}
-
sub read_param_file {
my %params;
my $datadir = bz_locations()->{'datadir'};
diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm
index d4e822816..e6141cf9e 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::Admin::sortkey = "01";
+our $sortkey = 200;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
new file mode 100644
index 000000000..1acf76f38
--- /dev/null
+++ b/Bugzilla/Config/Advanced.pm
@@ -0,0 +1,57 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Terry Weissman
+# Dawn Endico
+# Dan Mosedale
+# Joe Robins
+# Jacob Steenhagen
+# J. Paul Reed
+# Bradley Baetz
+# Joseph Heenan
+# Erik Stambaugh
+# Frédéric Buclin
+# Max Kanat-Alexander
+
+package Bugzilla::Config::Advanced;
+use strict;
+
+our $sortkey = 1700;
+
+use constant get_param_list => (
+ {
+ name => 'cookiedomain',
+ type => 't',
+ default => ''
+ },
+
+ {
+ name => 'inbound_proxies',
+ type => 't',
+ default => ''
+ },
+
+ {
+ name => 'proxy_url',
+ type => 't',
+ default => ''
+ },
+);
+
+1;
diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm
index f8887bb26..ef61b818f 100644
--- a/Bugzilla/Config/Attachment.pm
+++ b/Bugzilla/Config/Attachment.pm
@@ -35,12 +35,12 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::Attachment::sortkey = "025";
+our $sortkey = 400;
sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
+ my $class = shift;
+ my @param_list = (
+ {
name => 'allow_attachment_display',
type => 'b',
default => 0
@@ -54,65 +54,56 @@ sub get_param_list {
},
{
- name => 'allow_attachment_deletion',
- type => 'b',
- default => 0
- },
+ name => 'allow_attachment_deletion',
+ type => 'b',
+ default => 0
+ },
+ {
+ name => 'allow_attach_url',
+ type => 'b',
+ default => 0
+ },
- {
- name => 'allow_attach_url',
- type => 'b',
- default => 0
- },
+ {
+ name => 'maxattachmentsize',
+ type => 't',
+ default => '1000',
+ checker => \&check_maxattachmentsize
+ },
- {
- name => 'maxattachmentsize',
- type => 't',
- default => '1000',
- checker => \&check_maxattachmentsize
- },
+ {
+ name => 'inline_attachment_mime',
+ type => 't',
+ default => '^text/|^image/',
+ },
- # The maximum size (in bytes) for patches and non-patch attachments.
- # The default limit is 1000KB, which is 24KB less than mysql's default
- # maximum packet size (which determines how much data can be sent in a
- # single mysql packet and thus how much data can be inserted into the
- # database) to provide breathing space for the data in other fields of
- # the attachment record as well as any mysql packet overhead (I don't
- # know of any, but I suspect there may be some.)
+ {
+ name => 'mime_types_file',
+ type => 't',
+ default => '',
+ },
- {
- name => 'maxlocalattachment',
- type => 't',
- default => '0',
- checker => \&check_numeric
- },
+ {
+ name => 'force_attach_bigfile',
+ type => 'b',
+ default => 0,
+ },
- {
- name => 'convert_uncompressed_images',
- type => 'b',
- default => 0,
- checker => \&check_image_converter
- },
+ # The maximum size (in bytes) for patches and non-patch attachments.
+ # The default limit is 1000KB, which is 24KB less than mysql's default
+ # maximum packet size (which determines how much data can be sent in a
+ # single mysql packet and thus how much data can be inserted into the
+ # database) to provide breathing space for the data in other fields of
+ # the attachment record as well as any mysql packet overhead (I don't
+ # know of any, but I suspect there may be some.)
- {
- name => 'inline_attachment_mime',
- type => 't',
- default => '^text/|^image/',
- },
-
- {
- name => 'mime_types_file',
- type => 't',
- default => '',
- },
-
- {
- name => 'force_attach_bigfile',
- type => 'b',
- default => 0,
- },
- );
- return @param_list;
+ {
+ name => 'maxlocalattachment',
+ type => 't',
+ default => '0',
+ checker => \&check_numeric
+ } );
+ return @param_list;
}
1;
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index cbd94617a..c7d921ed5 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::Auth::sortkey = "02";
+our $sortkey = 300;
sub get_param_list {
my $class = shift;
@@ -90,13 +90,6 @@ sub get_param_list {
checker => \&check_multi
},
- {
- name => 'loginnetmask',
- type => 't',
- default => '0',
- checker => \&check_netmask
- },
-
{
name => 'requirelogin',
type => 'b',
diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm
index 0e518b689..4e197c5e9 100644
--- a/Bugzilla/Config/BugChange.pm
+++ b/Bugzilla/Config/BugChange.pm
@@ -36,7 +36,7 @@ use strict;
use Bugzilla::Config::Common;
use Bugzilla::Status;
-$Bugzilla::Config::BugChange::sortkey = "03";
+our $sortkey = 500;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm
index 43c7708df..e2ab1f605 100644
--- a/Bugzilla/Config/BugFields.pm
+++ b/Bugzilla/Config/BugFields.pm
@@ -36,7 +36,7 @@ use strict;
use Bugzilla::Config::Common;
use Bugzilla::Field;
-$Bugzilla::Config::BugFields::sortkey = "04";
+our $sortkey = 600;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/BugMove.pm b/Bugzilla/Config/BugMove.pm
index 87f6cbd73..2d973d8ca 100644
--- a/Bugzilla/Config/BugMove.pm
+++ b/Bugzilla/Config/BugMove.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::BugMove::sortkey = "05";
+our $sortkey = 700;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index b285b3bc9..7416b1794 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -34,6 +34,7 @@ package Bugzilla::Config::Common;
use strict;
+use Email::Address;
use Socket;
use Bugzilla::Util;
@@ -47,10 +48,10 @@ use base qw(Exporter);
qw(check_multi check_numeric check_regexp check_url check_group
check_sslbase check_priority check_severity check_platform
check_opsys check_shadowdb check_urlbase check_webdotbase
- check_netmask check_user_verify_class check_image_converter
+ check_user_verify_class
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
- check_maxattachmentsize
+ check_maxattachmentsize check_email
);
# Checking functions for the various values
@@ -94,6 +95,14 @@ sub check_regexp {
return $@;
}
+sub check_email {
+ my ($value) = @_;
+ if ($value !~ $Email::Address::mailbox) {
+ return "must be a valid email address.";
+ }
+ return "";
+}
+
sub check_sslbase {
my $url = shift;
if ($url ne '') {
@@ -248,21 +257,6 @@ sub check_webdotbase {
return "";
}
-sub check_netmask {
- my ($mask) = @_;
- my $res = check_numeric($mask);
- return $res if $res;
- if ($mask < 0 || $mask > 32) {
- return "an IPv4 netmask must be between 0 and 32 bits";
- }
- # Note that if we changed the netmask from anything apart from 32, then
- # existing logincookies which aren't for a single IP won't work
- # any more. We can't know which ones they are, though, so they'll just
- # take space until they're periodically cleared, later.
-
- return "";
-}
-
sub check_user_verify_class {
# doeditparams traverses the list of params, and for each one it checks,
# then updates. This means that if one param checker wants to look at
@@ -272,41 +266,39 @@ sub check_user_verify_class {
# the login method as LDAP, we won't notice, but all logins will fail.
# So don't do that.
+ my $params = Bugzilla->params;
my ($list, $entry) = @_;
$list || return 'You need to specify at least one authentication mechanism';
for my $class (split /,\s*/, $list) {
my $res = check_multi($class, $entry);
return $res if $res;
if ($class eq 'RADIUS') {
- eval "require Authen::Radius";
- return "Error requiring Authen::Radius: '$@'" if $@;
- return "RADIUS servername (RADIUS_server) is missing" unless Bugzilla->params->{"RADIUS_server"};
- return "RADIUS_secret is empty" unless Bugzilla->params->{"RADIUS_secret"};
+ if (!Bugzilla->feature('auth_radius')) {
+ return "RADIUS support is not available. Run checksetup.pl"
+ . " for more details";
+ }
+ return "RADIUS servername (RADIUS_server) is missing"
+ if !$params->{"RADIUS_server"};
+ return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"};
}
elsif ($class eq 'LDAP') {
- eval "require Net::LDAP";
- return "Error requiring Net::LDAP: '$@'" if $@;
- return "LDAP servername (LDAPserver) is missing" unless Bugzilla->params->{"LDAPserver"};
- return "LDAPBaseDN is empty" unless Bugzilla->params->{"LDAPBaseDN"};
+ if (!Bugzilla->feature('auth_ldap')) {
+ return "LDAP support is not available. Run checksetup.pl"
+ . " for more details";
+ }
+ return "LDAP servername (LDAPserver) is missing"
+ if !$params->{"LDAPserver"};
+ return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"};
}
}
return "";
}
-sub check_image_converter {
- my ($value, $hash) = @_;
- if ($value == 1){
- eval "require Image::Magick";
- return "Error requiring Image::Magick: '$@'" if $@;
- }
- return "";
-}
-
sub check_mail_delivery_method {
my $check = check_multi(@_);
return $check if $check;
my $mailer = shift;
- if ($mailer eq 'sendmail' && $^O =~ /MSWin32/i) {
+ if ($mailer eq 'sendmail' and ON_WINDOWS) {
# look for sendmail.exe
return "Failed to locate " . SENDMAIL_EXE
unless -e SENDMAIL_EXE;
@@ -342,20 +334,25 @@ sub check_notification {
"about the next stable release, you should select " .
"'latest_stable_release' instead";
}
+ if ($option ne 'disabled' && !Bugzilla->feature('updates')) {
+ return "Some Perl modules are missing to get notifications about " .
+ "new releases. See the output of checksetup.pl for more information";
+ }
return "";
}
sub check_smtp_auth {
my $username = shift;
- if ($username) {
- eval "require Authen::SASL";
- return "Error requiring Authen::SASL: '$@'" if $@;
+ if ($username and !Bugzilla->feature('smtp_auth')) {
+ return "SMTP Authentication is not available. Run checksetup.pl for"
+ . " more details";
}
return "";
}
sub check_theschwartz_available {
- if (!eval { require TheSchwartz; require Daemon::Generic; }) {
+ my $use_queue = shift;
+ if ($use_queue && !Bugzilla->feature('jobqueue')) {
return "Using the job queue requires that you have certain Perl"
. " modules installed. See the output of checksetup.pl"
. " for more information";
diff --git a/Bugzilla/Config/Core.pm b/Bugzilla/Config/Core.pm
index 7054bd893..1777bb7b1 100644
--- a/Bugzilla/Config/Core.pm
+++ b/Bugzilla/Config/Core.pm
@@ -35,17 +35,9 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::Core::sortkey = "00";
-
-sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'maintainer',
- type => 't',
- default => 'THE MAINTAINER HAS NOT YET BEEN SET'
- },
+our $sortkey = 100;
+use constant get_param_list => (
{
name => 'error_log',
type => 't',
@@ -72,10 +64,9 @@ sub get_param_list {
},
{
- name => 'docs_urlbase',
- type => 't',
- default => 'docs/%lang%/html/',
- checker => \&check_url
+ name => 'ssl_redirect',
+ type => 'b',
+ default => 0
},
{
@@ -85,59 +76,11 @@ sub get_param_list {
checker => \&check_sslbase
},
- {
- name => 'ssl',
- type => 's',
- choices => ['never', 'authenticated sessions', 'always'],
- default => 'never'
- },
-
- {
- name => 'cookiedomain',
- type => 't',
- default => ''
- },
-
{
name => 'cookiepath',
type => 't',
default => '/'
},
-
- {
- name => 'utf8',
- type => 'b',
- default => '0',
- checker => \&check_utf8
- },
-
- {
- name => 'shutdownhtml',
- type => 'l',
- default => ''
- },
-
- {
- name => 'announcehtml',
- type => 'l',
- default => ''
- },
-
- {
- name => 'proxy_url',
- type => 't',
- default => ''
- },
-
- {
- name => 'upgrade_notification',
- type => 's',
- choices => ['development_snapshot', 'latest_stable_release',
- 'stable_branch_release', 'disabled'],
- default => 'latest_stable_release',
- checker => \&check_notification
- } );
- return @param_list;
-}
+);
1;
diff --git a/Bugzilla/Config/DependencyGraph.pm b/Bugzilla/Config/DependencyGraph.pm
index 37c07f3d3..f717b2708 100644
--- a/Bugzilla/Config/DependencyGraph.pm
+++ b/Bugzilla/Config/DependencyGraph.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::DependencyGraph::sortkey = "06";
+our $sortkey = 800;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm
new file mode 100644
index 000000000..0f043548b
--- /dev/null
+++ b/Bugzilla/Config/General.pm
@@ -0,0 +1,83 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Terry Weissman
+# Dawn Endico
+# Dan Mosedale
+# Joe Robins
+# Jacob Steenhagen
+# J. Paul Reed
+# Bradley Baetz
+# Joseph Heenan
+# Erik Stambaugh
+# Frédéric Buclin
+# Max Kanat-Alexander
+
+package Bugzilla::Config::General;
+use strict;
+use Bugzilla::Config::Common;
+
+our $sortkey = 150;
+
+use constant get_param_list => (
+ {
+ name => 'maintainer',
+ type => 't',
+ no_reset => '1',
+ default => '',
+ checker => \&check_email
+ },
+
+ {
+ name => 'docs_urlbase',
+ type => 't',
+ default => 'docs/%lang%/html/',
+ checker => \&check_url
+ },
+
+ {
+ name => 'utf8',
+ type => 'b',
+ default => '0',
+ checker => \&check_utf8
+ },
+
+ {
+ name => 'shutdownhtml',
+ type => 'l',
+ default => ''
+ },
+
+ {
+ name => 'announcehtml',
+ type => 'l',
+ default => ''
+ },
+
+ {
+ name => 'upgrade_notification',
+ type => 's',
+ choices => ['development_snapshot', 'latest_stable_release',
+ 'stable_branch_release', 'disabled'],
+ default => 'latest_stable_release',
+ checker => \&check_notification
+ },
+);
+
+1;
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index 0a238f209..f7f717379 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -36,7 +36,7 @@ use strict;
use Bugzilla::Config::Common;
use Bugzilla::Group;
-$Bugzilla::Config::GroupSecurity::sortkey = "07";
+our $sortkey = 900;
sub get_param_list {
my $class = shift;
@@ -48,12 +48,6 @@ sub get_param_list {
default => 0
},
- {
- name => 'useentrygroupdefault',
- type => 'b',
- default => 0
- },
-
{
name => 'chartgroup',
type => 's',
diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm
index a9b46382e..e47f92308 100644
--- a/Bugzilla/Config/LDAP.pm
+++ b/Bugzilla/Config/LDAP.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::LDAP::sortkey = "09";
+our $sortkey = 1000;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm
index c7843e286..b1e3ab1a4 100644
--- a/Bugzilla/Config/MTA.pm
+++ b/Bugzilla/Config/MTA.pm
@@ -36,7 +36,7 @@ use strict;
use Bugzilla::Config::Common;
use Email::Send;
-$Bugzilla::Config::MTA::sortkey = "10";
+our $sortkey = 1200;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/PatchViewer.pm b/Bugzilla/Config/PatchViewer.pm
index 8de04ef76..6bd9557a9 100644
--- a/Bugzilla/Config/PatchViewer.pm
+++ b/Bugzilla/Config/PatchViewer.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::PatchViewer::sortkey = "11";
+our $sortkey = 1300;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm
index fbfdb4c22..808a9a102 100644
--- a/Bugzilla/Config/Query.pm
+++ b/Bugzilla/Config/Query.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::Query::sortkey = "12";
+our $sortkey = 1400;
sub get_param_list {
my $class = shift;
@@ -67,13 +67,6 @@ sub get_param_list {
default => 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&order=Importance&long_desc_type=substring'
},
- {
- name => 'quicksearch_comment_cutoff',
- type => 't',
- default => '4',
- checker => \&check_numeric
- },
-
{
name => 'specific_search_allow_empty_words',
type => 'b',
diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm
index 6701d6f08..bc072a9c4 100644
--- a/Bugzilla/Config/RADIUS.pm
+++ b/Bugzilla/Config/RADIUS.pm
@@ -25,7 +25,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::RADIUS::sortkey = "09";
+our $sortkey = 1100;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm
index f9af4fb6d..a605b2363 100644
--- a/Bugzilla/Config/ShadowDB.pm
+++ b/Bugzilla/Config/ShadowDB.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::ShadowDB::sortkey = "13";
+our $sortkey = 1500;
sub get_param_list {
my $class = shift;
diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm
index 1d76a515b..cc7289c1f 100644
--- a/Bugzilla/Config/UserMatch.pm
+++ b/Bugzilla/Config/UserMatch.pm
@@ -35,7 +35,7 @@ use strict;
use Bugzilla::Config::Common;
-$Bugzilla::Config::UserMatch::sortkey = "14";
+our $sortkey = 1600;
sub get_param_list {
my $class = shift;
@@ -46,13 +46,6 @@ sub get_param_list {
default => '0'
},
- {
- name => 'usermatchmode',
- type => 's',
- choices => ['off', 'wildcard', 'search'],
- default => 'off'
- },
-
{
name => 'maxusermatches',
type => 't',
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index f22e31933..147aa7f80 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -55,9 +55,9 @@ use File::Basename;
AUTH_LOGINFAILED
AUTH_DISABLED
AUTH_NO_SUCH_USER
+ AUTH_LOCKOUT
USER_PASSWORD_MIN_LENGTH
- USER_PASSWORD_MAX_LENGTH
LOGIN_OPTIONAL
LOGIN_NORMAL
@@ -79,6 +79,7 @@ use File::Basename;
DEFAULT_COLUMN_LIST
DEFAULT_QUERY_NAME
+ DEFAULT_MILESTONE
QUERY_LIST
LIST_OF_BUGS
@@ -92,6 +93,8 @@ use File::Basename;
CMT_HAS_DUPE
CMT_POPULAR_VOTES
CMT_MOVED_TO
+ CMT_ATTACHMENT_CREATED
+ CMT_ATTACHMENT_UPDATED
THROW_ERROR
@@ -103,7 +106,7 @@ use File::Basename;
EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
EVT_BUG_CREATED
-
+
NEG_EVENTS
EVT_UNCONFIRMED EVT_CHANGED_BY_ME
@@ -127,14 +130,18 @@ use File::Basename;
FIELD_TYPE_BUG_ID
FIELD_TYPE_BUG_URLS
+ TIMETRACKING_FIELDS
+
USAGE_MODE_BROWSER
USAGE_MODE_CMDLINE
- USAGE_MODE_WEBSERVICE
+ USAGE_MODE_XMLRPC
USAGE_MODE_EMAIL
+ USAGE_MODE_JSON
ERROR_MODE_WEBPAGE
ERROR_MODE_DIE
ERROR_MODE_DIE_SOAP_FAULT
+ ERROR_MODE_JSON_RPC
ERROR_MODE_AJAX
INSTALLATION_MODE_INTERACTIVE
@@ -146,8 +153,11 @@ use File::Basename;
MAX_TOKEN_AGE
MAX_LOGINCOOKIE_AGE
+ MAX_LOGIN_ATTEMPTS
+ LOGIN_LOCKOUT_INTERVAL
SAFE_PROTOCOLS
+ LEGAL_CONTENT_TYPES
MIN_SMALLINT
MAX_SMALLINT
@@ -172,7 +182,7 @@ use File::Basename;
# CONSTANTS
#
# Bugzilla version
-use constant BUGZILLA_VERSION => "3.4.6";
+use constant BUGZILLA_VERSION => "3.6";
# These are unique values that are unlikely to match a string or a number,
# to be used in criteria for match() functions and other things. They start
@@ -225,10 +235,10 @@ use constant AUTH_ERROR => 2;
use constant AUTH_LOGINFAILED => 3;
use constant AUTH_DISABLED => 4;
use constant AUTH_NO_SUCH_USER => 5;
+use constant AUTH_LOCKOUT => 6;
-# The minimum and maximum lengths a password must have.
-use constant USER_PASSWORD_MIN_LENGTH => 3;
-use constant USER_PASSWORD_MAX_LENGTH => 16;
+# The minimum length a password must have.
+use constant USER_PASSWORD_MIN_LENGTH => 6;
use constant LOGIN_OPTIONAL => 0;
use constant LOGIN_NORMAL => 1;
@@ -238,18 +248,6 @@ use constant LOGOUT_ALL => 0;
use constant LOGOUT_CURRENT => 1;
use constant LOGOUT_KEEP_CURRENT => 2;
-use constant contenttypes =>
- {
- "html"=> "text/html" ,
- "rdf" => "application/rdf+xml" ,
- "atom"=> "application/atom+xml" ,
- "xml" => "application/xml" ,
- "js" => "application/x-javascript" ,
- "csv" => "text/csv" ,
- "png" => "image/png" ,
- "ics" => "text/calendar" ,
- };
-
use constant GRANT_DIRECT => 0;
use constant GRANT_REGEXP => 2;
@@ -270,6 +268,9 @@ use constant DEFAULT_COLUMN_LIST => (
# for the default settings.
use constant DEFAULT_QUERY_NAME => '(Default query)';
+# The default "defaultmilestone" created for products.
+use constant DEFAULT_MILESTONE => '---';
+
# The possible types for saved searches.
use constant QUERY_LIST => 0;
use constant LIST_OF_BUGS => 1;
@@ -286,6 +287,8 @@ use constant CMT_DUPE_OF => 1;
use constant CMT_HAS_DUPE => 2;
use constant CMT_POPULAR_VOTES => 3;
use constant CMT_MOVED_TO => 4;
+use constant CMT_ATTACHMENT_CREATED => 5;
+use constant CMT_ATTACHMENT_UPDATED => 6;
# Determine whether a validation routine should return 0 or throw
# an error when the validation fails.
@@ -370,28 +373,58 @@ use constant FIELD_TYPE_DATETIME => 5;
use constant FIELD_TYPE_BUG_ID => 6;
use constant FIELD_TYPE_BUG_URLS => 7;
+# The fields from fielddefs that are blocked from non-timetracking users.
+# work_time is sometimes called actual_time.
+use constant TIMETRACKING_FIELDS =>
+ qw(estimated_time remaining_time work_time actual_time
+ percentage_complete deadline);
+
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
# How many days a logincookie will remain valid if not used.
use constant MAX_LOGINCOOKIE_AGE => 30;
+# Maximum failed logins to lock account for this IP
+use constant MAX_LOGIN_ATTEMPTS => 5;
+# If the maximum login attempts occur during this many minutes, the
+# account is locked.
+use constant LOGIN_LOCKOUT_INTERVAL => 30;
+
# Protocols which are considered as safe.
use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
'irc', 'mid', 'news', 'nntp', 'prospero', 'telnet',
'view-source', 'wais');
+# Valid MIME types for attachments.
+use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message',
+ 'model', 'multipart', 'text', 'video');
+
+use constant contenttypes =>
+ {
+ "html"=> "text/html" ,
+ "rdf" => "application/rdf+xml" ,
+ "atom"=> "application/atom+xml" ,
+ "xml" => "application/xml" ,
+ "js" => "application/x-javascript" ,
+ "csv" => "text/csv" ,
+ "png" => "image/png" ,
+ "ics" => "text/calendar" ,
+ };
+
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
use constant USAGE_MODE_BROWSER => 0;
use constant USAGE_MODE_CMDLINE => 1;
-use constant USAGE_MODE_WEBSERVICE => 2;
+use constant USAGE_MODE_XMLRPC => 2;
use constant USAGE_MODE_EMAIL => 3;
+use constant USAGE_MODE_JSON => 4;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
use constant ERROR_MODE_WEBPAGE => 0;
use constant ERROR_MODE_DIE => 1;
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
-use constant ERROR_MODE_AJAX => 3;
+use constant ERROR_MODE_JSON_RPC => 3;
+use constant ERROR_MODE_AJAX => 4;
# The various modes that checksetup.pl can run in.
use constant INSTALLATION_MODE_INTERACTIVE => 0;
@@ -425,13 +458,13 @@ use constant DB_MODULE => {
name => 'Oracle'},
};
-# The user who should be considered "root" when we're giving
-# instructions to Bugzilla administrators.
-use constant ROOT_USER => $^O =~ /MSWin32/i ? 'Administrator' : 'root';
-
# True if we're on Win32.
use constant ON_WINDOWS => ($^O =~ /MSWin32/i);
+# The user who should be considered "root" when we're giving
+# instructions to Bugzilla administrators.
+use constant ROOT_USER => ON_WINDOWS ? 'Administrator' : 'root';
+
use constant MIN_SMALLINT => -32768;
use constant MAX_SMALLINT => 32767;
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 238532155..830d2835e 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -65,7 +65,7 @@ use constant ISOLATION_LEVEL => 'REPEATABLE READ';
use constant ENUM_DEFAULTS => {
bug_severity => ['blocker', 'critical', 'major', 'normal',
'minor', 'trivial', 'enhancement'],
- priority => ["P1","P2","P3","P4","P5"],
+ priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"],
op_sys => ["All","Windows","Mac OS","Linux","Other"],
rep_platform => ["All","PC","Macintosh","Other"],
bug_status => ["UNCONFIRMED","NEW","ASSIGNED","REOPENED","RESOLVED",
@@ -271,9 +271,9 @@ EOT
}
# List of abstract methods we are checking the derived class implements
-our @_abstract_methods = qw(REQUIRED_VERSION PROGRAM_NAME DBD_VERSION
- new sql_regexp sql_not_regexp sql_limit sql_to_days
- sql_date_format sql_interval bz_explain);
+our @_abstract_methods = qw(new sql_regexp sql_not_regexp sql_limit sql_to_days
+ sql_date_format sql_interval bz_explain
+ sql_group_concat);
# This overridden import method will check implementation of inherited classes
# for missing implementation of abstract methods
@@ -286,7 +286,7 @@ sub import {
# make sure all abstract methods are implemented
foreach my $meth (@_abstract_methods) {
$pkg->can($meth)
- or croak("Class $pkg does not define method $meth");
+ or die("Class $pkg does not define method $meth");
}
}
@@ -537,6 +537,13 @@ sub bz_alter_column {
ThrowCodeError('column_not_null_no_default_alter',
{ name => "$table.$name" }) if ($any_nulls);
}
+ # Preserve foreign key definitions in the Schema object when altering
+ # types.
+ if (defined $current_def->{REFERENCES}) {
+ # Make sure we don't modify the caller's $new_def.
+ $new_def = dclone($new_def);
+ $new_def->{REFERENCES} = $current_def->{REFERENCES};
+ }
$self->bz_alter_column_raw($table, $name, $new_def, $current_def,
$set_nulls_to);
$self->_bz_real_schema->set_column($table, $name, $new_def);
@@ -689,7 +696,7 @@ sub bz_add_field_tables {
if ($field->type == FIELD_TYPE_MULTI_SELECT) {
my $ms_table = "bug_" . $field->name;
$self->_bz_add_field_table($ms_table,
- $self->_bz_schema->MULTI_SELECT_VALUE_TABLE);
+ $self->_bz_schema->MULTI_SELECT_VALUE_TABLE);
$self->bz_add_fk($ms_table, 'bug_id', {TABLE => 'bugs',
COLUMN => 'bug_id',
@@ -830,6 +837,14 @@ sub bz_drop_table {
}
}
+sub bz_fk_info {
+ my ($self, $table, $column) = @_;
+ my $col_info = $self->bz_column_info($table, $column);
+ return undef if !$col_info;
+ my $fk = $col_info->{REFERENCES};
+ return $fk;
+}
+
sub bz_rename_column {
my ($self, $table, $old_name, $new_name) = @_;
@@ -872,6 +887,16 @@ sub bz_rename_table {
$self->_bz_store_real_schema;
}
+sub bz_set_next_serial_value {
+ my ($self, $table, $column, $value) = @_;
+ if (!$value) {
+ $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0;
+ $value++;
+ }
+ my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value);
+ $self->do($_) foreach @sql;
+}
+
#####################################################################
# Schema Information Methods
#####################################################################
@@ -1001,7 +1026,7 @@ sub bz_start_transaction {
sub bz_commit_transaction {
my ($self) = @_;
-
+
if ($self->{private_bz_transaction_count} > 1) {
$self->{private_bz_transaction_count}--;
} elsif ($self->bz_in_transaction) {
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
index 451c4223e..d9e5462d0 100644
--- a/Bugzilla/DB/Mysql.pm
+++ b/Bugzilla/DB/Mysql.pm
@@ -68,9 +68,13 @@ sub new {
$dsn .= ";port=$port" if $port;
$dsn .= ";mysql_socket=$sock" if $sock;
- my $attrs = { mysql_enable_utf8 => Bugzilla->params->{'utf8'} };
+ my %attrs = (
+ mysql_enable_utf8 => Bugzilla->params->{'utf8'},
+ # Needs to be explicitly specified for command-line processes.
+ mysql_auto_reconnect => 1,
+ );
- my $self = $class->db_new($dsn, $user, $pass, $attrs);
+ my $self = $class->db_new($dsn, $user, $pass, \%attrs);
# This makes sure that if the tables are encoded as UTF-8, we
# return their data correctly.
@@ -158,15 +162,15 @@ sub sql_limit {
sub sql_string_concat {
my ($self, @params) = @_;
-
+
return 'CONCAT(' . join(', ', @params) . ')';
}
sub sql_fulltext_search {
my ($self, $column, $text) = @_;
- # quote un-quoted compound words
- my @words = quotewords('[\s()]+', 'delimiters', $text);
+ # quote un-quoted compound words
+ my @words = quotewords('[\s()]+', 'delimiters', $text);
if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/)
{
# already a boolean mode search
@@ -200,7 +204,7 @@ sub sql_fulltext_search {
sub sql_istring {
my ($self, $string) = @_;
-
+
return $string;
}
@@ -324,15 +328,11 @@ EOT
}
- # Figure out if any existing tables are of type ISAM and convert them
- # to type MyISAM if so. ISAM tables are deprecated in MySQL 3.23,
- # which Bugzilla now requires, and they don't support more than 16
- # indexes per table, which Bugzilla needs.
- my $table_status = $self->selectall_arrayref("SHOW TABLE STATUS");
+ my %table_status = @{ $self->selectcol_arrayref("SHOW TABLE STATUS",
+ {Columns=>[1,2]}) };
my @isam_tables;
- foreach my $row (@$table_status) {
- my ($name, $type) = @$row;
- push(@isam_tables, $name) if (defined($type) && $type eq "ISAM");
+ foreach my $name (keys %table_status) {
+ push(@isam_tables, $name) if (defined($table_status{$name}) && $table_status{$name} eq "ISAM");
}
if(scalar(@isam_tables)) {
@@ -354,7 +354,9 @@ EOT
# We want to convert tables to InnoDB, but it's possible that they have
# fulltext indexes on them, and conversion will fail unless we remove
# the indexes.
- if (grep($_ eq 'bugs', @tables)) {
+ if (grep($_ eq 'bugs', @tables)
+ and !grep($_ eq 'bugs_fulltext', @tables))
+ {
if ($self->bz_index_info_real('bugs', 'short_desc')) {
$self->bz_drop_index_raw('bugs', 'short_desc');
}
@@ -363,7 +365,9 @@ EOT
$sd_index_deleted = 1; # Used for later schema cleanup.
}
}
- if (grep($_ eq 'longdescs', @tables)) {
+ if (grep($_ eq 'longdescs', @tables)
+ and !grep($_ eq 'bugs_fulltext', @tables))
+ {
if ($self->bz_index_info_real('longdescs', 'thetext')) {
$self->bz_drop_index_raw('longdescs', 'thetext');
}
@@ -375,9 +379,9 @@ EOT
# Upgrade tables from MyISAM to InnoDB
my @myisam_tables;
- foreach my $row (@$table_status) {
- my ($name, $type) = @$row;
- if (defined ($type) && $type =~ /^MYISAM$/i
+ foreach my $name (keys %table_status) {
+ if (defined($table_status{$name})
+ && $table_status{$name} =~ /^MYISAM$/i
&& !grep($_ eq $name, Bugzilla::DB::Schema::Mysql::MYISAM_TABLES))
{
push(@myisam_tables, $name) ;
@@ -724,6 +728,7 @@ EOT
foreach my $table ($self->bz_table_list_real) {
my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table");
$info_sth->execute();
+ my (@binary_sql, @utf8_sql);
while (my $column = $info_sth->fetchrow_hashref) {
# Our conversion code doesn't work on enum fields, but they
# all go away later in checksetup anyway.
@@ -736,34 +741,13 @@ EOT
{
my $name = $column->{Field};
- # The code below doesn't work on a field with a FULLTEXT
- # index. So we drop it, which we'd do later anyway.
- if ($table eq 'longdescs' && $name eq 'thetext') {
- $self->bz_drop_index('longdescs',
- 'longdescs_thetext_idx');
- }
- if ($table eq 'bugs' && $name eq 'short_desc') {
- $self->bz_drop_index('bugs', 'bugs_short_desc_idx');
- }
- my %ft_indexes;
- if ($table eq 'bugs_fulltext') {
- %ft_indexes = $self->_bz_real_schema->get_indexes_on_column_abstract(
- 'bugs_fulltext', $name);
- foreach my $index (keys %ft_indexes) {
- $self->bz_drop_index('bugs_fulltext', $index);
- }
- }
- if ($table eq 'test_runs' && $name eq 'summary') {
- $self->bz_drop_index('test_runs', 'test_runs_summary_idx');
- }
+ print "$table.$name needs to be converted to UTF-8...\n";
my $dropped = $self->bz_drop_related_fks($table, $name);
push(@dropped_fks, @$dropped);
- print "Converting $table.$name to be stored as UTF-8...\n";
- my $col_info =
+ my $col_info =
$self->bz_column_info_real($table, $name);
-
# CHANGE COLUMN doesn't take PRIMARY KEY
delete $col_info->{PRIMARYKEY};
my $sql_def = $self->_bz_schema->get_type_ddl($col_info);
@@ -777,21 +761,41 @@ EOT
my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
$binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
$utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/;
- $self->do("ALTER TABLE $table CHANGE COLUMN $name $name
- $binary");
- $self->do("ALTER TABLE $table CHANGE COLUMN $name $name
- $utf8");
+ push(@binary_sql, "MODIFY COLUMN $name $binary");
+ push(@utf8_sql, "MODIFY COLUMN $name $utf8");
+ }
+ } # foreach column
- if ($table eq 'bugs_fulltext') {
- foreach my $index (keys %ft_indexes) {
- $self->bz_add_index('bugs_fulltext', $index,
- $ft_indexes{$index});
- }
+ if (@binary_sql) {
+ my %indexes = %{ $self->bz_table_indexes($table) };
+ foreach my $index_name (keys %indexes) {
+ my $index = $indexes{$index_name};
+ if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') {
+ $self->bz_drop_index($table, $index_name);
+ }
+ else {
+ delete $indexes{$index_name};
+ }
+ if ($table eq 'test_runs' && $index_name eq 'summary') {
+ $self->bz_drop_index('test_runs', 'test_runs_summary_idx');
}
}
- }
- $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8");
+ print "Converting the $table table to UTF-8...\n";
+ my $bin = "ALTER TABLE $table " . join(', ', @binary_sql);
+ my $utf = "ALTER TABLE $table " . join(', ', @utf8_sql,
+ 'DEFAULT CHARACTER SET utf8');
+ $self->do($bin);
+ $self->do($utf);
+
+ # Re-add any removed FULLTEXT indexes.
+ foreach my $index (keys %indexes) {
+ $self->bz_add_index($table, $index, $indexes{$index});
+ }
+ }
+ else {
+ $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8");
+ }
} # foreach my $table (@tables)
@@ -832,22 +836,40 @@ sub _fix_defaults {
# a default.
return unless (defined $assi_default && $assi_default ne '');
+ my %fix_columns;
foreach my $table ($self->_bz_real_schema->get_table_list()) {
foreach my $column ($self->bz_table_columns($table)) {
- my $abs_def = $self->bz_column_info($table, $column);
+ my $abs_def = $self->bz_column_info($table, $column);
+ # BLOB/TEXT columns never have defaults
+ next if $abs_def->{TYPE} =~ /BLOB|TEXT/i;
if (!defined $abs_def->{DEFAULT}) {
# Get the exact default from the database without any
# "fixing" by bz_column_info_real.
my $raw_info = $self->_bz_raw_column_info($table, $column);
my $raw_default = $raw_info->{COLUMN_DEF};
if (defined $raw_default) {
- $self->bz_alter_column_raw($table, $column, $abs_def);
- $raw_default = "''" if $raw_default eq '';
- print "Removed incorrect DB default: $raw_default\n";
+ if ($raw_default eq '') {
+ # Only (var)char columns can have empty strings as
+ # defaults, so if we got an empty string for some
+ # other default type, then it's bogus.
+ next unless $abs_def->{TYPE} =~ /char/i;
+ $raw_default = "''";
+ }
+ $fix_columns{$table} ||= [];
+ push(@{ $fix_columns{$table} }, $column);
+ print "$table.$column has incorrect DB default: $raw_default\n";
}
}
} # foreach $column
} # foreach $table
+
+ print "Fixing defaults...\n";
+ foreach my $table (reverse sort keys %fix_columns) {
+ my @alters = map("ALTER COLUMN $_ DROP DEFAULT",
+ @{ $fix_columns{$table} });
+ my $sql = "ALTER TABLE $table " . join(',', @alters);
+ $self->do($sql);
+ }
}
# There is a bug in MySQL 4.1.0 - 4.1.15 that makes certain SELECT
diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm
index a2c78e094..5dd127882 100644
--- a/Bugzilla/DB/Oracle.pm
+++ b/Bugzilla/DB/Oracle.pm
@@ -115,6 +115,12 @@ sub bz_explain {
return join("\n", @$explain);
}
+sub sql_group_concat {
+ my ($self, $text, $separator) = @_;
+ $separator ||= "','";
+ return "group_concat(T_CLOB_DELIM($text, $separator))";
+}
+
sub sql_regexp {
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
@@ -271,6 +277,10 @@ sub _fix_hashref {
sub adjust_statement {
my ($sql) = @_;
+
+ if ($sql =~ /^CREATE OR REPLACE.*/i){
+ return $sql;
+ }
# We can't just assume any occurrence of "''" in $sql is an empty
# string, since "''" can occur inside a string literal as a way of
@@ -337,6 +347,10 @@ sub adjust_statement {
# Oracle need no 'AS'
$nonstring =~ s/\bAS\b//ig;
+
+ # Take the first 4000 chars for comparison
+ $nonstring =~ s/\(\s*(longdescs_\d+\.thetext|attachdata_\d+\.thedata)/
+ \(DBMS_LOB.SUBSTR\($1, 4000, 1\)/ig;
# Look for a LIMIT clause
($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o);
@@ -529,6 +543,88 @@ sub bz_setup_database {
. " RETURN DATE IS BEGIN RETURN SYSDATE; END;");
$self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)"
. " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;");
+
+ # Create types for group_concat
+ my $t_clob_delim = $self->selectcol_arrayref("
+ SELECT TYPE_NAME FROM USER_TYPES WHERE TYPE_NAME=?",
+ undef, 'T_CLOB_DELIM');
+
+ if ( !@$t_clob_delim ) {
+ $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT "
+ . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256));");
+ }
+
+ $self->do("CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT
+ ( CLOB_CONTENT CLOB,
+ DELIMITER VARCHAR2(256),
+ STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
+ SCTX IN OUT NOCOPY T_GROUP_CONCAT)
+ RETURN NUMBER,
+ MEMBER FUNCTION ODCIAGGREGATEITERATE(
+ SELF IN OUT NOCOPY T_GROUP_CONCAT,
+ VALUE IN T_CLOB_DELIM)
+ RETURN NUMBER,
+ MEMBER FUNCTION ODCIAGGREGATETERMINATE(
+ SELF IN T_GROUP_CONCAT,
+ RETURNVALUE OUT NOCOPY CLOB,
+ FLAGS IN NUMBER)
+ RETURN NUMBER,
+ MEMBER FUNCTION ODCIAGGREGATEMERGE(
+ SELF IN OUT NOCOPY T_GROUP_CONCAT,
+ CTX2 IN T_GROUP_CONCAT)
+ RETURN NUMBER);");
+
+ $self->do("CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS
+ STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
+ SCTX IN OUT NOCOPY T_GROUP_CONCAT)
+ RETURN NUMBER IS
+ BEGIN
+ SCTX := T_GROUP_CONCAT(EMPTY_CLOB(), NULL);
+ DBMS_LOB.CREATETEMPORARY(SCTX.CLOB_CONTENT, TRUE);
+ RETURN ODCICONST.SUCCESS;
+ END;
+ MEMBER FUNCTION ODCIAGGREGATEITERATE(
+ SELF IN OUT NOCOPY T_GROUP_CONCAT,
+ VALUE IN T_CLOB_DELIM)
+ RETURN NUMBER IS
+ BEGIN
+ SELF.DELIMITER := VALUE.P_DELIMITER;
+ DBMS_LOB.WRITEAPPEND(SELF.CLOB_CONTENT,
+ LENGTH(SELF.DELIMITER),
+ SELF.DELIMITER);
+ DBMS_LOB.APPEND(SELF.CLOB_CONTENT, VALUE.P_CONTENT);
+
+ RETURN ODCICONST.SUCCESS;
+ END;
+ MEMBER FUNCTION ODCIAGGREGATETERMINATE(
+ SELF IN T_GROUP_CONCAT,
+ RETURNVALUE OUT NOCOPY CLOB,
+ FLAGS IN NUMBER)
+ RETURN NUMBER IS
+ BEGIN
+ RETURNVALUE := RTRIM(LTRIM(SELF.CLOB_CONTENT,
+ SELF.DELIMITER),
+ SELF.DELIMITER);
+ RETURN ODCICONST.SUCCESS;
+ END;
+ MEMBER FUNCTION ODCIAGGREGATEMERGE(
+ SELF IN OUT NOCOPY T_GROUP_CONCAT,
+ CTX2 IN T_GROUP_CONCAT)
+ RETURN NUMBER IS
+ BEGIN
+ DBMS_LOB.WRITEAPPEND(SELF.CLOB_CONTENT,
+ LENGTH(SELF.DELIMITER),
+ SELF.DELIMITER);
+ DBMS_LOB.APPEND(SELF.CLOB_CONTENT, CTX2.CLOB_CONTENT);
+ RETURN ODCICONST.SUCCESS;
+ END;
+ END;");
+
+ # Create user-defined aggregate function group_concat
+ $self->do("CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM)
+ RETURN CLOB
+ DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;");
+
# Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search
my $lexer = $self->selectcol_arrayref(
"SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND
@@ -580,6 +676,14 @@ sub bz_setup_database {
}
}
+ # Drop the trigger which causes bug 541553
+ my $trigger_name = "PRODUCTS_MILESTONEURL";
+ my $exist_trigger = $self->selectcol_arrayref(
+ "SELECT OBJECT_NAME FROM USER_OBJECTS
+ WHERE OBJECT_NAME = ?", undef, $trigger_name);
+ if(@$exist_trigger) {
+ $self->do("DROP TRIGGER $trigger_name");
+ }
}
package Bugzilla::DB::Oracle::st;
diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm
index ce9b3ec5e..b31d186bb 100644
--- a/Bugzilla/DB/Pg.pm
+++ b/Bugzilla/DB/Pg.pm
@@ -94,6 +94,12 @@ sub bz_last_key {
return $last_insert_id;
}
+sub sql_group_concat {
+ my ($self, $text, $separator) = @_;
+ $separator ||= "','";
+ return "array_to_string(array_accum($text), $separator)";
+}
+
sub sql_istring {
my ($self, $string) = @_;
@@ -201,6 +207,20 @@ sub bz_setup_database {
my $self = shift;
$self->SUPER::bz_setup_database(@_);
+ # Custom Functions
+ my $function = 'array_accum';
+ my $array_accum = $self->selectrow_array(
+ 'SELECT 1 FROM pg_proc WHERE proname = ?', undef, $function);
+ if (!$array_accum) {
+ print "Creating function $function...\n";
+ $self->do("CREATE AGGREGATE array_accum (
+ SFUNC = array_append,
+ BASETYPE = anyelement,
+ STYPE = anyarray,
+ INITCOND = '{}'
+ )");
+ }
+
# PostgreSQL doesn't like having *any* index on the thetext
# field, because it can't have index data longer than 2770
# characters on that field.
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 2c6ab6b89..0fa79d6f0 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -22,6 +22,7 @@
# Lance Larsh
# Dennis Melentyev
# Akamai Technologies
+# Elliotte Martin
package Bugzilla::DB::Schema;
@@ -240,7 +241,9 @@ use constant ABSTRACT_SCHEMA => {
FIELDS => [
bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
- assigned_to => {TYPE => 'INT3', NOTNULL => 1},
+ assigned_to => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid'}},
bug_file_loc => {TYPE => 'MEDIUMTEXT'},
bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1},
bug_status => {TYPE => 'varchar(64)', NOTNULL => 1},
@@ -249,16 +252,24 @@ use constant ABSTRACT_SCHEMA => {
short_desc => {TYPE => 'varchar(255)', NOTNULL => 1},
op_sys => {TYPE => 'varchar(64)', NOTNULL => 1},
priority => {TYPE => 'varchar(64)', NOTNULL => 1},
- product_id => {TYPE => 'INT2', NOTNULL => 1},
+ product_id => {TYPE => 'INT2', NOTNULL => 1,
+ REFERENCES => {TABLE => 'products',
+ COLUMN => 'id'}},
rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1},
- reporter => {TYPE => 'INT3', NOTNULL => 1},
+ reporter => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid'}},
version => {TYPE => 'varchar(64)', NOTNULL => 1},
- component_id => {TYPE => 'INT2', NOTNULL => 1},
+ component_id => {TYPE => 'INT2', NOTNULL => 1,
+ REFERENCES => {TABLE => 'components',
+ COLUMN => 'id'}},
resolution => {TYPE => 'varchar(64)',
NOTNULL => 1, DEFAULT => "''"},
target_milestone => {TYPE => 'varchar(20)',
NOTNULL => 1, DEFAULT => "'---'"},
- qa_contact => {TYPE => 'INT3'},
+ qa_contact => {TYPE => 'INT3',
+ REERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid'}},
status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
DEFAULT => "''"},
votes => {TYPE => 'INT3', NOTNULL => 1,
@@ -273,9 +284,9 @@ use constant ABSTRACT_SCHEMA => {
NOTNULL => 1, DEFAULT => 'TRUE'},
cclist_accessible => {TYPE => 'BOOLEAN',
NOTNULL => 1, DEFAULT => 'TRUE'},
- estimated_time => {TYPE => 'decimal(5,2)',
+ estimated_time => {TYPE => 'decimal(7,2)',
NOTNULL => 1, DEFAULT => '0'},
- remaining_time => {TYPE => 'decimal(5,2)',
+ remaining_time => {TYPE => 'decimal(7,2)',
NOTNULL => 1, DEFAULT => '0'},
deadline => {TYPE => 'DATETIME'},
alias => {TYPE => 'varchar(20)'},
@@ -341,7 +352,7 @@ use constant ABSTRACT_SCHEMA => {
fieldid => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'fielddefs',
COLUMN => 'id'}},
- added => {TYPE => 'TINYTEXT'},
+ added => {TYPE => 'varchar(255)'},
removed => {TYPE => 'TINYTEXT'},
],
INDEXES => [
@@ -349,6 +360,7 @@ use constant ABSTRACT_SCHEMA => {
bugs_activity_who_idx => ['who'],
bugs_activity_bug_when_idx => ['bug_when'],
bugs_activity_fieldid_idx => ['fieldid'],
+ bugs_activity_added_idx => ['added'],
],
},
@@ -374,10 +386,15 @@ use constant ABSTRACT_SCHEMA => {
FIELDS => [
comment_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
- bug_id => {TYPE => 'INT3', NOTNULL => 1},
- who => {TYPE => 'INT3', NOTNULL => 1},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'}},
+ who => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid'}},
bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
- work_time => {TYPE => 'decimal(5,2)', NOTNULL => 1,
+ work_time => {TYPE => 'decimal(7,2)', NOTNULL => 1,
DEFAULT => '0'},
thetext => {TYPE => 'LONGTEXT', NOTNULL => 1},
isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1,
@@ -592,10 +609,12 @@ use constant ABSTRACT_SCHEMA => {
DEFAULT => '0'},
grant_group_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'groups',
- COLUMN => 'id'}},
+ COLUMN => 'id',
+ DELETE => 'SET NULL'}},
request_group_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'groups',
- COLUMN => 'id'}},
+ COLUMN => 'id',
+ DELETE => 'SET NULL'}},
],
},
@@ -666,7 +685,7 @@ use constant ABSTRACT_SCHEMA => {
DEFAULT => 'FALSE'},
buglist => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'FALSE'},
- visibility_field_id => {TYPE => 'INT3',
+ visibility_field_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'fielddefs',
COLUMN => 'id'}},
# CustIS Bug 53617 - visibility_value_id is removed from here
@@ -784,8 +803,14 @@ use constant ABSTRACT_SCHEMA => {
status_workflow => {
FIELDS => [
# On bug creation, there is no old value.
- old_status => {TYPE => 'INT2'},
- new_status => {TYPE => 'INT2', NOTNULL => 1},
+ old_status => {TYPE => 'INT2',
+ REFERENCES => {TABLE => 'bug_status',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'}},
+ new_status => {TYPE => 'INT2', NOTNULL => 1,
+ REFERENCES => {TABLE => 'bug_status',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'}},
require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0},
],
INDEXES => [
@@ -946,7 +971,7 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
DELETE => 'CASCADE'}},
- ipaddr => {TYPE => 'varchar(40)', NOTNULL => 1},
+ ipaddr => {TYPE => 'varchar(40)'},
lastused => {TYPE => 'DATETIME', NOTNULL => 1},
],
INDEXES => [
@@ -954,6 +979,25 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ login_failure => {
+ FIELDS => [
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ login_time => {TYPE => 'DATETIME', NOTNULL => 1},
+ ip_addr => {TYPE => 'varchar(40)', NOTNULL => 1},
+ ],
+ INDEXES => [
+ # We do lookups by every item in the table simultaneously, but
+ # having an index with all three items would be the same size as
+ # the table. So instead we have an index on just the smallest item,
+ # to speed lookups.
+ login_failure_user_id_idx => ['user_id'],
+ ],
+ },
+
+
# "tokens" stores the tokens users receive when a password or email
# change is requested. Tokens provide an extra measure of security
# for these changes.
@@ -1004,10 +1048,12 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
- entry => {TYPE => 'BOOLEAN', NOTNULL => 1},
+ entry => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 'FALSE'},
membercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
othercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
- canedit => {TYPE => 'BOOLEAN', NOTNULL => 1},
+ canedit => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 'FALSE'},
editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'FALSE'},
editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1,
@@ -1159,12 +1205,13 @@ use constant ABSTRACT_SCHEMA => {
PRIMARYKEY => 1},
name => {TYPE => 'varchar(64)', NOTNULL => 1},
classification_id => {TYPE => 'INT2', NOTNULL => 1,
- DEFAULT => '1'},
+ DEFAULT => '1',
+ REFERENCES => {TABLE => 'classifications',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'}},
description => {TYPE => 'MEDIUMTEXT'},
- milestoneurl => {TYPE => 'TINYTEXT', NOTNULL => 1,
- DEFAULT => "''"},
- disallownew => {TYPE => 'BOOLEAN', NOTNULL => 1,
- DEFAULT => 0},
+ isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 1},
votesperuser => {TYPE => 'INT2', NOTNULL => 1,
DEFAULT => 0},
maxvotesperbug => {TYPE => 'INT2', NOTNULL => 1,
@@ -1173,6 +1220,8 @@ use constant ABSTRACT_SCHEMA => {
DEFAULT => 0},
defaultmilestone => {TYPE => 'varchar(20)',
NOTNULL => 1, DEFAULT => "'---'"},
+ allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 'FALSE'},
],
INDEXES => [
products_name_idx => {FIELDS => ['name'],
@@ -1215,7 +1264,7 @@ use constant ABSTRACT_SCHEMA => {
creator => {TYPE => 'INT3',
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
- DELETE => 'SET NULL'}},
+ DELETE => 'CASCADE'}},
category => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'series_categories',
COLUMN => 'id',
@@ -1226,7 +1275,6 @@ use constant ABSTRACT_SCHEMA => {
DELETE => 'CASCADE'}},
name => {TYPE => 'varchar(64)', NOTNULL => 1},
frequency => {TYPE => 'INT2', NOTNULL => 1},
- last_viewed => {TYPE => 'DATETIME'},
query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
is_public => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'FALSE'},
@@ -1321,6 +1369,8 @@ use constant ABSTRACT_SCHEMA => {
DELETE => 'CASCADE'}},
subject => {TYPE => 'varchar(128)'},
body => {TYPE => 'MEDIUMTEXT'},
+ mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 'FALSE'},
],
},
@@ -1388,7 +1438,10 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
DELETE => 'CASCADE'}},
- setting_name => {TYPE => 'varchar(32)', NOTNULL => 1},
+ setting_name => {TYPE => 'varchar(32)', NOTNULL => 1,
+ REFERENCES => {TABLE => 'setting',
+ COLUMN => 'name',
+ DELETE => 'CASCADE'}},
setting_value => {TYPE => 'varchar(32)', NOTNULL => 1},
],
INDEXES => [
@@ -1443,13 +1496,13 @@ use constant ABSTRACT_SCHEMA => {
},
ts_note => {
- FIELDS => [
+ FIELDS => [
# This is a BIGINT in standard TheSchwartz schemas.
jobid => {TYPE => 'INT4', NOTNULL => 1},
notekey => {TYPE => 'varchar(255)'},
value => {TYPE => 'LONGBLOB'},
- ],
- INDEXES => [
+ ],
+ INDEXES => [
ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)],
TYPE => 'UNIQUE'},
],
@@ -1477,7 +1530,7 @@ use constant ABSTRACT_SCHEMA => {
status => {TYPE => 'INT2'},
completion_time => {TYPE => 'INT4'},
delete_after => {TYPE => 'INT4'},
- ],
+ ],
INDEXES => [
ts_exitstatus_funcid_idx => ['funcid'],
ts_exitstatus_delete_after_idx => ['delete_after'],
@@ -1507,7 +1560,6 @@ use constant MULTI_SELECT_VALUE_TABLE => {
],
};
-
#--------------------------------------------------------------------------
=head1 METHODS
@@ -1599,7 +1651,7 @@ sub _initialize {
if exists $abstract_schema->{$table};
}
unlock_keys(%$abstract_schema);
- Bugzilla::Hook::process('db_schema-abstract_schema',
+ Bugzilla::Hook::process('db_schema_abstract_schema',
{ schema => $abstract_schema });
unlock_hash(%$abstract_schema);
}
diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm
index 627716970..a68c7c90d 100644
--- a/Bugzilla/DB/Schema/Mysql.pm
+++ b/Bugzilla/DB/Schema/Mysql.pm
@@ -178,13 +178,35 @@ sub get_alter_column_ddl {
delete $new_def_copy{PRIMARYKEY};
}
- my $new_ddl = $self->get_type_ddl(\%new_def_copy);
my @statements;
push(@statements, "UPDATE $table SET $column = $set_nulls_to
WHERE $column IS NULL") if defined $set_nulls_to;
- push(@statements, "ALTER TABLE $table CHANGE COLUMN
+
+ # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling
+ # CHANGE COLUMN, so just do that if we're just changing the default.
+ my %old_defaultless = %$old_def;
+ my %new_defaultless = %$new_def;
+ delete $old_defaultless{DEFAULT};
+ delete $new_defaultless{DEFAULT};
+ if (!$self->columns_equal($old_def, $new_def)
+ && $self->columns_equal(\%new_defaultless, \%old_defaultless))
+ {
+ if (!defined $new_def->{DEFAULT}) {
+ push(@statements,
+ "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT");
+ }
+ else {
+ push(@statements, "ALTER TABLE $table ALTER COLUMN $column
+ SET DEFAULT " . $new_def->{DEFAULT});
+ }
+ }
+ else {
+ my $new_ddl = $self->get_type_ddl(\%new_def_copy);
+ push(@statements, "ALTER TABLE $table CHANGE COLUMN
$column $column $new_ddl");
+ }
+
if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
# Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY
push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
@@ -241,6 +263,11 @@ sub get_rename_indexes_ddl {
return ($sql);
}
+sub get_set_serial_sql {
+ my ($self, $table, $column, $value) = @_;
+ return ("ALTER TABLE $table AUTO_INCREMENT = $value");
+}
+
# Converts a DBI column_info output to an abstract column definition.
# Expects to only be called by Bugzila::DB::Mysql::_bz_build_schema_from_disk,
# although there's a chance that it will also work properly if called
diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm
index 8332be707..e8905eb80 100644
--- a/Bugzilla/DB/Schema/Oracle.pm
+++ b/Bugzilla/DB/Schema/Oracle.pm
@@ -145,6 +145,9 @@ sub get_fk_ddl {
my $to_column = $references->{COLUMN} || confess "No column in reference";
my $fk_name = $self->_get_fk_name($table, $column, $references);
+ # 'ON DELETE RESTRICT' is enabled by default
+ $delete = "" if ( defined $delete && $delete =~ /RESTRICT/i);
+
my $fk_string = "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n"
. " REFERENCES $to_table($to_column)\n";
@@ -400,4 +403,29 @@ sub _get_create_seq_ddl {
return @ddl;
}
+sub get_set_serial_sql {
+ my ($self, $table, $column, $value) = @_;
+ my @sql;
+ my $seq_name = "${table}_${column}_SEQ";
+ push(@sql, "DROP SEQUENCE ${seq_name}");
+ push(@sql, $self->_get_create_seq_ddl($table, $column, $value));
+ return @sql;
+}
+
+sub get_drop_column_ddl {
+ my $self = shift;
+ my ($table, $column) = @_;
+ my @sql;
+ push(@sql, $self->SUPER::get_drop_column_ddl(@_));
+ my $dbh=Bugzilla->dbh;
+ my $trigger_name = uc($table . "_" . $column);
+ my $exist_trigger = $dbh->selectcol_arrayref(
+ "SELECT OBJECT_NAME FROM USER_OBJECTS
+ WHERE OBJECT_NAME = ?", undef, $trigger_name);
+ if(@$exist_trigger) {
+ push(@sql, "DROP TRIGGER $trigger_name");
+ }
+ return @sql;
+}
+
1;
diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm
index 070c0b03e..3559bae9c 100644
--- a/Bugzilla/DB/Schema/Pg.pm
+++ b/Bugzilla/DB/Schema/Pg.pm
@@ -119,6 +119,12 @@ sub get_rename_table_sql {
return ("ALTER TABLE $old_name RENAME TO $new_name");
}
+sub get_set_serial_sql {
+ my ($self, $table, $column, $value) = @_;
+ return ("SELECT setval('${table}_${column}_seq', $value, false)
+ FROM $table");
+}
+
sub _get_alter_type_sql {
my ($self, $table, $column, $new_def, $old_def) = @_;
my @statements;
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index ff812b287..e3d8f767b 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -66,7 +66,7 @@ sub _error_message
my $mesg = '';
$mesg .= "[$$] " . time2str("%D %H:%M:%S ", time());
$mesg .= uc($type)." $error ";
- $mesg .= "$ENV{REMOTE_ADDR}" if $ENV{REMOTE_ADDR};
+ $mesg .= remote_ip();
if (Bugzilla->user)
{
$mesg .= ' ' . Bugzilla->user->login;
@@ -161,19 +161,37 @@ sub _throw_error
print Bugzilla->cgi->header();
print $message;
}
- elsif ($mode == ERROR_MODE_DIE_SOAP_FAULT)
+ elsif ($mode == ERROR_MODE_DIE_SOAP_FAULT || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
{
# Clone the hash so we aren't modifying the constant.
my %error_map = %{ WS_ERROR_CODE() };
require Bugzilla::Hook;
- Bugzilla::Hook::process('webservice-error_codes',
+ Bugzilla::Hook::process('webservice_error_codes',
{ error_map => \%error_map });
my $code = $error_map{$error};
if (!$code) {
$code = ERROR_UNKNOWN_FATAL if $type eq 'code';
$code = ERROR_UNKNOWN_TRANSIENT if $type eq 'user';
}
- die bless { message => SOAP::Fault->faultcode($code)->faultstring($message) };
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ die bless { message => SOAP::Fault->faultcode($code)->faultstring($message) };
+ }
+ else {
+ my $server = Bugzilla->_json_server;
+ # 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);
+ # 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
+ # it checks $@, so it returns the proper error.
+ die if _in_eval();
+ $server->response($server->error_response_header);
+ }
}
elsif ($mode == ERROR_MODE_AJAX)
{
diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm
new file mode 100644
index 000000000..fd8eca33d
--- /dev/null
+++ b/Bugzilla/Extension.pm
@@ -0,0 +1,222 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# The contents of this file are subject to the Mozilla Public
+# License Version 1.1 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of
+# the License at http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS
+# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# rights and limitations under the License.
+#
+# The Original Code is the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developers are Copyright (C) 2009 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander
+
+package Bugzilla::Extension;
+
+use strict;
+
+# Don't use any more Bugzilla modules here as Bugzilla::Extension
+# could be used outside of normal running Bugzilla installation
+# (i.e. in checksetup.pl)
+
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::Hook;
+
+use Cwd qw(abs_path);
+use File::Basename;
+use File::Spec::Functions;
+
+use base 'Exporter';
+our @EXPORT = qw(extension_info required_modules optional_modules extension_version extension_include extension_template_dir extension_code_dir set_hook);
+
+my $extensions = {
+# name => {
+# required_modules => [],
+# optional_modules => [],
+# version => '',
+# loaded => boolean,
+# inc => [ 'path1', 'path2' ],
+# }
+};
+
+# List all available extension names
+sub available
+{
+ my $dir = bz_locations()->{extensionsdir};
+ my @extension_items = glob(catfile($dir, '*'));
+ my @r;
+ foreach my $item (@extension_items)
+ {
+ my $basename = basename($item);
+ # Skip CVS directories and any hidden files/dirs.
+ next if $basename eq 'CVS' or $basename =~ /^\./;
+ if (-d $item)
+ {
+ if (!-e catfile($item, "disabled"))
+ {
+ trick_taint($basename);
+ push @r, $basename;
+ }
+ }
+ }
+ return @r;
+}
+
+# List all loaded extensions
+sub loaded
+{
+ return grep { $extensions->{$_}->{loaded} } keys %$extensions;
+}
+
+# Get extensions information hashref
+sub extension_info
+{
+ shift if $_[0] eq __PACKAGE__ || ref $_[0];
+ my ($name) = @_;
+ return $extensions->{$name};
+}
+
+# Getters/setters for REQUIRED_MODULES, OPTIONAL_MODULES and version
+sub required_modules { setter('required_modules', @_) }
+sub optional_modules { setter('optional_modules', @_) }
+sub extension_version { setter('version', @_) }
+
+# Getter/setter for extension code directory (for old extension system)
+sub extension_code_dir
+{
+ my ($name, $new) = @_;
+ my $old = setter('code_dir', $name, $new);
+ return $old || catfile(bz_locations()->{extensionsdir}, $name, 'code');
+}
+
+# Getter/setter for extension template directory
+sub extension_template_dir
+{
+ my ($name, $new) = @_;
+ my $old = setter('template_dir', $name, $new);
+ return $old || catfile(bz_locations()->{extensionsdir}, $name, 'template');
+}
+
+# Getter/setter for extension include path (@INC)
+sub extension_include
+{
+ my ($name, $new) = @_;
+ if ($new)
+ {
+ if (ref $new && $new !~ /ARRAY/)
+ {
+ die __PACKAGE__."::extension_include('$name', '$new'): second argument should be an arrayref";
+ }
+ $new = [ $new ] if !ref $new;
+ $new = [ map { abs_path($_) } @$new ];
+ trick_taint($_) for @$new;
+ }
+ my $old = setter('inc', $name, $new);
+ # update @INC
+ my $oh = { map { $_ => 1 } @$old };
+ for (my $i = $#INC; $i >= 0; $i--)
+ {
+ splice @INC, $i, 1 if $oh->{$INC[$i]};
+ }
+ unshift @INC, @$new if $new;
+ return $old;
+}
+
+# Generic getter/setter
+sub setter
+{
+ my ($key, $name, $value) = @_;
+ $extensions->{$name} ||= {};
+ my $old = $extensions->{$name}->{$key};
+ $extensions->{$name}->{$key} = $value if defined $value;
+ return $old;
+}
+
+# Load all available extensions
+sub load_all
+{
+ shift if $_[0] && ($_[0] eq __PACKAGE__ || ref $_[0]);
+ foreach (available())
+ {
+ load($_);
+ }
+}
+
+# Load one extension
+sub load
+{
+ my ($name) = @_;
+ if ($extensions->{$name} && $extensions->{$name}->{loaded})
+ {
+ # Extension is already loaded
+ return;
+ }
+
+ my $dir = bz_locations()->{extensionsdir};
+ # Add default include path
+ extension_include($name, catfile($dir, $name, 'lib'));
+
+ # Load main extension file
+ my $file = catfile($dir, $name, "$name.pl");
+ if (-e $file)
+ {
+ trick_taint($file);
+ require $file;
+ }
+
+ # Support for old extension system
+ my $code_dir = extension_code_dir($name);
+ if (-d $code_dir)
+ {
+ my @hooks = glob(catfile($code_dir, '*.pl'));
+ my ($hook, $hook_sub);
+ foreach my $filename (@hooks)
+ {
+ trick_taint($filename);
+ $hook = basename($filename);
+ $hook =~ s/\.pl$//so;
+ if (!-r $filename)
+ {
+ warn __PACKAGE__."::load(): can't read $filename, skipping";
+ next;
+ }
+ set_hook($name, $hook, { type => 'file', filename => $filename });
+ }
+ }
+
+ $extensions->{$name}->{loaded} = 1;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension - Base class for Bugzilla Extensions.
+
+=head1 BUGZILLA::EXTENSION CLASS METHODS
+
+These are used internally by Bugzilla to load and set up extensions.
+If you are an extension author, you don't need to care about these.
+
+=head2 C
+
+Takes two arguments, the path to F and the path to F,
+for an extension. Loads the extension's code packages into memory using
+C, does some sanity-checking on the extension, and returns the
+package name of the loaded extension.
+
+=head2 C
+
+Calls L for every enabled extension installed into Bugzilla,
+and returns an arrayref of all the package names that were loaded.
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index 27c210c8f..4b3e51c4e 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -15,6 +15,7 @@
# Contributor(s): Dan Mosedale
# Frédéric Buclin
# Myk Melez
+# Greg Hendricks
=head1 NAME
@@ -106,14 +107,14 @@ use constant DB_COLUMNS => qw(
use constant REQUIRED_CREATE_FIELDS => qw(name description);
use constant VALIDATORS => {
- custom => \&_check_custom,
- description => \&_check_description,
- enter_bug => \&_check_enter_bug,
- buglist => \&Bugzilla::Object::check_boolean,
- mailhead => \&_check_mailhead,
- obsolete => \&_check_obsolete,
- sortkey => \&_check_sortkey,
- type => \&_check_type,
+ custom => \&_check_custom,
+ description => \&_check_description,
+ enter_bug => \&_check_enter_bug,
+ buglist => \&Bugzilla::Object::check_boolean,
+ mailhead => \&_check_mailhead,
+ obsolete => \&_check_obsolete,
+ sortkey => \&_check_sortkey,
+ type => \&_check_type,
visibility_field_id => \&_check_visibility_field_id,
};
@@ -217,7 +218,7 @@ use constant DEFAULT_FIELDS => (
{name => 'deadline', desc => 'Deadline',
in_new_bugmail => 1, buglist => 1},
{name => 'commenter', desc => 'Commenter'},
- {name => 'flagtypes.name', desc => 'Flag'},
+ {name => 'flagtypes.name', desc => 'Flags', buglist => 1},
{name => 'requestees.login_name', desc => 'Flag Requestee'},
{name => 'setters.login_name', desc => 'Flag Setter'},
{name => 'work_time', desc => 'Hours Worked', buglist => 1},
@@ -469,9 +470,9 @@ objects.
=cut
-sub is_select {
- return ($_[0]->type == FIELD_TYPE_SINGLE_SELECT
- || $_[0]->type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0
+sub is_select {
+ return ($_[0]->type == FIELD_TYPE_SINGLE_SELECT
+ || $_[0]->type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0
}
sub legal_values {
@@ -511,7 +512,7 @@ Returns undef if there is no field that controls this field's visibility.
sub visibility_field {
my $self = shift;
if ($self->{visibility_field_id}) {
- $self->{visibility_field} ||=
+ $self->{visibility_field} ||=
$self->new($self->{visibility_field_id});
}
return $self->{visibility_field};
@@ -572,7 +573,7 @@ field controls the visibility of.
sub controls_visibility_of {
my $self = shift;
- $self->{controls_visibility_of} ||=
+ $self->{controls_visibility_of} ||=
Bugzilla::Field->match({ visibility_field_id => $self->id });
return $self->{controls_visibility_of};
}
@@ -731,16 +732,14 @@ sub remove_from_db {
$bugs_query = "SELECT COUNT(*) FROM bug_$name";
}
else {
- $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL
- AND $name != ''";
+ $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
+ if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) {
+ $bugs_query .= " AND $name != ''";
+ }
# Ignore the default single select value
if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
$bugs_query .= " AND $name != '---'";
}
- # Ignore blank dates.
- if ($self->type == FIELD_TYPE_DATETIME) {
- $bugs_query .= " AND $name != '00-00-00 00:00:00'";
- }
}
my $has_bugs = $dbh->selectrow_array($bugs_query);
@@ -845,6 +844,11 @@ sub run_create_validators {
}
my $type = $params->{type} || 0;
+
+ if ($params->{custom} && !$type) {
+ ThrowCodeError('field_type_not_specified');
+ }
+
$params->{value_field_id} =
$class->_check_value_field_id($params->{value_field_id},
($type == FIELD_TYPE_SINGLE_SELECT
@@ -1032,8 +1036,14 @@ sub check_field {
my $dbh = Bugzilla->dbh;
# If $legalsRef is undefined, we use the default valid values.
+ # Valid values for this check are all possible values.
+ # Using get_legal_values would only return active values, but since
+ # some bugs may have inactive values set, we want to check them too.
unless (defined $legalsRef) {
- $legalsRef = get_legal_field_values($name);
+ $legalsRef = Bugzilla::Field->new({name => $name})->legal_values;
+ my @values = map($_->name, @$legalsRef);
+ $legalsRef = \@values;
+
}
if (!defined($value)
diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm
index 599d42f5f..89c0de874 100644
--- a/Bugzilla/Field/Choice.pm
+++ b/Bugzilla/Field/Choice.pm
@@ -17,6 +17,7 @@
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Max Kanat-Alexander
+# Greg Hendricks
# Vitaliy Filippov
use strict;
@@ -45,11 +46,13 @@ use constant DB_COLUMNS => qw(
id
value
sortkey
+ isactive
);
use constant UPDATE_COLUMNS => qw(
value
sortkey
+ isactive
);
use constant NAME_FIELD => 'value';
@@ -60,6 +63,7 @@ use constant REQUIRED_CREATE_FIELDS => qw(value);
use constant VALIDATORS => {
value => \&_check_value,
sortkey => \&_check_sortkey,
+ isactive => \&Bugzilla::Object::check_boolean,
};
use constant CLASS_MAP => {
@@ -214,7 +218,8 @@ sub _check_if_controller {
# Accessors #
#############
-sub sortkey { return $_[0]->{'sortkey'}; }
+sub is_active { return $_[0]->{'isactive'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
sub bug_count {
my $self = shift;
@@ -303,7 +308,7 @@ sub controlled_values
{
my $type = Bugzilla::Field::Choice->type($field);
$f = $type->match({ id => $f });
- }
+ }
$controlled_values->{$field->name} = $f;
}
$self->{controlled_values} = $controlled_values;
@@ -317,7 +322,7 @@ sub controlled_plus_generic
my $controlled_values;
unless ($controlled_values = $self->{controlled_plus_generic})
{
- my $fields = $self->field->controls_values_of;
+ my $fields = $self->field->controls_values_of;
foreach my $field (@$fields)
{
my $f = Bugzilla->dbh->selectcol_arrayref(
@@ -368,8 +373,9 @@ sub has_visibility_value
# Mutators #
############
-sub set_name { $_[0]->set('value', $_[1]); }
-sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
+sub set_is_active { $_[0]->set('isactive', $_[1]); }
+sub set_name { $_[0]->set('value', $_[1]); }
+sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub set_visibility_values
{
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index ab1599805..8e213b0df 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -53,6 +53,9 @@ whose names start with _ or a re specifically noted as being private.
=cut
+use Scalar::Util qw(blessed);
+use Storable qw(dclone);
+
use Bugzilla::FlagType;
use Bugzilla::Hook;
use Bugzilla::User;
@@ -69,22 +72,45 @@ use base qw(Bugzilla::Object Exporter);
#### Initialization ####
###############################
-use constant DB_COLUMNS => qw(
- flags.id
- flags.type_id
- flags.bug_id
- flags.attach_id
- flags.requestee_id
- flags.setter_id
- flags.status
- flags.creation_date
-);
-
use constant DB_TABLE => 'flags';
use constant LIST_ORDER => 'id';
use constant SKIP_REQUESTEE_ON_ERROR => 1;
+use constant DB_COLUMNS => qw(
+ id
+ type_id
+ bug_id
+ attach_id
+ requestee_id
+ setter_id
+ status
+ creation_date
+);
+
+use constant REQUIRED_CREATE_FIELDS => qw(
+ attach_id
+ bug_id
+ setter_id
+ status
+ type_id
+);
+
+use constant UPDATE_COLUMNS => qw(
+ requestee_id
+ setter_id
+ status
+ type_id
+);
+
+use constant VALIDATORS => {
+};
+
+use constant UPDATE_VALIDATORS => {
+ setter => \&_check_setter,
+ status => \&_check_status,
+};
+
###############################
#### Accessors ######
###############################
@@ -117,11 +143,14 @@ Returns the status '+', '-', '?' of the flag.
=cut
-sub id { return $_[0]->{'id'}; }
-sub name { return $_[0]->type->name; }
-sub bug_id { return $_[0]->{'bug_id'}; }
-sub attach_id { return $_[0]->{'attach_id'}; }
-sub status { return $_[0]->{'status'}; }
+sub id { return $_[0]->{'id'}; }
+sub name { return $_[0]->type->name; }
+sub type_id { return $_[0]->{'type_id'}; }
+sub bug_id { return $_[0]->{'bug_id'}; }
+sub attach_id { return $_[0]->{'attach_id'}; }
+sub status { return $_[0]->{'status'}; }
+sub setter_id { return $_[0]->{'setter_id'}; }
+sub requestee_id { return $_[0]->{'requestee_id'}; }
###############################
#### Methods ####
@@ -185,6 +214,14 @@ sub attachment {
return $self->{'attachment'};
}
+sub bug {
+ my $self = shift;
+
+ require Bugzilla::Bug;
+ $self->{'bug'} ||= new Bugzilla::Bug($self->bug_id);
+ return $self->{'bug'};
+}
+
################################
## Searching/Retrieving Flags ##
################################
@@ -269,260 +306,171 @@ sub count {
# Creating and Modifying
######################################################################
+sub set_flag {
+ my ($class, $obj, $params) = @_;
+
+ my ($bug, $attachment);
+ if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
+ $attachment = $obj;
+ $bug = $attachment->bug;
+ }
+ elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
+ $bug = $obj;
+ }
+ else {
+ ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj });
+ }
+
+ # Update (or delete) an existing flag.
+ if ($params->{id}) {
+ my $flag = $class->check({ id => $params->{id} });
+
+ # Security check: make sure the flag belongs to the bug/attachment.
+ # We don't check that the user editing the flag can see
+ # the bug/attachment. That's the job of the caller.
+ ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id)
+ || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id)
+ || ThrowCodeError('invalid_flag_association',
+ { bug_id => $bug->id,
+ attach_id => $attachment ? $attachment->id : undef });
+
+ # Extract the current flag object from the object.
+ my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
+ # If no flagtype can be found for this flag, this means the bug is being
+ # moved into a product/component where the flag is no longer valid.
+ # So either we can attach the flag to another flagtype having the same
+ # name, or we remove the flag.
+ if (!$obj_flagtype) {
+ my $success = $flag->retarget($obj);
+ return unless $success;
+
+ ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
+ push(@{$obj_flagtype->{flags}}, $flag);
+ }
+ my ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
+ # If the flag has the correct type but cannot be found above, this means
+ # the flag is going to be removed (e.g. because this is a pending request
+ # and the attachment is being marked as obsolete).
+ return unless $obj_flag;
+
+ $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
+ }
+ # Create a new flag.
+ elsif ($params->{type_id}) {
+ # Don't bother validating types the user didn't touch.
+ return if $params->{status} eq 'X';
+
+ my $flagtype = Bugzilla::FlagType->check({ id => $params->{type_id} });
+ # Security check: make sure the flag type belongs to the bug/attachment.
+ ($attachment && $flagtype->target_type eq 'attachment'
+ && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types}))
+ || (!$attachment && $flagtype->target_type eq 'bug'
+ && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types}))
+ || ThrowCodeError('invalid_flag_association',
+ { bug_id => $bug->id,
+ attach_id => $attachment ? $attachment->id : undef });
+
+ # Make sure the flag type is active.
+ $flagtype->is_active
+ || ThrowCodeError('flag_type_inactive', { type => $flagtype->name });
+
+ # Extract the current flagtype object from the object.
+ my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types};
+
+ # We cannot create a new flag if there is already one and this
+ # flag type is not multiplicable.
+ if (!$flagtype->is_multiplicable) {
+ if (scalar @{$obj_flagtype->{flags}}) {
+ ThrowUserError('flag_type_not_multiplicable', { type => $flagtype });
+ }
+ }
+
+ $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
+ }
+ else {
+ ThrowCodeError('param_required', { function => $class . '->set_flag',
+ param => 'id/type_id' });
+ }
+}
+
+sub _validate {
+ my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_;
+
+ # If it's a new flag, let's create it now.
+ my $obj_flag = $flag || bless({ type_id => $flag_type->id,
+ status => '',
+ bug_id => $bug->id,
+ attach_id => $attachment ?
+ $attachment->id : undef},
+ $class);
+
+ my $old_status = $obj_flag->status;
+ my $old_requestee_id = $obj_flag->requestee_id;
+
+ $obj_flag->_set_status($params->{status});
+ $obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe});
+
+ # The setter field MUST NOT be updated if neither the status
+ # nor the requestee fields changed.
+ if (($obj_flag->status ne $old_status)
+ # The requestee ID can be undefined.
+ || (($obj_flag->requestee_id || 0) != ($old_requestee_id || 0)))
+ {
+ $obj_flag->_set_setter($params->{setter});
+ }
+
+ # If the flag is deleted, remove it from the list.
+ if ($obj_flag->status eq 'X') {
+ @{$flag_type->{flags}} = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}};
+ }
+ # Add the newly created flag to the list.
+ elsif (!$obj_flag->id) {
+ push(@{$flag_type->{flags}}, $obj_flag);
+ }
+}
+
=pod
=over
-=item C
+=item C
-Validates fields containing flag modifications.
-
-If the attachment is new, it has no ID yet and $attach_id is set
-to -1 to force its check anyway.
+Creates a flag record in the database.
=back
=cut
-sub validate {
- my ($bug_id, $attach_id, $skip_requestee_on_error) = @_;
- my $cgi = Bugzilla->cgi;
- my $dbh = Bugzilla->dbh;
+sub create {
+ my ($class, $flag, $timestamp) = @_;
+ $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
- # Get a list of flags to validate. Uses the "map" function
- # to extract flag IDs from form field names by matching fields
- # whose name looks like "flag_type-nnn" (new flags) or "flag-nnn"
- # (existing flags), where "nnn" is the ID, and returning just
- # the ID portion of matching field names.
- my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
- my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
+ my $params = {};
+ my @columns = grep { $_ ne 'id' } $class->DB_COLUMNS;
+ $params->{$_} = $flag->{$_} foreach @columns;
- return unless (scalar(@flagtype_ids) || scalar(@flag_ids));
-
- # No flag reference should exist when changing several bugs at once.
- ThrowCodeError("flags_not_available", { type => 'b' }) unless $bug_id;
-
- # We don't check that these new flags are valid for this bug/attachment,
- # because the bug may be moved into another product meanwhile.
- # This check will be done later when creating new flags, see FormToNewFlags().
-
- if (scalar(@flag_ids)) {
- # No reference to existing flags should exist when creating a new
- # attachment.
- if ($attach_id && ($attach_id < 0)) {
- ThrowCodeError('flags_not_available', { type => 'a' });
- }
-
- # Make sure all existing flags belong to the bug/attachment
- # they pretend to be.
- my $field = ($attach_id) ? "attach_id" : "bug_id";
- my $field_id = $attach_id || $bug_id;
- my $not = ($attach_id) ? "" : "NOT";
-
- my $invalid_data =
- $dbh->selectrow_array(
- "SELECT 1 FROM flags
- WHERE "
- . $dbh->sql_in('id', \@flag_ids)
- . " AND ($field != ? OR attach_id IS $not NULL) "
- . $dbh->sql_limit(1), undef, $field_id);
-
- if ($invalid_data) {
- ThrowCodeError('invalid_flag_association',
- { bug_id => $bug_id,
- attach_id => $attach_id });
- }
- }
-
- # Validate new flags.
- foreach my $id (@flagtype_ids) {
- my $status = $cgi->param("flag_type-$id");
- my @requestees = $cgi->param("requestee_type-$id");
- my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
-
- # Don't bother validating types the user didn't touch.
- next if $status eq 'X';
-
- # Make sure the flag type exists. If it doesn't, FormToNewFlags()
- # will ignore it, so it's safe to ignore it here.
- my $flag_type = new Bugzilla::FlagType($id);
- next unless $flag_type;
-
- # Make sure the flag type is active.
- unless ($flag_type->is_active) {
- ThrowCodeError('flag_type_inactive', {'type' => $flag_type->name});
- }
-
- _validate(undef, $flag_type, $status, undef, \@requestees, $private_attachment,
- $bug_id, $attach_id, $skip_requestee_on_error);
- }
-
- # Validate existing flags.
- foreach my $id (@flag_ids) {
- my $status = $cgi->param("flag-$id");
- my @requestees = $cgi->param("requestee-$id");
- my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
-
- # Make sure the flag exists. If it doesn't, process() will ignore it,
- # so it's safe to ignore it here.
- my $flag = new Bugzilla::Flag($id);
- next unless $flag;
-
- _validate($flag, $flag->type, $status, undef, \@requestees, $private_attachment,
- undef, undef, $skip_requestee_on_error);
- }
+ $params->{creation_date} = $params->{modification_date} = $timestamp;
+ $flag = $class->SUPER::create($params);
+ return $flag;
}
-sub _validate {
- my ($flag, $flag_type, $status, $setter, $requestees, $private_attachment,
- $bug_id, $attach_id, $skip_requestee_on_error) = @_;
+sub update {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $timestamp = shift || $dbh->selectrow_array('SELECT NOW()');
- # By default, the flag setter (or requester) is the current user.
- $setter ||= Bugzilla->user;
+ my $changes = $self->SUPER::update(@_);
- my $id = $flag ? $flag->id : $flag_type->id; # Used in the error messages below.
- $bug_id ||= $flag->bug_id;
- $attach_id ||= $flag->attach_id if $flag; # Maybe it's a bug flag.
-
- # Make sure the user chose a valid status.
- grep($status eq $_, qw(X + - ?))
- || ThrowCodeError('flag_status_invalid',
- { id => $id, status => $status });
-
- # Make sure the user didn't request the flag unless it's requestable.
- # If the flag existed and was requested before it became unrequestable,
- # leave it as is.
- if ($status eq '?'
- && (!$flag || $flag->status ne '?')
- && !$flag_type->is_requestable)
- {
- ThrowCodeError('flag_status_invalid',
- { id => $id, status => $status });
+ if (scalar(keys %$changes)) {
+ $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
+ undef, ($timestamp, $self->id));
}
-
- # Make sure the user didn't specify a requestee unless the flag
- # is specifically requestable. For existing flags, if the requestee
- # was set before the flag became specifically unrequestable, don't
- # let the user change the requestee, but let the user remove it by
- # entering an empty string for the requestee.
- if ($status eq '?' && !$flag_type->is_requesteeble) {
- my $old_requestee = ($flag && $flag->requestee) ?
- $flag->requestee->login : '';
- my $new_requestee = join('', @$requestees);
- if ($new_requestee && $new_requestee ne $old_requestee) {
- ThrowCodeError('flag_requestee_disabled',
- { type => $flag_type });
- }
- }
-
- # Make sure the user didn't enter multiple requestees for a flag
- # that can't be requested from more than one person at a time.
- if ($status eq '?'
- && !$flag_type->is_multiplicable
- && scalar(@$requestees) > 1)
- {
- ThrowUserError('flag_not_multiplicable', { type => $flag_type });
- }
-
- # Make sure the requestees are authorized to access the bug
- # (and attachment, if this installation is using the "insider group"
- # feature and the attachment is marked private).
- if ($status eq '?' && $flag_type->is_requesteeble) {
- my $old_requestee = ($flag && $flag->requestee) ?
- $flag->requestee->login : '';
-
- my @legal_requestees;
- foreach my $login (@$requestees) {
- if ($login eq $old_requestee) {
- # This requestee was already set. Leave him alone.
- push(@legal_requestees, $login);
- next;
- }
-
- # We know the requestee exists because we ran
- # Bugzilla::User::match_field before getting here.
- my $requestee = new Bugzilla::User({ name => $login });
-
- # Throw an error if the user can't see the bug.
- # Note that if permissions on this bug are changed,
- # can_see_bug() will refer to old settings.
- if (!$requestee->can_see_bug($bug_id))
- {
- if (Bugzilla->params->{auto_add_flag_requestees_to_cc})
- {
- # Bug 55712 - Add flag requestees to CC list
- Bugzilla->cgi->param(-name => 'newcc', -value => [ Bugzilla->cgi->param('newcc'), $requestee->login ]);
- }
- else
- {
- next if $skip_requestee_on_error;
- ThrowUserError('flag_requestee_unauthorized',
- { flag_type => $flag_type,
- requestee => $requestee,
- bug_id => $bug_id,
- attach_id => $attach_id });
- }
- }
-
- # Throw an error if the target is a private attachment and
- # the requestee isn't in the group of insiders who can see it.
- if ($attach_id
- && $private_attachment
- && Bugzilla->params->{'insidergroup'}
- && !$requestee->in_group(Bugzilla->params->{'insidergroup'}))
- {
- next if $skip_requestee_on_error;
- ThrowUserError('flag_requestee_unauthorized_attachment',
- { flag_type => $flag_type,
- requestee => $requestee,
- bug_id => $bug_id,
- attach_id => $attach_id });
- }
-
- # Throw an error if the user won't be allowed to set the flag.
- if (!$requestee->can_set_flag($flag_type)) {
- next if $skip_requestee_on_error;
- ThrowUserError('flag_requestee_needs_privs',
- {'requestee' => $requestee,
- 'flagtype' => $flag_type});
- }
-
- # This requestee can be set.
- push(@legal_requestees, $login);
- }
-
- # Update the requestee list for this flag.
- if (scalar(@legal_requestees) < scalar(@$requestees)) {
- my $field_name = 'requestee_type-' . $flag_type->id;
- Bugzilla->cgi->delete($field_name);
- Bugzilla->cgi->param(-name => $field_name, -value => \@legal_requestees);
- }
- }
-
- # Make sure the user is authorized to modify flags, see bug 180879
- # - The flag exists and is unchanged.
- return if ($flag && ($status eq $flag->status));
-
- # - User in the request_group can clear pending requests and set flags
- # and can rerequest set flags.
- return if (($status eq 'X' || $status eq '?')
- && $setter->can_request_flag($flag_type));
-
- # - User in the grant_group can set/clear flags, including "+" and "-".
- return if $setter->can_set_flag($flag_type);
-
- # - Any other flag modification is denied
- ThrowUserError('flag_update_denied',
- { name => $flag_type->name,
- status => $status,
- old_status => $flag ? $flag->status : 'X' });
+ return $changes;
}
sub snapshot {
- my ($class, $bug_id, $attach_id) = @_;
+ my ($class, $flags) = @_;
- my $flags = $class->match({ 'bug_id' => $bug_id,
- 'attach_id' => $attach_id });
my @summaries;
foreach my $flag (@$flags) {
my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status;
@@ -532,108 +480,8 @@ sub snapshot {
return @summaries;
}
-
-=pod
-
-=over
-
-=item C
-
-Processes changes to flags.
-
-The bug and/or the attachment objects are the ones this flag is about,
-the timestamp is the date/time the bug was last touched (so that changes
-to the flag can be stamped with the same date/time).
-
-=back
-
-=cut
-
-sub process {
- my ($class, $bug, $attachment, $timestamp, $hr_vars) = @_;
- my $dbh = Bugzilla->dbh;
- my $cgi = Bugzilla->cgi;
-
- # Make sure the bug (and attachment, if given) exists and is accessible
- # to the current user. Moreover, if an attachment object is passed,
- # make sure it belongs to the given bug.
- return if ($bug->error || ($attachment && $bug->bug_id != $attachment->bug_id));
-
- my $bug_id = $bug->bug_id;
- my $attach_id = $attachment ? $attachment->id : undef;
-
- # Use the date/time we were given if possible (allowing calling code
- # to synchronize the comment's timestamp with those of other records).
- $timestamp ||= $dbh->selectrow_array('SELECT NOW()');
-
- # Take a snapshot of flags before any changes.
- my @old_summaries = $class->snapshot($bug_id, $attach_id);
-
- # Cancel pending requests if we are obsoleting an attachment.
- if ($attachment && $cgi->param('isobsolete')) {
- $class->CancelRequests($bug, $attachment);
- }
-
- # Create new flags and update existing flags.
- my $new_flags = FormToNewFlags($bug, $attachment, $cgi, $hr_vars);
- foreach my $flag (@$new_flags) { create($flag, $bug, $attachment, $timestamp) }
- modify($bug, $attachment, $cgi, $timestamp);
-
- # In case the bug's product/component has changed, clear flags that are
- # no longer valid.
- my $flag_ids = $dbh->selectcol_arrayref(
- "SELECT DISTINCT flags.id
- FROM flags
- INNER JOIN bugs
- ON flags.bug_id = bugs.bug_id
- LEFT JOIN flaginclusions AS i
- ON flags.type_id = i.type_id
- AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
- AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
- WHERE bugs.bug_id = ?
- AND i.type_id IS NULL",
- undef, $bug_id);
-
- my $flags = Bugzilla::Flag->new_from_list($flag_ids);
- foreach my $flag (@$flags) {
- my $is_retargetted = retarget($flag, $bug);
- unless ($is_retargetted) {
- clear($flag, $bug, $flag->attachment);
- $hr_vars->{'message'} = 'flag_cleared';
- }
- }
-
- $flag_ids = $dbh->selectcol_arrayref(
- "SELECT DISTINCT flags.id
- FROM flags, bugs, flagexclusions e
- WHERE bugs.bug_id = ?
- AND flags.bug_id = bugs.bug_id
- AND flags.type_id = e.type_id
- AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
- AND (bugs.component_id = e.component_id OR e.component_id IS NULL)",
- undef, $bug_id);
-
- $flags = Bugzilla::Flag->new_from_list($flag_ids);
- foreach my $flag (@$flags) {
- my $is_retargetted = retarget($flag, $bug);
- clear($flag, $bug, $flag->attachment) unless $is_retargetted;
- }
-
- # Take a snapshot of flags after changes.
- my @new_summaries = $class->snapshot($bug_id, $attach_id);
-
- update_activity($bug_id, $attach_id, $timestamp, \@old_summaries, \@new_summaries);
-
- Bugzilla::Hook::process('flag-end_of_update', { bug => $bug,
- timestamp => $timestamp,
- old_flags => \@old_summaries,
- new_flags => \@new_summaries,
- });
-}
-
sub update_activity {
- my ($bug_id, $attach_id, $timestamp, $old_summaries, $new_summaries) = @_;
- my $dbh = Bugzilla->dbh;
+ my ($class, $old_summaries, $new_summaries) = @_;
my ($removed, $added) = diff_arrays($old_summaries, $new_summaries);
if (scalar @$removed || scalar @$added) {
@@ -644,99 +492,351 @@ sub update_activity {
$added = join(", ", @$added);
trick_taint($removed);
trick_taint($added);
- my $field_id = get_field_id('flagtypes.name');
- $dbh->do('INSERT INTO bugs_activity
- (bug_id, attach_id, who, bug_when, fieldid, removed, added)
- VALUES (?, ?, ?, ?, ?, ?, ?)',
- undef, ($bug_id, $attach_id, Bugzilla->user->id,
- $timestamp, $field_id, $removed, $added));
-
- $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
- undef, ($timestamp, $bug_id));
+ return ($removed, $added);
}
+ return ();
}
+sub update_flags {
+ my ($class, $self, $old_self, $timestamp) = @_;
+
+ my @old_summaries = $class->snapshot($old_self->flags);
+ my %old_flags = map { $_->id => $_ } @{$old_self->flags};
+
+ foreach my $new_flag (@{$self->flags}) {
+ if (!$new_flag->id) {
+ # This is a new flag.
+ my $flag = $class->create($new_flag, $timestamp);
+ $new_flag->{id} = $flag->id;
+ $class->notify($new_flag, undef, $self);
+ }
+ else {
+ my $changes = $new_flag->update($timestamp);
+ if (scalar(keys %$changes)) {
+ $class->notify($new_flag, $old_flags{$new_flag->id}, $self);
+ }
+ delete $old_flags{$new_flag->id};
+ }
+ }
+ # These flags have been deleted.
+ foreach my $old_flag (values %old_flags) {
+ $class->notify(undef, $old_flag, $self);
+ $old_flag->remove_from_db();
+ }
+
+ # If the bug has been moved into another product or component,
+ # we must also take care of attachment flags which are no longer valid,
+ # as well as all bug flags which haven't been forgotten above.
+ if ($self->isa('Bugzilla::Bug')
+ && ($self->{_old_product_name} || $self->{_old_component_name}))
+ {
+ my @removed = $class->force_cleanup($self);
+ push(@old_summaries, @removed);
+ }
+
+ my @new_summaries = $class->snapshot($self->flags);
+ my @changes = $class->update_activity(\@old_summaries, \@new_summaries);
+
+ Bugzilla::Hook::process('flag_end_of_update', { object => $self,
+ timestamp => $timestamp,
+ old_flags => \@old_summaries,
+ new_flags => \@new_summaries,
+ });
+ return @changes;
+}
+
+sub retarget {
+ my ($self, $obj) = @_;
+
+ my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types};
+
+ my $success = 0;
+ foreach my $flagtype (@flagtypes) {
+ next if !$flagtype->is_active;
+ next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}});
+ next unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype))
+ || $self->setter->can_set_flag($flagtype));
+
+ $self->{type_id} = $flagtype->id;
+ delete $self->{type};
+ $success = 1;
+ last;
+ }
+ return $success;
+}
+
+# In case the bug's product/component has changed, clear flags that are
+# no longer valid.
+sub force_cleanup {
+ my ($class, $bug) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $flag_ids = $dbh->selectcol_arrayref(
+ 'SELECT DISTINCT flags.id
+ FROM flags
+ INNER JOIN bugs
+ ON flags.bug_id = bugs.bug_id
+ LEFT JOIN flaginclusions AS i
+ ON flags.type_id = i.type_id
+ AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
+ AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
+ WHERE bugs.bug_id = ? AND i.type_id IS NULL',
+ undef, $bug->id);
+
+ my @removed = $class->force_retarget($flag_ids, $bug);
+
+ $flag_ids = $dbh->selectcol_arrayref(
+ 'SELECT DISTINCT flags.id
+ FROM flags, bugs, flagexclusions e
+ WHERE bugs.bug_id = ?
+ AND flags.bug_id = bugs.bug_id
+ AND flags.type_id = e.type_id
+ AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
+ AND (bugs.component_id = e.component_id OR e.component_id IS NULL)',
+ undef, $bug->id);
+
+ push(@removed , $class->force_retarget($flag_ids, $bug));
+ return @removed;
+}
+
+sub force_retarget {
+ my ($class, $flag_ids, $bug) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $flags = $class->new_from_list($flag_ids);
+ my @removed;
+ foreach my $flag (@$flags) {
+ # $bug is undefined when e.g. editing inclusion and exclusion lists.
+ my $obj = $flag->attachment || $bug || $flag->bug;
+ my $is_retargetted = $flag->retarget($obj);
+ if ($is_retargetted) {
+ $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
+ undef, ($flag->type_id, $flag->id));
+ }
+ else {
+ # Track deleted attachment flags.
+ push(@removed, $class->snapshot([$flag])) if $flag->attach_id;
+ $class->notify(undef, $flag, $bug || $flag->bug);
+ $flag->remove_from_db();
+ }
+ }
+ return @removed;
+}
+
+###############################
+#### Validators ######
+###############################
+
+sub _set_requestee {
+ my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
+
+ # Used internally to check if the requestee is retargetting the request.
+ $self->{_old_requestee_id} = $self->requestee ? $self->requestee->id : 0;
+ $self->{requestee} =
+ $self->_check_requestee($requestee, $attachment, $skip_requestee_on_error);
+
+ $self->{requestee_id} =
+ $self->{requestee} ? $self->{requestee}->id : undef;
+}
+
+sub _set_setter {
+ my ($self, $setter) = @_;
+
+ $self->set('setter', $setter);
+ $self->{setter_id} = $self->setter->id;
+}
+
+sub _set_status {
+ my ($self, $status) = @_;
+
+ # Store the old flag status. It's needed by _check_setter().
+ $self->{_old_status} = $self->status;
+ $self->set('status', $status);
+}
+
+sub _check_requestee {
+ my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
+
+ # If the flag status is not "?", then no requestee can be defined.
+ return undef if ($self->status ne '?');
+
+ # Store this value before updating the flag object.
+ my $old_requestee = $self->requestee ? $self->requestee->login : '';
+
+ if ($self->status eq '?' && $requestee) {
+ $requestee = Bugzilla::User->check($requestee);
+ }
+ else {
+ undef $requestee;
+ }
+
+ if ($requestee && $requestee->login ne $old_requestee) {
+ # Make sure the user didn't specify a requestee unless the flag
+ # 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.
+ ThrowCodeError('flag_requestee_disabled', { type => $self->type })
+ if !$self->type->is_requesteeble;
+
+ # 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) {
+ undef $requestee;
+ }
+ else {
+ ThrowUserError('flag_requestee_unauthorized',
+ { flag_type => $self->type,
+ requestee => $requestee,
+ bug_id => $self->bug_id,
+ attach_id => $self->attach_id });
+ }
+ }
+ # Make sure the requestee can see the private attachment.
+ elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) {
+ if ($skip_requestee_on_error) {
+ undef $requestee;
+ }
+ else {
+ ThrowUserError('flag_requestee_unauthorized_attachment',
+ { flag_type => $self->type,
+ requestee => $requestee,
+ bug_id => $self->bug_id,
+ attach_id => $self->attach_id });
+ }
+ }
+ # Make sure the user is allowed to set the flag.
+ elsif (!$requestee->can_set_flag($self->type)) {
+ if ($skip_requestee_on_error) {
+ undef $requestee;
+ }
+ else {
+ ThrowUserError('flag_requestee_needs_privs',
+ {'requestee' => $requestee,
+ 'flagtype' => $self->type});
+ }
+ }
+ }
+ return $requestee;
+}
+
+sub _check_setter {
+ my ($self, $setter) = @_;
+
+ # By default, the currently logged in user is the setter.
+ $setter ||= Bugzilla->user;
+ (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id)
+ || ThrowCodeError('invalid_user');
+
+ # set_status() has already been called. So this refers
+ # to the new flag status.
+ my $status = $self->status;
+
+ # Make sure the user is authorized to modify flags, see bug 180879:
+ # - The flag exists and is unchanged.
+ # - The flag setter can unset flag.
+ # - Users in the request_group can clear pending requests and set flags
+ # and can rerequest set flags.
+ # - Users in the grant_group can set/clear flags, including "+" and "-".
+ unless (($status eq $self->{_old_status})
+ || ($status eq 'X' && $setter->id == Bugzilla->user->id)
+ || (($status eq 'X' || $status eq '?')
+ && $setter->can_request_flag($self->type))
+ || $setter->can_set_flag($self->type))
+ {
+ ThrowUserError('flag_update_denied',
+ { name => $self->type->name,
+ status => $status,
+ old_status => $self->{_old_status} });
+ }
+
+ # If the requester is retargetting the request, we don't
+ # update the setter, so that the setter gets the notification.
+ if ($status eq '?' && $self->{_old_requestee_id} == $setter->id) {
+ return $self->setter;
+ }
+ return $setter;
+}
+
+sub _check_status {
+ my ($self, $status) = @_;
+
+ # - Make sure the status is valid.
+ # - Make sure the user didn't request the flag unless it's requestable.
+ # If the flag existed and was requested before it became unrequestable,
+ # leave it as is.
+ if (!grep($status eq $_ , qw(X + - ?))
+ || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable))
+ {
+ ThrowCodeError('flag_status_invalid', { id => $self->id,
+ status => $status });
+ }
+ return $status;
+}
+
+######################################################################
+# Utility Functions
+######################################################################
+
=pod
=over
-=item C
+=item C
-Creates a flag record in the database.
+Checks whether or not there are new flags to create and returns an
+array of hashes. This array is then passed to Flag::create().
=back
=cut
-sub create {
- my ($flag, $bug, $attachment, $timestamp) = @_;
- my $dbh = Bugzilla->dbh;
+sub extract_flags_from_cgi {
+ my ($class, $bug, $attachment, $vars, $skip) = @_;
+ my $cgi = Bugzilla->cgi;
- my $attach_id = $attachment ? $attachment->id : undef;
- my $requestee_id;
- # Be careful! At this point, $flag is *NOT* yet an object!
- $requestee_id = $flag->{'requestee'}->id if $flag->{'requestee'};
+ my $match_status = Bugzilla::User::match_field({
+ '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
+ }, undef, $skip);
- $dbh->do('INSERT INTO flags (type_id, bug_id, attach_id, requestee_id,
- setter_id, status, creation_date, modification_date)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
- undef, ($flag->{'type'}->id, $bug->bug_id,
- $attach_id, $requestee_id, $flag->{'setter'}->id,
- $flag->{'status'}, $timestamp, $timestamp));
-
- # Now that the new flag has been added to the DB, create a real flag object.
- # This is required to call notify() correctly.
- my $flag_id = $dbh->bz_last_key('flags', 'id');
- $flag = new Bugzilla::Flag($flag_id);
-
- # Send an email notifying the relevant parties about the flag creation.
- if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
- $flag->{'addressee'} = $flag->requestee;
+ $vars->{'match_field'} = 'requestee';
+ if ($match_status == USER_MATCH_FAILED) {
+ $vars->{'message'} = 'user_match_failed';
+ }
+ elsif ($match_status == USER_MATCH_MULTIPLE) {
+ $vars->{'message'} = 'user_match_multiple';
}
- notify($flag, $bug, $attachment);
+ # Extract a list of flag type IDs from field names.
+ my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
+ @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids);
- # Return the new flag object.
- return $flag;
-}
+ # Extract a list of existing flag IDs.
+ my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
-=pod
+ return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
-=over
-
-=item C
-
-Modifies flags in the database when a user changes them.
-
-=back
-
-=cut
-
-sub modify {
- my ($bug, $attachment, $cgi, $timestamp) = @_;
- my $setter = Bugzilla->user;
- my $dbh = Bugzilla->dbh;
-
- # Extract a list of flags from the form data.
- my @ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
-
- # Loop over flags and update their record in the database if necessary.
- # Two kinds of changes can happen to a flag: it can be set to a different
- # state, and someone else can be asked to set it. We take care of both
- # those changes.
- my @flags;
- foreach my $id (@ids) {
- my $flag = new Bugzilla::Flag($id);
+ my (@new_flags, @flags);
+ foreach my $flag_id (@flag_ids) {
+ my $flag = $class->new($flag_id);
# If the flag no longer exists, ignore it.
next unless $flag;
- my $status = $cgi->param("flag-$id");
+ my $status = $cgi->param("flag-$flag_id");
# If the user entered more than one name into the requestee field
# (i.e. they want more than one person to set the flag) we can reuse
# the existing flag for the first person (who may well be the existing
- # requestee), but we have to create new flags for each additional.
- my @requestees = $cgi->param("requestee-$id");
+ # requestee), but we have to create new flags for each additional requestee.
+ my @requestees = $cgi->param("requestee-$flag_id");
my $requestee_email;
if ($status eq "?"
&& scalar(@requestees) > 1
@@ -747,282 +847,39 @@ sub modify {
# Create new flags like the existing one for each additional person.
foreach my $login (@requestees) {
- create({ type => $flag->type,
- setter => $setter,
- status => "?",
- requestee => new Bugzilla::User({ name => $login }) },
- $bug, $attachment, $timestamp);
+ push(@new_flags, { type_id => $flag->type_id,
+ status => "?",
+ requestee => $login,
+ skip_roe => $skip });
}
}
- else {
- $requestee_email = trim($cgi->param("requestee-$id") || '');
+ elsif ($status eq "?" && scalar(@requestees)) {
+ # If there are several requestees and the flag type is not multiplicable,
+ # this will fail. But that's the job of the validator to complain. All
+ # we do here is to extract and convert data from the CGI.
+ $requestee_email = trim($cgi->param("requestee-$flag_id") || '');
}
- # Ignore flags the user didn't change. There are two components here:
- # either the status changes (trivial) or the requestee changes.
- # Change of either field will cause full update of the flag.
-
- my $status_changed = ($status ne $flag->status);
-
- # Requestee is considered changed, if all of the following apply:
- # 1. Flag status is '?' (requested)
- # 2. Flag can have a requestee
- # 3. The requestee specified on the form is different from the
- # requestee specified in the db.
-
- my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
-
- my $requestee_changed =
- ($status eq "?" &&
- $flag->type->is_requesteeble &&
- $old_requestee ne $requestee_email);
-
- next unless ($status_changed || $requestee_changed);
-
- # Since the status is validated, we know it's safe, but it's still
- # tainted, so we have to detaint it before using it in a query.
- trick_taint($status);
-
- if ($status eq '+' || $status eq '-') {
- $dbh->do('UPDATE flags
- SET setter_id = ?, requestee_id = NULL,
- status = ?, modification_date = ?
- WHERE id = ?',
- undef, ($setter->id, $status, $timestamp, $flag->id));
-
- # If the status of the flag was "?", we have to notify
- # the requester (if he wants to).
- my $requester;
- if ($flag->status eq '?') {
- $requester = $flag->setter;
- $flag->{'requester'} = $requester;
- }
- # Now update the flag object with its new values.
- $flag->{'setter'} = $setter;
- $flag->{'requestee'} = undef;
- $flag->{'requestee_id'} = undef;
- $flag->{'status'} = $status;
-
- # Send an email notifying the relevant parties about the fulfillment,
- # including the requester.
- if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
- $flag->{'addressee'} = $requester;
- }
-
- notify($flag, $bug, $attachment);
- }
- elsif ($status eq '?') {
- # If the one doing the change is the requestee, then this means he doesn't
- # want to reply to the request and he simply reassigns the request to
- # someone else. In this case, we keep the requester unaltered.
- my $new_setter = $setter;
- if ($flag->requestee && $flag->requestee->id == $setter->id) {
- $new_setter = $flag->setter;
- }
-
- # Get the requestee, if any.
- my $requestee_id;
- if ($requestee_email) {
- $requestee_id = login_to_id($requestee_email);
- $flag->{'requestee'} = new Bugzilla::User($requestee_id);
- $flag->{'requestee_id'} = $requestee_id;
- }
- else {
- # If the status didn't change but we only removed the
- # requestee, we have to clear the requestee field.
- $flag->{'requestee'} = undef;
- $flag->{'requestee_id'} = undef;
- }
-
- # Update the database with the changes.
- $dbh->do('UPDATE flags
- SET setter_id = ?, requestee_id = ?,
- status = ?, modification_date = ?
- WHERE id = ?',
- undef, ($new_setter->id, $requestee_id, $status,
- $timestamp, $flag->id));
-
- # Now update the flag object with its new values.
- $flag->{'setter'} = $new_setter;
- $flag->{'status'} = $status;
-
- # Send an email notifying the relevant parties about the request.
- if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
- $flag->{'addressee'} = $flag->requestee;
- }
-
- notify($flag, $bug, $attachment);
- }
- elsif ($status eq 'X') {
- clear($flag, $bug, $attachment);
- }
-
- push(@flags, $flag);
+ push(@flags, { id => $flag_id,
+ status => $status,
+ requestee => $requestee_email,
+ skip_roe => $skip });
}
- return \@flags;
-}
-
-=pod
-
-=over
-
-=item C
-
-Change the type of the flag, if possible. The new flag type must have
-the same name as the current flag type, must exist in the product and
-component the bug is in, and the current settings of the flag must pass
-validation. If no such flag type can be found, the type remains unchanged.
-
-Retargetting flags is a good way to keep flags when moving bugs from one
-product where a flag type is available to another product where the flag
-type is unavailable, but another flag type having the same name exists.
-Most of the time, if they have the same name, this means that they have
-the same meaning, but with different settings.
-
-=back
-
-=cut
-
-sub retarget {
- my ($flag, $bug) = @_;
- my $dbh = Bugzilla->dbh;
-
- # We are looking for flagtypes having the same name as the flagtype
- # to which the current flag belongs, and being in the new product and
- # component of the bug.
- my $flagtypes = Bugzilla::FlagType::match(
- {'name' => $flag->name,
- 'target_type' => $flag->type->target_type,
- 'is_active' => 1,
- 'product_id' => $bug->product_id,
- 'component_id' => $bug->component_id});
-
- # If we found no flagtype, the flag will be deleted.
- return 0 unless scalar(@$flagtypes);
-
- # If we found at least one, change the type of the flag,
- # assuming the setter/requester is allowed to set/request flags
- # belonging to this flagtype.
- my $requestee = $flag->requestee ? [$flag->requestee->login] : [];
- my $is_private = ($flag->attachment) ? $flag->attachment->isprivate : 0;
- my $is_retargetted = 0;
-
- foreach my $flagtype (@$flagtypes) {
- # Get the number of flags of this type already set for this target.
- my $has_flags = __PACKAGE__->count(
- { 'type_id' => $flagtype->id,
- 'bug_id' => $bug->bug_id,
- 'attach_id' => $flag->attach_id });
-
- # Do not create a new flag of this type if this flag type is
- # not multiplicable and already has a flag set.
- next if (!$flagtype->is_multiplicable && $has_flags);
-
- # Check user privileges.
- my $error_mode_cache = Bugzilla->error_mode;
- Bugzilla->error_mode(ERROR_MODE_DIE);
- eval {
- _validate(undef, $flagtype, $flag->status, $flag->setter,
- $requestee, $is_private, $bug->bug_id, $flag->attach_id);
- };
- Bugzilla->error_mode($error_mode_cache);
- # If the validation failed, then we cannot use this flagtype.
- next if ($@);
-
- # Checks are successful, we can retarget the flag to this flagtype.
- $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
- undef, ($flagtype->id, $flag->id));
-
- $is_retargetted = 1;
- last;
- }
- return $is_retargetted;
-}
-
-=pod
-
-=over
-
-=item C
-
-Remove a flag from the DB.
-
-=back
-
-=cut
-
-sub clear {
- my ($flag, $bug, $attachment) = @_;
- my $dbh = Bugzilla->dbh;
-
- $dbh->do('DELETE FROM flags WHERE id = ?', undef, $flag->id);
-
- # If we cancel a pending request, we have to notify the requester
- # (if he wants to).
- my $requester;
- if ($flag->status eq '?') {
- $requester = $flag->setter;
- $flag->{'requester'} = $requester;
- }
-
- # Now update the flag object to its new values. The last
- # requester/setter and requestee are kept untouched (for the
- # record). Else we could as well delete the flag completely.
- $flag->{'exists'} = 0;
- $flag->{'status'} = "X";
-
- if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
- $flag->{'addressee'} = $requester;
- }
-
- notify($flag, $bug, $attachment);
-}
-
-
-######################################################################
-# Utility Functions
-######################################################################
-
-=pod
-
-=over
-
-=item C
-
-Checks whether or not there are new flags to create and returns an
-array of flag objects. This array is then passed to Flag::create().
-
-=back
-
-=cut
-
-sub FormToNewFlags {
- my ($bug, $attachment, $cgi, $hr_vars) = @_;
- my $dbh = Bugzilla->dbh;
- my $setter = Bugzilla->user;
-
- # Extract a list of flag type IDs from field names.
- my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
- @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
-
- return () unless scalar(@type_ids);
-
# Get a list of active flag types available for this product/component.
my $flag_types = Bugzilla::FlagType::match(
{ 'product_id' => $bug->{'product_id'},
'component_id' => $bug->{'component_id'},
'is_active' => 1 });
- foreach my $type_id (@type_ids) {
+ foreach my $flagtype_id (@flagtype_ids) {
# Checks if there are unexpected flags for the product/component.
- if (!scalar(grep { $_->id == $type_id } @$flag_types)) {
- $hr_vars->{'message'} = 'unexpected_flag_types';
+ if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
+ $vars->{'message'} = 'unexpected_flag_types';
last;
}
}
- my @flags;
foreach my $flag_type (@$flag_types) {
my $type_id = $flag_type->id;
@@ -1031,10 +888,10 @@ sub FormToNewFlags {
next unless ($flag_type->target_type eq 'bug' xor $attachment);
# We are only interested in flags the user tries to create.
- next unless scalar(grep { $_ == $type_id } @type_ids);
+ next unless scalar(grep { $_ == $type_id } @flagtype_ids);
# Get the number of flags of this type already set for this target.
- my $has_flags = __PACKAGE__->count(
+ my $has_flags = $class->count(
{ 'type_id' => $type_id,
'target_type' => $attachment ? 'attachment' : 'bug',
'bug_id' => $bug->bug_id,
@@ -1048,25 +905,23 @@ sub FormToNewFlags {
trick_taint($status);
my @logins = $cgi->param("requestee_type-$type_id");
- if ($status eq "?" && scalar(@logins) > 0) {
+ if ($status eq "?" && scalar(@logins)) {
foreach my $login (@logins) {
- push (@flags, { type => $flag_type ,
- setter => $setter ,
- status => $status ,
- requestee =>
- new Bugzilla::User({ name => $login }) });
+ push (@new_flags, { type_id => $type_id,
+ status => $status,
+ requestee => $login,
+ skip_roe => $skip });
last unless $flag_type->is_multiplicable;
}
}
else {
- push (@flags, { type => $flag_type ,
- setter => $setter ,
- status => $status });
+ push (@new_flags, { type_id => $type_id,
+ status => $status });
}
}
- # Return the list of flags.
- return \@flags;
+ # Return the list of flags to update and/or to create.
+ return (\@flags, \@new_flags);
}
=pod
@@ -1083,10 +938,41 @@ or deleted.
=cut
sub notify {
- my ($flag, $bug, $attachment) = @_;
+ my ($class, $flag, $old_flag, $obj) = @_;
- # There is nobody to notify.
- return unless ($flag->{'addressee'} || $flag->type->cc_list);
+ my ($bug, $attachment);
+ if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
+ $attachment = $obj;
+ $bug = $attachment->bug;
+ }
+ elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) {
+ $bug = $obj;
+ }
+ else {
+ # Not a good time to throw an error.
+ return;
+ }
+
+ my $addressee;
+ # If the flag is set to '?', maybe the requestee wants a notification.
+ if ($flag && $flag->requestee_id
+ && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id))
+ {
+ if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
+ $addressee = $flag->requestee;
+ }
+ }
+ elsif ($old_flag && $old_flag->status eq '?'
+ && (!$flag || $flag->status ne '?'))
+ {
+ if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) {
+ $addressee = $old_flag->setter;
+ }
+ }
+
+ my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list;
+ # Is there someone to notify?
+ return unless ($addressee || $cc_list);
# If the target bug is restricted to one or more groups, then we need
# to make sure we don't send email about it to unauthorized users
@@ -1096,7 +982,7 @@ sub notify {
my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
my %recipients;
- foreach my $cc (split(/[, ]+/, $flag->type->cc_list)) {
+ foreach my $cc (split(/[, ]+/, $cc_list)) {
my $ccuser = new Bugzilla::User({ name => $cc });
next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);
@@ -1106,16 +992,15 @@ sub notify {
}
# Only notify if the addressee is allowed to receive the email.
- if ($flag->{'addressee'} && $flag->{'addressee'}->email_enabled) {
- $recipients{$flag->{'addressee'}->email} = $flag->{'addressee'};
+ if ($addressee && $addressee->email_enabled) {
+ $recipients{$addressee->email} = $addressee;
}
# Process and send notification for each recipient.
# If there are users in the CC list who don't have an account,
# use the default language for email notifications.
my $default_lang;
if (grep { !$_ } values %recipients) {
- my $default_user = new Bugzilla::User();
- $default_lang = $default_user->settings->{'lang'}->{'value'};
+ $default_lang = Bugzilla::User->new()->settings->{'lang'}->{'value'};
}
foreach my $to (keys %recipients) {
@@ -1124,6 +1009,7 @@ sub notify {
my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;
my $vars = { 'flag' => $flag,
+ 'old_flag' => $old_flag,
'to' => $to,
'bug' => $bug,
'attachment' => $attachment,
@@ -1142,43 +1028,12 @@ sub notify {
}
}
-# Cancel all request flags from the attachment being obsoleted.
-sub CancelRequests {
- my ($class, $bug, $attachment, $timestamp) = @_;
- my $dbh = Bugzilla->dbh;
-
- my $request_ids =
- $dbh->selectcol_arrayref("SELECT flags.id
- FROM flags
- LEFT JOIN attachments ON flags.attach_id = attachments.attach_id
- WHERE flags.attach_id = ?
- AND flags.status = '?'
- AND attachments.isobsolete = 0",
- undef, $attachment->id);
-
- return if (!scalar(@$request_ids));
-
- # Take a snapshot of flags before any changes.
- my @old_summaries = $class->snapshot($bug->bug_id, $attachment->id)
- if ($timestamp);
- my $flags = Bugzilla::Flag->new_from_list($request_ids);
- foreach my $flag (@$flags) { clear($flag, $bug, $attachment) }
-
- # If $timestamp is undefined, do not update the activity table
- return unless ($timestamp);
-
- # Take a snapshot of flags after any changes.
- my @new_summaries = $class->snapshot($bug->bug_id, $attachment->id);
- update_activity($bug->bug_id, $attachment->id, $timestamp,
- \@old_summaries, \@new_summaries);
-}
-
# This is an internal function used by $bug->flag_types
# and $attachment->flag_types to collect data about available
# flag types and existing flags set on them. You should never
# call this function directly.
sub _flag_types {
- my $vars = shift;
+ my ($class, $vars) = @_;
my $target_type = $vars->{target_type};
my $flags;
@@ -1186,29 +1041,31 @@ sub _flag_types {
# Retrieve all existing flags for this bug/attachment.
if ($target_type eq 'bug') {
my $bug_id = delete $vars->{bug_id};
- $flags = Bugzilla::Flag->match({target_type => 'bug', bug_id => $bug_id});
+ $flags = $class->match({target_type => 'bug', bug_id => $bug_id});
}
elsif ($target_type eq 'attachment') {
my $attach_id = delete $vars->{attach_id};
- $flags = Bugzilla::Flag->match({attach_id => $attach_id});
+ $flags = $class->match({attach_id => $attach_id});
}
else {
ThrowCodeError('bad_arg', {argument => 'target_type',
- function => 'Bugzilla::Flag::_flag_types'});
+ function => $class . '->_flag_types'});
}
# Get all available flag types for the given product and component.
- my $flag_types = Bugzilla::FlagType::match($vars);
+ my $cache = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} ||= {};
+ my $flag_data = $cache->{$vars->{component_id}} ||= Bugzilla::FlagType::match($vars);
+ my $flag_types = dclone($flag_data);
$_->{flags} = [] foreach @$flag_types;
my %flagtypes = map { $_->id => $_ } @$flag_types;
- # Group existing flags per type.
- # Call the internal 'type_id' variable instead of the method
- # to not create a flagtype object.
- push(@{$flagtypes{$_->{type_id}}->{flags}}, $_) foreach @$flags;
-
- return [sort {$a->sortkey <=> $b->sortkey || $a->name cmp $b->name} values %flagtypes];
+ # Group existing flags per type, and skip those becoming invalid
+ # (which can happen when a bug is being moved into a new product
+ # or component).
+ @$flags = grep { exists $flagtypes{$_->type_id} } @$flags;
+ push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags;
+ return $flag_types;
}
=head1 SEE ALSO
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 2e8a975d2..65df4ee81 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -84,21 +84,49 @@ sub user_regexp { return $_[0]->{'userregexp'}; }
sub is_active { return $_[0]->{'isactive'}; }
sub icon_url { return $_[0]->{'icon_url'}; }
+sub bugs {
+ my $self = shift;
+ return $self->{bugs} if exists $self->{bugs};
+ my $bug_ids = Bugzilla->dbh->selectcol_arrayref(
+ 'SELECT bug_id FROM bug_group_map WHERE group_id = ?',
+ undef, $self->id);
+ require Bugzilla::Bug;
+ $self->{bugs} = Bugzilla::Bug->new_from_list($bug_ids);
+ return $self->{bugs};
+}
+
sub members_direct {
my ($self) = @_;
- return $self->{members_direct} if defined $self->{members_direct};
- my $dbh = Bugzilla->dbh;
- my $user_ids = $dbh->selectcol_arrayref(
- "SELECT user_group_map.user_id
- FROM user_group_map
- WHERE user_group_map.group_id = ?
- AND grant_type = " . GRANT_DIRECT . "
- AND isbless = 0", undef, $self->id);
- require Bugzilla::User;
- $self->{members_direct} = Bugzilla::User->new_from_list($user_ids);
+ $self->{members_direct} ||= $self->_get_members(GRANT_DIRECT);
return $self->{members_direct};
}
+sub members_non_inherited {
+ my ($self) = @_;
+ $self->{members_non_inherited} ||= $self->_get_members();
+ return $self->{members_non_inherited};
+}
+
+# A helper for members_direct and members_non_inherited
+sub _get_members {
+ my ($self, $grant_type) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $grant_clause = $grant_type ? "AND grant_type = $grant_type" : "";
+ my $user_ids = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT user_id
+ FROM user_group_map
+ WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id);
+ require Bugzilla::User;
+ return Bugzilla::User->new_from_list($user_ids);
+}
+
+sub flag_types {
+ my $self = shift;
+ require Bugzilla::FlagType;
+ $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id });
+ return $self->{flag_types};
+}
+
sub grant_direct {
my ($self, $type) = @_;
$self->{grant_direct} ||= {};
@@ -131,6 +159,30 @@ sub granted_by_direct {
return $self->{granted_by_direct}->{$type};
}
+sub products {
+ my $self = shift;
+ return $self->{products} if exists $self->{products};
+ my $product_data = Bugzilla->dbh->selectall_arrayref(
+ 'SELECT product_id, entry, membercontrol, othercontrol,
+ canedit, editcomponents, editbugs, canconfirm
+ FROM group_control_map WHERE group_id = ?', {Slice=>{}},
+ $self->id);
+ my @ids = map { $_->{product_id} } @$product_data;
+ require Bugzilla::Product;
+ my $products = Bugzilla::Product->new_from_list(\@ids);
+ my %data_map = map { $_->{product_id} => $_ } @$product_data;
+ my @retval;
+ foreach my $product (@$products) {
+ # Data doesn't need to contain product_id--we already have
+ # the product object.
+ delete $data_map{$product->id}->{product_id};
+ push(@retval, { controls => $data_map{$product->id},
+ product => $product });
+ }
+ $self->{products} = \@retval;
+ return $self->{products};
+}
+
###############################
#### Methods ####
###############################
@@ -143,6 +195,8 @@ sub set_icon_url { $_[0]->set('icon_url', $_[1]); }
sub update {
my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
my $changes = $self->SUPER::update(@_);
if (exists $changes->{name}) {
@@ -162,9 +216,76 @@ sub update {
&& $changes->{isactive}->[1]);
$self->_rederive_regexp() if exists $changes->{userregexp};
+
+ Bugzilla::Hook::process('group_end_of_update',
+ { group => $self, changes => $changes });
+ $dbh->bz_commit_transaction();
return $changes;
}
+sub check_remove {
+ my ($self, $params) = @_;
+
+ # System groups cannot be deleted!
+ if (!$self->is_bug_group) {
+ ThrowUserError("system_group_not_deletable", { name => $self->name });
+ }
+
+ # Groups having a special role cannot be deleted.
+ my @special_groups;
+ foreach my $special_group (GROUP_PARAMS) {
+ if ($self->name eq Bugzilla->params->{$special_group}) {
+ push(@special_groups, $special_group);
+ }
+ }
+ if (scalar(@special_groups)) {
+ ThrowUserError('group_has_special_role',
+ { name => $self->name,
+ groups => \@special_groups });
+ }
+
+ return if $params->{'test_only'};
+
+ my $cantdelete = 0;
+
+ my $users = $self->members_non_inherited;
+ if (scalar(@$users) && !$params->{'remove_from_users'}) {
+ $cantdelete = 1;
+ }
+
+ my $bugs = $self->bugs;
+ if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) {
+ $cantdelete = 1;
+ }
+
+ my $products = $self->products;
+ if (scalar(@$products) && !$params->{'remove_from_products'}) {
+ $cantdelete = 1;
+ }
+
+ my $flag_types = $self->flag_types;
+ if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) {
+ $cantdelete = 1;
+ }
+
+ ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete;
+}
+
+sub remove_from_db {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ $self->check_remove(@_);
+ $dbh->bz_start_transaction();
+ Bugzilla::Hook::process('group_before_delete', { group => $self });
+ $dbh->do('DELETE FROM whine_schedules
+ WHERE mailto_type = ? AND mailto = ?',
+ undef, MAILTO_GROUP, $self->id);
+ # All the other tables will be handled by foreign keys when we
+ # drop the main "groups" row.
+ $self->SUPER::remove_from_db(@_);
+ $dbh->bz_commit_transaction();
+}
+
# Add missing entries in bug_group_map for bugs created while
# a mandatory group was disabled and which is now enabled again.
sub _enforce_mandatory {
@@ -224,20 +345,6 @@ sub _rederive_regexp {
}
}
-sub members_non_inherited {
- my ($self) = @_;
- return $self->{members_non_inherited}
- if exists $self->{members_non_inherited};
-
- my $member_ids = Bugzilla->dbh->selectcol_arrayref(
- 'SELECT DISTINCT user_id FROM user_group_map
- WHERE isbless = 0 AND group_id = ?',
- undef, $self->id) || [];
- require Bugzilla::User;
- $self->{members_non_inherited} = Bugzilla::User->new_from_list($member_ids);
- return $self->{members_non_inherited};
-}
-
sub flatten_group_membership {
my ($self, @groups) = @_;
@@ -277,6 +384,8 @@ sub create {
print get_text('install_group_create', { name => $params->{name} }) . "\n"
if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
+ $dbh->bz_start_transaction();
+
my $group = $class->SUPER::create(@_);
# Since we created a new group, give the "admin" group all privileges
@@ -294,6 +403,9 @@ sub create {
}
$group->_rederive_regexp() if $group->user_regexp;
+
+ Bugzilla::Hook::process('group_end_of_create', { group => $group });
+ $dbh->bz_commit_transaction();
return $group;
}
@@ -414,6 +526,53 @@ be a member of this group.
=over
+=item C
+
+=over
+
+=item B
+
+Determines whether it's OK to remove this group from the database, and
+throws an error if it's not OK.
+
+=item B
+
+=over
+
+=item C
+
+C If you want to only check if the group can be deleted I,
+under any circumstances, specify C to just do the most basic tests
+(the other parameters will be ignored in this situation, as those tests won't
+be run).
+
+=item C
+
+C True if it would be OK to remove all users who are in this group
+from this group.
+
+=item C
+
+C True if it would be OK to remove all bugs that are in this group
+from this group.
+
+=item C
+
+C True if it would be OK to stop all flagtypes that reference
+this group from referencing this group (e.g., as their grantgroup or
+requestgroup).
+
+=item C
+
+C True if it would be OK to remove this group from all group controls
+on products.
+
+=back
+
+=item B (nothing)
+
+=back
+
=item C
Returns an arrayref of L objects representing people who are
diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm
index 6e77defd8..e800ffcf3 100644
--- a/Bugzilla/Hook.pm
+++ b/Bugzilla/Hook.pm
@@ -18,69 +18,112 @@
# Rights Reserved.
#
# Contributor(s): Zach Lipton
-#
+# Max Kanat-Alexander
package Bugzilla::Hook;
-use Bugzilla::Constants;
-use Bugzilla::Util;
-use Bugzilla::Error;
-
use strict;
+no strict 'subs';
+use Bugzilla::Util;
+use base 'Exporter';
+our @EXPORT = qw(set_hook run_hooks);
-sub process {
- my ($name, $args) = @_;
-
- # get a list of all extensions
- my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
-
- # check each extension to see if it uses the hook
- # if so, invoke the extension source file:
- foreach my $extension (@extensions) {
- # all of these variables come directly from code or directory names.
- # If there's malicious data here, we have much bigger issues to
- # worry about, so we can safely detaint them:
- trick_taint($extension);
- # Skip CVS directories and any hidden files/dirs.
- next if $extension =~ m{/CVS$} || $extension =~ m{/\.[^/]+$};
- next if -e "$extension/disabled";
- if (-e $extension.'/code/'.$name.'.pl') {
- Bugzilla->hook_args($args);
- # Allow extensions to load their own libraries.
- local @INC = ("$extension/lib", @INC);
- do($extension.'/code/'.$name.'.pl');
- if ($@)
- {
- $@->throw if ref($@) && $@->isa('Bugzilla::Error');
- ThrowCodeError('extension_invalid',
- { errstr => $@, name => $name, extension => $extension });
- }
- # Flush stored data.
- Bugzilla->hook_args({});
- }
- }
+my %hooks;
+my @hook_stack;
+my %hook_hash;
+
+# Set extension hook or hooks
+sub set_hook
+{
+ my ($extension, $hook, $callable) = @_;
+ $hook =~ tr/-/_/;
+ $hooks{$hook}{$extension} = $callable;
}
-sub enabled_plugins {
- my $extdir = bz_locations()->{'extensionsdir'};
- my @extensions = glob("$extdir/*");
- my %enabled;
- foreach my $extension (@extensions) {
- trick_taint($extension);
- my $extname = $extension;
- $extname =~ s{^\Q$extdir\E/}{};
- next if $extname eq 'CVS' || $extname =~ /^\./;
- next if -e "$extension/disabled";
- # Allow extensions to load their own libraries.
- local @INC = ("$extension/lib", @INC);
- $enabled{$extname} = do("$extension/info.pl");
- ThrowCodeError('extension_invalid',
- { errstr => $@, name => 'version',
- extension => $extension }) if $@;
+# An alias
+sub run_hooks
+{
+ goto &process;
+}
+# Process all hooks with name $name
+sub process
+{
+ my ($name, $args) = @_;
+ $name =~ tr/-/_/;
+
+ push @hook_stack, $name;
+ $hook_hash{$name}++;
+
+ my @process = values %{$hooks{$name}};
+ for my $f (@process)
+ {
+ if (!defined $f)
+ {
+ next;
+ }
+ elsif (ref $f eq 'ARRAY')
+ {
+ push @process, @$f;
+ next;
+ }
+ elsif (ref $f eq 'CODE')
+ {
+ # Fall through if()
+ }
+ elsif (!ref $f && $f =~ /^(.*)::[^:]*$/)
+ {
+ my $pk = $1;
+ if ($pk)
+ {
+ eval { require $pk };
+ if ($@)
+ {
+ warn "Error autoloading hook package $pk: $@";
+ }
+ }
+ }
+ elsif (ref $f eq 'HASH' && $f->{type} eq 'file' &&
+ open my $fd, $f->{filename})
+ {
+ # Slurp file content into $hook_sub
+ my $sub;
+ {
+ local $/ = undef;
+ $sub = <$fd>;
+ trick_taint($sub);
+ }
+ close $fd;
+ $sub =~ s/Bugzilla->hook_args/\$args/gso;
+ my $pk = $f->{filename};
+ $pk =~ s/\W+/_/gso;
+ $pk = "Bugzilla::Hook::$pk";
+ $sub = eval "package $pk; sub { my (\$args) = \@_; $sub; return 1; };";
+ if ($@)
+ {
+ warn __PACKAGE__."::load(): error during loading $f->{filename} into a subroutine (note that Bugzilla->hook_args was replaced by \$args): $@";
+ next;
+ }
+ $f = $sub;
+ }
+ else
+ {
+ die "Don't know what to do with hook callable \"$f\". Is it really callable?";
+ }
+ # OK, call the function!
+ # When a hook returns TRUE, other hooks are also called
+ # When a hook returns FALSE, hook processing is stopped
+ &$f($args) || last;
}
- return \%enabled;
+ $hook_hash{$name}--;
+ pop @hook_stack;
+}
+
+sub in
+{
+ my $hook_name = shift;
+ return $hook_hash{$hook_name} || 0;
}
1;
@@ -105,36 +148,19 @@ hooks. When a piece of standard Bugzilla code wants to allow an extension
to perform additional functions, it uses Bugzilla::Hook's L
subroutine to invoke any extension code if installed.
-There is a sample extension in F that demonstrates
-most of the things described in this document, as well as many of the
-hooks available.
+The implementation of extensions is described in L.
+
+There is sample code for every hook in the Example extension, located in
+F.
=head2 How Hooks Work
-When a hook named C is run, Bugzilla will attempt to invoke any
-source files named F.
+When a hook named C is run, Bugzilla looks through all
+enabled L for extensions that implement
+a subroutined named C.
-So, for example, if your extension is called "testopia", and you
-want to have code run during the L hook, you
-would have a file called F
-that contained perl code to run during that hook.
-
-=head2 Arguments Passed to Hooks
-
-Some L have params that are passed to them.
-
-These params are accessible through L.
-That returns a hashref. Very frequently, if you want your
-hook to do anything, you have to modify these variables.
-
-=head2 Versioning Extensions
-
-Every extension must have a file in its root called F.
-This file must return a hash when called with C.
-The hash must contain a 'version' key with the current version of the
-extension. Extension authors can also add any extra infomration to this hash if
-required, by adding a new key beginning with x_ which will not be used the
-core Bugzilla code.
+See L for more details about how an extension
+can run code during a hook.
=head1 SUBROUTINES
@@ -174,7 +200,25 @@ This describes what hooks exist in Bugzilla currently. They are mostly
in alphabetical order, but some related hooks are near each other instead
of being alphabetical.
-=head2 auth-login_methods
+=head2 attachment_process_data
+
+This happens at the very beginning process of the attachment creation.
+You can edit the attachment content itself as well as all attributes
+of the attachment, before they are validated and inserted into the DB.
+
+Params:
+
+=over
+
+=item C - A reference pointing either to the content of the file
+being uploaded or pointing to the filehandle associated with the file.
+
+=item C - A hashref whose keys are the same as
+L. The data it contains hasn't been checked yet.
+
+=back
+
+=head2 auth_login_methods
This allows you to add new login types to Bugzilla.
(See L.)
@@ -205,16 +249,16 @@ login methods that weren't passed to L.)
=back
-=head2 auth-verify_methods
+=head2 auth_verify_methods
-This works just like L except it's for
+This works just like L except it's for
login verification methods (See L.) It also
-takes a C parameter, just like L.
+takes a C parameter, just like L.
-=head2 bug-columns
+=head2 bug_columns
This allows you to add new fields that will show up in every L
-object. Note that you will also need to use the L hook in
+object. Note that you will also need to use the L hook in
conjunction with this hook to make this work.
Params:
@@ -226,7 +270,7 @@ your column name(s) onto the array.
=back
-=head2 bug-end_of_create
+=head2 bug_end_of_create
This happens at the end of L, after all other changes are
made to the database. This occurs inside a database transaction.
@@ -242,7 +286,22 @@ values.
=back
-=head2 bug-end_of_update
+=head2 bug_end_of_create_validators
+
+This happens during L, after all parameters have
+been validated, but before anything has been inserted into the database.
+
+Params:
+
+=over
+
+=item C
+
+A hashref. The validated parameters passed to C.
+
+=back
+
+=head2 bug_end_of_update
This happens at the end of L, after all other changes are
made to the database. This generally occurs inside a database transaction.
@@ -251,23 +310,32 @@ Params:
=over
-=item C - The changed bug object, with all fields set to their updated
-values.
+=item C
-=item C - The timestamp used for all updates in this transaction.
+The changed bug object, with all fields set to their updated values.
-=item C - The hash of changed fields.
-C<$changes-E{field} = [old, new]>
+=item C
+
+A bug object pulled from the database before the fields were set to
+their updated values (so it has the old values available for each field).
+
+=item C
+
+The timestamp used for all updates in this transaction.
+
+=item C
+
+The hash of changed fields. C<< $changes->{field} = [old, new] >>
=back
-=head2 bug-fields
+=head2 bug_fields
Allows the addition of database fields from the bugs table to the standard
list of allowable fields in a L object, so that
you can call the field as a method.
-Note: You should add here the names of any fields you added in L.
+Note: You should add here the names of any fields you added in L.
Params:
@@ -278,7 +346,72 @@ your column name(s) onto the array.
=back
-=head2 buglist-columns
+=head2 bug_format_comment
+
+Allows you to do custom parsing on comments before they are displayed. You do
+this by returning two regular expressions: one that matches the section you
+want to replace, and then another that says what you want to replace that
+match with.
+
+The matching and replacement will be run with the C switch on the regex.
+
+Params:
+
+=over
+
+=item C
+
+An arrayref of hashrefs.
+
+You should push a hashref containing two keys (C and C)
+in to this array. C is the regular expression that matches the
+text you want to replace, C is what you want to replace that
+text with. (This gets passed into a regular expression like
+C.)
+
+Instead of specifying a regular expression for C you can also
+return a coderef (a reference to a subroutine). If you want to use
+backreferences (using C<$1>, C<$2>, etc. in your C), you have to use
+this method--it won't work if you specify C<$1>, C<$2> in a regular expression
+for C. Your subroutine will get a hashref as its only argument. This
+hashref contains a single key, C. C is an arrayref that
+contains C<$1>, C<$2>, C<$3>, etc. in order, up to C<$10>. Your subroutine
+should return what you want to replace the full C with. (See the code
+example for this hook if you want to see how this actually all works in code.
+It's simpler than it sounds.)
+
+B Failing to
+do so could open a security hole in Bugzilla.
+
+=item C
+
+A B to the exact text that you are parsing.
+
+Generally you should not modify this yourself. Instead you should be
+returning regular expressions using the C array.
+
+The text has already been word-wrapped, but has not been parsed in any way
+otherwise. (So, for example, it is not HTML-escaped. You get "&", not
+"&".)
+
+=item C
+
+The L object that this comment is on. Sometimes this is
+C, meaning that we are parsing text that is not on a bug.
+
+=item C
+
+A hashref representing the comment you are about to parse, including
+all of the fields that comments contain when they are returned by
+by L.
+
+Sometimes this is C, meaning that we are parsing text that is
+not a bug comment (but could still be some other part of a bug, like
+the summary line).
+
+=back
+
+=head2 buglist_columns
This happens in buglist.cgi after the standard columns have been defined and
right before the display column determination. It gives you the opportunity
@@ -306,7 +439,49 @@ The definition is structured as:
=back
-=head2 colchange-columns
+=head2 bugmail_recipients
+
+This allows you to modify the list of users who are going to be receiving
+a particular bugmail. It also allows you to specify why they are receiving
+the bugmail.
+
+Users' bugmail preferences will be applied to any users that you add
+to the list. (So, for example, if you add somebody as though they were
+a CC on the bug, and their preferences state that they don't get email
+when they are a CC, they won't get email.)
+
+This hook is called before watchers or globalwatchers are added to the
+recipient list.
+
+Params:
+
+=over
+
+=item C
+
+The L that bugmail is being sent about.
+
+=item C
+
+This is a hashref. The keys are numeric user ids from the C
+table in the database, for each user who should be receiving this bugmail.
+The values are hashrefs. The keys in I hashrefs correspond to
+the "relationship" that the user has to the bug they're being emailed
+about, and the value should always be C<1>. The "relationships"
+are described by the various C constants in L.
+
+Here's an example of adding userid C<123> to the recipient list
+as though he were on the CC list:
+
+ $recipients->{123}->{+REL_CC} = 1
+
+(We use C<+> in front of C so that Perl interprets it as a constant
+instead of as a string.)
+
+=back
+
+
+=head2 colchange_columns
This happens in F right after the list of possible display
columns have been defined and gives you the opportunity to add additional
@@ -317,12 +492,11 @@ Params:
=over
=item C - An arrayref containing an array of column IDs. Any IDs
-added by this hook must have been defined in the the buglist-columns hook.
-See L.
+added by this hook must have been defined in the the L hook.
=back
-=head2 config-add_panels
+=head2 config_add_panels
If you want to add new panels to the Parameters administrative interface,
this is where you do it.
@@ -343,7 +517,7 @@ extension.)
=back
-=head2 config-modify_panels
+=head2 config_modify_panels
This is how you modify already-existing panels in the Parameters
administrative interface. For example, if you wanted to add a new
@@ -363,11 +537,13 @@ C for that module. You can modify C and
your changes will be reflected in the interface.
Adding new keys to C will have no effect. You should use
-L if you want to add new panels.
+L if you want to add new panels.
=back
-=head2 enter_bug-entrydefaultvars
+=head2 enter_bug_entrydefaultvars
+
+B - Use L instead.
This happens right before the template is loaded on enter_bug.cgi.
@@ -379,9 +555,9 @@ Params:
=back
-=head2 flag-end_of_update
+=head2 flag_end_of_update
-This happens at the end of L, after all other changes
+This happens at the end of L, after all other changes
are made to the database and after emails are sent. It gives you a before/after
snapshot of flags so you can react to specific flag changes.
This generally occurs inside a database transaction.
@@ -393,7 +569,7 @@ Params:
=over
-=item C - The changed bug object.
+=item C