diff options
author | 2019-11-30 23:12:11 -0800 | |
---|---|---|
committer | 2019-12-01 14:53:51 -0800 | |
commit | 70780e40e5586c6882e33dd65a3dc3f31031a321 (patch) | |
tree | 51fc3608bd44e7b92d07a976ca3112fd5d87d843 /Bugzilla | |
parent | Merge commit '3395d78cc8b0bd660e56f73a2689d495f2a22628' into bugstest (diff) | |
download | bugzilla-70780e40e5586c6882e33dd65a3dc3f31031a321.tar.gz bugzilla-70780e40e5586c6882e33dd65a3dc3f31031a321.tar.bz2 bugzilla-70780e40e5586c6882e33dd65a3dc3f31031a321.zip |
Gentoo-local version of 7f3a749d7bd78a3e4aee163f562d7e95b0954b44 w/ Perl-Tidy-20180220
Reformat all code using Perl-Tidy v20180220 and .perltidyrc from
matching upstream 7f3a749d7bd78a3e4aee163f562d7e95b0954b44 commit.
Signed-off-by: Robin H. Johnson <robbat2@gentoo.org>
Diffstat (limited to 'Bugzilla')
150 files changed, 41167 insertions, 39472 deletions
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm index 33183797b..326534dd8 100644 --- a/Bugzilla/Attachment.pm +++ b/Bugzilla/Attachment.pm @@ -57,55 +57,51 @@ use parent qw(Bugzilla::Object); use constant DB_TABLE => 'attachments'; use constant ID_FIELD => 'attach_id'; use constant LIST_ORDER => ID_FIELD; + # Attachments are tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant DB_COLUMNS => qw( - attach_id - bug_id - creation_ts - description - filename - isobsolete - ispatch - isprivate - mimetype - modification_time - submitter_id + attach_id + bug_id + creation_ts + description + filename + isobsolete + ispatch + isprivate + mimetype + modification_time + submitter_id ); -use constant REQUIRED_FIELD_MAP => { - bug_id => 'bug', -}; +use constant REQUIRED_FIELD_MAP => {bug_id => 'bug',}; use constant EXTRA_REQUIRED_FIELDS => qw(data); use constant UPDATE_COLUMNS => qw( - description - filename - isobsolete - ispatch - isprivate - mimetype + description + filename + isobsolete + ispatch + isprivate + mimetype ); use constant VALIDATORS => { - bug => \&_check_bug, - description => \&_check_description, - filename => \&_check_filename, - ispatch => \&Bugzilla::Object::check_boolean, - isprivate => \&_check_is_private, - mimetype => \&_check_content_type, + bug => \&_check_bug, + description => \&_check_description, + filename => \&_check_filename, + ispatch => \&Bugzilla::Object::check_boolean, + isprivate => \&_check_is_private, + mimetype => \&_check_content_type, }; -use constant VALIDATOR_DEPENDENCIES => { - content_type => ['ispatch'], - mimetype => ['ispatch'], -}; +use constant VALIDATOR_DEPENDENCIES => + {content_type => ['ispatch'], mimetype => ['ispatch'],}; -use constant UPDATE_VALIDATORS => { - isobsolete => \&Bugzilla::Object::check_boolean, -}; +use constant UPDATE_VALIDATORS => + {isobsolete => \&Bugzilla::Object::check_boolean,}; ############################### #### Accessors ###### @@ -126,7 +122,7 @@ the ID of the bug to which the attachment is attached =cut sub bug_id { - return $_[0]->{bug_id}; + return $_[0]->{bug_id}; } =over @@ -140,8 +136,8 @@ the bug object to which the attachment is attached =cut sub bug { - require Bugzilla::Bug; - return $_[0]->{bug} //= Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 }); + require Bugzilla::Bug; + return $_[0]->{bug} //= Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1}); } =over @@ -155,7 +151,7 @@ user-provided text describing the attachment =cut sub description { - return $_[0]->{description}; + return $_[0]->{description}; } =over @@ -169,7 +165,7 @@ the attachment's MIME media type =cut sub contenttype { - return $_[0]->{mimetype}; + return $_[0]->{mimetype}; } =over @@ -183,8 +179,8 @@ the user who attached the attachment =cut sub attacher { - return $_[0]->{attacher} - //= new Bugzilla::User({ id => $_[0]->{submitter_id}, cache => 1 }); + return $_[0]->{attacher} + //= new Bugzilla::User({id => $_[0]->{submitter_id}, cache => 1}); } =over @@ -198,7 +194,7 @@ the date and time on which the attacher attached the attachment =cut sub attached { - return $_[0]->{creation_ts}; + return $_[0]->{creation_ts}; } =over @@ -212,7 +208,7 @@ the date and time on which the attachment was last modified. =cut sub modification_time { - return $_[0]->{modification_time}; + return $_[0]->{modification_time}; } =over @@ -226,7 +222,7 @@ the name of the file the attacher attached =cut sub filename { - return $_[0]->{filename}; + return $_[0]->{filename}; } =over @@ -240,7 +236,7 @@ whether or not the attachment is a patch =cut sub ispatch { - return $_[0]->{ispatch}; + return $_[0]->{ispatch}; } =over @@ -254,7 +250,7 @@ whether or not the attachment is obsolete =cut sub isobsolete { - return $_[0]->{isobsolete}; + return $_[0]->{isobsolete}; } =over @@ -268,7 +264,7 @@ whether or not the attachment is private =cut sub isprivate { - return $_[0]->{isprivate}; + return $_[0]->{isprivate}; } =over @@ -285,23 +281,24 @@ matches, because this will return a value even if it's matched by the generic =cut sub is_viewable { - my $contenttype = $_[0]->contenttype; - my $cgi = Bugzilla->cgi; + my $contenttype = $_[0]->contenttype; + my $cgi = Bugzilla->cgi; - # We assume we can view all text and image types. - return 1 if ($contenttype =~ /^(text|image)\//); + # We assume we can view all text and image types. + return 1 if ($contenttype =~ /^(text|image)\//); - # Mozilla can view XUL. Note the trailing slash on the Gecko detection to - # avoid sending XUL to Safari. - return 1 if (($contenttype =~ /^application\/vnd\.mozilla\./) - && ($cgi->user_agent() =~ /Gecko\//)); + # Mozilla can view XUL. Note the trailing slash on the Gecko detection to + # avoid sending XUL to Safari. + return 1 + if (($contenttype =~ /^application\/vnd\.mozilla\./) + && ($cgi->user_agent() =~ /Gecko\//)); - # If it's not one of the above types, we check the Accept: header for any - # types mentioned explicitly. - my $accept = join(",", $cgi->Accept()); - return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/); + # If it's not one of the above types, we check the Accept: header for any + # types mentioned explicitly. + my $accept = join(",", $cgi->Accept()); + return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/); - return 0; + return 0; } =over @@ -315,28 +312,29 @@ the content of the attachment =cut sub data { - my $self = shift; - return $self->{data} if exists $self->{data}; + my $self = shift; + return $self->{data} if exists $self->{data}; - # First try to get the attachment data from the database. - ($self->{data}) = Bugzilla->dbh->selectrow_array("SELECT thedata + # First try to get the attachment data from the database. + ($self->{data}) = Bugzilla->dbh->selectrow_array( + "SELECT thedata FROM attach_data - WHERE id = ?", - undef, - $self->id); - - # If there's no attachment data in the database, the attachment is stored - # in a local file, so retrieve it from there. - if (length($self->{data}) == 0) { - if (open(AH, '<', $self->_get_local_filename())) { - local $/; - binmode AH; - $self->{data} = <AH>; - close(AH); - } + WHERE id = ?", undef, + $self->id + ); + + # If there's no attachment data in the database, the attachment is stored + # in a local file, so retrieve it from there. + if (length($self->{data}) == 0) { + if (open(AH, '<', $self->_get_local_filename())) { + local $/; + binmode AH; + $self->{data} = <AH>; + close(AH); } + } - return $self->{data}; + return $self->{data}; } =over @@ -358,37 +356,37 @@ the length (in bytes) of the attachment content # LENGTH() function or stat()ing the file instead. I've left it in for now. sub datasize { - my $self = shift; - return $self->{datasize} if defined $self->{datasize}; + my $self = shift; + return $self->{datasize} if defined $self->{datasize}; - # If we have already retrieved the data, return its size. - return length($self->{data}) if exists $self->{data}; + # If we have already retrieved the data, return its size. + return length($self->{data}) if exists $self->{data}; - $self->{datasize} = - Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata) + $self->{datasize} = Bugzilla->dbh->selectrow_array( + "SELECT LENGTH(thedata) FROM attach_data - WHERE id = ?", - undef, $self->id) || 0; - - # If there's no attachment data in the database, either the attachment - # is stored in a local file, and so retrieve its size from the file, - # or the attachment has been deleted. - unless ($self->{datasize}) { - if (open(AH, '<', $self->_get_local_filename())) { - binmode AH; - $self->{datasize} = (stat(AH))[7]; - close(AH); - } + WHERE id = ?", undef, $self->id + ) || 0; + + # If there's no attachment data in the database, either the attachment + # is stored in a local file, and so retrieve its size from the file, + # or the attachment has been deleted. + unless ($self->{datasize}) { + if (open(AH, '<', $self->_get_local_filename())) { + binmode AH; + $self->{datasize} = (stat(AH))[7]; + close(AH); } + } - return $self->{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; + my $self = shift; + my $hash = ($self->id % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id; } =over @@ -402,8 +400,9 @@ flags that have been set on the attachment =cut sub flags { - # Don't cache it as it must be in sync with ->flag_types. - return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; + + # Don't cache it as it must be in sync with ->flag_types. + return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; } =over @@ -418,202 +417,216 @@ already set, grouped by flag type. =cut sub flag_types { - my $self = shift; - return $self->{flag_types} if exists $self->{flag_types}; + my $self = shift; + return $self->{flag_types} if exists $self->{flag_types}; - my $vars = { target_type => 'attachment', - product_id => $self->bug->product_id, - component_id => $self->bug->component_id, - attach_id => $self->id }; + my $vars = { + target_type => 'attachment', + product_id => $self->bug->product_id, + component_id => $self->bug->component_id, + attach_id => $self->id + }; - return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); + return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); } ############################### #### Validators ###### ############################### -sub set_content_type { $_[0]->set('mimetype', $_[1]); } +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 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 set_filename { $_[0]->set('filename', $_[1]); } +sub set_is_patch { $_[0]->set('ispatch', $_[1]); } +sub set_is_private { $_[0]->set('isprivate', $_[1]); } + +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 set_flags { - my ($self, $flags, $new_flags) = @_; + my ($self, $flags, $new_flags) = @_; - Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); + Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); } sub _check_bug { - my ($invocant, $bug) = @_; - my $user = Bugzilla->user; + my ($invocant, $bug) = @_; + my $user = Bugzilla->user; - $bug = ref $invocant ? $invocant->bug : $bug; + $bug = ref $invocant ? $invocant->bug : $bug; - $bug || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'bug' }); + $bug + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'bug'}); - ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) - || ThrowUserError("illegal_attachment_edit_bug", { bug_id => $bug->id }); + ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) + || ThrowUserError("illegal_attachment_edit_bug", {bug_id => $bug->id}); - return $bug; + return $bug; } sub _check_content_type { - my ($invocant, $content_type, undef, $params) = @_; - - my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; - $content_type = 'text/plain' if $is_patch; - $content_type = clean_text($content_type); - # The subsets below cover all existing MIME types and charsets registered by IANA. - # (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) - my $legal_types = join('|', LEGAL_CONTENT_TYPES); - if (!$content_type - || $content_type !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) - { - ThrowUserError("invalid_content_type", { contenttype => $content_type }); + my ($invocant, $content_type, undef, $params) = @_; + + my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; + $content_type = 'text/plain' if $is_patch; + $content_type = clean_text($content_type); + +# The subsets below cover all existing MIME types and charsets registered by IANA. +# (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) + my $legal_types = join('|', LEGAL_CONTENT_TYPES); + if (!$content_type + || $content_type + !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) + { + ThrowUserError("invalid_content_type", {contenttype => $content_type}); + } + trick_taint($content_type); + + # $ENV{HOME} must be defined when using File::MimeInfo::Magic, + # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. + local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); + + # If we have autodetected application/octet-stream from the Content-Type + # header, let's have a better go using a sniffer if available. + if ( defined Bugzilla->input_params->{contenttypemethod} + && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' + && $content_type eq 'application/octet-stream' + && Bugzilla->feature('typesniffer')) + { + import File::MimeInfo::Magic qw(mimetype); + require IO::Scalar; + + # data is either a filehandle, or the data itself. + my $fh = $params->{data}; + if (!ref($fh)) { + $fh = new IO::Scalar \$fh; } - trick_taint($content_type); - - # $ENV{HOME} must be defined when using File::MimeInfo::Magic, - # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. - local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); - - # If we have autodetected application/octet-stream from the Content-Type - # header, let's have a better go using a sniffer if available. - if (defined Bugzilla->input_params->{contenttypemethod} - && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' - && $content_type eq 'application/octet-stream' - && Bugzilla->feature('typesniffer')) - { - import File::MimeInfo::Magic qw(mimetype); - require IO::Scalar; - - # data is either a filehandle, or the data itself. - my $fh = $params->{data}; - if (!ref($fh)) { - $fh = new IO::Scalar \$fh; - } - elsif (!$fh->isa('IO::Handle')) { - # CGI.pm sends us an Fh that isn't actually an IO::Handle, but - # has a method for getting an actual handle out of it. - $fh = $fh->handle; - # ->handle returns an literal IO::Handle, even though the - # underlying object is a file. So we rebless it to be a proper - # IO::File object so that we can call ->seek on it and so on. - # Just in case CGI.pm fixes this some day, we check ->isa first. - if (!$fh->isa('IO::File')) { - bless $fh, 'IO::File'; - } - } - - my $mimetype = mimetype($fh); - $fh->seek(0, 0); - $content_type = $mimetype if $mimetype; + elsif (!$fh->isa('IO::Handle')) { + + # CGI.pm sends us an Fh that isn't actually an IO::Handle, but + # has a method for getting an actual handle out of it. + $fh = $fh->handle; + + # ->handle returns an literal IO::Handle, even though the + # underlying object is a file. So we rebless it to be a proper + # IO::File object so that we can call ->seek on it and so on. + # Just in case CGI.pm fixes this some day, we check ->isa first. + if (!$fh->isa('IO::File')) { + bless $fh, 'IO::File'; + } } - # Make sure patches are viewable in the browser - if (!ref($invocant) - && defined Bugzilla->input_params->{contenttypemethod} - && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' - && $content_type =~ m{text/x-(?:diff|patch)}) - { - $params->{ispatch} = 1; - $content_type = 'text/plain'; - } - - return $content_type; + my $mimetype = mimetype($fh); + $fh->seek(0, 0); + $content_type = $mimetype if $mimetype; + } + + # Make sure patches are viewable in the browser + if (!ref($invocant) + && defined Bugzilla->input_params->{contenttypemethod} + && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' + && $content_type =~ m{text/x-(?:diff|patch)}) + { + $params->{ispatch} = 1; + $content_type = 'text/plain'; + } + + return $content_type; } sub _check_data { - my ($invocant, $params) = @_; + my ($invocant, $params) = @_; - my $data = $params->{data}; - $params->{filesize} = ref $data ? -s $data : length($data); + my $data = $params->{data}; + $params->{filesize} = ref $data ? -s $data : length($data); - Bugzilla::Hook::process('attachment_process_data', { data => \$data, - attributes => $params }); + Bugzilla::Hook::process('attachment_process_data', + {data => \$data, attributes => $params}); - $params->{filesize} || ThrowUserError('zero_length_file'); - # Make sure the attachment does not exceed the maximum permitted size. - my $max_size = max(Bugzilla->params->{'maxlocalattachment'} * 1048576, - Bugzilla->params->{'maxattachmentsize'} * 1024); + $params->{filesize} || ThrowUserError('zero_length_file'); - if ($params->{filesize} > $max_size) { - my $vars = { filesize => sprintf("%.0f", $params->{filesize}/1024) }; - ThrowUserError('file_too_large', $vars); - } - return $data; + # Make sure the attachment does not exceed the maximum permitted size. + my $max_size = max( + Bugzilla->params->{'maxlocalattachment'} * 1048576, + Bugzilla->params->{'maxattachmentsize'} * 1024 + ); + + if ($params->{filesize} > $max_size) { + my $vars = {filesize => sprintf("%.0f", $params->{filesize} / 1024)}; + ThrowUserError('file_too_large', $vars); + } + return $data; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('missing_attachment_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('missing_attachment_description'); + return $description; } sub _check_filename { - my ($invocant, $filename) = @_; - - $filename = clean_text($filename); - if (!$filename) { - if (ref $invocant) { - ThrowUserError('filename_not_specified'); - } - else { - ThrowUserError('file_not_specified'); - } - } + my ($invocant, $filename) = @_; - # 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 - # mess up file names with slashes in them, but them's the breaks. We only - # use this as a hint to users downloading attachments anyway, so it's not - # a big deal if it munges incorrectly occasionally. - $filename =~ s/^.*[\/\\]//; - - # Truncate the filename to MAX_ATTACH_FILENAME_LENGTH characters, counting - # from the end of the string to make sure we keep the filename extension. - $filename = substr($filename, - -&MAX_ATTACH_FILENAME_LENGTH, - MAX_ATTACH_FILENAME_LENGTH); - trick_taint($filename); - - return $filename; + $filename = clean_text($filename); + if (!$filename) { + if (ref $invocant) { + ThrowUserError('filename_not_specified'); + } + else { + 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 + # mess up file names with slashes in them, but them's the breaks. We only + # use this as a hint to users downloading attachments anyway, so it's not + # a big deal if it munges incorrectly occasionally. + $filename =~ s/^.*[\/\\]//; + + # Truncate the filename to MAX_ATTACH_FILENAME_LENGTH characters, counting + # from the end of the string to make sure we keep the filename extension. + $filename + = substr($filename, -&MAX_ATTACH_FILENAME_LENGTH, MAX_ATTACH_FILENAME_LENGTH); + trick_taint($filename); + + return $filename; } sub _check_is_private { - my ($invocant, $is_private) = @_; - - $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'); - } - return $is_private; + my ($invocant, $is_private) = @_; + + $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'); + } + return $is_private; } =pod @@ -635,69 +648,74 @@ Returns: a reference to an array of attachment objects. =cut sub get_attachments_by_bug { - my ($class, $bug, $vars) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - # By default, private attachments are not accessible, unless the user - # is in the insider group or submitted the attachment. - my $and_restriction = ''; - my @values = ($bug->id); - - unless ($user->is_insider) { - $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)'; - push(@values, $user->id); + my ($class, $bug, $vars) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + # By default, private attachments are not accessible, unless the user + # is in the insider group or submitted the attachment. + my $and_restriction = ''; + my @values = ($bug->id); + + unless ($user->is_insider) { + $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)'; + push(@values, $user->id); + } + + my $attach_ids = $dbh->selectcol_arrayref( + "SELECT attach_id FROM attachments + WHERE bug_id = ? $and_restriction", + undef, @values + ); + + my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); + $_->{bug} = $bug foreach @$attachments; + + # To avoid $attachment->flags and $attachment->flag_types running SQL queries + # themselves for each attachment listed here, we collect all the data at once and + # populate $attachment->{flag_types} ourselves. We also load all attachers and + # datasizes at once for the same reason. + if ($vars->{preload}) { + + # Preload flag types and flags + my $vars = { + target_type => 'attachment', + product_id => $bug->product_id, + component_id => $bug->component_id, + attach_id => $attach_ids + }; + my $flag_types = Bugzilla::Flag->_flag_types($vars); + + foreach my $attachment (@$attachments) { + $attachment->{flag_types} = []; + my $new_types = dclone($flag_types); + foreach my $new_type (@$new_types) { + $new_type->{flags} + = [grep($_->attach_id == $attachment->id, @{$new_type->{flags}})]; + push(@{$attachment->{flag_types}}, $new_type); + } } - my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments - WHERE bug_id = ? $and_restriction", - undef, @values); - - my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); - $_->{bug} = $bug foreach @$attachments; - - # To avoid $attachment->flags and $attachment->flag_types running SQL queries - # themselves for each attachment listed here, we collect all the data at once and - # populate $attachment->{flag_types} ourselves. We also load all attachers and - # datasizes at once for the same reason. - if ($vars->{preload}) { - # Preload flag types and flags - my $vars = { target_type => 'attachment', - product_id => $bug->product_id, - component_id => $bug->component_id, - attach_id => $attach_ids }; - my $flag_types = Bugzilla::Flag->_flag_types($vars); - - foreach my $attachment (@$attachments) { - $attachment->{flag_types} = []; - my $new_types = dclone($flag_types); - foreach my $new_type (@$new_types) { - $new_type->{flags} = [ grep($_->attach_id == $attachment->id, - @{ $new_type->{flags} }) ]; - push(@{ $attachment->{flag_types} }, $new_type); - } - } - - # Preload attachers. - my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; - my $users = Bugzilla::User->new_from_list([keys %user_ids]); - my %user_map = map { $_->id => $_ } @$users; - foreach my $attachment (@$attachments) { - $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; - } - - # Preload datasizes. - my $sizes = - $dbh->selectall_hashref('SELECT attach_id, LENGTH(thedata) AS datasize + # Preload attachers. + my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; + my $users = Bugzilla::User->new_from_list([keys %user_ids]); + my %user_map = map { $_->id => $_ } @$users; + foreach my $attachment (@$attachments) { + $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; + } + + # Preload datasizes. + my $sizes = $dbh->selectall_hashref( + 'SELECT attach_id, LENGTH(thedata) AS datasize FROM attachments LEFT JOIN attach_data ON attach_id = id - WHERE bug_id = ?', - 'attach_id', undef, $bug->id); + WHERE bug_id = ?', 'attach_id', undef, $bug->id + ); - # Force the size of attachments not in the DB to be recalculated. - $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; - } + # Force the size of attachments not in the DB to be recalculated. + $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; + } - return $attachments; + return $attachments; } =pod @@ -716,13 +734,15 @@ Returns: 1 on success, 0 otherwise. =cut sub validate_can_edit { - my $attachment = shift; - my $user = Bugzilla->user; - - # The submitter can edit their attachments. - return ($attachment->attacher->id == $user->id - || ((!$attachment->isprivate || $user->is_insider) - && $user->in_group('editbugs', $attachment->bug->product_id))) ? 1 : 0; + my $attachment = shift; + my $user = Bugzilla->user; + + # The submitter can edit their attachments. + return ( + $attachment->attacher->id == $user->id + || ((!$attachment->isprivate || $user->is_insider) + && $user->in_group('editbugs', $attachment->bug->product_id)) + ) ? 1 : 0; } =item C<validate_obsolete($bug, $attach_ids)> @@ -741,37 +761,36 @@ Returns: The list of attachment objects to mark as obsolete. =cut sub validate_obsolete { - my ($class, $bug, $list) = @_; + 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 (@$list) { - my $vars = {}; - $vars->{'attach_id'} = $attachid; + # 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 (@$list) { + my $vars = {}; + $vars->{'attach_id'} = $attachid; - detaint_natural($attachid) - || ThrowUserError('invalid_attach_id', $vars); + detaint_natural($attachid) || ThrowUserError('invalid_attach_id', $vars); - # Make sure the attachment exists in the database. - my $attachment = new Bugzilla::Attachment($attachid) - || ThrowUserError('invalid_attach_id', $vars); + # Make sure the attachment exists in the database. + my $attachment = new Bugzilla::Attachment($attachid) + || ThrowUserError('invalid_attach_id', $vars); - # Check that the user can view and edit this attachment. - $attachment->validate_can_edit - || ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id }); + # Check that the user can view and edit this attachment. + $attachment->validate_can_edit + || ThrowUserError('illegal_attachment_edit', {attach_id => $attachment->id}); - if ($attachment->bug_id != $bug->bug_id) { - $vars->{'my_bug_id'} = $bug->bug_id; - ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); - } + if ($attachment->bug_id != $bug->bug_id) { + $vars->{'my_bug_id'} = $bug->bug_id; + ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); + } - next if $attachment->isobsolete; + next if $attachment->isobsolete; - push(@obsolete_attachments, $attachment); - } - return @obsolete_attachments; + push(@obsolete_attachments, $attachment); + } + return @obsolete_attachments; } ############################### @@ -806,112 +825,119 @@ Returns: The new attachment object. =cut sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; - - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - - # Extract everything which is not a valid column name. - my $bug = delete $params->{bug}; - $params->{bug_id} = $bug->id; - my $data = delete $params->{data}; - my $size = delete $params->{filesize}; - - my $attachment = $class->insert_create_data($params); - my $attachid = $attachment->id; - - # The file is too large to be stored in the DB, so we store it locally. - if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { - my $attachdir = bz_locations()->{'attachdir'}; - my $hash = ($attachid % 100) + 100; - $hash =~ s/.*(\d\d)$/group.$1/; - mkdir "$attachdir/$hash", 0770; - chmod 0770, "$attachdir/$hash"; - if (ref $data) { - copy($data, "$attachdir/$hash/attachment.$attachid"); - close $data; - } - else { - open(AH, '>', "$attachdir/$hash/attachment.$attachid"); - binmode AH; - print AH $data; - close AH; - } - $data = ''; # Will be stored in the DB. - } - # If we have a filehandle, we need its content to store it in the DB. - elsif (ref $data) { - local $/; - # Store the content in a temp variable while we close the FH. - my $tmp = <$data>; - close $data; - $data = $tmp; - } + my $class = shift; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("INSERT INTO attach_data - (id, thedata) VALUES ($attachid, ?)"); + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); - trick_taint($data); - $sth->bind_param(1, $data, $dbh->BLOB_TYPE); - $sth->execute(); + # Extract everything which is not a valid column name. + my $bug = delete $params->{bug}; + $params->{bug_id} = $bug->id; + my $data = delete $params->{data}; + my $size = delete $params->{filesize}; - $attachment->{bug} = $bug; + my $attachment = $class->insert_create_data($params); + my $attachid = $attachment->id; - # Return the new attachment object. - return $attachment; -} + # The file is too large to be stored in the DB, so we store it locally. + if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { + my $attachdir = bz_locations()->{'attachdir'}; + my $hash = ($attachid % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + mkdir "$attachdir/$hash", 0770; + chmod 0770, "$attachdir/$hash"; + if (ref $data) { + copy($data, "$attachdir/$hash/attachment.$attachid"); + close $data; + } + else { + open(AH, '>', "$attachdir/$hash/attachment.$attachid"); + binmode AH; + print AH $data; + close AH; + } + $data = ''; # Will be stored in the DB. + } -sub run_create_validators { - my ($class, $params) = @_; + # If we have a filehandle, we need its content to store it in the DB. + elsif (ref $data) { + local $/; + + # Store the content in a temp variable while we close the FH. + my $tmp = <$data>; + close $data; + $data = $tmp; + } - $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); + my $sth = $dbh->prepare( + "INSERT INTO attach_data + (id, thedata) VALUES ($attachid, ?)" + ); - # 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); + trick_taint($data); + $sth->bind_param(1, $data, $dbh->BLOB_TYPE); + $sth->execute(); - $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $params->{modification_time} = $params->{creation_ts}; + $attachment->{bug} = $bug; - return $params; + # Return the new attachment object. + return $attachment; } -sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +sub run_create_validators { + my ($class, $params) = @_; - my ($changes, $old_self) = $self->SUPER::update(@_); + $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_self, $timestamp); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } + # 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); - # Record changes in the activity table. - require Bugzilla::Bug; - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; - $field = "attachments.$field" unless $field eq "flagtypes.name"; - Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], - $change->[1], $user->id, $timestamp, undef, $self->id); - } + $params->{creation_ts} + ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $params->{modification_time} = $params->{creation_ts}; - 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)); - $self->{modification_time} = $timestamp; - # because we updated the attachments table after SUPER::update(), we - # need to ensure the cache is flushed. - Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); - } + return $params; +} - return $changes; +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. + require Bugzilla::Bug; + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + $field = "attachments.$field" unless $field eq "flagtypes.name"; + Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], + $change->[1], $user->id, $timestamp, undef, $self->id); + } + + 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)); + $self->{modification_time} = $timestamp; + + # because we updated the attachments table after SUPER::update(), we + # need to ensure the cache is flushed. + Bugzilla->memcached->clear({table => 'attachments', id => $self->id}); + } + + return $changes; } =pod @@ -929,30 +955,33 @@ Returns: nothing =cut sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my $flag_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM flags WHERE attach_id = ?', undef, $self->id); - $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) - if @$flag_ids; - $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); - $dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? - WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id)); - $dbh->bz_commit_transaction(); - - my $filename = $self->_get_local_filename; - if (-e $filename) { - unlink $filename or warn "Couldn't unlink $filename: $!"; - } - - # As we don't call SUPER->remove_from_db we need to manually clear - # memcached here. - Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); - foreach my $flag_id (@$flag_ids) { - Bugzilla->memcached->clear({ table => 'flags', id => $flag_id }); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $flag_ids + = $dbh->selectcol_arrayref('SELECT id FROM flags WHERE attach_id = ?', + undef, $self->id); + $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) + if @$flag_ids; + $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); + $dbh->do( + 'UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? + WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id) + ); + $dbh->bz_commit_transaction(); + + my $filename = $self->_get_local_filename; + if (-e $filename) { + unlink $filename or warn "Couldn't unlink $filename: $!"; + } + + # As we don't call SUPER->remove_from_db we need to manually clear + # memcached here. + Bugzilla->memcached->clear({table => 'attachments', id => $self->id}); + foreach my $flag_id (@$flag_ids) { + Bugzilla->memcached->clear({table => 'flags', id => $flag_id}); + } } ############################### @@ -961,37 +990,39 @@ sub remove_from_db { # Extract the content type from the attachment form. sub get_content_type { - my $cgi = Bugzilla->cgi; + my $cgi = Bugzilla->cgi; - return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); + return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); - my $content_type; - my $method = $cgi->param('contenttypemethod') || ''; + my $content_type; + my $method = $cgi->param('contenttypemethod') || ''; - if ($method eq 'list') { - # The user selected a content type from the list, so use their - # selection. - $content_type = $cgi->param('contenttypeselection'); - } - elsif ($method eq 'manual') { - # The user entered a content type manually, so use their entry. - $content_type = $cgi->param('contenttypeentry'); - } - else { - 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'}; - $content_type || ThrowUserError("missing_content_type"); - - # Internet Explorer sends image/x-png for PNG images, - # so convert that to image/png to match other browsers. - if ($content_type eq 'image/x-png') { - $content_type = 'image/png'; - } + if ($method eq 'list') { + + # The user selected a content type from the list, so use their + # selection. + $content_type = $cgi->param('contenttypeselection'); + } + elsif ($method eq 'manual') { + + # The user entered a content type manually, so use their entry. + $content_type = $cgi->param('contenttypeentry'); + } + else { + 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'}; + $content_type || ThrowUserError("missing_content_type"); + + # Internet Explorer sends image/x-png for PNG images, + # so convert that to image/png to match other browsers. + if ($content_type eq 'image/x-png') { + $content_type = 'image/png'; } - return $content_type; + } + return $content_type; } diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm index d0e221220..01ed51518 100644 --- a/Bugzilla/Attachment/PatchReader.pm +++ b/Bugzilla/Attachment/PatchReader.pm @@ -23,184 +23,199 @@ use Bugzilla::Util; use constant PERLIO_IS_ENABLED => $Config{useperlio}; sub process_diff { - my ($attachment, $format) = @_; - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $lc = Bugzilla->localconfig; - my $vars = {}; - - require PatchReader::Raw; - my $reader = new PatchReader::Raw; - - if ($format eq 'raw') { - require PatchReader::DiffPrinter::raw; - $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); - # Actually print out the patch. - print $cgi->header(-type => 'text/plain'); - disable_utf8(); - $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); - } - else { - my @other_patches = (); - if ($lc->{interdiffbin} && $lc->{diffpath}) { - # Get the list of attachments that the user can view in this bug. - my @attachments = - @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug)}; - # Extract patches only. - @attachments = grep {$_->ispatch == 1} @attachments; - # We want them sorted from newer to older. - @attachments = sort { $b->id <=> $a->id } @attachments; - - # Ignore the current patch, but select the one right before it - # chronologically. - my $select_next_patch = 0; - foreach my $attach (@attachments) { - if ($attach->id == $attachment->id) { - $select_next_patch = 1; - } - else { - push(@other_patches, { 'id' => $attach->id, - 'desc' => $attach->description, - 'selected' => $select_next_patch }); - $select_next_patch = 0; - } - } - } + my ($attachment, $format) = @_; + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $lc = Bugzilla->localconfig; + my $vars = {}; - $vars->{'bugid'} = $attachment->bug_id; - $vars->{'attachid'} = $attachment->id; - $vars->{'description'} = $attachment->description; - $vars->{'other_patches'} = \@other_patches; - - setup_template_patch_reader($reader, $vars); - # The patch is going to be displayed in a HTML page and if the utf8 - # param is enabled, we have to encode attachment data as utf8. - if (Bugzilla->params->{'utf8'}) { - $attachment->data; # Populate ->{data} - utf8::decode($attachment->{data}); - } - $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); - } -} - -sub process_interdiff { - my ($old_attachment, $new_attachment, $format) = @_; - my $cgi = Bugzilla->cgi; - my $lc = Bugzilla->localconfig; - my $vars = {}; - - require PatchReader::Raw; - - # Encode attachment data as utf8 if it's going to be displayed in a HTML - # page using the UTF-8 encoding. - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - $old_attachment->data; # Populate ->{data} - utf8::decode($old_attachment->{data}); - $new_attachment->data; # Populate ->{data} - utf8::decode($new_attachment->{data}); - } - - # Get old patch data. - my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format); - # Get new patch data. - my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format); - - my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); - - # Send through interdiff, send output directly to template. - # Must hack path so that interdiff will work. - local $ENV{'PATH'} = $lc->{diffpath}; - - # Open the interdiff pipe, reading from both STDOUT and STDERR - # To avoid deadlocks, we have to read the entire output from all handles - my ($stdout, $stderr) = ('', ''); - my ($pid, $interdiff_stdout, $interdiff_stderr, $use_select); - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - require Apache2::SubProcess; - my $request = Apache2::RequestUtil->request; - (undef, $interdiff_stdout, $interdiff_stderr) = $request->spawn_proc_prog( - $lc->{interdiffbin}, [$old_filename, $new_filename] - ); - $use_select = !PERLIO_IS_ENABLED; - } else { - $interdiff_stderr = gensym; - $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, - $lc->{interdiffbin}, $old_filename, $new_filename); - $use_select = 1; - } + require PatchReader::Raw; + my $reader = new PatchReader::Raw; - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - binmode $interdiff_stdout, ':utf8'; - binmode $interdiff_stderr, ':utf8'; - } else { - binmode $interdiff_stdout; - binmode $interdiff_stderr; - } - - if ($use_select) { - my $select = IO::Select->new(); - $select->add($interdiff_stdout, $interdiff_stderr); - while (my @handles = $select->can_read) { - foreach my $handle (@handles) { - my $line = <$handle>; - if (!defined $line) { - $select->remove($handle); - next; - } - if ($handle == $interdiff_stdout) { - $stdout .= $line; - } else { - $stderr .= $line; - } + if ($format eq 'raw') { + require PatchReader::DiffPrinter::raw; + $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); + + # Actually print out the patch. + print $cgi->header(-type => 'text/plain'); + disable_utf8(); + $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); + } + else { + my @other_patches = (); + if ($lc->{interdiffbin} && $lc->{diffpath}) { + + # Get the list of attachments that the user can view in this bug. + my @attachments + = @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug)}; + + # Extract patches only. + @attachments = grep { $_->ispatch == 1 } @attachments; + + # We want them sorted from newer to older. + @attachments = sort { $b->id <=> $a->id } @attachments; + + # Ignore the current patch, but select the one right before it + # chronologically. + my $select_next_patch = 0; + foreach my $attach (@attachments) { + if ($attach->id == $attachment->id) { + $select_next_patch = 1; + } + else { + push( + @other_patches, + { + 'id' => $attach->id, + 'desc' => $attach->description, + 'selected' => $select_next_patch } + ); + $select_next_patch = 0; } - waitpid($pid, 0) if $pid; - - } else { - local $/ = undef; - $stdout = <$interdiff_stdout>; - $stdout //= ''; - $stderr = <$interdiff_stderr>; - $stderr //= ''; + } } - close($interdiff_stdout), - close($interdiff_stderr); + $vars->{'bugid'} = $attachment->bug_id; + $vars->{'attachid'} = $attachment->id; + $vars->{'description'} = $attachment->description; + $vars->{'other_patches'} = \@other_patches; - # Tidy up - unlink($old_filename) or warn "Could not unlink $old_filename: $!"; - unlink($new_filename) or warn "Could not unlink $new_filename: $!"; + setup_template_patch_reader($reader, $vars); - # Any output on STDERR means interdiff failed to full process the patches. - # Interdiff's error messages are generic and not useful to end users, so we - # show a generic failure message. - if ($stderr) { - warn($stderr); - $warning = 'interdiff3'; + # The patch is going to be displayed in a HTML page and if the utf8 + # param is enabled, we have to encode attachment data as utf8. + if (Bugzilla->params->{'utf8'}) { + $attachment->data; # Populate ->{data} + utf8::decode($attachment->{data}); } + $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); + } +} - my $reader = new PatchReader::Raw; - - if ($format eq 'raw') { - require PatchReader::DiffPrinter::raw; - $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); - # Actually print out the patch. - print $cgi->header(-type => 'text/plain'); - disable_utf8(); - } - else { - $vars->{'warning'} = $warning if $warning; - $vars->{'bugid'} = $new_attachment->bug_id; - $vars->{'oldid'} = $old_attachment->id; - $vars->{'old_desc'} = $old_attachment->description; - $vars->{'newid'} = $new_attachment->id; - $vars->{'new_desc'} = $new_attachment->description; - - setup_template_patch_reader($reader, $vars); +sub process_interdiff { + my ($old_attachment, $new_attachment, $format) = @_; + my $cgi = Bugzilla->cgi; + my $lc = Bugzilla->localconfig; + my $vars = {}; + + require PatchReader::Raw; + + # Encode attachment data as utf8 if it's going to be displayed in a HTML + # page using the UTF-8 encoding. + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { + $old_attachment->data; # Populate ->{data} + utf8::decode($old_attachment->{data}); + $new_attachment->data; # Populate ->{data} + utf8::decode($new_attachment->{data}); + } + + # Get old patch data. + my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format); + + # Get new patch data. + my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format); + + my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); + + # Send through interdiff, send output directly to template. + # Must hack path so that interdiff will work. + local $ENV{'PATH'} = $lc->{diffpath}; + + # Open the interdiff pipe, reading from both STDOUT and STDERR + # To avoid deadlocks, we have to read the entire output from all handles + my ($stdout, $stderr) = ('', ''); + my ($pid, $interdiff_stdout, $interdiff_stderr, $use_select); + if ($ENV{MOD_PERL}) { + require Apache2::RequestUtil; + require Apache2::SubProcess; + my $request = Apache2::RequestUtil->request; + (undef, $interdiff_stdout, $interdiff_stderr) + = $request->spawn_proc_prog($lc->{interdiffbin}, + [$old_filename, $new_filename]); + $use_select = !PERLIO_IS_ENABLED; + } + else { + $interdiff_stderr = gensym; + $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, $lc->{interdiffbin}, + $old_filename, $new_filename); + $use_select = 1; + } + + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { + binmode $interdiff_stdout, ':utf8'; + binmode $interdiff_stderr, ':utf8'; + } + else { + binmode $interdiff_stdout; + binmode $interdiff_stderr; + } + + if ($use_select) { + my $select = IO::Select->new(); + $select->add($interdiff_stdout, $interdiff_stderr); + while (my @handles = $select->can_read) { + foreach my $handle (@handles) { + my $line = <$handle>; + if (!defined $line) { + $select->remove($handle); + next; + } + if ($handle == $interdiff_stdout) { + $stdout .= $line; + } + else { + $stderr .= $line; + } + } } - $reader->iterate_string('interdiff #' . $old_attachment->id . - ' #' . $new_attachment->id, $stdout); + waitpid($pid, 0) if $pid; + + } + else { + local $/ = undef; + $stdout = <$interdiff_stdout>; + $stdout //= ''; + $stderr = <$interdiff_stderr>; + $stderr //= ''; + } + + close($interdiff_stdout), close($interdiff_stderr); + + # Tidy up + unlink($old_filename) or warn "Could not unlink $old_filename: $!"; + unlink($new_filename) or warn "Could not unlink $new_filename: $!"; + + # Any output on STDERR means interdiff failed to full process the patches. + # Interdiff's error messages are generic and not useful to end users, so we + # show a generic failure message. + if ($stderr) { + warn($stderr); + $warning = 'interdiff3'; + } + + my $reader = new PatchReader::Raw; + + if ($format eq 'raw') { + require PatchReader::DiffPrinter::raw; + $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); + + # Actually print out the patch. + print $cgi->header(-type => 'text/plain'); + disable_utf8(); + } + else { + $vars->{'warning'} = $warning if $warning; + $vars->{'bugid'} = $new_attachment->bug_id; + $vars->{'oldid'} = $old_attachment->id; + $vars->{'old_desc'} = $old_attachment->description; + $vars->{'newid'} = $new_attachment->id; + $vars->{'new_desc'} = $new_attachment->description; + + setup_template_patch_reader($reader, $vars); + } + $reader->iterate_string( + 'interdiff #' . $old_attachment->id . ' #' . $new_attachment->id, $stdout); } ###################### @@ -208,92 +223,92 @@ sub process_interdiff { ###################### sub get_unified_diff { - my ($attachment, $format) = @_; + my ($attachment, $format) = @_; - # Bring in the modules we need. - require PatchReader::Raw; - require PatchReader::DiffPrinter::raw; - require PatchReader::PatchInfoGrabber; - require File::Temp; - - $attachment->ispatch - || ThrowCodeError('must_be_patch', { 'attach_id' => $attachment->id }); - - # Reads in the patch, converting to unified diff in a temp file. - my $reader = new PatchReader::Raw; - my $last_reader = $reader; - - # Grabs the patch file info. - my $patch_info_grabber = new PatchReader::PatchInfoGrabber(); - $last_reader->sends_data_to($patch_info_grabber); - $last_reader = $patch_info_grabber; - - # Prints out to temporary file. - my ($fh, $filename) = File::Temp::tempfile(); - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - # The HTML page will be displayed with the UTF-8 encoding. - binmode $fh, ':utf8'; - } - my $raw_printer = new PatchReader::DiffPrinter::raw($fh); - $last_reader->sends_data_to($raw_printer); - $last_reader = $raw_printer; + # Bring in the modules we need. + require PatchReader::Raw; + require PatchReader::DiffPrinter::raw; + require PatchReader::PatchInfoGrabber; + require File::Temp; - # Iterate! - $reader->iterate_string($attachment->id, $attachment->data); + $attachment->ispatch + || ThrowCodeError('must_be_patch', {'attach_id' => $attachment->id}); - return ($filename, $patch_info_grabber->patch_info()->{files}); -} + # Reads in the patch, converting to unified diff in a temp file. + my $reader = new PatchReader::Raw; + my $last_reader = $reader; -sub warn_if_interdiff_might_fail { - my ($old_file_list, $new_file_list) = @_; + # Grabs the patch file info. + my $patch_info_grabber = new PatchReader::PatchInfoGrabber(); + $last_reader->sends_data_to($patch_info_grabber); + $last_reader = $patch_info_grabber; - # Verify that the list of files diffed is the same. - my @old_files = sort keys %{$old_file_list}; - my @new_files = sort keys %{$new_file_list}; - if (@old_files != @new_files - || join(' ', @old_files) ne join(' ', @new_files)) - { - return 'interdiff1'; - } + # Prints out to temporary file. + my ($fh, $filename) = File::Temp::tempfile(); + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - # Verify that the revisions in the files are the same. - foreach my $file (keys %{$old_file_list}) { - if (exists $old_file_list->{$file}{old_revision} - && exists $new_file_list->{$file}{old_revision} - && $old_file_list->{$file}{old_revision} ne - $new_file_list->{$file}{old_revision}) - { - return 'interdiff2'; - } - } - return undef; -} + # The HTML page will be displayed with the UTF-8 encoding. + binmode $fh, ':utf8'; + } + my $raw_printer = new PatchReader::DiffPrinter::raw($fh); + $last_reader->sends_data_to($raw_printer); + $last_reader = $raw_printer; -sub setup_template_patch_reader { - my ($last_reader, $vars) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; + # Iterate! + $reader->iterate_string($attachment->id, $attachment->data); - require PatchReader::DiffPrinter::template; + return ($filename, $patch_info_grabber->patch_info()->{files}); +} - # Define the vars for templates. - if (defined $cgi->param('headers')) { - $vars->{'headers'} = $cgi->param('headers'); - } - else { - $vars->{'headers'} = 1; +sub warn_if_interdiff_might_fail { + my ($old_file_list, $new_file_list) = @_; + + # Verify that the list of files diffed is the same. + my @old_files = sort keys %{$old_file_list}; + my @new_files = sort keys %{$new_file_list}; + if (@old_files != @new_files || join(' ', @old_files) ne join(' ', @new_files)) + { + return 'interdiff1'; + } + + # Verify that the revisions in the files are the same. + foreach my $file (keys %{$old_file_list}) { + if ( exists $old_file_list->{$file}{old_revision} + && exists $new_file_list->{$file}{old_revision} + && $old_file_list->{$file}{old_revision} ne + $new_file_list->{$file}{old_revision}) + { + return 'interdiff2'; } + } + return undef; +} - $vars->{'collapsed'} = $cgi->param('collapsed'); - - # Print everything out. - print $cgi->header(-type => 'text/html'); - - $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template, - 'attachment/diff-header.html.tmpl', - 'attachment/diff-file.html.tmpl', - 'attachment/diff-footer.html.tmpl', - $vars)); +sub setup_template_patch_reader { + my ($last_reader, $vars) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + + require PatchReader::DiffPrinter::template; + + # Define the vars for templates. + if (defined $cgi->param('headers')) { + $vars->{'headers'} = $cgi->param('headers'); + } + else { + $vars->{'headers'} = 1; + } + + $vars->{'collapsed'} = $cgi->param('collapsed'); + + # Print everything out. + print $cgi->header(-type => 'text/html'); + + $last_reader->sends_data_to(new PatchReader::DiffPrinter::template( + $template, 'attachment/diff-header.html.tmpl', + 'attachment/diff-file.html.tmpl', 'attachment/diff-footer.html.tmpl', + $vars + )); } 1; diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index c830f0506..23de9b4bd 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -12,9 +12,9 @@ use strict; use warnings; use fields qw( - _info_getter - _verifier - _persister + _info_getter + _verifier + _persister ); use Bugzilla::Constants; @@ -28,218 +28,223 @@ use Bugzilla::Auth::Persist::Cookie; use Socket; sub new { - my ($class, $params) = @_; - my $self = fields::new($class); + my ($class, $params) = @_; + my $self = fields::new($class); - $params ||= {}; - $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie,APIKey'; - $params->{Verify} ||= Bugzilla->params->{'user_verify_class'}; + $params ||= {}; + $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie,APIKey'; + $params->{Verify} ||= Bugzilla->params->{'user_verify_class'}; - $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); - $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify}); - # If we ever have any other login persistence methods besides cookies, - # this could become more configurable. - $self->{_persister} = new Bugzilla::Auth::Persist::Cookie(); + $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); + $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify}); - return $self; + # If we ever have any other login persistence methods besides cookies, + # this could become more configurable. + $self->{_persister} = new Bugzilla::Auth::Persist::Cookie(); + + return $self; } sub login { - my ($self, $type) = @_; - - # Get login info from the cookie, form, environment variables, etc. - my $login_info = $self->{_info_getter}->get_login_info(); + my ($self, $type) = @_; - if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); - } + # Get login info from the cookie, form, environment variables, etc. + my $login_info = $self->{_info_getter}->get_login_info(); - # Now verify their username and password against the DB, LDAP, etc. - if ($self->{_info_getter}->{successful}->requires_verification) { - $login_info = $self->{_verifier}->check_credentials($login_info); - if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); - } - $login_info = - $self->{_verifier}->{successful}->create_or_update_user($login_info); - } - else { - $login_info = $self->{_verifier}->create_or_update_user($login_info); - } + if ($login_info->{failure}) { + return $self->_handle_login_result($login_info, $type); + } + # Now verify their username and password against the DB, LDAP, etc. + if ($self->{_info_getter}->{successful}->requires_verification) { + $login_info = $self->{_verifier}->check_credentials($login_info); if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); + return $self->_handle_login_result($login_info, $type); } + $login_info + = $self->{_verifier}->{successful}->create_or_update_user($login_info); + } + else { + $login_info = $self->{_verifier}->create_or_update_user($login_info); + } + + if ($login_info->{failure}) { + return $self->_handle_login_result($login_info, $type); + } - # Make sure the user isn't disabled. - my $user = $login_info->{user}; - if (!$user->is_enabled) { - return $self->_handle_login_result({ failure => AUTH_DISABLED, - user => $user }, $type); - } - $user->set_authorizer($self); + # Make sure the user isn't disabled. + my $user = $login_info->{user}; + if (!$user->is_enabled) { + return $self->_handle_login_result({failure => AUTH_DISABLED, user => $user}, + $type); + } + $user->set_authorizer($self); - return $self->_handle_login_result($login_info, $type); + return $self->_handle_login_result($login_info, $type); } sub can_change_password { - my ($self) = @_; - my $verifier = $self->{_verifier}->{successful}; - $verifier ||= $self->{_verifier}; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $verifier->can_change_password && - $getter->user_can_create_account; + my ($self) = @_; + my $verifier = $self->{_verifier}->{successful}; + $verifier ||= $self->{_verifier}; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $verifier->can_change_password && $getter->user_can_create_account; } sub can_login { - my ($self) = @_; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $getter->can_login; + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $getter->can_login; } sub can_logout { - my ($self) = @_; - my $getter = $self->{_info_getter}->{successful}; - # If there's no successful getter, we're not logged in, so of - # course we can't log out! - return 0 unless $getter; - return $getter->can_logout; + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + + # If there's no successful getter, we're not logged in, so of + # course we can't log out! + return 0 unless $getter; + return $getter->can_logout; } sub login_token { - my ($self) = @_; - my $getter = $self->{_info_getter}->{successful}; - if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) { - return $getter->login_token; - } - return undef; + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) { + return $getter->login_token; + } + return undef; } sub user_can_create_account { - my ($self) = @_; - my $verifier = $self->{_verifier}->{successful}; - $verifier ||= $self->{_verifier}; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $verifier->user_can_create_account - && $getter->user_can_create_account; + my ($self) = @_; + my $verifier = $self->{_verifier}->{successful}; + $verifier ||= $self->{_verifier}; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $verifier->user_can_create_account && $getter->user_can_create_account; } sub extern_id_used { - my ($self) = @_; - return $self->{_info_getter}->extern_id_used - || $self->{_verifier}->extern_id_used; + my ($self) = @_; + return $self->{_info_getter}->extern_id_used + || $self->{_verifier}->extern_id_used; } sub can_change_email { - return $_[0]->user_can_create_account; + return $_[0]->user_can_create_account; } sub _handle_login_result { - my ($self, $result, $login_type) = @_; - my $dbh = Bugzilla->dbh; - - my $user = $result->{user}; - my $fail_code = $result->{failure}; - - if (!$fail_code) { - # We don't persist logins over GET requests in the WebService, - # because the persistance information can't be re-used again. - # (See Bugzilla::WebService::Server::JSONRPC for more info.) - if ($self->{_info_getter}->{successful}->requires_persistence - and !Bugzilla->request_cache->{auth_no_automatic_login}) - { - $user->{_login_token} = $self->{_persister}->persist_login($user); - } - } - elsif ($fail_code == AUTH_ERROR) { - if ($result->{user_error}) { - ThrowUserError($result->{user_error}, $result->{details}); - } - else { - ThrowCodeError($result->{error}, $result->{details}); - } - } - elsif ($fail_code == AUTH_NODATA) { - $self->{_info_getter}->fail_nodata($self) - if $login_type == LOGIN_REQUIRED; + my ($self, $result, $login_type) = @_; + my $dbh = Bugzilla->dbh; - # If we're not LOGIN_REQUIRED, we just return the default user. - $user = Bugzilla->user; - } - # The username/password may be wrong - # Don't let the user know whether the username exists or whether - # 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) { - my $remaining_attempts = MAX_LOGIN_ATTEMPTS - - ($result->{failure_count} || 0); - ThrowUserError("invalid_login_or_password", - { remaining => $remaining_attempts }); - } - # The account may be disabled - elsif ($fail_code == AUTH_DISABLED) { - $self->{_persister}->logout(); - # XXX This is NOT a good way to do this, architecturally. - $self->{_persister}->clear_browser_cookies(); - # and throw a user error - ThrowUserError("account_disabled", - {'disabled_reason' => $result->{user}->disabledtext}); + my $user = $result->{user}; + my $fail_code = $result->{failure}; + + if (!$fail_code) { + + # We don't persist logins over GET requests in the WebService, + # because the persistance information can't be re-used again. + # (See Bugzilla::WebService::Server::JSONRPC for more info.) + if ($self->{_info_getter}->{successful}->requires_persistence + and !Bugzilla->request_cache->{auth_no_automatic_login}) + { + $user->{_login_token} = $self->{_persister}->persist_login($user); } - 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}->{default_value}); - my $address = $attempts->[0]->{ip_addr}; - # Note: inet_aton will only resolve IPv4 addresses. - # For IPv6 we'll need to use inet_pton which requires Perl 5.12. - my $n = inet_aton($address); - if ($n) { - $address = gethostbyaddr($n, AF_INET) . " ($address)" - } - my $vars = { - locked_user => $user, - attempts => $attempts, - unlock_at => $unlock_at, - address => $address, - }; - 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 }); + } + elsif ($fail_code == AUTH_ERROR) { + if ($result->{user_error}) { + ThrowUserError($result->{user_error}, $result->{details}); } - # If we get here, then we've run out of options, which shouldn't happen. else { - ThrowCodeError("authres_unhandled", { value => $fail_code }); + ThrowCodeError($result->{error}, $result->{details}); + } + } + elsif ($fail_code == AUTH_NODATA) { + $self->{_info_getter}->fail_nodata($self) if $login_type == LOGIN_REQUIRED; + + # If we're not LOGIN_REQUIRED, we just return the default user. + $user = Bugzilla->user; + } + + # The username/password may be wrong + # Don't let the user know whether the username exists or whether + # 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) { + my $remaining_attempts = MAX_LOGIN_ATTEMPTS - ($result->{failure_count} || 0); + ThrowUserError("invalid_login_or_password", {remaining => $remaining_attempts}); + } + + # The account may be disabled + elsif ($fail_code == AUTH_DISABLED) { + $self->{_persister}->logout(); + + # XXX This is NOT a good way to do this, architecturally. + $self->{_persister}->clear_browser_cookies(); + + # and throw a user error + 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}->{default_value}); + my $address = $attempts->[0]->{ip_addr}; + + # Note: inet_aton will only resolve IPv4 addresses. + # For IPv6 we'll need to use inet_pton which requires Perl 5.12. + my $n = inet_aton($address); + if ($n) { + $address = gethostbyaddr($n, AF_INET) . " ($address)"; + } + my $vars = { + locked_user => $user, + attempts => $attempts, + unlock_at => $unlock_at, + address => $address, + }; + my $message; + $template->process('email/lockout.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error); + MessageToMTA($message); } - return $user; + $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}); + } + + return $user; } 1; diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm index a5f089777..68c7464f2 100644 --- a/Bugzilla/Auth/Login.pm +++ b/Bugzilla/Auth/Login.pm @@ -16,18 +16,18 @@ use fields qw(); # Determines whether or not a user can logout. It's really a subroutine, # but we implement it here as a constant. Override it in subclasses if # that particular type of login method cannot log out. -use constant can_logout => 1; -use constant can_login => 1; -use constant requires_persistence => 1; -use constant requires_verification => 1; +use constant can_logout => 1; +use constant can_login => 1; +use constant requires_persistence => 1; +use constant requires_verification => 1; use constant user_can_create_account => 0; -use constant is_automatic => 0; -use constant extern_id_used => 0; +use constant is_automatic => 0; +use constant extern_id_used => 0; sub new { - my ($class) = @_; - my $self = fields::new($class); - return $self; + my ($class) = @_; + my $self = fields::new($class); + return $self; } 1; diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm index 63e35578a..79c16825e 100644 --- a/Bugzilla/Auth/Login/APIKey.pm +++ b/Bugzilla/Auth/Login/APIKey.pm @@ -26,28 +26,29 @@ use constant can_logout => 0; # This method is only available to web services. An API key can never # be used to authenticate a Web request. sub get_login_info { - my ($self) = @_; - my $params = Bugzilla->input_params; - my ($user_id, $login_cookie); + my ($self) = @_; + my $params = Bugzilla->input_params; + my ($user_id, $login_cookie); - my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); - if (!i_am_webservice() || !$api_key_text) { - return { failure => AUTH_NODATA }; - } + my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); + if (!i_am_webservice() || !$api_key_text) { + return {failure => AUTH_NODATA}; + } - my $api_key = Bugzilla::User::APIKey->new({ name => $api_key_text }); + my $api_key = Bugzilla::User::APIKey->new({name => $api_key_text}); - if (!$api_key or $api_key->api_key ne $api_key_text) { - # The second part checks the correct capitalisation. Silly MySQL - ThrowUserError("api_key_not_valid"); - } - elsif ($api_key->revoked) { - ThrowUserError('api_key_revoked'); - } + if (!$api_key or $api_key->api_key ne $api_key_text) { - $api_key->update_last_used(); + # The second part checks the correct capitalisation. Silly MySQL + ThrowUserError("api_key_not_valid"); + } + elsif ($api_key->revoked) { + ThrowUserError('api_key_revoked'); + } - return { user_id => $api_key->user_id }; + $api_key->update_last_used(); + + return {user_id => $api_key->user_id}; } 1; diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm index 6003d62a5..0824f1ebd 100644 --- a/Bugzilla/Auth/Login/CGI.pm +++ b/Bugzilla/Auth/Login/CGI.pm @@ -21,65 +21,71 @@ use Bugzilla::Error; use Bugzilla::Token; sub get_login_info { - my ($self) = @_; - my $params = Bugzilla->input_params; - my $cgi = Bugzilla->cgi; - - my $login = trim(delete $params->{'Bugzilla_login'}); - my $password = delete $params->{'Bugzilla_password'}; - # The token must match the cookie to authenticate the request. - my $login_token = delete $params->{'Bugzilla_login_token'}; - my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie'); - - my $valid = 0; - # If the web browser accepts cookies, use them. - if ($login_token && $login_cookie) { - my ($time, undef) = split(/-/, $login_token); - # Regenerate the token based on the information we have. - my $expected_token = issue_hash_token(['login_request', $login_cookie], $time); - $valid = 1 if $expected_token eq $login_token; - $cgi->remove_cookie('Bugzilla_login_request_cookie'); - } - # WebServices and other local scripts can bypass this check. - # This is safe because we won't store a login cookie in this case. - elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { - $valid = 1; - } - # Else falls back to the Referer header and accept local URLs. - # Attachments are served from a separate host (ideally), and so - # an evil attachment cannot abuse this check with a redirect. - elsif (my $referer = $cgi->referer) { - my $urlbase = correct_urlbase(); - $valid = 1 if $referer =~ /^\Q$urlbase\E/; - } - # If the web browser doesn't accept cookies and the Referer header - # is missing, we have no way to make sure that the authentication - # request comes from the user. - elsif ($login && $password) { - ThrowUserError('auth_untrusted_request', { login => $login }); - } - - if (!defined($login) || !defined($password) || !$valid) { - return { failure => AUTH_NODATA }; - } - - return { username => $login, password => $password }; + my ($self) = @_; + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + + my $login = trim(delete $params->{'Bugzilla_login'}); + my $password = delete $params->{'Bugzilla_password'}; + + # The token must match the cookie to authenticate the request. + my $login_token = delete $params->{'Bugzilla_login_token'}; + my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie'); + + my $valid = 0; + + # If the web browser accepts cookies, use them. + if ($login_token && $login_cookie) { + my ($time, undef) = split(/-/, $login_token); + + # Regenerate the token based on the information we have. + my $expected_token = issue_hash_token(['login_request', $login_cookie], $time); + $valid = 1 if $expected_token eq $login_token; + $cgi->remove_cookie('Bugzilla_login_request_cookie'); + } + + # WebServices and other local scripts can bypass this check. + # This is safe because we won't store a login cookie in this case. + elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { + $valid = 1; + } + + # Else falls back to the Referer header and accept local URLs. + # Attachments are served from a separate host (ideally), and so + # an evil attachment cannot abuse this check with a redirect. + elsif (my $referer = $cgi->referer) { + my $urlbase = correct_urlbase(); + $valid = 1 if $referer =~ /^\Q$urlbase\E/; + } + + # If the web browser doesn't accept cookies and the Referer header + # is missing, we have no way to make sure that the authentication + # request comes from the user. + elsif ($login && $password) { + ThrowUserError('auth_untrusted_request', {login => $login}); + } + + if (!defined($login) || !defined($password) || !$valid) { + return {failure => AUTH_NODATA}; + } + + return {username => $login, password => $password}; } sub fail_nodata { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - - if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { - ThrowUserError('login_required'); - } - - print $cgi->header(); - $template->process("account/auth/login.html.tmpl", - { 'target' => $cgi->url(-relative=>1) }) - || ThrowTemplateError($template->error()); - exit; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + + if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { + ThrowUserError('login_required'); + } + + print $cgi->header(); + $template->process("account/auth/login.html.tmpl", + {'target' => $cgi->url(-relative => 1)}) + || ThrowTemplateError($template->error()); + exit; } 1; diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm index c09f08d24..1983dbd4c 100644 --- a/Bugzilla/Auth/Login/Cookie.pm +++ b/Bugzilla/Auth/Login/Cookie.pm @@ -23,121 +23,124 @@ use List::Util qw(first); use constant requires_persistence => 0; use constant requires_verification => 0; -use constant can_login => 0; +use constant can_login => 0; sub is_automatic { return $_[0]->login_token ? 0 : 1; } # Note that Cookie never consults the Verifier, it always assumes # it has a valid DB account or it fails. sub get_login_info { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my ($user_id, $login_cookie); - - if (!Bugzilla->request_cache->{auth_no_automatic_login}) { - $login_cookie = $cgi->cookie("Bugzilla_logincookie"); - $user_id = $cgi->cookie("Bugzilla_login"); - - # If cookies cannot be found, this could mean that they haven't - # been made available yet. In this case, look at Bugzilla_cookie_list. - unless ($login_cookie) { - my $cookie = first {$_->name eq 'Bugzilla_logincookie'} - @{$cgi->{'Bugzilla_cookie_list'}}; - $login_cookie = $cookie->value if $cookie; - } - unless ($user_id) { - my $cookie = first {$_->name eq 'Bugzilla_login'} - @{$cgi->{'Bugzilla_cookie_list'}}; - $user_id = $cookie->value if $cookie; - } - - # If the call is for a web service, and an api token is provided, check - # it is valid. - if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) { - my $api_token = Bugzilla->input_params->{Bugzilla_api_token}; - my ($token_user_id, undef, undef, $token_type) - = Bugzilla::Token::GetTokenData($api_token); - if (!defined $token_type - || $token_type ne 'api_token' - || $user_id != $token_user_id) - { - ThrowUserError('auth_invalid_token', { token => $api_token }); - } - } + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my ($user_id, $login_cookie); + + if (!Bugzilla->request_cache->{auth_no_automatic_login}) { + $login_cookie = $cgi->cookie("Bugzilla_logincookie"); + $user_id = $cgi->cookie("Bugzilla_login"); + + # If cookies cannot be found, this could mean that they haven't + # been made available yet. In this case, look at Bugzilla_cookie_list. + unless ($login_cookie) { + my $cookie = first { $_->name eq 'Bugzilla_logincookie' } + @{$cgi->{'Bugzilla_cookie_list'}}; + $login_cookie = $cookie->value if $cookie; + } + unless ($user_id) { + my $cookie = first { $_->name eq 'Bugzilla_login' } + @{$cgi->{'Bugzilla_cookie_list'}}; + $user_id = $cookie->value if $cookie; } - # If no cookies were provided, we also look for a login token - # passed in the parameters of a webservice - my $token = $self->login_token; - if ($token && (!$login_cookie || !$user_id)) { - ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'}); + # If the call is for a web service, and an api token is provided, check + # it is valid. + if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) { + my $api_token = Bugzilla->input_params->{Bugzilla_api_token}; + my ($token_user_id, undef, undef, $token_type) + = Bugzilla::Token::GetTokenData($api_token); + if ( !defined $token_type + || $token_type ne 'api_token' + || $user_id != $token_user_id) + { + ThrowUserError('auth_invalid_token', {token => $api_token}); + } } + } + + # If no cookies were provided, we also look for a login token + # passed in the parameters of a webservice + my $token = $self->login_token; + if ($token && (!$login_cookie || !$user_id)) { + ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'}); + } - my $ip_addr = remote_ip(); + my $ip_addr = remote_ip(); - if ($login_cookie && $user_id) { - # Anything goes for these params - they're just strings which - # we're going to verify against the db - trick_taint($ip_addr); - trick_taint($login_cookie); - detaint_natural($user_id); + if ($login_cookie && $user_id) { - my $db_cookie = - $dbh->selectrow_array('SELECT cookie + # Anything goes for these params - they're just strings which + # we're going to verify against the db + trick_taint($ip_addr); + trick_taint($login_cookie); + detaint_natural($user_id); + + my $db_cookie = $dbh->selectrow_array( + 'SELECT cookie FROM logincookies WHERE cookie = ? AND userid = ? - AND (ipaddr = ? OR ipaddr IS NULL)', - undef, ($login_cookie, $user_id, $ip_addr)); - - # If the cookie or token is valid, return a valid username. - # If they were not valid and we are using a webservice, then - # throw an error notifying the client. - if (defined $db_cookie && $login_cookie eq $db_cookie) { - # If we logged in successfully, then update the lastused - # time on the login cookie - $dbh->do("UPDATE logincookies SET lastused = NOW() - WHERE cookie = ?", undef, $login_cookie); - return { user_id => $user_id }; - } - elsif (i_am_webservice()) { - ThrowUserError('invalid_cookies_or_token'); - } + AND (ipaddr = ? OR ipaddr IS NULL)', undef, + ($login_cookie, $user_id, $ip_addr) + ); + + # If the cookie or token is valid, return a valid username. + # If they were not valid and we are using a webservice, then + # throw an error notifying the client. + if (defined $db_cookie && $login_cookie eq $db_cookie) { + + # If we logged in successfully, then update the lastused + # time on the login cookie + $dbh->do( + "UPDATE logincookies SET lastused = NOW() + WHERE cookie = ?", undef, $login_cookie + ); + return {user_id => $user_id}; } - - # Either the cookie or token is invalid and we are not authenticating - # via a webservice, or we did not receive a cookie or token. We don't - # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to - # actually throw an error when it gets a bad cookie or token. It should just - # look like there was no cookie or token to begin with. - return { failure => AUTH_NODATA }; + elsif (i_am_webservice()) { + ThrowUserError('invalid_cookies_or_token'); + } + } + + # Either the cookie or token is invalid and we are not authenticating + # via a webservice, or we did not receive a cookie or token. We don't + # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to + # actually throw an error when it gets a bad cookie or token. It should just + # look like there was no cookie or token to begin with. + return {failure => AUTH_NODATA}; } sub login_token { - my ($self) = @_; - my $input = Bugzilla->input_params; - my $usage_mode = Bugzilla->usage_mode; + my ($self) = @_; + my $input = Bugzilla->input_params; + my $usage_mode = Bugzilla->usage_mode; - return $self->{'_login_token'} if exists $self->{'_login_token'}; + return $self->{'_login_token'} if exists $self->{'_login_token'}; - if (!i_am_webservice()) { - return $self->{'_login_token'} = undef; - } + if (!i_am_webservice()) { + return $self->{'_login_token'} = undef; + } - # Check if a token was passed in via requests for WebServices - my $token = trim(delete $input->{'Bugzilla_token'}); - return $self->{'_login_token'} = undef if !$token; + # Check if a token was passed in via requests for WebServices + my $token = trim(delete $input->{'Bugzilla_token'}); + return $self->{'_login_token'} = undef if !$token; - my ($user_id, $login_token) = split('-', $token, 2); - if (!detaint_natural($user_id) || !$login_token) { - return $self->{'_login_token'} = undef; - } + my ($user_id, $login_token) = split('-', $token, 2); + if (!detaint_natural($user_id) || !$login_token) { + return $self->{'_login_token'} = undef; + } - return $self->{'_login_token'} = { - user_id => $user_id, - login_token => $login_token - }; + return $self->{'_login_token'} + = {user_id => $user_id, login_token => $login_token}; } 1; diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm index 653df2bb3..5fc33921b 100644 --- a/Bugzilla/Auth/Login/Env.pm +++ b/Bugzilla/Auth/Login/Env.pm @@ -16,28 +16,31 @@ use parent qw(Bugzilla::Auth::Login); use Bugzilla::Constants; use Bugzilla::Error; -use constant can_logout => 0; -use constant can_login => 0; +use constant can_logout => 0; +use constant can_login => 0; use constant requires_persistence => 0; use constant requires_verification => 0; -use constant is_automatic => 1; -use constant extern_id_used => 1; +use constant is_automatic => 1; +use constant extern_id_used => 1; sub get_login_info { - my ($self) = @_; + my ($self) = @_; - my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || ''; - my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || ''; - my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || ''; + my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || ''; + my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || ''; + my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || ''; - return { failure => AUTH_NODATA } if !$env_email; + return {failure => AUTH_NODATA} if !$env_email; - return { username => $env_email, extern_id => $env_id, - realname => $env_realname }; + return { + username => $env_email, + extern_id => $env_id, + realname => $env_realname + }; } sub fail_nodata { - ThrowCodeError('env_no_email'); + ThrowCodeError('env_no_email'); } 1; diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm index dc35998e4..7786f26c8 100644 --- a/Bugzilla/Auth/Login/Stack.pm +++ b/Bugzilla/Auth/Login/Stack.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Bugzilla::Auth::Login); use fields qw( - _stack - successful + _stack + successful ); use Hash::Util qw(lock_keys); use Bugzilla::Hook; @@ -22,81 +22,87 @@ use Bugzilla::Constants; use List::MoreUtils qw(any); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - my $list = shift; - my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list); - lock_keys(%methods); - Bugzilla::Hook::process('auth_login_methods', { modules => \%methods }); - - $self->{_stack} = []; - foreach my $login_method (split(',', $list)) { - my $module = $methods{$login_method}; - require $module; - $module =~ s|/|::|g; - $module =~ s/.pm$//; - push(@{$self->{_stack}}, $module->new(@_)); - } - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + my $list = shift; + my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list); + lock_keys(%methods); + Bugzilla::Hook::process('auth_login_methods', {modules => \%methods}); + + $self->{_stack} = []; + foreach my $login_method (split(',', $list)) { + my $module = $methods{$login_method}; + require $module; + $module =~ s|/|::|g; + $module =~ s/.pm$//; + push(@{$self->{_stack}}, $module->new(@_)); + } + return $self; } sub get_login_info { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - # See Bugzilla::WebService::Server::JSONRPC for where and why - # auth_no_automatic_login is used. - if (Bugzilla->request_cache->{auth_no_automatic_login}) { - next if $object->is_automatic; - } - $result = $object->get_login_info(@_); - $self->{successful} = $object; - - # We only carry on down the stack if this method denied all knowledge. - last unless ($result->{failure} - && ($result->{failure} eq AUTH_NODATA - || $result->{failure} eq AUTH_NO_SUCH_USER)); - - # If none of the methods succeed, it's undef. - $self->{successful} = undef; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + + # See Bugzilla::WebService::Server::JSONRPC for where and why + # auth_no_automatic_login is used. + if (Bugzilla->request_cache->{auth_no_automatic_login}) { + next if $object->is_automatic; } - return $result; + $result = $object->get_login_info(@_); + $self->{successful} = $object; + + # We only carry on down the stack if this method denied all knowledge. + last + unless ($result->{failure} + && ( $result->{failure} eq AUTH_NODATA + || $result->{failure} eq AUTH_NO_SUCH_USER)); + + # If none of the methods succeed, it's undef. + $self->{successful} = undef; + } + return $result; } sub fail_nodata { - my $self = shift; - # We fail from the bottom of the stack. - my @reverse_stack = reverse @{$self->{_stack}}; - foreach my $object (@reverse_stack) { - # We pick the first object that actually has the method - # implemented. - if ($object->can('fail_nodata')) { - $object->fail_nodata(@_); - } + my $self = shift; + + # We fail from the bottom of the stack. + my @reverse_stack = reverse @{$self->{_stack}}; + foreach my $object (@reverse_stack) { + + # We pick the first object that actually has the method + # implemented. + if ($object->can('fail_nodata')) { + $object->fail_nodata(@_); } + } } sub can_login { - my ($self) = @_; - # We return true if any method can log in. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->can_login; - } - return 0; + my ($self) = @_; + + # We return true if any method can log in. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->can_login; + } + return 0; } sub user_can_create_account { - my ($self) = @_; - # We return true if any method allows users to create accounts. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->user_can_create_account; - } - return 0; + my ($self) = @_; + + # We return true if any method allows users to create accounts. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->user_can_create_account; + } + return 0; } sub extern_id_used { - my ($self) = @_; - return any { $_->extern_id_used } @{ $self->{_stack} }; + my ($self) = @_; + return any { $_->extern_id_used } @{$self->{_stack}}; } 1; diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm index 2d1291f3b..af6b0d77d 100644 --- a/Bugzilla/Auth/Persist/Cookie.pm +++ b/Bugzilla/Auth/Persist/Cookie.pm @@ -20,145 +20,154 @@ use Bugzilla::Token; use List::Util qw(first); sub new { - my ($class) = @_; - my $self = fields::new($class); - return $self; + my ($class) = @_; + my $self = fields::new($class); + return $self; } sub persist_login { - my ($self, $user) = @_; - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $input_params = Bugzilla->input_params; - - 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); - } - - $dbh->bz_start_transaction(); - - my $login_cookie = - Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); - - $dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused) - VALUES (?, ?, ?, NOW())", - undef, $login_cookie, $user->id, $ip_addr); - - # Issuing a new cookie is a good time to clean up the old - # cookies. - $dbh->do("DELETE FROM logincookies WHERE lastused < " - . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', - MAX_LOGINCOOKIE_AGE, 'DAY')); - - $dbh->bz_commit_transaction(); - - # We do not want WebServices to generate login cookies. - # All we need is the login token for User.login. - return $login_cookie if i_am_webservice(); - - # Prevent JavaScript from accessing login cookies. - my %cookieargs = ('-httponly' => 1); - - # Remember cookie only if admin has told so - # or admin didn't forbid it and user told to remember. - if ( Bugzilla->params->{'rememberlogin'} eq 'on' || - (Bugzilla->params->{'rememberlogin'} ne 'off' && - $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_redirect'}) { - # Make these cookies only be sent to us by the browser during - # HTTPS sessions, if we're using SSL. - $cookieargs{'-secure'} = 1; - } - - $cgi->send_cookie(-name => 'Bugzilla_login', - -value => $user->id, - %cookieargs); - $cgi->send_cookie(-name => 'Bugzilla_logincookie', - -value => $login_cookie, - %cookieargs); + my ($self, $user) = @_; + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $input_params = Bugzilla->input_params; + + 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); + } + + $dbh->bz_start_transaction(); + + my $login_cookie + = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); + + $dbh->do( + "INSERT INTO logincookies (cookie, userid, ipaddr, lastused) + VALUES (?, ?, ?, NOW())", undef, $login_cookie, $user->id, $ip_addr + ); + + # Issuing a new cookie is a good time to clean up the old + # cookies. + $dbh->do("DELETE FROM logincookies WHERE lastused < " + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', MAX_LOGINCOOKIE_AGE, 'DAY')); + + $dbh->bz_commit_transaction(); + + # We do not want WebServices to generate login cookies. + # All we need is the login token for User.login. + return $login_cookie if i_am_webservice(); + + # Prevent JavaScript from accessing login cookies. + my %cookieargs = ('-httponly' => 1); + + # Remember cookie only if admin has told so + # or admin didn't forbid it and user told to remember. + if ( + Bugzilla->params->{'rememberlogin'} eq 'on' + || ( Bugzilla->params->{'rememberlogin'} ne 'off' + && $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_redirect'}) { + + # Make these cookies only be sent to us by the browser during + # HTTPS sessions, if we're using SSL. + $cookieargs{'-secure'} = 1; + } + + $cgi->send_cookie(-name => 'Bugzilla_login', -value => $user->id, %cookieargs); + $cgi->send_cookie( + -name => 'Bugzilla_logincookie', + -value => $login_cookie, + %cookieargs + ); } sub logout { - my ($self, $param) = @_; - - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $input = Bugzilla->input_params; - $param = {} unless $param; - my $user = $param->{user} || Bugzilla->user; - my $type = $param->{type} || LOGOUT_ALL; - - if ($type == LOGOUT_ALL) { - $dbh->do("DELETE FROM logincookies WHERE userid = ?", - undef, $user->id); - return; - } - - # The LOGOUT_*_CURRENT options require the current login cookie. - # If a new cookie has been issued during this run, that's the current one. - # If not, it's the one we've received. - my @login_cookies; - my $cookie = first {$_->name eq 'Bugzilla_logincookie'} - @{$cgi->{'Bugzilla_cookie_list'}}; - if ($cookie) { - push(@login_cookies, $cookie->value); - } - elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) { - push(@login_cookies, $cookie); - } - - # If we are a webservice using a token instead of cookie - # then add that as well to the login cookies to delete - if (my $login_token = $user->authorizer->login_token) { - push(@login_cookies, $login_token->{'login_token'}); - } - - # Make sure that @login_cookies is not empty to not break SQL statements. - push(@login_cookies, '') unless @login_cookies; - - # These queries use both the cookie ID and the user ID as keys. Even - # though we know the userid must match, we still check it in the SQL - # as a sanity check, since there is no locking here, and if the user - # logged out from two machines simultaneously, while someone else - # logged in and got the same cookie, we could be logging the other - # user out here. Yes, this is very very very unlikely, but why take - # chances? - bbaetz - map { trick_taint($_) } @login_cookies; - @login_cookies = map { $dbh->quote($_) } @login_cookies; - if ($type == LOGOUT_KEEP_CURRENT) { - $dbh->do("DELETE FROM logincookies WHERE " . - $dbh->sql_in('cookie', \@login_cookies, 1) . - " AND userid = ?", - undef, $user->id); - } elsif ($type == LOGOUT_CURRENT) { - $dbh->do("DELETE FROM logincookies WHERE " . - $dbh->sql_in('cookie', \@login_cookies) . - " AND userid = ?", - undef, $user->id); - } else { - die("Invalid type $type supplied to logout()"); - } - - if ($type != LOGOUT_KEEP_CURRENT) { - clear_browser_cookies(); - } + my ($self, $param) = @_; + + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $input = Bugzilla->input_params; + $param = {} unless $param; + my $user = $param->{user} || Bugzilla->user; + my $type = $param->{type} || LOGOUT_ALL; + + if ($type == LOGOUT_ALL) { + $dbh->do("DELETE FROM logincookies WHERE userid = ?", undef, $user->id); + return; + } + + # The LOGOUT_*_CURRENT options require the current login cookie. + # If a new cookie has been issued during this run, that's the current one. + # If not, it's the one we've received. + my @login_cookies; + my $cookie = first { $_->name eq 'Bugzilla_logincookie' } + @{$cgi->{'Bugzilla_cookie_list'}}; + if ($cookie) { + push(@login_cookies, $cookie->value); + } + elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) { + push(@login_cookies, $cookie); + } + + # If we are a webservice using a token instead of cookie + # then add that as well to the login cookies to delete + if (my $login_token = $user->authorizer->login_token) { + push(@login_cookies, $login_token->{'login_token'}); + } + + # Make sure that @login_cookies is not empty to not break SQL statements. + push(@login_cookies, '') unless @login_cookies; + + # These queries use both the cookie ID and the user ID as keys. Even + # though we know the userid must match, we still check it in the SQL + # as a sanity check, since there is no locking here, and if the user + # logged out from two machines simultaneously, while someone else + # logged in and got the same cookie, we could be logging the other + # user out here. Yes, this is very very very unlikely, but why take + # chances? - bbaetz + map { trick_taint($_) } @login_cookies; + @login_cookies = map { $dbh->quote($_) } @login_cookies; + if ($type == LOGOUT_KEEP_CURRENT) { + $dbh->do( + "DELETE FROM logincookies WHERE " + . $dbh->sql_in('cookie', \@login_cookies, 1) + . " AND userid = ?", + undef, $user->id + ); + } + elsif ($type == LOGOUT_CURRENT) { + $dbh->do( + "DELETE FROM logincookies WHERE " + . $dbh->sql_in('cookie', \@login_cookies) + . " AND userid = ?", + undef, $user->id + ); + } + else { + die("Invalid type $type supplied to logout()"); + } + + if ($type != LOGOUT_KEEP_CURRENT) { + clear_browser_cookies(); + } } sub clear_browser_cookies { - my $cgi = Bugzilla->cgi; - $cgi->remove_cookie('Bugzilla_login'); - $cgi->remove_cookie('Bugzilla_logincookie'); - $cgi->remove_cookie('sudo'); + my $cgi = Bugzilla->cgi; + $cgi->remove_cookie('Bugzilla_login'); + $cgi->remove_cookie('Bugzilla_logincookie'); + $cgi->remove_cookie('sudo'); } 1; diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm index 9dc83273b..2e49175d7 100644 --- a/Bugzilla/Auth/Verify.pm +++ b/Bugzilla/Auth/Verify.pm @@ -19,113 +19,125 @@ use Bugzilla::User; use Bugzilla::Util; use constant user_can_create_account => 1; -use constant extern_id_used => 0; +use constant extern_id_used => 0; sub new { - my ($class, $login_type) = @_; - my $self = fields::new($class); - return $self; + my ($class, $login_type) = @_; + my $self = fields::new($class); + return $self; } sub can_change_password { - return $_[0]->can('change_password'); + return $_[0]->can('change_password'); } sub create_or_update_user { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - my $extern_id = $params->{extern_id}; - my $username = $params->{bz_username} || $params->{username}; - my $password = $params->{password} || '*'; - my $real_name = $params->{realname} || ''; - my $user_id = $params->{user_id}; - - # A passed-in user_id always overrides anything else, for determining - # what account we should return. - if (!$user_id) { - my $username_user_id = login_to_id($username || ''); - my $extern_user_id; - if ($extern_id) { - trick_taint($extern_id); - $extern_user_id = $dbh->selectrow_array('SELECT userid - FROM profiles WHERE extern_id = ?', undef, $extern_id); - } - - # If we have both a valid extern_id and a valid username, and they are - # not the same id, then we have a conflict. - if ($username_user_id && $extern_user_id - && $username_user_id ne $extern_user_id) - { - my $extern_name = Bugzilla::User->new($extern_user_id)->login; - return { failure => AUTH_ERROR, error => "extern_id_conflict", - details => {extern_id => $extern_id, - extern_user => $extern_name, - username => $username} }; - } - - # If we have a valid username, but no valid id, - # then we have to create the user. This happens when we're - # passed only a username, and that username doesn't exist already. - if ($username && !$username_user_id && !$extern_user_id) { - validate_email_syntax($username) - || return { failure => AUTH_ERROR, - error => 'auth_invalid_email', - details => {addr => $username} }; - # Usually we'd call validate_password, but external authentication - # systems might follow different standards than ours. So in this - # place here, we call trick_taint without checks. - trick_taint($password); - - # XXX Theoretically this could fail with an error, but the fix for - # that is too involved to be done right now. - my $user = Bugzilla::User->create({ - login_name => $username, - cryptpassword => $password, - realname => $real_name}); - $username_user_id = $user->id; - } - - # If we have a valid username id and an extern_id, but no valid - # extern_user_id, then we have to set the user's extern_id. - if ($extern_id && $username_user_id && !$extern_user_id) { - $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?', - undef, $extern_id, $username_user_id); - Bugzilla->memcached->clear({ table => 'profiles', id => $username_user_id }); - } - - # Finally, at this point, one of these will give us a valid user id. - $user_id = $extern_user_id || $username_user_id; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + my $extern_id = $params->{extern_id}; + my $username = $params->{bz_username} || $params->{username}; + my $password = $params->{password} || '*'; + my $real_name = $params->{realname} || ''; + my $user_id = $params->{user_id}; + + # A passed-in user_id always overrides anything else, for determining + # what account we should return. + if (!$user_id) { + my $username_user_id = login_to_id($username || ''); + my $extern_user_id; + if ($extern_id) { + trick_taint($extern_id); + $extern_user_id = $dbh->selectrow_array( + 'SELECT userid + FROM profiles WHERE extern_id = ?', undef, $extern_id + ); } - # If we still don't have a valid user_id, then we weren't passed - # enough information in $params, and we should die right here. - ThrowCodeError('bad_arg', {argument => 'params', function => - 'Bugzilla::Auth::Verify::create_or_update_user'}) - unless $user_id; - - my $user = new Bugzilla::User($user_id); - - # Now that we have a valid User, we need to see if any data has to be updated. - my $changed = 0; + # If we have both a valid extern_id and a valid username, and they are + # not the same id, then we have a conflict. + if ( $username_user_id + && $extern_user_id + && $username_user_id ne $extern_user_id) + { + my $extern_name = Bugzilla::User->new($extern_user_id)->login; + return { + failure => AUTH_ERROR, + error => "extern_id_conflict", + details => + {extern_id => $extern_id, extern_user => $extern_name, username => $username} + }; + } - if ($username && lc($user->login) ne lc($username)) { - validate_email_syntax($username) - || return { failure => AUTH_ERROR, error => 'auth_invalid_email', - details => {addr => $username} }; - $user->set_login($username); - $changed = 1; + # If we have a valid username, but no valid id, + # then we have to create the user. This happens when we're + # passed only a username, and that username doesn't exist already. + if ($username && !$username_user_id && !$extern_user_id) { + validate_email_syntax($username) || return { + failure => AUTH_ERROR, + error => 'auth_invalid_email', + details => {addr => $username} + }; + + # Usually we'd call validate_password, but external authentication + # systems might follow different standards than ours. So in this + # place here, we call trick_taint without checks. + trick_taint($password); + + # XXX Theoretically this could fail with an error, but the fix for + # that is too involved to be done right now. + my $user = Bugzilla::User->create( + {login_name => $username, cryptpassword => $password, realname => $real_name}); + $username_user_id = $user->id; } - if ($real_name && $user->name ne $real_name) { - # $real_name is more than likely tainted, but we only use it - # in a placeholder and we never use it after this. - trick_taint($real_name); - $user->set_name($real_name); - $changed = 1; + + # If we have a valid username id and an extern_id, but no valid + # extern_user_id, then we have to set the user's extern_id. + if ($extern_id && $username_user_id && !$extern_user_id) { + $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?', + undef, $extern_id, $username_user_id); + Bugzilla->memcached->clear({table => 'profiles', id => $username_user_id}); } - $user->update() if $changed; - return { user => $user }; + # Finally, at this point, one of these will give us a valid user id. + $user_id = $extern_user_id || $username_user_id; + } + + # If we still don't have a valid user_id, then we weren't passed + # enough information in $params, and we should die right here. + ThrowCodeError( + 'bad_arg', + { + argument => 'params', + function => 'Bugzilla::Auth::Verify::create_or_update_user' + } + ) unless $user_id; + + my $user = new Bugzilla::User($user_id); + + # Now that we have a valid User, we need to see if any data has to be updated. + my $changed = 0; + + if ($username && lc($user->login) ne lc($username)) { + validate_email_syntax($username) || return { + failure => AUTH_ERROR, + error => 'auth_invalid_email', + details => {addr => $username} + }; + $user->set_login($username); + $changed = 1; + } + if ($real_name && $user->name ne $real_name) { + + # $real_name is more than likely tainted, but we only use it + # in a placeholder and we never use it after this. + trick_taint($real_name); + $user->set_name($real_name); + $changed = 1; + } + $user->update() if $changed; + + return {user => $user}; } 1; diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm index 28a9310c9..951aaaf9f 100644 --- a/Bugzilla/Auth/Verify/DB.pm +++ b/Bugzilla/Auth/Verify/DB.pm @@ -19,95 +19,97 @@ use Bugzilla::Util; use Bugzilla::User; sub check_credentials { - my ($self, $login_data) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $login_data) = @_; + my $dbh = Bugzilla->dbh; - my $username = $login_data->{username}; - my $user = new Bugzilla::User({ name => $username }); + my $username = $login_data->{username}; + my $user = new Bugzilla::User({name => $username}); - return { failure => AUTH_NO_SUCH_USER } unless $user; + return {failure => AUTH_NO_SUCH_USER} unless $user; - $login_data->{user} = $user; - $login_data->{bz_username} = $user->login; + $login_data->{user} = $user; + $login_data->{bz_username} = $user->login; + if ($user->account_is_locked_out) { + return {failure => AUTH_LOCKOUT, user => $user}; + } + + my $password = $login_data->{password}; + 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); + + 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 }; + return {failure => AUTH_LOCKOUT, user => $user, just_locked_out => 1}; } - my $password = $login_data->{password}; - 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); - - 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 }), - }; - } - - # Force the user to change their password if it does not meet the current - # criteria. This should usually only happen if the criteria has changed. - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER && - Bugzilla->params->{password_check_on_login}) - { - my $check = validate_password_check($password); - if ($check) { - return { - failure => AUTH_ERROR, - user_error => $check, - details => { locked_user => $user } - } - } + return { + failure => AUTH_LOGINFAILED, + failure_count => scalar(@{$user->account_ip_login_failures}), + }; + } + + # Force the user to change their password if it does not meet the current + # criteria. This should usually only happen if the criteria has changed. + if ( Bugzilla->usage_mode == USAGE_MODE_BROWSER + && Bugzilla->params->{password_check_on_login}) + { + my $check = validate_password_check($password); + if ($check) { + return { + failure => AUTH_ERROR, + user_error => $check, + details => {locked_user => $user} + }; } + } - # The user's credentials are okay, so delete any outstanding - # password tokens or login failures they may have generated. - Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in"); - $user->clear_login_failures(); + # The user's credentials are okay, so delete any outstanding + # password tokens or login failures they may have generated. + Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in"); + $user->clear_login_failures(); - my $update_password = 0; + my $update_password = 0; - # 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; - $update_password = 1 if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/); + # 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; + $update_password = 1 if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/); - # If their old password was using a different length salt than what - # we're using now, update the password to use the new salt length. - if ($real_password_crypted =~ /^([^,]+),/) { - $update_password = 1 if (length($1) != PASSWORD_SALT_LENGTH); - } + # If their old password was using a different length salt than what + # we're using now, update the password to use the new salt length. + if ($real_password_crypted =~ /^([^,]+),/) { + $update_password = 1 if (length($1) != PASSWORD_SALT_LENGTH); + } - # If needed, update the user's password. - if ($update_password) { - # We can't call $user->set_password because we don't want the password - # complexity rules to apply here. - $user->{cryptpassword} = bz_crypt($password); - $user->update(); - } + # If needed, update the user's password. + if ($update_password) { + + # We can't call $user->set_password because we don't want the password + # complexity rules to apply here. + $user->{cryptpassword} = bz_crypt($password); + $user->update(); + } - return $login_data; + return $login_data; } sub change_password { - my ($self, $user, $password) = @_; - my $dbh = Bugzilla->dbh; - my $cryptpassword = bz_crypt($password); - $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?", - undef, $cryptpassword, $user->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $user->id }); + my ($self, $user, $password) = @_; + my $dbh = Bugzilla->dbh; + my $cryptpassword = bz_crypt($password); + $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?", + undef, $cryptpassword, $user->id); + Bugzilla->memcached->clear({table => 'profiles', id => $user->id}); } 1; diff --git a/Bugzilla/Auth/Verify/LDAP.pm b/Bugzilla/Auth/Verify/LDAP.pm index e37f55793..c92a38909 100644 --- a/Bugzilla/Auth/Verify/LDAP.pm +++ b/Bugzilla/Auth/Verify/LDAP.pm @@ -13,7 +13,7 @@ use warnings; use base qw(Bugzilla::Auth::Verify); use fields qw( - ldap + ldap ); use Bugzilla::Constants; @@ -28,126 +28,139 @@ use constant admin_can_create_account => 0; use constant user_can_create_account => 0; sub check_credentials { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - # We need to bind anonymously to the LDAP server. This is - # because we need to get the Distinguished Name of the user trying - # to log in. Some servers (such as iPlanet) allow you to have unique - # uids spread out over a subtree of an area (such as "People"), so - # 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. + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + # We need to bind anonymously to the LDAP server. This is + # because we need to get the Distinguished Name of the user trying + # to log in. Some servers (such as iPlanet) allow you to have unique + # uids spread out over a subtree of an area (such as "People"), so + # 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_for_search(); + + # Now, we verify that the user exists, and get a LDAP Distinguished + # Name for the user. + my $username = $params->{username}; + my $dn_result + = $self->ldap->search(_bz_search_params($username), attrs => ['dn']); + return { + failure => AUTH_ERROR, + error => "ldap_search_error", + details => {errstr => $dn_result->error, username => $username} + } + if $dn_result->code; + + return {failure => AUTH_NO_SUCH_USER} if !$dn_result->count; + + my $dn = $dn_result->shift_entry->dn; + + # Check the password. + my $pw_result = $self->ldap->bind($dn, password => $params->{password}); + return {failure => AUTH_LOGINFAILED} if $pw_result->code; + + # And now we fill in the user's details. + + # 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(); - # Now, we verify that the user exists, and get a LDAP Distinguished - # Name for the user. - my $username = $params->{username}; - my $dn_result = $self->ldap->search(_bz_search_params($username), - attrs => ['dn']); - return { failure => AUTH_ERROR, error => "ldap_search_error", - details => {errstr => $dn_result->error, username => $username} - } if $dn_result->code; - - return { failure => AUTH_NO_SUCH_USER } if !$dn_result->count; - - my $dn = $dn_result->shift_entry->dn; - - # Check the password. - my $pw_result = $self->ldap->bind($dn, password => $params->{password}); - return { failure => AUTH_LOGINFAILED } if $pw_result->code; - - # And now we fill in the user's details. - - # 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; + $detail_result = $self->ldap->search(_bz_search_params($username)); + if (!$detail_result->code) { + $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; - # 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) { + if (!$user_entry->exists($mail_attr)) { + return { + failure => AUTH_ERROR, + error => "ldap_cannot_retreive_attr", + details => {attr => $mail_attr} + }; + } - my $mail_attr = Bugzilla->params->{"LDAPmailattribute"}; - if ($mail_attr) { - if (!$user_entry->exists($mail_attr)) { - return { failure => AUTH_ERROR, - error => "ldap_cannot_retreive_attr", - details => {attr => $mail_attr} }; - } + my @emails = $user_entry->get_value($mail_attr); - my @emails = $user_entry->get_value($mail_attr); + # Default to the first email address returned. + $params->{bz_username} = $emails[0]; - # Default to the first email address returned. - $params->{bz_username} = $emails[0]; + if (@emails > 1) { - if (@emails > 1) { - # Cycle through the adresses and check if they're Bugzilla logins. - # Use the first one that returns a valid id. - foreach my $email (@emails) { - if ( login_to_id($email) ) { - $params->{bz_username} = $email; - last; - } - } + # Cycle through the adresses and check if they're Bugzilla logins. + # Use the first one that returns a valid id. + foreach my $email (@emails) { + if (login_to_id($email)) { + $params->{bz_username} = $email; + last; } - - } else { - $params->{bz_username} = $username; + } } - $params->{realname} ||= $user_entry->get_value("displayName"); - $params->{realname} ||= $user_entry->get_value("cn"); + } + else { + $params->{bz_username} = $username; + } + + $params->{realname} ||= $user_entry->get_value("displayName"); + $params->{realname} ||= $user_entry->get_value("cn"); - $params->{extern_id} = $username; + $params->{extern_id} = $username; - return $params; + return $params; } sub _bz_search_params { - my ($username) = @_; - $username = escape_filter_value($username); - return (base => Bugzilla->params->{"LDAPBaseDN"}, - scope => "sub", - filter => '(&(' . Bugzilla->params->{"LDAPuidattribute"} - . "=$username)" - . Bugzilla->params->{"LDAPfilter"} . ')'); + my ($username) = @_; + $username = escape_filter_value($username); + return ( + base => Bugzilla->params->{"LDAPBaseDN"}, + scope => "sub", + filter => '(&(' + . Bugzilla->params->{"LDAPuidattribute"} + . "=$username)" + . Bugzilla->params->{"LDAPfilter"} . ')' + ); } sub _bind_ldap_for_search { - my ($self) = @_; - my $bind_result; - if (Bugzilla->params->{"LDAPbinddn"}) { - my ($LDAPbinddn,$LDAPbindpass) = - split(":",Bugzilla->params->{"LDAPbinddn"}); - $bind_result = - $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass); - } - else { - $bind_result = $self->ldap->bind(); - } - ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error}) - if $bind_result->code; + my ($self) = @_; + my $bind_result; + if (Bugzilla->params->{"LDAPbinddn"}) { + my ($LDAPbinddn, $LDAPbindpass) = split(":", Bugzilla->params->{"LDAPbinddn"}); + $bind_result = $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass); + } + else { + $bind_result = $self->ldap->bind(); + } + ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error}) + if $bind_result->code; } # We can't just do this in new(), because we're not allowed to throw any @@ -156,27 +169,27 @@ sub _bind_ldap_for_search { # to fix their mistake. (Because Bugzilla->login always calls # Bugzilla::Auth->new, and almost every page calls Bugzilla->login.) sub ldap { - my ($self) = @_; - return $self->{ldap} if $self->{ldap}; - - my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"}); - ThrowCodeError("ldap_server_not_defined") unless @servers; - - foreach (@servers) { - $self->{ldap} = new Net::LDAP(trim($_)); - last if $self->{ldap}; - } - ThrowCodeError("ldap_connect_failed", { server => join(", ", @servers) }) - unless $self->{ldap}; - - # try to start TLS if needed - if (Bugzilla->params->{"LDAPstarttls"}) { - my $mesg = $self->{ldap}->start_tls(); - ThrowCodeError("ldap_start_tls_failed", { error => $mesg->error() }) - if $mesg->code(); - } - - return $self->{ldap}; + my ($self) = @_; + return $self->{ldap} if $self->{ldap}; + + my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"}); + ThrowCodeError("ldap_server_not_defined") unless @servers; + + foreach (@servers) { + $self->{ldap} = new Net::LDAP(trim($_)); + last if $self->{ldap}; + } + ThrowCodeError("ldap_connect_failed", {server => join(", ", @servers)}) + unless $self->{ldap}; + + # try to start TLS if needed + if (Bugzilla->params->{"LDAPstarttls"}) { + my $mesg = $self->{ldap}->start_tls(); + ThrowCodeError("ldap_start_tls_failed", {error => $mesg->error()}) + if $mesg->code(); + } + + return $self->{ldap}; } 1; diff --git a/Bugzilla/Auth/Verify/RADIUS.pm b/Bugzilla/Auth/Verify/RADIUS.pm index 283d9b466..2cbde0404 100644 --- a/Bugzilla/Auth/Verify/RADIUS.pm +++ b/Bugzilla/Auth/Verify/RADIUS.pm @@ -23,33 +23,37 @@ use constant admin_can_create_account => 0; use constant user_can_create_account => 0; sub check_credentials { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'}; - my $username = $params->{username}; - - # If we're using RADIUS_email_suffix, we may need to cut it off from - # the login name. - if ($address_suffix) { - $username =~ s/\Q$address_suffix\E$//i; - } - - # Create RADIUS object. - my $radius = - new Authen::Radius(Host => Bugzilla->params->{'RADIUS_server'}, - Secret => Bugzilla->params->{'RADIUS_secret'}) - || return { failure => AUTH_ERROR, error => 'radius_preparation_error', - details => {errstr => Authen::Radius::strerror() } }; - - # Check the password. - $radius->check_pwd($username, $params->{password}, - Bugzilla->params->{'RADIUS_NAS_IP'} || undef) - || return { failure => AUTH_LOGINFAILED }; - - # Build the user account's e-mail address. - $params->{bz_username} = $username . $address_suffix; - - return $params; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'}; + my $username = $params->{username}; + + # If we're using RADIUS_email_suffix, we may need to cut it off from + # the login name. + if ($address_suffix) { + $username =~ s/\Q$address_suffix\E$//i; + } + + # Create RADIUS object. + my $radius = new Authen::Radius( + Host => Bugzilla->params->{'RADIUS_server'}, + Secret => Bugzilla->params->{'RADIUS_secret'} + ) + || return { + failure => AUTH_ERROR, + error => 'radius_preparation_error', + details => {errstr => Authen::Radius::strerror()} + }; + + # Check the password. + $radius->check_pwd($username, $params->{password}, + Bugzilla->params->{'RADIUS_NAS_IP'} || undef) + || return {failure => AUTH_LOGINFAILED}; + + # Build the user account's e-mail address. + $params->{bz_username} = $username . $address_suffix; + + return $params; } 1; diff --git a/Bugzilla/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm index 3e5db3cec..9a9412915 100644 --- a/Bugzilla/Auth/Verify/Stack.pm +++ b/Bugzilla/Auth/Verify/Stack.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Bugzilla::Auth::Verify); use fields qw( - _stack - successful + _stack + successful ); use Bugzilla::Hook; @@ -23,70 +23,75 @@ use Hash::Util qw(lock_keys); use List::MoreUtils qw(any); sub new { - my $class = shift; - my $list = shift; - 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 }); - - $self->{_stack} = []; - foreach my $verify_method (split(',', $list)) { - my $module = $methods{$verify_method}; - require $module; - $module =~ s|/|::|g; - $module =~ s/.pm$//; - push(@{$self->{_stack}}, $module->new(@_)); - } - return $self; + my $class = shift; + my $list = shift; + 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}); + + $self->{_stack} = []; + foreach my $verify_method (split(',', $list)) { + my $module = $methods{$verify_method}; + require $module; + $module =~ s|/|::|g; + $module =~ s/.pm$//; + push(@{$self->{_stack}}, $module->new(@_)); + } + return $self; } sub can_change_password { - my ($self) = @_; - # We return true if any method can change passwords. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->can_change_password; - } - return 0; + my ($self) = @_; + + # We return true if any method can change passwords. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->can_change_password; + } + return 0; } sub check_credentials { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - $result = $object->check_credentials(@_); - $self->{successful} = $object; - last if !$result->{failure}; - # So that if none of them succeed, it's undef. - $self->{successful} = undef; - } - # Returns the result at the bottom of the stack if they all fail. - return $result; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + $result = $object->check_credentials(@_); + $self->{successful} = $object; + last if !$result->{failure}; + + # So that if none of them succeed, it's undef. + $self->{successful} = undef; + } + + # Returns the result at the bottom of the stack if they all fail. + return $result; } sub create_or_update_user { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - $result = $object->create_or_update_user(@_); - last if !$result->{failure}; - } - # Returns the result at the bottom of the stack if they all fail. - return $result; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + $result = $object->create_or_update_user(@_); + last if !$result->{failure}; + } + + # Returns the result at the bottom of the stack if they all fail. + return $result; } sub user_can_create_account { - my ($self) = @_; - # We return true if any method allows the user to create an account. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->user_can_create_account; - } - return 0; + my ($self) = @_; + + # We return true if any method allows the user to create an account. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->user_can_create_account; + } + return 0; } sub extern_id_used { - my ($self) = @_; - return any { $_->extern_id_used } @{ $self->{_stack} }; + my ($self) = @_; + return any { $_->extern_id_used } @{$self->{_stack}}; } 1; diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 8b4493f85..42bccaeba 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -38,9 +38,9 @@ use Scalar::Util qw(blessed); use parent qw(Bugzilla::Object Exporter); @Bugzilla::Bug::EXPORT = qw( - bug_alias_to_id - LogActivityEntry - editable_bug_fields + bug_alias_to_id + LogActivityEntry + editable_bug_fields ); ##################################################################### @@ -51,198 +51,199 @@ use constant DB_TABLE => 'bugs'; use constant ID_FIELD => 'bug_id'; use constant NAME_FIELD => 'bug_id'; use constant LIST_ORDER => ID_FIELD; + # Bugs have their own auditing table, bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; + # This will be enabled later use constant USE_MEMCACHED => 0; # This is a sub because it needs to call other subroutines. sub DB_COLUMNS { - my $dbh = Bugzilla->dbh; - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my @custom_names = map {$_->name} @custom; - - my @columns = (qw( - assigned_to - bug_file_loc - bug_id - bug_severity - bug_status - cclist_accessible - component_id - creation_ts - delta_ts - estimated_time - everconfirmed - lastdiffed - op_sys - priority - product_id - qa_contact - remaining_time - rep_platform - reporter_accessible - resolution - short_desc - status_whiteboard - target_milestone - version - ), - 'reporter AS reporter_id', - $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', - @custom_names); - - Bugzilla::Hook::process("bug_columns", { columns => \@columns }); - - return @columns; + my $dbh = Bugzilla->dbh; + my @custom + = grep { $_->type != FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + my @custom_names = map { $_->name } @custom; + + my @columns = ( + qw( + assigned_to + bug_file_loc + bug_id + bug_severity + bug_status + cclist_accessible + component_id + creation_ts + delta_ts + estimated_time + everconfirmed + lastdiffed + op_sys + priority + product_id + qa_contact + remaining_time + rep_platform + reporter_accessible + resolution + short_desc + status_whiteboard + target_milestone + version + ), 'reporter AS reporter_id', + $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', @custom_names + ); + + Bugzilla::Hook::process("bug_columns", {columns => \@columns}); + + return @columns; } sub VALIDATORS { - my $validators = { - alias => \&_check_alias, - assigned_to => \&_check_assigned_to, - blocked => \&_check_dependencies, - bug_file_loc => \&_check_bug_file_loc, - bug_severity => \&_check_select_field, - bug_status => \&_check_bug_status, - cc => \&_check_cc, - comment => \&_check_comment, - component => \&_check_component, - creation_ts => \&_check_creation_ts, - deadline => \&_check_deadline, - dependson => \&_check_dependencies, - dup_id => \&_check_dup_id, - estimated_time => \&_check_time_field, - everconfirmed => \&Bugzilla::Object::check_boolean, - groups => \&_check_groups, - keywords => \&_check_keywords, - op_sys => \&_check_select_field, - priority => \&_check_priority, - product => \&_check_product, - qa_contact => \&_check_qa_contact, - remaining_time => \&_check_time_field, - rep_platform => \&_check_select_field, - resolution => \&_check_resolution, - short_desc => \&_check_short_desc, - status_whiteboard => \&_check_status_whiteboard, - target_milestone => \&_check_target_milestone, - version => \&_check_version, - - cclist_accessible => \&Bugzilla::Object::check_boolean, - reporter_accessible => \&Bugzilla::Object::check_boolean, - }; - - # Set up validators for custom fields. - foreach my $field (Bugzilla->active_custom_fields) { - my $validator; - if ($field->type == FIELD_TYPE_SINGLE_SELECT) { - $validator = \&_check_select_field; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - $validator = \&_check_multi_select_field; - } - elsif ($field->type == FIELD_TYPE_DATETIME) { - $validator = \&_check_datetime_field; - } - elsif ($field->type == FIELD_TYPE_DATE) { - $validator = \&_check_date_field; - } - elsif ($field->type == FIELD_TYPE_FREETEXT) { - $validator = \&_check_freetext_field; - } - elsif ($field->type == FIELD_TYPE_BUG_ID) { - $validator = \&_check_bugid_field; - } - elsif ($field->type == FIELD_TYPE_TEXTAREA) { - $validator = \&_check_textarea_field; - } - elsif ($field->type == FIELD_TYPE_INTEGER) { - $validator = \&_check_integer_field; - } - else { - $validator = \&_check_default_field; - } - $validators->{$field->name} = $validator; + my $validators = { + alias => \&_check_alias, + assigned_to => \&_check_assigned_to, + blocked => \&_check_dependencies, + bug_file_loc => \&_check_bug_file_loc, + bug_severity => \&_check_select_field, + bug_status => \&_check_bug_status, + cc => \&_check_cc, + comment => \&_check_comment, + component => \&_check_component, + creation_ts => \&_check_creation_ts, + deadline => \&_check_deadline, + dependson => \&_check_dependencies, + dup_id => \&_check_dup_id, + estimated_time => \&_check_time_field, + everconfirmed => \&Bugzilla::Object::check_boolean, + groups => \&_check_groups, + keywords => \&_check_keywords, + op_sys => \&_check_select_field, + priority => \&_check_priority, + product => \&_check_product, + qa_contact => \&_check_qa_contact, + remaining_time => \&_check_time_field, + rep_platform => \&_check_select_field, + resolution => \&_check_resolution, + short_desc => \&_check_short_desc, + status_whiteboard => \&_check_status_whiteboard, + target_milestone => \&_check_target_milestone, + version => \&_check_version, + + cclist_accessible => \&Bugzilla::Object::check_boolean, + reporter_accessible => \&Bugzilla::Object::check_boolean, + }; + + # Set up validators for custom fields. + foreach my $field (Bugzilla->active_custom_fields) { + my $validator; + if ($field->type == FIELD_TYPE_SINGLE_SELECT) { + $validator = \&_check_select_field; + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + $validator = \&_check_multi_select_field; + } + elsif ($field->type == FIELD_TYPE_DATETIME) { + $validator = \&_check_datetime_field; + } + elsif ($field->type == FIELD_TYPE_DATE) { + $validator = \&_check_date_field; + } + elsif ($field->type == FIELD_TYPE_FREETEXT) { + $validator = \&_check_freetext_field; + } + elsif ($field->type == FIELD_TYPE_BUG_ID) { + $validator = \&_check_bugid_field; + } + elsif ($field->type == FIELD_TYPE_TEXTAREA) { + $validator = \&_check_textarea_field; + } + elsif ($field->type == FIELD_TYPE_INTEGER) { + $validator = \&_check_integer_field; + } + else { + $validator = \&_check_default_field; } + $validators->{$field->name} = $validator; + } - return $validators; -}; + return $validators; +} sub VALIDATOR_DEPENDENCIES { - my $cache = Bugzilla->request_cache; - return $cache->{bug_validator_dependencies} - if $cache->{bug_validator_dependencies}; - - my %deps = ( - assigned_to => ['component'], - blocked => ['product'], - bug_status => ['product', 'comment', 'target_milestone'], - cc => ['component'], - comment => ['creation_ts'], - component => ['product'], - dependson => ['product'], - dup_id => ['bug_status', 'resolution'], - groups => ['product'], - keywords => ['product'], - resolution => ['bug_status', 'dependson'], - qa_contact => ['component'], - target_milestone => ['product'], - version => ['product'], - ); - - foreach my $field (@{ Bugzilla->fields }) { - $deps{$field->name} = [ $field->visibility_field->name ] - if $field->{visibility_field_id}; - } - - $cache->{bug_validator_dependencies} = \%deps; - return \%deps; -}; + my $cache = Bugzilla->request_cache; + return $cache->{bug_validator_dependencies} + if $cache->{bug_validator_dependencies}; + + my %deps = ( + assigned_to => ['component'], + blocked => ['product'], + bug_status => ['product', 'comment', 'target_milestone'], + cc => ['component'], + comment => ['creation_ts'], + component => ['product'], + dependson => ['product'], + dup_id => ['bug_status', 'resolution'], + groups => ['product'], + keywords => ['product'], + resolution => ['bug_status', 'dependson'], + qa_contact => ['component'], + target_milestone => ['product'], + version => ['product'], + ); + + foreach my $field (@{Bugzilla->fields}) { + $deps{$field->name} = [$field->visibility_field->name] + if $field->{visibility_field_id}; + } + + $cache->{bug_validator_dependencies} = \%deps; + return \%deps; +} sub UPDATE_COLUMNS { - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my @custom_names = map {$_->name} @custom; - my @columns = qw( - assigned_to - bug_file_loc - bug_severity - bug_status - cclist_accessible - component_id - deadline - estimated_time - everconfirmed - op_sys - priority - product_id - qa_contact - remaining_time - rep_platform - reporter_accessible - resolution - short_desc - status_whiteboard - target_milestone - version - ); - push(@columns, @custom_names); - return @columns; -}; - -use constant NUMERIC_COLUMNS => qw( + my @custom + = grep { $_->type != FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + my @custom_names = map { $_->name } @custom; + my @columns = qw( + assigned_to + bug_file_loc + bug_severity + bug_status + cclist_accessible + component_id + deadline estimated_time + everconfirmed + op_sys + priority + product_id + qa_contact remaining_time + rep_platform + reporter_accessible + resolution + short_desc + status_whiteboard + target_milestone + version + ); + push(@columns, @custom_names); + return @columns; +} + +use constant NUMERIC_COLUMNS => qw( + estimated_time + remaining_time ); sub DATE_COLUMNS { - my @fields = (@{ Bugzilla->fields({ type => [FIELD_TYPE_DATETIME, - FIELD_TYPE_DATE] }) - }); - return map { $_->name } @fields; + my @fields + = (@{Bugzilla->fields({type => [FIELD_TYPE_DATETIME, FIELD_TYPE_DATE]})}); + return map { $_->name } @fields; } # Used in LogActivityEntry(). Gives the max length of lines in the @@ -254,30 +255,28 @@ use constant MAX_LINE_LENGTH => 254; # of Bugzilla. (These are the field names that the WebService and email_in.pl # use.) use constant FIELD_MAP => { - blocks => 'blocked', - commentprivacy => 'comment_is_private', - creation_time => 'creation_ts', - creator => 'reporter', - description => 'comment', - depends_on => 'dependson', - dupe_of => 'dup_id', - id => 'bug_id', - is_confirmed => 'everconfirmed', - is_cc_accessible => 'cclist_accessible', - is_creator_accessible => 'reporter_accessible', - last_change_time => 'delta_ts', - platform => 'rep_platform', - severity => 'bug_severity', - status => 'bug_status', - summary => 'short_desc', - url => 'bug_file_loc', - whiteboard => 'status_whiteboard', + blocks => 'blocked', + commentprivacy => 'comment_is_private', + creation_time => 'creation_ts', + creator => 'reporter', + description => 'comment', + depends_on => 'dependson', + dupe_of => 'dup_id', + id => 'bug_id', + is_confirmed => 'everconfirmed', + is_cc_accessible => 'cclist_accessible', + is_creator_accessible => 'reporter_accessible', + last_change_time => 'delta_ts', + platform => 'rep_platform', + severity => 'bug_severity', + status => 'bug_status', + summary => 'short_desc', + url => 'bug_file_loc', + whiteboard => 'status_whiteboard', }; -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', - component_id => 'component', -}; +use constant REQUIRED_FIELD_MAP => + {product_id => 'product', component_id => 'component',}; # Creation timestamp is here because it needs to be validated # but it can be NULL in the database (see comments in create above) @@ -295,360 +294,374 @@ use constant REQUIRED_FIELD_MAP => { # # Groups are in a separate table, but must always be validated so that # mandatory groups get set on bugs. -use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_contact groups); +use constant EXTRA_REQUIRED_FIELDS => + qw(creation_ts target_milestone cc qa_contact groups); ##################################################################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $param = shift; - - # Remove leading "#" mark if we've just been passed an id. - if (!ref $param && $param =~ /^#([0-9]+)$/) { - $param = $1; - } - - # If we get something that looks like a word (not a number), - # make it the "name" param. - if (!defined $param - || (!ref($param) && $param !~ /^[0-9]+$/) - || (ref($param) && $param->{id} !~ /^[0-9]+$/)) - { - if ($param) { - my $alias = ref($param) ? $param->{id} : $param; - my $bug_id = bug_alias_to_id($alias); - if (! $bug_id) { - my $error_self = {}; - bless $error_self, $class; - $error_self->{'bug_id'} = $alias; - $error_self->{'error'} = 'InvalidBugId'; - return $error_self; - } - $param = { id => $bug_id, - cache => ref($param) ? $param->{cache} : 0 }; - } - else { - # We got something that's not a number. - my $error_self = {}; - bless $error_self, $class; - $error_self->{'bug_id'} = $param; - $error_self->{'error'} = 'InvalidBugId'; - return $error_self; - } - } - - unshift @_, $param; - my $self = $class->SUPER::new(@_); - - # Bugzilla::Bug->new always returns something, but sets $self->{error} - # if the bug wasn't found in the database. - if (!$self) { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $param = shift; + + # Remove leading "#" mark if we've just been passed an id. + if (!ref $param && $param =~ /^#([0-9]+)$/) { + $param = $1; + } + + # If we get something that looks like a word (not a number), + # make it the "name" param. + if ( !defined $param + || (!ref($param) && $param !~ /^[0-9]+$/) + || (ref($param) && $param->{id} !~ /^[0-9]+$/)) + { + if ($param) { + my $alias = ref($param) ? $param->{id} : $param; + my $bug_id = bug_alias_to_id($alias); + if (!$bug_id) { my $error_self = {}; - if (ref $param) { - $error_self->{bug_id} = $param->{name}; - $error_self->{error} = 'InvalidBugId'; - } - else { - $error_self->{bug_id} = $param; - $error_self->{error} = 'NotFound'; - } bless $error_self, $class; + $error_self->{'bug_id'} = $alias; + $error_self->{'error'} = 'InvalidBugId'; return $error_self; + } + $param = {id => $bug_id, cache => ref($param) ? $param->{cache} : 0}; + } + else { + # We got something that's not a number. + my $error_self = {}; + bless $error_self, $class; + $error_self->{'bug_id'} = $param; + $error_self->{'error'} = 'InvalidBugId'; + return $error_self; + } + } + + unshift @_, $param; + my $self = $class->SUPER::new(@_); + + # Bugzilla::Bug->new always returns something, but sets $self->{error} + # if the bug wasn't found in the database. + if (!$self) { + my $error_self = {}; + if (ref $param) { + $error_self->{bug_id} = $param->{name}; + $error_self->{error} = 'InvalidBugId'; + } + else { + $error_self->{bug_id} = $param; + $error_self->{error} = 'NotFound'; } + bless $error_self, $class; + return $error_self; + } - return $self; + return $self; } sub initialize { - $_[0]->_create_cf_accessors(); + $_[0]->_create_cf_accessors(); } sub object_cache_key { - my $class = shift; - my $key = $class->SUPER::object_cache_key(@_) - || return; - return $key . ',' . Bugzilla->user->id; + my $class = shift; + my $key = $class->SUPER::object_cache_key(@_) || return; + return $key . ',' . Bugzilla->user->id; } sub check { - my $class = shift; - my ($param, $field) = @_; + my $class = shift; + my ($param, $field) = @_; - # Bugzilla::Bug throws lots of special errors, so we don't call - # SUPER::check, we just call our new and do our own checks. - my $id = ref($param) - ? ($param->{id} = trim($param->{id})) - : ($param = trim($param)); - ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id; + # Bugzilla::Bug throws lots of special errors, so we don't call + # SUPER::check, we just call our new and do our own checks. + my $id + = ref($param) ? ($param->{id} = trim($param->{id})) : ($param = trim($param)); + ThrowUserError('improper_bug_id_field_value', {field => $field}) + unless defined $id; - my $self = $class->new($param); + my $self = $class->new($param); - if ($self->{error}) { - # For error messages, use the id that was returned by new(), because - # it's cleaned up. - $id = $self->id; + if ($self->{error}) { - if ($self->{error} eq 'NotFound') { - ThrowUserError("bug_id_does_not_exist", { bug_id => $id }); - } - if ($self->{error} eq 'InvalidBugId') { - ThrowUserError("improper_bug_id_field_value", - { bug_id => $id, - field => $field }); - } - } + # For error messages, use the id that was returned by new(), because + # it's cleaned up. + $id = $self->id; - unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) { - $self->check_is_visible($id); + if ($self->{error} eq 'NotFound') { + ThrowUserError("bug_id_does_not_exist", {bug_id => $id}); } - return $self; + if ($self->{error} eq 'InvalidBugId') { + ThrowUserError("improper_bug_id_field_value", {bug_id => $id, field => $field}); + } + } + + unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) { + $self->check_is_visible($id); + } + return $self; } sub check_for_edit { - my $class = shift; - my $bug = $class->check(@_); + my $class = shift; + my $bug = $class->check(@_); - Bugzilla->user->can_edit_product($bug->product_id) - || ThrowUserError("product_edit_denied", { product => $bug->product }); + Bugzilla->user->can_edit_product($bug->product_id) + || ThrowUserError("product_edit_denied", {product => $bug->product}); - return $bug; + return $bug; } sub check_is_visible { - my ($self, $input_id) = @_; - $input_id ||= $self->id; - my $user = Bugzilla->user; + my ($self, $input_id) = @_; + $input_id ||= $self->id; + my $user = Bugzilla->user; - if (!$user->can_see_bug($self->id)) { - # The error the user sees depends on whether or not they are - # logged in (i.e. $user->id contains the user's positive integer ID). - # If we are validating an alias, then use it in the error message - # instead of its corresponding bug ID, to not disclose it. - if ($user->id) { - ThrowUserError("bug_access_denied", { bug_id => $input_id }); - } else { - ThrowUserError("bug_access_query", { bug_id => $input_id }); - } - } -} + if (!$user->can_see_bug($self->id)) { -sub match { - my $class = shift; - my ($params) = @_; - - # Allow matching certain fields by name (in addition to matching by ID). - my %translate_fields = ( - assigned_to => 'Bugzilla::User', - qa_contact => 'Bugzilla::User', - reporter => 'Bugzilla::User', - product => 'Bugzilla::Product', - component => 'Bugzilla::Component', - ); - my %translated; - - foreach my $field (keys %translate_fields) { - my @ids; - # Convert names to ids. We use "exists" everywhere since people can - # legally specify "undef" to mean IS NULL (even though most of these - # fields can't be NULL, people can still specify it...). - if (exists $params->{$field}) { - my $names = $params->{$field}; - my $type = $translate_fields{$field}; - my $param = $type eq 'Bugzilla::User' ? 'login_name' : 'name'; - # We call Bugzilla::Object::match directly to avoid the - # Bugzilla::User::match implementation which is different. - my $objects = Bugzilla::Object::match($type, { $param => $names }); - push(@ids, map { $_->id } @$objects); - } - # You can also specify ids directly as arguments to this function, - # so include them in the list if they have been specified. - if (exists $params->{"${field}_id"}) { - my $current_ids = $params->{"${field}_id"}; - my @id_array = ref $current_ids ? @$current_ids : ($current_ids); - push(@ids, @id_array); - } - # We do this "or" instead of a "scalar(@ids)" to handle the case - # when people passed only invalid object names. Otherwise we'd - # end up with a SUPER::match call with zero criteria (which dies). - if (exists $params->{$field} or exists $params->{"${field}_id"}) { - $translated{$field} = scalar(@ids) == 1 ? $ids[0] : \@ids; - } + # The error the user sees depends on whether or not they are + # logged in (i.e. $user->id contains the user's positive integer ID). + # If we are validating an alias, then use it in the error message + # instead of its corresponding bug ID, to not disclose it. + if ($user->id) { + ThrowUserError("bug_access_denied", {bug_id => $input_id}); } - - # The user fields don't have an _id on the end of them in the database, - # but the product & component fields do, so we have to have separate - # 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} - if exists $translated{$field}; - } - foreach my $field (qw(product component)) { - delete $params->{$field}; - $params->{"${field}_id"} = $translated{$field} - if exists $translated{$field}; + else { + ThrowUserError("bug_access_query", {bug_id => $input_id}); } + } +} - return $class->SUPER::match(@_); +sub match { + my $class = shift; + my ($params) = @_; + + # Allow matching certain fields by name (in addition to matching by ID). + my %translate_fields = ( + assigned_to => 'Bugzilla::User', + qa_contact => 'Bugzilla::User', + reporter => 'Bugzilla::User', + product => 'Bugzilla::Product', + component => 'Bugzilla::Component', + ); + my %translated; + + foreach my $field (keys %translate_fields) { + my @ids; + + # Convert names to ids. We use "exists" everywhere since people can + # legally specify "undef" to mean IS NULL (even though most of these + # fields can't be NULL, people can still specify it...). + if (exists $params->{$field}) { + my $names = $params->{$field}; + my $type = $translate_fields{$field}; + my $param = $type eq 'Bugzilla::User' ? 'login_name' : 'name'; + + # We call Bugzilla::Object::match directly to avoid the + # Bugzilla::User::match implementation which is different. + my $objects = Bugzilla::Object::match($type, {$param => $names}); + push(@ids, map { $_->id } @$objects); + } + + # You can also specify ids directly as arguments to this function, + # so include them in the list if they have been specified. + if (exists $params->{"${field}_id"}) { + my $current_ids = $params->{"${field}_id"}; + my @id_array = ref $current_ids ? @$current_ids : ($current_ids); + push(@ids, @id_array); + } + + # We do this "or" instead of a "scalar(@ids)" to handle the case + # when people passed only invalid object names. Otherwise we'd + # end up with a SUPER::match call with zero criteria (which dies). + if (exists $params->{$field} or exists $params->{"${field}_id"}) { + $translated{$field} = scalar(@ids) == 1 ? $ids[0] : \@ids; + } + } + + # The user fields don't have an _id on the end of them in the database, + # but the product & component fields do, so we have to have separate + # 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} if exists $translated{$field}; + } + foreach my $field (qw(product component)) { + delete $params->{$field}; + $params->{"${field}_id"} = $translated{$field} if exists $translated{$field}; + } + + return $class->SUPER::match(@_); } # Helps load up information for bugs for show_bug.cgi and other situations # that will need to access info on lots of bugs. sub preload { - my ($class, $bugs) = @_; - my $user = Bugzilla->user; - - # It would be faster but MUCH more complicated to select all the - # deps for the entire list in one SQL statement. If we ever have - # a profile that proves that that's necessary, we can switch over - # to the more complex method. - my @all_dep_ids; - foreach my $bug (@$bugs) { - push @all_dep_ids, @{ $bug->blocked }, @{ $bug->dependson }; - push @all_dep_ids, @{ $bug->duplicate_ids }; - push @all_dep_ids, @{ $bug->_preload_referenced_bugs }; - } - @all_dep_ids = uniq @all_dep_ids; - # If we don't do this, can_see_bug will do one call per bug in - # the dependency and duplicate lists, in Bugzilla::Template::get_bug_link. - $user->visible_bugs(\@all_dep_ids); + my ($class, $bugs) = @_; + my $user = Bugzilla->user; + + # It would be faster but MUCH more complicated to select all the + # deps for the entire list in one SQL statement. If we ever have + # a profile that proves that that's necessary, we can switch over + # to the more complex method. + my @all_dep_ids; + foreach my $bug (@$bugs) { + push @all_dep_ids, @{$bug->blocked}, @{$bug->dependson}; + push @all_dep_ids, @{$bug->duplicate_ids}; + push @all_dep_ids, @{$bug->_preload_referenced_bugs}; + } + @all_dep_ids = uniq @all_dep_ids; + + # If we don't do this, can_see_bug will do one call per bug in + # the dependency and duplicate lists, in Bugzilla::Template::get_bug_link. + $user->visible_bugs(\@all_dep_ids); } # Helps load up bugs referenced in comments by retrieving them with a single # query from the database and injecting bug objects into the object-cache. sub _preload_referenced_bugs { - my $self = shift; + my $self = shift; - # inject current duplicates into the object-cache first - foreach my $bug (@{ $self->duplicates }) { - $bug->object_cache_set() unless Bugzilla::Bug->object_cache_get($bug->id); - } + # inject current duplicates into the object-cache first + foreach my $bug (@{$self->duplicates}) { + $bug->object_cache_set() unless Bugzilla::Bug->object_cache_get($bug->id); + } - # preload bugs from comments - my $referenced_bug_ids = _extract_bug_ids($self->comments); - my @ref_bug_ids = grep { !Bugzilla::Bug->object_cache_get($_) } @$referenced_bug_ids; + # preload bugs from comments + my $referenced_bug_ids = _extract_bug_ids($self->comments); + my @ref_bug_ids + = grep { !Bugzilla::Bug->object_cache_get($_) } @$referenced_bug_ids; - # inject into object-cache - my $referenced_bugs = Bugzilla::Bug->new_from_list(\@ref_bug_ids); - $_->object_cache_set() foreach @$referenced_bugs; + # inject into object-cache + my $referenced_bugs = Bugzilla::Bug->new_from_list(\@ref_bug_ids); + $_->object_cache_set() foreach @$referenced_bugs; - return $referenced_bug_ids; + return $referenced_bug_ids; } # Extract bug IDs mentioned in comments. This is much faster than calling quoteUrls(). sub _extract_bug_ids { - my $comments = shift; - my @bug_ids; - - my $params = Bugzilla->params; - my @urlbases = ($params->{'urlbase'}); - push(@urlbases, $params->{'sslbase'}) if $params->{'sslbase'}; - my $urlbase_re = '(?:' . join('|', map { qr/$_/ } @urlbases) . ')'; - my $bug_word = template_var('terms')->{bug}; - my $bugs_word = template_var('terms')->{bugs}; - - foreach my $comment (@$comments) { - if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { - push @bug_ids, $comment->extra_data; - next; - } - my $s = $comment->already_wrapped ? qr/\s/ : qr/\h/; - my $text = $comment->body; - # Full bug links - push @bug_ids, $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E([0-9]+)(?:\#c[0-9]+)?/g; - # bug X - my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i; - push @bug_ids, $text =~ /\b$bug_re/g; - # bugs X, Y, Z - my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*([0-9]+)(?:$s*,$s*\#?$s*([0-9]+))+/i; - push @bug_ids, $text =~ /\b$bugs_re/g; - # Old duplicate markers - push @bug_ids, $text =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )([0-9]+)(?=\ \*\*\*\Z)/; - } - # Make sure to filter invalid bug IDs. - @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids; - return [uniq @bug_ids]; -} + my $comments = shift; + my @bug_ids; -sub possible_duplicates { - my ($class, $params) = @_; - my $short_desc = $params->{summary}; - my $products = $params->{products} || []; - my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES; - $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES; - $products = [$products] if !ref($products) eq 'ARRAY'; - - my $orig_limit = $limit; - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', - { function => 'possible_duplicates', - param => $orig_limit }); + my $params = Bugzilla->params; + my @urlbases = ($params->{'urlbase'}); + push(@urlbases, $params->{'sslbase'}) if $params->{'sslbase'}; + my $urlbase_re = '(?:' . join('|', map {qr/$_/} @urlbases) . ')'; + my $bug_word = template_var('terms')->{bug}; + my $bugs_word = template_var('terms')->{bugs}; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my @words = split(/[\b\s]+/, $short_desc || ''); - # Remove leading/trailing punctuation from words - foreach my $word (@words) { - $word =~ s/(?:^\W+|\W+$)//g; + foreach my $comment (@$comments) { + if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { + push @bug_ids, $comment->extra_data; + next; } - # And make sure that each word is longer than 2 characters. - @words = grep { defined $_ and length($_) > 2 } @words; + my $s = $comment->already_wrapped ? qr/\s/ : qr/\h/; + my $text = $comment->body; - return [] if !@words; + # Full bug links + push @bug_ids, + $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E([0-9]+)(?:\#c[0-9]+)?/g; - my ($where_sql, $relevance_sql); - if ($dbh->FULLTEXT_OR) { - my $joined_terms = join($dbh->FULLTEXT_OR, @words); - ($where_sql, $relevance_sql) = - $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms); - $relevance_sql ||= $where_sql; - } - else { - my (@where, @relevance); - foreach my $word (@words) { - my ($term, $rel_term) = $dbh->sql_fulltext_search( - 'bugs_fulltext.short_desc', $word); - push(@where, $term); - push(@relevance, $rel_term || $term); - } + # bug X + my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i; + push @bug_ids, $text =~ /\b$bug_re/g; + + # bugs X, Y, Z + my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*([0-9]+)(?:$s*,$s*\#?$s*([0-9]+))+/i; + push @bug_ids, $text =~ /\b$bugs_re/g; + + # Old duplicate markers + push @bug_ids, $text + =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )([0-9]+)(?=\ \*\*\*\Z)/; + } + + # Make sure to filter invalid bug IDs. + @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids; + return [uniq @bug_ids]; +} - $where_sql = join(' OR ', @where); - $relevance_sql = join(' + ', @relevance); +sub possible_duplicates { + my ($class, $params) = @_; + my $short_desc = $params->{summary}; + my $products = $params->{products} || []; + my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES; + $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES; + $products = [$products] if !ref($products) eq 'ARRAY'; + + my $orig_limit = $limit; + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {function => 'possible_duplicates', param => $orig_limit}); + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my @words = split(/[\b\s]+/, $short_desc || ''); + + # Remove leading/trailing punctuation from words + foreach my $word (@words) { + $word =~ s/(?:^\W+|\W+$)//g; + } + + # And make sure that each word is longer than 2 characters. + @words = grep { defined $_ and length($_) > 2 } @words; + + return [] if !@words; + + my ($where_sql, $relevance_sql); + if ($dbh->FULLTEXT_OR) { + my $joined_terms = join($dbh->FULLTEXT_OR, @words); + ($where_sql, $relevance_sql) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms); + $relevance_sql ||= $where_sql; + } + else { + my (@where, @relevance); + foreach my $word (@words) { + my ($term, $rel_term) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $word); + push(@where, $term); + push(@relevance, $rel_term || $term); } - my $product_ids = join(',', map { $_->id } @$products); - my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; + $where_sql = join(' OR ', @where); + $relevance_sql = join(' + ', @relevance); + } + + my $product_ids = join(',', map { $_->id } @$products); + my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; - # Because we collapse duplicates, we want to get slightly more bugs - # than were actually asked for. - my $sql_limit = $limit + 5; + # Because we collapse duplicates, we want to get slightly more bugs + # than were actually asked for. + my $sql_limit = $limit + 5; - my $possible_dupes = $dbh->selectall_arrayref( - "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution, + my $possible_dupes = $dbh->selectall_arrayref( + "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution, ($relevance_sql) AS relevance FROM bugs INNER JOIN bugs_fulltext ON bugs.bug_id = bugs_fulltext.bug_id WHERE ($where_sql) $product_sql - ORDER BY relevance DESC, bug_id DESC " . - $dbh->sql_limit($sql_limit), {Slice=>{}}); - - my @actual_dupe_ids; - # Resolve duplicates into their ultimate target duplicates. - foreach my $bug (@$possible_dupes) { - my $push_id = $bug->{bug_id}; - if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') { - $push_id = _resolve_ultimate_dup_id($bug->{bug_id}); - } - push(@actual_dupe_ids, $push_id); - } - @actual_dupe_ids = uniq @actual_dupe_ids; - if (scalar @actual_dupe_ids > $limit) { - @actual_dupe_ids = @actual_dupe_ids[0..($limit-1)]; + ORDER BY relevance DESC, bug_id DESC " . $dbh->sql_limit($sql_limit), + {Slice => {}} + ); + + my @actual_dupe_ids; + + # Resolve duplicates into their ultimate target duplicates. + foreach my $bug (@$possible_dupes) { + my $push_id = $bug->{bug_id}; + if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') { + $push_id = _resolve_ultimate_dup_id($bug->{bug_id}); } + push(@actual_dupe_ids, $push_id); + } + @actual_dupe_ids = uniq @actual_dupe_ids; + if (scalar @actual_dupe_ids > $limit) { + @actual_dupe_ids = @actual_dupe_ids[0 .. ($limit - 1)]; + } - my $visible = $user->visible_bugs(\@actual_dupe_ids); - return $class->new_from_list($visible); + my $visible = $user->visible_bugs(\@actual_dupe_ids); + return $class->new_from_list($visible); } # Docs for create() (there's no POD in this file yet, but we very @@ -680,591 +693,621 @@ sub possible_duplicates { # # C<assigned_to> - The full login name of the user who the bug is # initially assigned to. -# C<qa_contact> - The full login name of the QA Contact for this bug. +# C<qa_contact> - The full login name of the QA Contact for this bug. # Will be ignored if C<useqacontact> is off. # -# C<estimated_time> - For time-tracking. Will be ignored if +# C<estimated_time> - For time-tracking. Will be ignored if # C<timetrackinggroup> is not set, or if the current # user is not a member of the timetrackinggroup. # C<deadline> - For time-tracking. Will be ignored for the same # reasons as C<estimated_time>. sub create { - my ($class, $params) = @_; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - # These fields have default values which we can use if they are undefined. - $params->{bug_severity} = Bugzilla->params->{defaultseverity} - unless defined $params->{bug_severity}; - $params->{priority} = Bugzilla->params->{defaultpriority} - unless defined $params->{priority}; - $params->{op_sys} = Bugzilla->params->{defaultopsys} - unless defined $params->{op_sys}; - $params->{rep_platform} = Bugzilla->params->{defaultplatform} - unless defined $params->{rep_platform}; - # Make sure a comment is always defined. - $params->{comment} = '' unless defined $params->{comment}; - - $class->check_required_create_fields($params); - $params = $class->run_create_validators($params); - - # These are not a fields in the bugs table, so we don't pass them to - # insert_create_data. - my $bug_aliases = delete $params->{alias}; - my $cc_ids = delete $params->{cc}; - my $groups = delete $params->{groups}; - my $depends_on = delete $params->{dependson}; - my $blocked = delete $params->{blocked}; - my $keywords = delete $params->{keywords}; - my $creation_comment = delete $params->{comment}; - my $see_also = delete $params->{see_also}; - - # 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 $ms_values = $class->_extract_multi_selects($params); - my $bug = $class->insert_create_data($params); - - # Add the group restrictions - my $sth_group = $dbh->prepare( - 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); - foreach my $group (@$groups) { - $sth_group->execute($bug->bug_id, $group->id); - } - - $dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', undef, - $timestamp, $bug->bug_id); - # Update the bug instance as well - $bug->{creation_ts} = $timestamp; - - # Add the CCs - my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)'); - foreach my $user_id (@$cc_ids) { - $sth_cc->execute($bug->bug_id, $user_id); - } - - # Add in keywords - my $sth_keyword = $dbh->prepare( - 'INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)'); - foreach my $keyword_id (map($_->id, @$keywords)) { - $sth_keyword->execute($bug->bug_id, $keyword_id); - } - - # Set up dependencies (blocked/dependson) - my $sth_deps = $dbh->prepare( - 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); - my $sth_bug_time = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); - - foreach my $depends_on_id (@$depends_on) { - $sth_deps->execute($bug->bug_id, $depends_on_id); - # Log the reverse action on the other bug. - LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, - $bug->{reporter_id}, $timestamp); - $sth_bug_time->execute($timestamp, $depends_on_id); - } - foreach my $blocked_id (@$blocked) { - $sth_deps->execute($blocked_id, $bug->bug_id); - # Log the reverse action on the other bug. - LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, - $bug->{reporter_id}, $timestamp); - $sth_bug_time->execute($timestamp, $blocked_id); - } - - # Insert the values into the multiselect value tables - foreach my $field (keys %$ms_values) { - $dbh->do("DELETE FROM bug_$field where bug_id = ?", - undef, $bug->bug_id); - foreach my $value ( @{$ms_values->{$field}} ) { - $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)", - undef, $bug->bug_id, $value); - } - } - - # Insert any see_also values - if ($see_also) { - my $see_also_array = $see_also; - if (!ref $see_also_array) { - $see_also = trim($see_also); - $see_also_array = [ split(/[\s,]+/, $see_also) ]; - } - foreach my $value (@$see_also_array) { - $bug->add_see_also($value); - } - foreach my $see_also (@{ $bug->see_also }) { - $see_also->insert_create_data($see_also); - } - foreach my $ref_bug (@{ $bug->{_update_ref_bugs} || [] }) { - $ref_bug->update(); - } - delete $bug->{_update_ref_bugs}; - } - - # Comment #0 handling... - - # We now have a bug id so we can fill this out - $creation_comment->{'bug_id'} = $bug->id; - - # Insert the comment. We always insert a comment on bug creation, - # but sometimes it's blank. - Bugzilla::Comment->insert_create_data($creation_comment); - - # Set up aliases - my $sth_aliases = $dbh->prepare('INSERT INTO bugs_aliases (alias, bug_id) VALUES (?, ?)'); - foreach my $alias (@$bug_aliases) { - trick_taint($alias); - $sth_aliases->execute($alias, $bug->bug_id); - } + my ($class, $params) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + # These fields have default values which we can use if they are undefined. + $params->{bug_severity} = Bugzilla->params->{defaultseverity} + unless defined $params->{bug_severity}; + $params->{priority} = Bugzilla->params->{defaultpriority} + unless defined $params->{priority}; + $params->{op_sys} = Bugzilla->params->{defaultopsys} + unless defined $params->{op_sys}; + $params->{rep_platform} = Bugzilla->params->{defaultplatform} + unless defined $params->{rep_platform}; + + # Make sure a comment is always defined. + $params->{comment} = '' unless defined $params->{comment}; + + $class->check_required_create_fields($params); + $params = $class->run_create_validators($params); + + # These are not a fields in the bugs table, so we don't pass them to + # insert_create_data. + my $bug_aliases = delete $params->{alias}; + my $cc_ids = delete $params->{cc}; + my $groups = delete $params->{groups}; + my $depends_on = delete $params->{dependson}; + my $blocked = delete $params->{blocked}; + my $keywords = delete $params->{keywords}; + my $creation_comment = delete $params->{comment}; + my $see_also = delete $params->{see_also}; + + # 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 $ms_values = $class->_extract_multi_selects($params); + my $bug = $class->insert_create_data($params); + + # Add the group restrictions + my $sth_group + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); + foreach my $group (@$groups) { + $sth_group->execute($bug->bug_id, $group->id); + } + + $dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', + undef, $timestamp, $bug->bug_id); + + # Update the bug instance as well + $bug->{creation_ts} = $timestamp; + + # Add the CCs + my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)'); + foreach my $user_id (@$cc_ids) { + $sth_cc->execute($bug->bug_id, $user_id); + } + + # Add in keywords + my $sth_keyword + = $dbh->prepare('INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)'); + foreach my $keyword_id (map($_->id, @$keywords)) { + $sth_keyword->execute($bug->bug_id, $keyword_id); + } + + # Set up dependencies (blocked/dependson) + my $sth_deps = $dbh->prepare( + 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); + my $sth_bug_time + = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); + + foreach my $depends_on_id (@$depends_on) { + $sth_deps->execute($bug->bug_id, $depends_on_id); + + # Log the reverse action on the other bug. + LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + $sth_bug_time->execute($timestamp, $depends_on_id); + } + foreach my $blocked_id (@$blocked) { + $sth_deps->execute($blocked_id, $bug->bug_id); + + # Log the reverse action on the other bug. + LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + $sth_bug_time->execute($timestamp, $blocked_id); + } + + # Insert the values into the multiselect value tables + foreach my $field (keys %$ms_values) { + $dbh->do("DELETE FROM bug_$field where bug_id = ?", undef, $bug->bug_id); + foreach my $value (@{$ms_values->{$field}}) { + $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)", + undef, $bug->bug_id, $value); + } + } + + # Insert any see_also values + if ($see_also) { + my $see_also_array = $see_also; + if (!ref $see_also_array) { + $see_also = trim($see_also); + $see_also_array = [split(/[\s,]+/, $see_also)]; + } + foreach my $value (@$see_also_array) { + $bug->add_see_also($value); + } + foreach my $see_also (@{$bug->see_also}) { + $see_also->insert_create_data($see_also); + } + foreach my $ref_bug (@{$bug->{_update_ref_bugs} || []}) { + $ref_bug->update(); + } + delete $bug->{_update_ref_bugs}; + } + + # Comment #0 handling... + + # We now have a bug id so we can fill this out + $creation_comment->{'bug_id'} = $bug->id; + + # Insert the comment. We always insert a comment on bug creation, + # but sometimes it's blank. + Bugzilla::Comment->insert_create_data($creation_comment); + + # Set up aliases + my $sth_aliases + = $dbh->prepare('INSERT INTO bugs_aliases (alias, bug_id) VALUES (?, ?)'); + foreach my $alias (@$bug_aliases) { + trick_taint($alias); + $sth_aliases->execute($alias, $bug->bug_id); + } - Bugzilla::Hook::process('bug_end_of_create', { bug => $bug, - timestamp => $timestamp, - }); + Bugzilla::Hook::process('bug_end_of_create', + {bug => $bug, timestamp => $timestamp,}); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Because MySQL doesn't support transactions on the fulltext table, - # we do this after we've committed the transaction. That way we're - # sure we're inserting a good Bug ID. - $bug->_sync_fulltext( new_bug => 1 ); + # Because MySQL doesn't support transactions on the fulltext table, + # we do this after we've committed the transaction. That way we're + # sure we're inserting a good Bug ID. + $bug->_sync_fulltext(new_bug => 1); - return $bug; + return $bug; } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - - # Add classification for checking mandatory fields which depend on it - $params->{classification} = $params->{product}->classification->name; - - my @mandatory_fields = @{ Bugzilla->fields({ is_mandatory => 1, - enter_bug => 1, - obsolete => 0 }) }; - foreach my $field (@mandatory_fields) { - $class->_check_field_is_mandatory($params->{$field->name}, $field, - $params); - } - - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - my $component = delete $params->{component}; - $params->{component_id} = $component->id; - - # Callers cannot set reporter, creation_ts, or delta_ts. - $params->{reporter} = $class->_check_reporter(); - $params->{delta_ts} = $params->{creation_ts}; - - if ($params->{estimated_time}) { - $params->{remaining_time} = $params->{estimated_time}; - } - - $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, - $params->{qa_contact}, $product); + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); - # You can't set these fields. - delete $params->{lastdiffed}; - delete $params->{bug_id}; - delete $params->{classification}; + # Add classification for checking mandatory fields which depend on it + $params->{classification} = $params->{product}->classification->name; - Bugzilla::Hook::process('bug_end_of_create_validators', - { params => $params }); + my @mandatory_fields + = @{Bugzilla->fields({is_mandatory => 1, enter_bug => 1, obsolete => 0})}; + foreach my $field (@mandatory_fields) { + $class->_check_field_is_mandatory($params->{$field->name}, $field, $params); + } - # And this is not a valid DB field, it's just used as part of - # _check_dependencies to avoid running it twice for both blocked - # and dependson. - delete $params->{_dependencies_validated}; + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + my $component = delete $params->{component}; + $params->{component_id} = $component->id; - return $params; -} + # Callers cannot set reporter, creation_ts, or delta_ts. + $params->{reporter} = $class->_check_reporter(); + $params->{delta_ts} = $params->{creation_ts}; -sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + if ($params->{estimated_time}) { + $params->{remaining_time} = $params->{estimated_time}; + } - # XXX This is just a temporary hack until all updating happens - # inside this function. - my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, + $params->{qa_contact}, $product); - $dbh->bz_start_transaction(); + # You can't set these fields. + delete $params->{lastdiffed}; + delete $params->{bug_id}; + delete $params->{classification}; - my ($changes, $old_bug) = $self->SUPER::update(@_); + Bugzilla::Hook::process('bug_end_of_create_validators', {params => $params}); - Bugzilla::Hook::process('bug_start_of_update', - { timestamp => $delta_ts, bug => $self, - old_bug => $old_bug, changes => $changes }); + # And this is not a valid DB field, it's just used as part of + # _check_dependencies to avoid running it twice for both blocked + # and dependson. + delete $params->{_dependencies_validated}; - # 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)) { - my $change = delete $changes->{$field}; - if ($change) { - my $new_field = $field; - $new_field =~ s/_id$//; - $changes->{$new_field} = - [$self->{"_old_${new_field}_name"}, $self->$new_field]; - } - } - foreach my $field (qw(qa_contact assigned_to)) { - if ($changes->{$field}) { - my ($from, $to) = @{ $changes->{$field} }; - $from = $old_bug->$field->login if $from; - $to = $self->$field->login if $to; - $changes->{$field} = [$from, $to]; - } - } - - # CC - my @old_cc = map {$_->id} @{$old_bug->cc_users}; - my @new_cc = map {$_->id} @{$self->cc_users}; - 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->sql_in('who', $removed_cc), undef, $self->id); - } - foreach my $user_id (@$added_cc) { - $dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)', - undef, $self->id, $user_id); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_cc || scalar @$added_cc) { - my $removed_users = Bugzilla::User->new_from_list($removed_cc); - my $added_users = Bugzilla::User->new_from_list($added_cc); - my $removed_names = join(', ', (map {$_->login} @$removed_users)); - my $added_names = join(', ', (map {$_->login} @$added_users)); - $changes->{cc} = [$removed_names, $added_names]; - } - - # Aliases - my $old_aliases = $old_bug->alias; - my $new_aliases = $self->alias; - my ($removed_aliases, $added_aliases) = diff_arrays($old_aliases, $new_aliases); - - foreach my $alias (@$removed_aliases) { - $dbh->do('DELETE FROM bugs_aliases WHERE bug_id = ? AND alias = ?', - undef, $self->id, $alias); - } - foreach my $alias (@$added_aliases) { - trick_taint($alias); - $dbh->do('INSERT INTO bugs_aliases (bug_id, alias) VALUES (?,?)', - undef, $self->id, $alias); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_aliases || scalar @$added_aliases) { - $changes->{alias} = [join(', ', @$removed_aliases), join(', ', @$added_aliases)]; - } - - # Keywords - my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects}; - my @new_kw_ids = map { $_->id } @{$self->keyword_objects}; - - 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->sql_in('keywordid', $removed_kw), undef, $self->id); - } - foreach my $keyword_id (@$added_kw) { - $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)', - undef, $self->id, $keyword_id); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_kw || scalar @$added_kw) { - my $removed_keywords = Bugzilla::Keyword->new_from_list($removed_kw); - my $added_keywords = Bugzilla::Keyword->new_from_list($added_kw); - my $removed_names = join(', ', (map {$_->name} @$removed_keywords)); - my $added_names = join(', ', (map {$_->name} @$added_keywords)); - $changes->{keywords} = [$removed_names, $added_names]; - } - - # Dependencies - foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) { - 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); - # Update delta_ts on the other bug so that we trigger mid-airs. - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $delta_ts, $removed_id); - } - 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); - # Update delta_ts on the other bug so that we trigger mid-airs. - $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)]; - } - } - - # Groups - my %old_groups = map {$_->id => $_} @{$old_bug->groups_in}; - my %new_groups = map {$_->id => $_} @{$self->groups_in}; - my ($removed_gr, $added_gr) = diff_arrays([keys %old_groups], - [keys %new_groups]); - if (scalar @$removed_gr || scalar @$added_gr) { - if (@$removed_gr) { - my $qmarks = join(',', ('?') x @$removed_gr); - $dbh->do("DELETE FROM bug_group_map - WHERE bug_id = ? AND group_id IN ($qmarks)", undef, - $self->id, @$removed_gr); - } - my $sth_insert = $dbh->prepare( - 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?,?)'); - foreach my $gid (@$added_gr) { - $sth_insert->execute($self->id, $gid); - } - my @removed_names = map { $old_groups{$_}->name } @$removed_gr; - my @added_names = map { $new_groups{$_}->name } @$added_gr; - $changes->{'bug_group'} = [join(', ', @removed_names), - join(', ', @added_names)]; - } - - # Comments - foreach my $comment (@{$self->{added_comments} || []}) { - # Override the Comment's timestamp to be identical to the update - # timestamp. - $comment->{bug_when} = $delta_ts; - $comment = Bugzilla::Comment->insert_create_data($comment); - if ($comment->work_time) { - LogActivityEntry($self->id, "work_time", "", $comment->work_time, - $user->id, $delta_ts); - } - } - - # Comment Privacy - foreach my $comment (@{$self->{comment_isprivate} || []}) { - $comment->update(); - - my ($from, $to) - = $comment->is_private ? (0, 1) : (1, 0); - LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, - $user->id, $delta_ts, $comment->id); - } - - # Clear the cache of comments - delete $self->{comments}; - - # Insert the values into the multiselect value tables - my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - foreach my $field (@multi_selects) { - my $name = $field->name; - my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name); - if (scalar @$removed || scalar @$added) { - $changes->{$name} = [join(', ', @$removed), join(', ', @$added)]; - - $dbh->do("DELETE FROM bug_$name where bug_id = ?", - undef, $self->id); - foreach my $value (@{$self->$name}) { - $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)", - undef, $self->id, $value); - } - } - } - - # See Also - - my ($removed_see, $added_see) = - diff_arrays($old_bug->see_also, $self->see_also, 'name'); - - $_->remove_from_db foreach @$removed_see; - $_->insert_create_data($_) foreach @$added_see; - - # If any changes were found, record it in the activity log - if (scalar @$removed_see || scalar @$added_see) { - $changes->{see_also} = [join(', ', map { $_->name } @$removed_see), - join(', ', map { $_->name } @$added_see)]; - } + return $params; +} - # Flags - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - $_->update foreach @{ $self->{_update_ref_bugs} || [] }; - delete $self->{_update_ref_bugs}; - - # Log bugs_activity items - # XXX Eventually, when bugs_activity is able to track the dupe_id, - # this code should go below the duplicates-table-updating code below. - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; - my $from = defined $change->[0] ? $change->[0] : ''; - my $to = defined $change->[1] ? $change->[1] : ''; - LogActivityEntry($self->id, $field, $from, $to, - $user->id, $delta_ts); - } + # XXX This is just a temporary hack until all updating happens + # inside this function. + my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - # Check if we have to update the duplicates table and the other bug. - my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0); - if ($old_dup != $cur_dup) { - $dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id); - if ($cur_dup) { - $dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)', - undef, $self->id, $cur_dup); - if (my $update_dup = delete $self->{_dup_for_update}) { - $update_dup->update(); - } - } + $dbh->bz_start_transaction(); - $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef]; - } + my ($changes, $old_bug) = $self->SUPER::update(@_); - 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} - || $self->{comment_isprivate}) + Bugzilla::Hook::process( + 'bug_start_of_update', { - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, ($delta_ts, $self->id)); - $self->{delta_ts} = $delta_ts; - } - - # Update last-visited - if ($user->is_involved_in_bug($self)) { - $self->update_user_last_visit($user, $delta_ts); - } - - # If a user is no longer involved, remove their last visit entry - my $last_visits = - Bugzilla::BugUserLastVisit->match({ bug_id => $self->id }); - foreach my $lv (@$last_visits) { - $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self); - } - - # Update bug ignore data if user wants to ignore mail for this bug - if (exists $self->{'bug_ignored'}) { - my $bug_ignored_changed; - if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) { - $dbh->do('INSERT INTO email_bug_ignore - (user_id, bug_id) VALUES (?, ?)', - undef, $user->id, $self->id); - $bug_ignored_changed = 1; - - } - elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) { - $dbh->do('DELETE FROM email_bug_ignore - WHERE user_id = ? AND bug_id = ?', - undef, $user->id, $self->id); - $bug_ignored_changed = 1; - } - delete $user->{bugs_ignored} if $bug_ignored_changed; - } - - $dbh->bz_commit_transaction(); - - # The only problem with this here is that update() is often called - # in the middle of a transaction, and if that transaction is rolled - # back, this change will *not* be rolled back. As we expect rollbacks - # to be extremely rare, that is OK for us. - $self->_sync_fulltext( - update_short_desc => $changes->{short_desc}, - update_comments => $self->{added_comments} || $self->{comment_isprivate} + timestamp => $delta_ts, + bug => $self, + old_bug => $old_bug, + changes => $changes + } + ); + + # 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)) { + my $change = delete $changes->{$field}; + if ($change) { + my $new_field = $field; + $new_field =~ s/_id$//; + $changes->{$new_field} = [$self->{"_old_${new_field}_name"}, $self->$new_field]; + } + } + foreach my $field (qw(qa_contact assigned_to)) { + if ($changes->{$field}) { + my ($from, $to) = @{$changes->{$field}}; + $from = $old_bug->$field->login if $from; + $to = $self->$field->login if $to; + $changes->{$field} = [$from, $to]; + } + } + + # CC + my @old_cc = map { $_->id } @{$old_bug->cc_users}; + my @new_cc = map { $_->id } @{$self->cc_users}; + 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->sql_in('who', $removed_cc), + undef, $self->id); + } + foreach my $user_id (@$added_cc) { + $dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)', + undef, $self->id, $user_id); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_cc || scalar @$added_cc) { + my $removed_users = Bugzilla::User->new_from_list($removed_cc); + my $added_users = Bugzilla::User->new_from_list($added_cc); + my $removed_names = join(', ', (map { $_->login } @$removed_users)); + my $added_names = join(', ', (map { $_->login } @$added_users)); + $changes->{cc} = [$removed_names, $added_names]; + } + + # Aliases + my $old_aliases = $old_bug->alias; + my $new_aliases = $self->alias; + my ($removed_aliases, $added_aliases) = diff_arrays($old_aliases, $new_aliases); + + foreach my $alias (@$removed_aliases) { + $dbh->do('DELETE FROM bugs_aliases WHERE bug_id = ? AND alias = ?', + undef, $self->id, $alias); + } + foreach my $alias (@$added_aliases) { + trick_taint($alias); + $dbh->do('INSERT INTO bugs_aliases (bug_id, alias) VALUES (?,?)', + undef, $self->id, $alias); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_aliases || scalar @$added_aliases) { + $changes->{alias} + = [join(', ', @$removed_aliases), join(', ', @$added_aliases)]; + } + + # Keywords + my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects}; + my @new_kw_ids = map { $_->id } @{$self->keyword_objects}; + + 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->sql_in('keywordid', $removed_kw), + undef, $self->id ); - - # Remove obsolete internal variables. - delete $self->{'_old_assigned_to'}; - delete $self->{'_old_qa_contact'}; - - # Also flush the visible_bugs cache for this bug as the user's - # relationship with this bug may have changed. - delete $user->{_visible_bugs_cache}->{$self->id}; - - return $changes; + } + foreach my $keyword_id (@$added_kw) { + $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)', + undef, $self->id, $keyword_id); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_kw || scalar @$added_kw) { + my $removed_keywords = Bugzilla::Keyword->new_from_list($removed_kw); + my $added_keywords = Bugzilla::Keyword->new_from_list($added_kw); + my $removed_names = join(', ', (map { $_->name } @$removed_keywords)); + my $added_names = join(', ', (map { $_->name } @$added_keywords)); + $changes->{keywords} = [$removed_names, $added_names]; + } + + # Dependencies + foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) { + 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); + + # Update delta_ts on the other bug so that we trigger mid-airs. + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $delta_ts, $removed_id); + } + 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); + + # Update delta_ts on the other bug so that we trigger mid-airs. + $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)]; + } + } + + # Groups + my %old_groups = map { $_->id => $_ } @{$old_bug->groups_in}; + my %new_groups = map { $_->id => $_ } @{$self->groups_in}; + my ($removed_gr, $added_gr) + = diff_arrays([keys %old_groups], [keys %new_groups]); + if (scalar @$removed_gr || scalar @$added_gr) { + if (@$removed_gr) { + my $qmarks = join(',', ('?') x @$removed_gr); + $dbh->do( + "DELETE FROM bug_group_map + WHERE bug_id = ? AND group_id IN ($qmarks)", undef, $self->id, + @$removed_gr + ); + } + my $sth_insert + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?,?)'); + foreach my $gid (@$added_gr) { + $sth_insert->execute($self->id, $gid); + } + my @removed_names = map { $old_groups{$_}->name } @$removed_gr; + my @added_names = map { $new_groups{$_}->name } @$added_gr; + $changes->{'bug_group'} + = [join(', ', @removed_names), join(', ', @added_names)]; + } + + # Comments + foreach my $comment (@{$self->{added_comments} || []}) { + + # Override the Comment's timestamp to be identical to the update + # timestamp. + $comment->{bug_when} = $delta_ts; + $comment = Bugzilla::Comment->insert_create_data($comment); + if ($comment->work_time) { + LogActivityEntry($self->id, "work_time", "", $comment->work_time, $user->id, + $delta_ts); + } + } + + # Comment Privacy + foreach my $comment (@{$self->{comment_isprivate} || []}) { + $comment->update(); + + my ($from, $to) = $comment->is_private ? (0, 1) : (1, 0); + LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, $user->id, + $delta_ts, $comment->id); + } + + # Clear the cache of comments + delete $self->{comments}; + + # Insert the values into the multiselect value tables + my @multi_selects + = grep { $_->type == FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + foreach my $field (@multi_selects) { + my $name = $field->name; + my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name); + if (scalar @$removed || scalar @$added) { + $changes->{$name} = [join(', ', @$removed), join(', ', @$added)]; + + $dbh->do("DELETE FROM bug_$name where bug_id = ?", undef, $self->id); + foreach my $value (@{$self->$name}) { + $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)", + undef, $self->id, $value); + } + } + } + + # See Also + + my ($removed_see, $added_see) + = diff_arrays($old_bug->see_also, $self->see_also, 'name'); + + $_->remove_from_db foreach @$removed_see; + $_->insert_create_data($_) foreach @$added_see; + + # If any changes were found, record it in the activity log + if (scalar @$removed_see || scalar @$added_see) { + $changes->{see_also} = [ + join(', ', map { $_->name } @$removed_see), + join(', ', map { $_->name } @$added_see) + ]; + } + + # Flags + my ($removed, $added) + = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); + if ($removed || $added) { + $changes->{'flagtypes.name'} = [$removed, $added]; + } + + $_->update foreach @{$self->{_update_ref_bugs} || []}; + delete $self->{_update_ref_bugs}; + + # Log bugs_activity items + # XXX Eventually, when bugs_activity is able to track the dupe_id, + # this code should go below the duplicates-table-updating code below. + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + my $from = defined $change->[0] ? $change->[0] : ''; + my $to = defined $change->[1] ? $change->[1] : ''; + LogActivityEntry($self->id, $field, $from, $to, $user->id, $delta_ts); + } + + # Check if we have to update the duplicates table and the other bug. + my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0); + if ($old_dup != $cur_dup) { + $dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id); + if ($cur_dup) { + $dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)', + undef, $self->id, $cur_dup); + if (my $update_dup = delete $self->{_dup_for_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, + old_bug => $old_bug + } + ); + + # If any change occurred, refresh the timestamp of the bug. + if ( scalar(keys %$changes) + || $self->{added_comments} + || $self->{comment_isprivate}) + { + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, ($delta_ts, $self->id)); + $self->{delta_ts} = $delta_ts; + } + + # Update last-visited + if ($user->is_involved_in_bug($self)) { + $self->update_user_last_visit($user, $delta_ts); + } + + # If a user is no longer involved, remove their last visit entry + my $last_visits = Bugzilla::BugUserLastVisit->match({bug_id => $self->id}); + foreach my $lv (@$last_visits) { + $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self); + } + + # Update bug ignore data if user wants to ignore mail for this bug + if (exists $self->{'bug_ignored'}) { + my $bug_ignored_changed; + if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) { + $dbh->do( + 'INSERT INTO email_bug_ignore + (user_id, bug_id) VALUES (?, ?)', undef, $user->id, $self->id + ); + $bug_ignored_changed = 1; + + } + elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) { + $dbh->do( + 'DELETE FROM email_bug_ignore + WHERE user_id = ? AND bug_id = ?', undef, $user->id, $self->id + ); + $bug_ignored_changed = 1; + } + delete $user->{bugs_ignored} if $bug_ignored_changed; + } + + $dbh->bz_commit_transaction(); + + # The only problem with this here is that update() is often called + # in the middle of a transaction, and if that transaction is rolled + # back, this change will *not* be rolled back. As we expect rollbacks + # to be extremely rare, that is OK for us. + $self->_sync_fulltext( + update_short_desc => $changes->{short_desc}, + update_comments => $self->{added_comments} || $self->{comment_isprivate} + ); + + # Remove obsolete internal variables. + delete $self->{'_old_assigned_to'}; + delete $self->{'_old_qa_contact'}; + + # Also flush the visible_bugs cache for this bug as the user's + # relationship with this bug may have changed. + delete $user->{_visible_bugs_cache}->{$self->id}; + + return $changes; } # Used by create(). # We need to handle multi-select fields differently than normal fields, # because they're arrays and don't go into the bugs table. sub _extract_multi_selects { - my ($invocant, $params) = @_; - - my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my %ms_values; - foreach my $field (@multi_selects) { - my $name = $field->name; - if (exists $params->{$name}) { - my $array = delete($params->{$name}) || []; - $ms_values{$name} = $array; - } + my ($invocant, $params) = @_; + + my @multi_selects + = grep { $_->type == FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + my %ms_values; + foreach my $field (@multi_selects) { + my $name = $field->name; + if (exists $params->{$name}) { + my $array = delete($params->{$name}) || []; + $ms_values{$name} = $array; } - return \%ms_values; + } + return \%ms_values; } # Should be called any time you update short_desc or change a comment. sub _sync_fulltext { - my ($self, %options) = @_; - my $dbh = Bugzilla->dbh; - - my($all_comments, $public_comments); - if ($options{new_bug} || $options{update_comments}) { - my $comments = $dbh->selectall_arrayref( - 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?', - undef, $self->id); - $all_comments = join("\n", map { $_->[0] } @$comments); - my @no_private = grep { !$_->[1] } @$comments; - $public_comments = join("\n", map { $_->[0] } @no_private); - } - - if ($options{new_bug}) { - $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc, comments, + my ($self, %options) = @_; + my $dbh = Bugzilla->dbh; + + my ($all_comments, $public_comments); + if ($options{new_bug} || $options{update_comments}) { + my $comments + = $dbh->selectall_arrayref( + 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?', + undef, $self->id); + $all_comments = join("\n", map { $_->[0] } @$comments); + my @no_private = grep { !$_->[1] } @$comments; + $public_comments = join("\n", map { $_->[0] } @no_private); + } + + if ($options{new_bug}) { + $dbh->do( + 'INSERT INTO bugs_fulltext (bug_id, short_desc, comments, comments_noprivate) - VALUES (?, ?, ?, ?)', - undef, - $self->id, $self->short_desc, $all_comments, $public_comments); - } else { - my(@names, @values); - if ($options{update_short_desc}) { - push @names, 'short_desc'; - push @values, $self->short_desc; - } - if ($options{update_comments}) { - push @names, ('comments', 'comments_noprivate'); - push @values, ($all_comments, $public_comments); - } - if (@names) { - $dbh->do('UPDATE bugs_fulltext SET ' . - join(', ', map { "$_ = ?" } @names) . - ' WHERE bug_id = ?', - undef, - @values, $self->id); - } + VALUES (?, ?, ?, ?)', undef, $self->id, $self->short_desc, + $all_comments, $public_comments + ); + } + else { + my (@names, @values); + if ($options{update_short_desc}) { + push @names, 'short_desc'; + push @values, $self->short_desc; } + if ($options{update_comments}) { + push @names, ('comments', 'comments_noprivate'); + push @values, ($all_comments, $public_comments); + } + if (@names) { + $dbh->do( + 'UPDATE bugs_fulltext SET ' + . join(', ', map {"$_ = ?"} @names) + . ' WHERE bug_id = ?', + undef, @values, $self->id + ); + } + } } sub remove_from_db { - my ($self) = @_; - my $dbh = Bugzilla->dbh; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + ThrowCodeError("bug_error", {bug => $self}) if $self->{'error'}; - ThrowCodeError("bug_error", { bug => $self }) if $self->{'error'}; + my $bug_id = $self->{'bug_id'}; + $self->SUPER::remove_from_db(); - my $bug_id = $self->{'bug_id'}; - $self->SUPER::remove_from_db(); - # The bugs_fulltext table doesn't support foreign keys. - $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id); + # The bugs_fulltext table doesn't support foreign keys. + $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id); } ##################################################################### @@ -1272,96 +1315,93 @@ sub remove_from_db { ##################################################################### sub send_changes { - my ($self, $changes, $vars) = @_; - - my $user = Bugzilla->user; - - my $old_qa = $changes->{'qa_contact'} - ? $changes->{'qa_contact'}->[0] : ''; - my $old_own = $changes->{'assigned_to'} - ? $changes->{'assigned_to'}->[0] : ''; - my $old_cc = $changes->{cc} - ? $changes->{cc}->[0] : ''; - - my %forced = ( - cc => [split(/[,;]+/, $old_cc)], - owner => $old_own, - qacontact => $old_qa, - changer => $user, - ); - - _send_bugmail({ id => $self->id, type => 'bug', forced => \%forced }, - $vars); - - # If the bug was marked as a duplicate, we need to notify users on the - # other bug of any changes to that bug. - my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef; - if ($new_dup_id) { - _send_bugmail({ forced => { changer => $user }, type => "dupe", - id => $new_dup_id }, $vars); - } - - # If there were changes in dependencies, we need to notify those - # dependencies. - if ($changes->{'bug_status'}) { - my ($old_status, $new_status) = @{ $changes->{'bug_status'} }; - - # If this bug has changed from opened to closed or vice-versa, - # then all of the bugs we block need to be notified. - if (is_open_state($old_status) ne is_open_state($new_status)) { - my $params = { forced => { changer => $user }, - type => 'dep', - dep_only => 1, - blocker => $self, - changes => $changes }; - - foreach my $id (@{ $self->blocked }) { - $params->{id} = $id; - _send_bugmail($params, $vars); - } - } - } - - # To get a list of all changed dependencies, convert the "changes" arrays - # into a long string, then collapse that string into unique numbers in - # a hash. - my $all_changed_deps = join(', ', @{ $changes->{'dependson'} || [] }); - $all_changed_deps = join(', ', @{ $changes->{'blocked'} || [] }, - $all_changed_deps); - my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps); - # When clearning one field (say, blocks) and filling in the other - # (say, dependson), an empty string can get into the hash and cause - # an error later. - delete $changed_deps{''}; - - foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { - _send_bugmail({ forced => { changer => $user }, type => "dep", - id => $id }, $vars); - } - - # Sending emails for the referenced bugs. - foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) { - _send_bugmail({ forced => { changer => $user }, - id => $ref_bug_id }, $vars); - } + my ($self, $changes, $vars) = @_; + + my $user = Bugzilla->user; + + my $old_qa = $changes->{'qa_contact'} ? $changes->{'qa_contact'}->[0] : ''; + my $old_own = $changes->{'assigned_to'} ? $changes->{'assigned_to'}->[0] : ''; + my $old_cc = $changes->{cc} ? $changes->{cc}->[0] : ''; + + my %forced = ( + cc => [split(/[,;]+/, $old_cc)], + owner => $old_own, + qacontact => $old_qa, + changer => $user, + ); + + _send_bugmail({id => $self->id, type => 'bug', forced => \%forced}, $vars); + + # If the bug was marked as a duplicate, we need to notify users on the + # other bug of any changes to that bug. + my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef; + if ($new_dup_id) { + _send_bugmail({forced => {changer => $user}, type => "dupe", id => $new_dup_id}, + $vars); + } + + # If there were changes in dependencies, we need to notify those + # dependencies. + if ($changes->{'bug_status'}) { + my ($old_status, $new_status) = @{$changes->{'bug_status'}}; + + # If this bug has changed from opened to closed or vice-versa, + # then all of the bugs we block need to be notified. + if (is_open_state($old_status) ne is_open_state($new_status)) { + my $params = { + forced => {changer => $user}, + type => 'dep', + dep_only => 1, + blocker => $self, + changes => $changes + }; + + foreach my $id (@{$self->blocked}) { + $params->{id} = $id; + _send_bugmail($params, $vars); + } + } + } + + # To get a list of all changed dependencies, convert the "changes" arrays + # into a long string, then collapse that string into unique numbers in + # a hash. + my $all_changed_deps = join(', ', @{$changes->{'dependson'} || []}); + $all_changed_deps + = join(', ', @{$changes->{'blocked'} || []}, $all_changed_deps); + my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps); + + # When clearning one field (say, blocks) and filling in the other + # (say, dependson), an empty string can get into the hash and cause + # an error later. + delete $changed_deps{''}; + + foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { + _send_bugmail({forced => {changer => $user}, type => "dep", id => $id}, $vars); + } + + # Sending emails for the referenced bugs. + foreach my $ref_bug_id (uniq @{$self->{see_also_changes} || []}) { + _send_bugmail({forced => {changer => $user}, id => $ref_bug_id}, $vars); + } } sub _send_bugmail { - my ($params, $vars) = @_; + my ($params, $vars) = @_; - require Bugzilla::BugMail; + require Bugzilla::BugMail; - my $results = - Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); + my $results + = Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - my $template = Bugzilla->template; - $vars->{$_} = $params->{$_} foreach keys %$params; - $vars->{'sent_bugmail'} = $results; - $template->process("bug/process/results.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - $vars->{'header_done'} = 1; - } + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + my $template = Bugzilla->template; + $vars->{$_} = $params->{$_} foreach keys %$params; + $vars->{'sent_bugmail'} = $results; + $template->process("bug/process/results.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + $vars->{'header_done'} = 1; + } } ##################################################################### @@ -1369,928 +1409,956 @@ sub _send_bugmail { ##################################################################### sub _check_alias { - my ($invocant, $aliases) = @_; - $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)]; + my ($invocant, $aliases) = @_; + $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)]; - # Remove empty aliases - @$aliases = grep { $_ } @$aliases; + # Remove empty aliases + @$aliases = grep {$_} @$aliases; - foreach my $alias (@$aliases) { - $alias = trim($alias); + foreach my $alias (@$aliases) { + $alias = trim($alias); - # Make sure the alias isn't too long. - if (length($alias) > 40) { - ThrowUserError("alias_too_long"); - } - # Make sure the alias isn't just a number. - if ($alias =~ /^\d+$/) { - ThrowUserError("alias_is_numeric", { alias => $alias }); - } - # Make sure the alias has no commas or spaces. - if ($alias =~ /[, ]/) { - ThrowUserError("alias_has_comma_or_space", { alias => $alias }); - } - # Make sure the alias is unique, or that it's already our alias. - my $other_bug = new Bugzilla::Bug($alias); - if (!$other_bug->{error} - && (!ref $invocant || $other_bug->id != $invocant->id)) - { - ThrowUserError("alias_in_use", { alias => $alias, - bug_id => $other_bug->id }); - } + # Make sure the alias isn't too long. + if (length($alias) > 40) { + ThrowUserError("alias_too_long"); } - return $aliases; -} + # Make sure the alias isn't just a number. + if ($alias =~ /^\d+$/) { + ThrowUserError("alias_is_numeric", {alias => $alias}); + } -sub _check_assigned_to { - my ($invocant, $assignee, undef, $params) = @_; - my $user = Bugzilla->user; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - - # Default assignee is the component owner. - my $id; - # If this is a new bug, you can only set the assignee if you have editbugs. - # If you didn't specify the assignee, we use the default assignee. - if (!ref $invocant - && (!$user->in_group('editbugs', $component->product_id) || !$assignee)) + # Make sure the alias has no commas or spaces. + if ($alias =~ /[, ]/) { + ThrowUserError("alias_has_comma_or_space", {alias => $alias}); + } + + # Make sure the alias is unique, or that it's already our alias. + my $other_bug = new Bugzilla::Bug($alias); + if (!$other_bug->{error} && (!ref $invocant || $other_bug->id != $invocant->id)) { - $id = $component->default_assignee->id; - } else { - if (!ref $assignee) { - $assignee = trim($assignee); - # When updating a bug, assigned_to can't be empty. - ThrowUserError("reassign_to_empty") if ref $invocant && !$assignee; - $assignee = Bugzilla::User->check($assignee); - } - $id = $assignee->id; - # create() checks this another way, so we don't have to run this - # check during create(). - $invocant->_check_strict_isolation_for_user($assignee) if ref $invocant; + ThrowUserError("alias_in_use", {alias => $alias, bug_id => $other_bug->id}); } - return $id; + } + + return $aliases; } -sub _check_bug_file_loc { - my ($invocant, $url) = @_; - $url = '' if !defined($url); - $url = trim($url); - # 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://') { - $url = ''; - } - return $url; +sub _check_assigned_to { + my ($invocant, $assignee, undef, $params) = @_; + my $user = Bugzilla->user; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + + # Default assignee is the component owner. + my $id; + + # If this is a new bug, you can only set the assignee if you have editbugs. + # If you didn't specify the assignee, we use the default assignee. + if (!ref $invocant + && (!$user->in_group('editbugs', $component->product_id) || !$assignee)) + { + $id = $component->default_assignee->id; + } + else { + if (!ref $assignee) { + $assignee = trim($assignee); + + # When updating a bug, assigned_to can't be empty. + ThrowUserError("reassign_to_empty") if ref $invocant && !$assignee; + $assignee = Bugzilla::User->check($assignee); + } + $id = $assignee->id; + + # create() checks this another way, so we don't have to run this + # check during create(). + $invocant->_check_strict_isolation_for_user($assignee) if ref $invocant; + } + return $id; } -sub _check_bug_status { - my ($invocant, $new_status, undef, $params) = @_; - my $user = Bugzilla->user; - my @valid_statuses; - my $old_status; # Note that this is undef for new bugs. +sub _check_bug_file_loc { + my ($invocant, $url) = @_; + $url = '' if !defined($url); + $url = trim($url); - my ($product, $comment); - if (ref $invocant) { - @valid_statuses = @{$invocant->statuses_available}; - $product = $invocant->product_obj; - $old_status = $invocant->status; - my $comments = $invocant->{added_comments} || []; - $comment = $comments->[-1]; - } - else { - $product = $params->{product}; - $comment = $params->{comment}; - @valid_statuses = @{ Bugzilla::Bug->statuses_available($product) }; - } + # 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://') { + $url = ''; + } + return $url; +} - # Check permissions for users filing new bugs. - if (!ref $invocant) { - if ($user->in_group('editbugs', $product->id) - || $user->in_group('canconfirm', $product->id)) { - # If the user with privs hasn't selected another status, - # select the first one of the list. - unless ($new_status) { - if (scalar(@valid_statuses) == 1) { - $new_status = $valid_statuses[0]; - } - else { - $new_status = ($valid_statuses[0]->name ne 'UNCONFIRMED') ? - $valid_statuses[0] : $valid_statuses[1]; - } - } +sub _check_bug_status { + my ($invocant, $new_status, undef, $params) = @_; + my $user = Bugzilla->user; + my @valid_statuses; + my $old_status; # Note that this is undef for new bugs. + + my ($product, $comment); + if (ref $invocant) { + @valid_statuses = @{$invocant->statuses_available}; + $product = $invocant->product_obj; + $old_status = $invocant->status; + my $comments = $invocant->{added_comments} || []; + $comment = $comments->[-1]; + } + else { + $product = $params->{product}; + $comment = $params->{comment}; + @valid_statuses = @{Bugzilla::Bug->statuses_available($product)}; + } + + # Check permissions for users filing new bugs. + if (!ref $invocant) { + if ( $user->in_group('editbugs', $product->id) + || $user->in_group('canconfirm', $product->id)) + { + # If the user with privs hasn't selected another status, + # select the first one of the list. + unless ($new_status) { + if (scalar(@valid_statuses) == 1) { + $new_status = $valid_statuses[0]; } else { - # A user with no privs cannot choose the initial status. - # If UNCONFIRMED is valid for this product, use it; else - # use the first bug status available. - if (grep {$_->name eq 'UNCONFIRMED'} @valid_statuses) { - $new_status = 'UNCONFIRMED'; - } - else { - $new_status = $valid_statuses[0]; - } + $new_status + = ($valid_statuses[0]->name ne 'UNCONFIRMED') + ? $valid_statuses[0] + : $valid_statuses[1]; } + } } + else { + # A user with no privs cannot choose the initial status. + # If UNCONFIRMED is valid for this product, use it; else + # use the first bug status available. + if (grep { $_->name eq 'UNCONFIRMED' } @valid_statuses) { + $new_status = 'UNCONFIRMED'; + } + else { + $new_status = $valid_statuses[0]; + } + } + } + + # Time to validate the bug status. + $new_status = Bugzilla::Status->check($new_status) unless ref($new_status); + + # 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}); + } + + # Check if a comment is required for this change. + if ($new_status->comment_required_on_change_from($old_status) + && !$comment->{'thetext'}) + { + ThrowUserError( + 'comment_required', + { + old => $old_status ? $old_status->name : undef, + new => $new_status->name, + field => 'bug_status' + } + ); + } - # Time to validate the bug status. - $new_status = Bugzilla::Status->check($new_status) unless ref($new_status); - # 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 }); - } + if ( + ref $invocant && ( + $new_status->name eq 'IN_PROGRESS' - # Check if a comment is required for this change. - if ($new_status->comment_required_on_change_from($old_status) - && !$comment->{'thetext'}) - { - ThrowUserError('comment_required', - { old => $old_status ? $old_status->name : undef, - new => $new_status->name, field => 'bug_status' }); - } - - if (ref $invocant - && ($new_status->name eq 'IN_PROGRESS' - # Backwards-compat for the old default workflow. - or $new_status->name eq 'ASSIGNED') - && Bugzilla->params->{"usetargetmilestone"} - && Bugzilla->params->{"musthavemilestoneonaccept"} - # musthavemilestoneonaccept applies only if at least two - # target milestones are defined for the product. - && scalar(@{ $product->milestones }) > 1 - && $invocant->target_milestone eq $product->default_milestone) - { - ThrowUserError("milestone_required", { bug => $invocant }); - } + # Backwards-compat for the old default workflow. + or $new_status->name eq 'ASSIGNED' + ) + && Bugzilla->params->{"usetargetmilestone"} + && Bugzilla->params->{"musthavemilestoneonaccept"} - if (!blessed $invocant) { - $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; - } + # musthavemilestoneonaccept applies only if at least two + # target milestones are defined for the product. + && scalar(@{$product->milestones}) > 1 + && $invocant->target_milestone eq $product->default_milestone + ) + { + ThrowUserError("milestone_required", {bug => $invocant}); + } + + if (!blessed $invocant) { + $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; + } - return $new_status->name; + return $new_status->name; } sub _check_cc { - my ($invocant, $ccs, undef, $params) = @_; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - return [map {$_->id} @{$component->initial_cc}] unless $ccs; - - # Allow comma-separated input as well as arrayrefs. - $ccs = [split(/[,;]+/, $ccs)] if !ref $ccs; - - my %cc_ids; - foreach my $person (@$ccs) { - $person = trim($person); - next unless $person; - my $id = login_to_id($person, THROW_ERROR); - $cc_ids{$id} = 1; - } + my ($invocant, $ccs, undef, $params) = @_; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + return [map { $_->id } @{$component->initial_cc}] unless $ccs; + + # Allow comma-separated input as well as arrayrefs. + $ccs = [split(/[,;]+/, $ccs)] if !ref $ccs; + + my %cc_ids; + foreach my $person (@$ccs) { + $person = trim($person); + next unless $person; + my $id = login_to_id($person, THROW_ERROR); + $cc_ids{$id} = 1; + } - # Enforce Default CC - $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); + # Enforce Default CC + $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); - return [keys %cc_ids]; + return [keys %cc_ids]; } sub _check_comment { - my ($invocant, $comment_txt, undef, $params) = @_; + my ($invocant, $comment_txt, undef, $params) = @_; - # Comment can be empty. We should force it to be empty if the text is undef - if (!defined $comment_txt) { - $comment_txt = ''; - } + # Comment can be empty. We should force it to be empty if the text is undef + if (!defined $comment_txt) { + $comment_txt = ''; + } - # Load up some data - my $isprivate = delete $params->{comment_is_private}; - my $timestamp = $params->{creation_ts}; + # Load up some data + my $isprivate = delete $params->{comment_is_private}; + my $timestamp = $params->{creation_ts}; - # Create the new comment so we can check it - my $comment = { - thetext => $comment_txt, - bug_when => $timestamp, - }; + # Create the new comment so we can check it + my $comment = {thetext => $comment_txt, bug_when => $timestamp,}; - # We don't include the "isprivate" column unless it was specified. - # This allows it to fall back to its database default. - if (defined $isprivate) { - $comment->{isprivate} = $isprivate; - } + # We don't include the "isprivate" column unless it was specified. + # This allows it to fall back to its database default. + if (defined $isprivate) { + $comment->{isprivate} = $isprivate; + } - # Validate comment. We have to do this special as a comment normally - # requires a bug to be already created. For a new bug, the first comment - # obviously can't get the bug if the bug is created after this - # (see bug 590334) - Bugzilla::Comment->check_required_create_fields($comment); - $comment = Bugzilla::Comment->run_create_validators($comment, - { skip => ['bug_id'] } - ); + # Validate comment. We have to do this special as a comment normally + # requires a bug to be already created. For a new bug, the first comment + # obviously can't get the bug if the bug is created after this + # (see bug 590334) + Bugzilla::Comment->check_required_create_fields($comment); + $comment + = Bugzilla::Comment->run_create_validators($comment, {skip => ['bug_id']}); - return $comment; + return $comment; } sub _check_commenton { - my ($invocant, $new_value, $field, $params) = @_; + my ($invocant, $new_value, $field, $params) = @_; - my $has_comment = - ref($invocant) ? $invocant->{added_comments} - : (defined $params->{comment} - and $params->{comment}->{thetext} ne ''); + my $has_comment + = ref($invocant) + ? $invocant->{added_comments} + : (defined $params->{comment} and $params->{comment}->{thetext} ne ''); - my $is_changing = ref($invocant) ? $invocant->$field ne $new_value - : $new_value ne ''; + my $is_changing + = ref($invocant) ? $invocant->$field ne $new_value : $new_value ne ''; - if ($is_changing && !$has_comment) { - my $old_value = ref($invocant) ? $invocant->$field : undef; - ThrowUserError('comment_required', - { field => $field, old => $old_value, new => $new_value }); - } + if ($is_changing && !$has_comment) { + my $old_value = ref($invocant) ? $invocant->$field : undef; + ThrowUserError('comment_required', + {field => $field, old => $old_value, new => $new_value}); + } } sub _check_component { - my ($invocant, $name, undef, $params) = @_; - $name = trim($name); - $name || ThrowUserError("require_component"); - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_comp = blessed($invocant) ? $invocant->component : ''; - my $object = Bugzilla::Component->check({ product => $product, name => $name }); - if ($object->name ne $old_comp && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $name }); - } - return $object; + my ($invocant, $name, undef, $params) = @_; + $name = trim($name); + $name || ThrowUserError("require_component"); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_comp = blessed($invocant) ? $invocant->component : ''; + my $object = Bugzilla::Component->check({product => $product, name => $name}); + if ($object->name ne $old_comp && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $name}); + } + return $object; } sub _check_creation_ts { - return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); } sub _check_deadline { - my ($invocant, $date) = @_; + my ($invocant, $date) = @_; - # When filing bugs, we're forgiving and just return undef if - # the user isn't a timetracker. When updating bugs, check_can_change_field - # controls permissions, so we don't want to check them here. - if (!ref $invocant and !Bugzilla->user->is_timetracker) { - return undef; - } + # When filing bugs, we're forgiving and just return undef if + # the user isn't a timetracker. When updating bugs, check_can_change_field + # controls permissions, so we don't want to check them here. + if (!ref $invocant and !Bugzilla->user->is_timetracker) { + return undef; + } - # Validate entered deadline - $date = trim($date); - return undef if !$date; - validate_date($date) - || ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD' }); - return $date; + # Validate entered deadline + $date = trim($date); + return undef if !$date; + validate_date($date) + || ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'}); + return $date; } # Takes two comma/space-separated strings and returns arrayrefs # of valid bug IDs. sub _check_dependencies { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - return $value if $params->{_dependencies_validated}; + return $value if $params->{_dependencies_validated}; - if (!ref $invocant) { - # Only editbugs users can set dependencies on bug entry. - return ([], []) unless Bugzilla->user->in_group( - 'editbugs', $params->{product}->id); - } + if (!ref $invocant) { + + # Only editbugs users can set dependencies on bug entry. + return ([], []) + unless Bugzilla->user->in_group('editbugs', $params->{product}->id); + } - # This is done this way so that dependson and blocked can be in - # VALIDATORS, meaning that they can be in VALIDATOR_DEPENDENCIES, - # which means that they can be checked in the right order during - # bug creation. - my $opposite = $field eq 'dependson' ? 'blocked' : 'dependson'; - my %deps_in = ($field => $value || '', - $opposite => $params->{$opposite} || ''); - - foreach my $type (qw(dependson blocked)) { - my @bug_ids = ref($deps_in{$type}) - ? @{$deps_in{$type}} - : split(/[\s,]+/, $deps_in{$type}); - # Eliminate nulls. - @bug_ids = grep {$_} @bug_ids; - - my @check_access = @bug_ids; - # 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; - if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { - ThrowUserError('illegal_change', { field => $type, - privs => $privs }); - } - } + # This is done this way so that dependson and blocked can be in + # VALIDATORS, meaning that they can be in VALIDATOR_DEPENDENCIES, + # which means that they can be checked in the right order during + # bug creation. + my $opposite = $field eq 'dependson' ? 'blocked' : 'dependson'; + my %deps_in = ($field => $value || '', $opposite => $params->{$opposite} || ''); + + foreach my $type (qw(dependson blocked)) { + my @bug_ids + = ref($deps_in{$type}) + ? @{$deps_in{$type}} + : split(/[\s,]+/, $deps_in{$type}); + + # Eliminate nulls. + @bug_ids = grep {$_} @bug_ids; + + my @check_access = @bug_ids; + + # 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; + if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { + ThrowUserError('illegal_change', {field => $type, privs => $privs}); } + } + } - my $user = Bugzilla->user; - foreach my $modified_id (@check_access) { - my $delta_bug = $invocant->check($modified_id); - # Under strict isolation, you can't modify a bug if you can't - # edit it, even if you can see it. - if (Bugzilla->params->{"strict_isolation"}) { - if (!$user->can_edit_product($delta_bug->{'product_id'})) { - ThrowUserError("illegal_change_deps", {field => $type}); - } - } + my $user = Bugzilla->user; + foreach my $modified_id (@check_access) { + my $delta_bug = $invocant->check($modified_id); + + # Under strict isolation, you can't modify a bug if you can't + # edit it, even if you can see it. + if (Bugzilla->params->{"strict_isolation"}) { + if (!$user->can_edit_product($delta_bug->{'product_id'})) { + ThrowUserError("illegal_change_deps", {field => $type}); } - # Replace all aliases by their corresponding bug ID. - @bug_ids = map { $_ =~ /^(\d+)$/ ? $1 : $invocant->check($_, $type)->id } @bug_ids; - $deps_in{$type} = \@bug_ids; + } } - # And finally, check for dependency loops. - my $bug_id = ref($invocant) ? $invocant->id : 0; - my %deps = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, - $bug_id); + # Replace all aliases by their corresponding bug ID. + @bug_ids + = map { $_ =~ /^(\d+)$/ ? $1 : $invocant->check($_, $type)->id } @bug_ids; + $deps_in{$type} = \@bug_ids; + } - $params->{$opposite} = $deps{$opposite}; - $params->{_dependencies_validated} = 1; - return $deps{$field}; + # And finally, check for dependency loops. + my $bug_id = ref($invocant) ? $invocant->id : 0; + my %deps + = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, $bug_id); + + $params->{$opposite} = $deps{$opposite}; + $params->{_dependencies_validated} = 1; + return $deps{$field}; } sub _check_dup_id { - my ($self, $dupe_of) = @_; - my $dbh = Bugzilla->dbh; - - # Store the bug ID/alias passed by the user for visibility checks. - my $orig_dupe_of = $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 - # make sure that the bug exists, and convert the alias to the bug ID - # if a string is passed. Group restrictions are checked below. - my $dupe_of_bug = $self->check($dupe_of, 'dup_id'); - $dupe_of = $dupe_of_bug->id; - - # If the dupe is unchanged, we have nothing more to check. - return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of); - - # If we come here, then the duplicate is new. We have to make sure - # that we can view/change it (issue A on bug 96085). - $dupe_of_bug->check_is_visible($orig_dupe_of); - - # Make sure a loop isn't created when marking this bug - # as duplicate. - _resolve_ultimate_dup_id($self->id, $dupe_of, 1); - - my $cur_dup = $self->dup_id || 0; - if ($cur_dup != $dupe_of && Bugzilla->params->{'commentonduplicate'} - && !$self->{added_comments}) - { - ThrowUserError('comment_required'); + my ($self, $dupe_of) = @_; + my $dbh = Bugzilla->dbh; + + # Store the bug ID/alias passed by the user for visibility checks. + my $orig_dupe_of = $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 + # make sure that the bug exists, and convert the alias to the bug ID + # if a string is passed. Group restrictions are checked below. + my $dupe_of_bug = $self->check($dupe_of, 'dup_id'); + $dupe_of = $dupe_of_bug->id; + + # If the dupe is unchanged, we have nothing more to check. + return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of); + + # If we come here, then the duplicate is new. We have to make sure + # that we can view/change it (issue A on bug 96085). + $dupe_of_bug->check_is_visible($orig_dupe_of); + + # Make sure a loop isn't created when marking this bug + # as duplicate. + _resolve_ultimate_dup_id($self->id, $dupe_of, 1); + + my $cur_dup = $self->dup_id || 0; + if ( $cur_dup != $dupe_of + && Bugzilla->params->{'commentonduplicate'} + && !$self->{added_comments}) + { + ThrowUserError('comment_required'); + } + + # Should we add the reporter to the CC list of the new bug? + # If they can see the bug... + if ($self->reporter->can_see_bug($dupe_of)) { + + # We only add them if they're not the reporter of the other bug. + $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 + # 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... + my $cgi = Bugzilla->cgi; + my $add_confirmed = $cgi->param('confirm_add_duplicate'); + if (defined $add_confirmed) { + $self->{_add_dup_cc} = $add_confirmed; } + else { + # Note that here we don't check if the user is already the reporter + # of the dupe_of bug, since we already checked if they can *see* + # the bug, above. People might have reporter_accessible turned + # off, but cclist_accessible turned on, so they might want to + # add the reporter even though they're already the reporter of the + # dup_of bug. + my $vars = {}; + my $template = Bugzilla->template; - # Should we add the reporter to the CC list of the new bug? - # If they can see the bug... - if ($self->reporter->can_see_bug($dupe_of)) { - # We only add them if they're not the reporter of the other bug. - $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 - # 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... - my $cgi = Bugzilla->cgi; - my $add_confirmed = $cgi->param('confirm_add_duplicate'); - if (defined $add_confirmed) { - $self->{_add_dup_cc} = $add_confirmed; - } - else { - # Note that here we don't check if the user is already the reporter - # of the dupe_of bug, since we already checked if they can *see* - # the bug, above. People might have reporter_accessible turned - # off, but cclist_accessible turned on, so they might want to - # add the reporter even though they're already the reporter of the - # dup_of bug. - my $vars = {}; - my $template = Bugzilla->template; - # Ask the user what they want to do about the reporter. - $vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible; - $vars->{'original_bug_id'} = $dupe_of; - $vars->{'duplicate_bug_id'} = $self->id; - print $cgi->header(); - $template->process("bug/process/confirm-duplicate.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + # Ask the user what they want to do about the reporter. + $vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible; + $vars->{'original_bug_id'} = $dupe_of; + $vars->{'duplicate_bug_id'} = $self->id; + print $cgi->header(); + $template->process("bug/process/confirm-duplicate.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } + } - return $dupe_of; + return $dupe_of; } sub _check_groups { - my ($invocant, $group_names, undef, $params) = @_; - - my $bug_id = blessed($invocant) ? $invocant->id : undef; - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my %add_groups; - - # In email or WebServices, when the "groups" item actually - # isn't specified, then just add the default groups. - if (!defined $group_names) { - my $available = $product->groups_available; - foreach my $group (@$available) { - $add_groups{$group->id} = $group if $group->{is_default}; - } + my ($invocant, $group_names, undef, $params) = @_; + + my $bug_id = blessed($invocant) ? $invocant->id : undef; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my %add_groups; + + # In email or WebServices, when the "groups" item actually + # isn't specified, then just add the default groups. + if (!defined $group_names) { + my $available = $product->groups_available; + foreach my $group (@$available) { + $add_groups{$group->id} = $group if $group->{is_default}; } - else { - # Allow a comma-separated list, for email_in.pl. - $group_names = [map { trim($_) } split(',', $group_names)] - if !ref $group_names; - - # First check all the groups they chose to set. - my %args = ( product => $product->name, bug_id => $bug_id, action => 'add' ); - foreach my $name (@$group_names) { - my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name }); - - if (!$product->group_is_settable($group)) { - ThrowUserError('group_restriction_not_allowed', { %args, name => $name }); - } - $add_groups{$group->id} = $group; - } + } + else { + # Allow a comma-separated list, for email_in.pl. + $group_names = [map { trim($_) } split(',', $group_names)] if !ref $group_names; + + # First check all the groups they chose to set. + my %args = (product => $product->name, bug_id => $bug_id, action => 'add'); + foreach my $name (@$group_names) { + my $group = Bugzilla::Group->check_no_disclose({%args, name => $name}); + + if (!$product->group_is_settable($group)) { + ThrowUserError('group_restriction_not_allowed', {%args, name => $name}); + } + $add_groups{$group->id} = $group; } + } - # Now enforce mandatory groups. - $add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory }; + # Now enforce mandatory groups. + $add_groups{$_->id} = $_ foreach @{$product->groups_mandatory}; - my @add_groups = values %add_groups; - return \@add_groups; + my @add_groups = values %add_groups; + return \@add_groups; } sub _check_keywords { - my ($invocant, $keywords_in, undef, $params) = @_; + my ($invocant, $keywords_in, undef, $params) = @_; - return [] if !defined $keywords_in; + return [] if !defined $keywords_in; - my $keyword_array = $keywords_in; - if (!ref $keyword_array) { - $keywords_in = trim($keywords_in); - $keyword_array = [split(/[\s,]+/, $keywords_in)]; - } - - my %keywords; - foreach my $keyword (@$keyword_array) { - next unless $keyword; - my $obj = Bugzilla::Keyword->check($keyword); - $keywords{$obj->id} = $obj; - } - return [values %keywords]; + my $keyword_array = $keywords_in; + if (!ref $keyword_array) { + $keywords_in = trim($keywords_in); + $keyword_array = [split(/[\s,]+/, $keywords_in)]; + } + + my %keywords; + foreach my $keyword (@$keyword_array) { + next unless $keyword; + my $obj = Bugzilla::Keyword->check($keyword); + $keywords{$obj->id} = $obj; + } + return [values %keywords]; } sub _check_product { - my ($invocant, $name) = @_; - $name = trim($name); - # If we're updating the bug and they haven't changed the product, - # always allow it. - if (ref $invocant && lc($invocant->product_obj->name) eq lc($name)) { - return $invocant->product_obj; - } - # Check that the product exists and that the user - # is allowed to enter bugs into this product. - my $product = Bugzilla->user->can_enter_product($name, THROW_ERROR); - return $product; + my ($invocant, $name) = @_; + $name = trim($name); + + # If we're updating the bug and they haven't changed the product, + # always allow it. + if (ref $invocant && lc($invocant->product_obj->name) eq lc($name)) { + return $invocant->product_obj; + } + + # Check that the product exists and that the user + # is allowed to enter bugs into this product. + my $product = Bugzilla->user->can_enter_product($name, THROW_ERROR); + return $product; } sub _check_priority { - my ($invocant, $priority) = @_; - if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { - $priority = Bugzilla->params->{'defaultpriority'}; - } - return $invocant->_check_select_field($priority, 'priority'); + my ($invocant, $priority) = @_; + if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { + $priority = Bugzilla->params->{'defaultpriority'}; + } + return $invocant->_check_select_field($priority, 'priority'); } sub _check_qa_contact { - my ($invocant, $qa_contact, undef, $params) = @_; - $qa_contact = trim($qa_contact) if !ref $qa_contact; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - if (!ref $invocant) { - # Bugs get no QA Contact on creation if useqacontact is off. - return undef if !Bugzilla->params->{useqacontact}; - # Set the default QA Contact if one isn't specified or if the - # user doesn't have editbugs. - if (!Bugzilla->user->in_group('editbugs', $component->product_id) - || !$qa_contact) - { - return $component->default_qa_contact ? $component->default_qa_contact->id : undef; - } + my ($invocant, $qa_contact, undef, $params) = @_; + $qa_contact = trim($qa_contact) if !ref $qa_contact; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + if (!ref $invocant) { + + # Bugs get no QA Contact on creation if useqacontact is off. + return undef if !Bugzilla->params->{useqacontact}; + + # Set the default QA Contact if one isn't specified or if the + # user doesn't have editbugs. + if ( !Bugzilla->user->in_group('editbugs', $component->product_id) + || !$qa_contact) + { + return $component->default_qa_contact + ? $component->default_qa_contact->id + : undef; } + } - # If a QA Contact was specified or if we're updating, check - # the QA Contact for validity. - my $id; - if ($qa_contact) { - $qa_contact = Bugzilla::User->check($qa_contact) if !ref $qa_contact; - $id = $qa_contact->id; - # create() checks this another way, so we don't have to run this - # check during create(). - # If there is no QA contact, this check is not required. - $invocant->_check_strict_isolation_for_user($qa_contact) - if (ref $invocant && $id); - } + # If a QA Contact was specified or if we're updating, check + # the QA Contact for validity. + my $id; + if ($qa_contact) { + $qa_contact = Bugzilla::User->check($qa_contact) if !ref $qa_contact; + $id = $qa_contact->id; + + # create() checks this another way, so we don't have to run this + # check during create(). + # If there is no QA contact, this check is not required. + $invocant->_check_strict_isolation_for_user($qa_contact) + if (ref $invocant && $id); + } - # "0" always means "undef", for QA Contact. - return $id || undef; + # "0" always means "undef", for QA Contact. + return $id || undef; } sub _check_reporter { - my $invocant = shift; - my $reporter; - if (ref $invocant) { - # You cannot change the reporter of a bug. - $reporter = $invocant->reporter->id; - } - else { - # On bug creation, the reporter is the logged in user - # (meaning that they must be logged in first!). - Bugzilla->login(LOGIN_REQUIRED); - $reporter = Bugzilla->user->id; - } - return $reporter; + my $invocant = shift; + my $reporter; + if (ref $invocant) { + + # You cannot change the reporter of a bug. + $reporter = $invocant->reporter->id; + } + else { + # On bug creation, the reporter is the logged in user + # (meaning that they must be logged in first!). + Bugzilla->login(LOGIN_REQUIRED); + $reporter = Bugzilla->user->id; + } + return $reporter; } sub _check_resolution { - my ($invocant, $resolution, undef, $params) = @_; - $resolution = trim($resolution); - my $status = ref($invocant) ? $invocant->status->name - : $params->{bug_status}; - my $is_open = ref($invocant) ? $invocant->status->is_open - : is_open_state($status); - - # 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 => $status }) - if !$resolution && !$is_open; - - # Make sure this is a valid resolution. - $resolution = $invocant->_check_select_field($resolution, 'resolution'); - - # Don't allow open bugs to have resolutions. - ThrowUserError('resolution_not_allowed') if $is_open; - - # Check noresolveonopenblockers. - my $dependson = ref($invocant) ? $invocant->dependson - : ($params->{dependson} || []); - if (Bugzilla->params->{"noresolveonopenblockers"} - && $resolution eq 'FIXED' - && (!ref $invocant or !$invocant->resolution - or $resolution ne $invocant->resolution) - && scalar @$dependson) - { - my $dep_bugs = Bugzilla::Bug->new_from_list($dependson); - my $count_open = grep { $_->isopened } @$dep_bugs; - if ($count_open) { - my $bug_id = ref($invocant) ? $invocant->id : undef; - ThrowUserError("still_unresolved_bugs", - { bug_id => $bug_id, dep_count => $count_open }); - } - } - - # Check if they're changing the resolution and need to comment. - if (Bugzilla->params->{'commentonchange_resolution'}) { - $invocant->_check_commenton($resolution, 'resolution', $params); - } - - return $resolution; + my ($invocant, $resolution, undef, $params) = @_; + $resolution = trim($resolution); + my $status = ref($invocant) ? $invocant->status->name : $params->{bug_status}; + my $is_open + = ref($invocant) ? $invocant->status->is_open : is_open_state($status); + + # 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 => $status}) + if !$resolution && !$is_open; + + # Make sure this is a valid resolution. + $resolution = $invocant->_check_select_field($resolution, 'resolution'); + + # Don't allow open bugs to have resolutions. + ThrowUserError('resolution_not_allowed') if $is_open; + + # Check noresolveonopenblockers. + my $dependson + = ref($invocant) ? $invocant->dependson : ($params->{dependson} || []); + if ( + Bugzilla->params->{"noresolveonopenblockers"} + && $resolution eq 'FIXED' + && ( !ref $invocant + or !$invocant->resolution + or $resolution ne $invocant->resolution) + && scalar @$dependson + ) + { + my $dep_bugs = Bugzilla::Bug->new_from_list($dependson); + my $count_open = grep { $_->isopened } @$dep_bugs; + if ($count_open) { + my $bug_id = ref($invocant) ? $invocant->id : undef; + ThrowUserError("still_unresolved_bugs", + {bug_id => $bug_id, dep_count => $count_open}); + } + } + + # Check if they're changing the resolution and need to comment. + if (Bugzilla->params->{'commentonchange_resolution'}) { + $invocant->_check_commenton($resolution, 'resolution', $params); + } + + return $resolution; } sub _check_short_desc { - my ($invocant, $short_desc) = @_; - # Set the parameter to itself, but cleaned up - $short_desc = clean_text($short_desc) if $short_desc; + my ($invocant, $short_desc) = @_; - if (!defined $short_desc || $short_desc eq '') { - ThrowUserError("require_summary"); - } - if (length($short_desc) > MAX_FREETEXT_LENGTH) { - ThrowUserError('freetext_too_long', - { field => 'short_desc', text => $short_desc }); - } - return $short_desc; + # Set the parameter to itself, but cleaned up + $short_desc = clean_text($short_desc) if $short_desc; + + if (!defined $short_desc || $short_desc eq '') { + ThrowUserError("require_summary"); + } + if (length($short_desc) > MAX_FREETEXT_LENGTH) { + ThrowUserError('freetext_too_long', + {field => 'short_desc', text => $short_desc}); + } + return $short_desc; } sub _check_status_whiteboard { return defined $_[1] ? $_[1] : ''; } # Unlike other checkers, this one doesn't return anything. sub _check_strict_isolation { - my ($invocant, $ccs, $assignee, $qa_contact, $product) = @_; - return unless Bugzilla->params->{'strict_isolation'}; - - if (ref $invocant) { - my $original = $invocant->new($invocant->id); - - # We only check people if they've been added. This way, if - # strict_isolation is turned on when there are invalid users - # on bugs, people can still add comments and so on. - my @old_cc = map { $_->id } @{$original->cc_users}; - my @new_cc = map { $_->id } @{$invocant->cc_users}; - my ($removed, $added) = diff_arrays(\@old_cc, \@new_cc); - $ccs = Bugzilla::User->new_from_list($added); - - $assignee = $invocant->assigned_to - if $invocant->assigned_to->id != $original->assigned_to->id; - if ($invocant->qa_contact - && (!$original->qa_contact - || $invocant->qa_contact->id != $original->qa_contact->id)) - { - $qa_contact = $invocant->qa_contact; - } - $product = $invocant->product_obj; + my ($invocant, $ccs, $assignee, $qa_contact, $product) = @_; + return unless Bugzilla->params->{'strict_isolation'}; + + if (ref $invocant) { + my $original = $invocant->new($invocant->id); + + # We only check people if they've been added. This way, if + # strict_isolation is turned on when there are invalid users + # on bugs, people can still add comments and so on. + my @old_cc = map { $_->id } @{$original->cc_users}; + my @new_cc = map { $_->id } @{$invocant->cc_users}; + my ($removed, $added) = diff_arrays(\@old_cc, \@new_cc); + $ccs = Bugzilla::User->new_from_list($added); + + $assignee = $invocant->assigned_to + if $invocant->assigned_to->id != $original->assigned_to->id; + if ( + $invocant->qa_contact + && (!$original->qa_contact + || $invocant->qa_contact->id != $original->qa_contact->id) + ) + { + $qa_contact = $invocant->qa_contact; } + $product = $invocant->product_obj; + } - my @related_users = @$ccs; - push(@related_users, $assignee) if $assignee; + my @related_users = @$ccs; + push(@related_users, $assignee) if $assignee; - if (Bugzilla->params->{'useqacontact'} && $qa_contact) { - push(@related_users, $qa_contact); - } + if (Bugzilla->params->{'useqacontact'} && $qa_contact) { + push(@related_users, $qa_contact); + } - @related_users = @{Bugzilla::User->new_from_list(\@related_users)} - if !ref $invocant; - - # For each unique user in @related_users...(assignee and qa_contact - # could be duplicates of users in the CC list) - my %unique_users = map {$_->id => $_} @related_users; - my @blocked_users; - foreach my $id (keys %unique_users) { - my $related_user = $unique_users{$id}; - if (!$related_user->can_edit_product($product->id) || - !$related_user->can_see_product($product->name)) { - push (@blocked_users, $related_user->login); - } + @related_users = @{Bugzilla::User->new_from_list(\@related_users)} + if !ref $invocant; + + # For each unique user in @related_users...(assignee and qa_contact + # could be duplicates of users in the CC list) + my %unique_users = map { $_->id => $_ } @related_users; + my @blocked_users; + foreach my $id (keys %unique_users) { + my $related_user = $unique_users{$id}; + if ( !$related_user->can_edit_product($product->id) + || !$related_user->can_see_product($product->name)) + { + push(@blocked_users, $related_user->login); } - if (scalar(@blocked_users)) { - my %vars = ( users => \@blocked_users, - product => $product->name ); - if (ref $invocant) { - $vars{'bug_id'} = $invocant->id; - } - else { - $vars{'new'} = 1; - } - ThrowUserError("invalid_user_group", \%vars); + } + if (scalar(@blocked_users)) { + my %vars = (users => \@blocked_users, product => $product->name); + if (ref $invocant) { + $vars{'bug_id'} = $invocant->id; } + else { + $vars{'new'} = 1; + } + ThrowUserError("invalid_user_group", \%vars); + } } # This is used by various set_ checkers, to make their code simpler. sub _check_strict_isolation_for_user { - my ($self, $user) = @_; - return unless Bugzilla->params->{"strict_isolation"}; - if (!$user->can_edit_product($self->{product_id})) { - ThrowUserError('invalid_user_group', - { users => $user->login, - product => $self->product, - bug_id => $self->id }); - } + my ($self, $user) = @_; + return unless Bugzilla->params->{"strict_isolation"}; + if (!$user->can_edit_product($self->{product_id})) { + ThrowUserError('invalid_user_group', + {users => $user->login, product => $self->product, bug_id => $self->id}); + } } sub _check_tag_name { - my ($invocant, $tag) = @_; + my ($invocant, $tag) = @_; - $tag = clean_text($tag); - $tag || ThrowUserError('no_tag_to_edit'); - ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME; - trick_taint($tag); - # Tags are all lowercase. - return lc($tag); + $tag = clean_text($tag); + $tag || ThrowUserError('no_tag_to_edit'); + ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME; + trick_taint($tag); + + # Tags are all lowercase. + return lc($tag); } sub _check_target_milestone { - my ($invocant, $target, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_target = blessed($invocant) ? $invocant->target_milestone : ''; - $target = trim($target); - $target = $product->default_milestone if !defined $target; - my $object = Bugzilla::Milestone->check( - { product => $product, name => $target }); - if ($old_target && $object->name ne $old_target && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $target }); - } - return $object->name; + my ($invocant, $target, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_target = blessed($invocant) ? $invocant->target_milestone : ''; + $target = trim($target); + $target = $product->default_milestone if !defined $target; + my $object = Bugzilla::Milestone->check({product => $product, name => $target}); + if ($old_target && $object->name ne $old_target && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $target}); + } + return $object->name; } sub _check_time_field { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - # When filing bugs, we're forgiving and just return 0 if - # the user isn't a timetracker. When updating bugs, check_can_change_field - # controls permissions, so we don't want to check them here. - if (!ref $invocant and !Bugzilla->user->is_timetracker) { - return 0; - } + # When filing bugs, we're forgiving and just return 0 if + # the user isn't a timetracker. When updating bugs, check_can_change_field + # controls permissions, so we don't want to check them here. + if (!ref $invocant and !Bugzilla->user->is_timetracker) { + return 0; + } - # check_time is in Bugzilla::Object. - return $invocant->check_time($value, $field, $params); + # check_time is in Bugzilla::Object. + return $invocant->check_time($value, $field, $params); } sub _check_version { - my ($invocant, $version, undef, $params) = @_; - $version = trim($version); - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_vers = blessed($invocant) ? $invocant->version : ''; - my $object = Bugzilla::Version->check({ product => $product, name => $version }); - if ($object->name ne $old_vers && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $version }); - } - return $object->name; + my ($invocant, $version, undef, $params) = @_; + $version = trim($version); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_vers = blessed($invocant) ? $invocant->version : ''; + my $object = Bugzilla::Version->check({product => $product, name => $version}); + if ($object->name ne $old_vers && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $version}); + } + return $object->name; } # Custom Field Validators sub _check_field_is_mandatory { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - if (!blessed($field)) { - $field = Bugzilla::Field->new({ name => $field }); - return if !$field; - } + if (!blessed($field)) { + $field = Bugzilla::Field->new({name => $field}); + return if !$field; + } - return if !$field->is_mandatory; + return if !$field->is_mandatory; - return if !$field->is_visible_on_bug($params || $invocant); + return if !$field->is_visible_on_bug($params || $invocant); - return if ($field->type == FIELD_TYPE_SINGLE_SELECT - && scalar @{ get_legal_field_values($field->name) } == 1); + return + if ($field->type == FIELD_TYPE_SINGLE_SELECT + && scalar @{get_legal_field_values($field->name)} == 1); - return if ($field->type == FIELD_TYPE_MULTI_SELECT - && !scalar @{ get_legal_field_values($field->name) }); + return + if ($field->type == FIELD_TYPE_MULTI_SELECT + && !scalar @{get_legal_field_values($field->name)}); - if (ref($value) eq 'ARRAY') { - $value = join('', @$value); - } + if (ref($value) eq 'ARRAY') { + $value = join('', @$value); + } - $value = trim($value); - if (!defined($value) - or $value eq "" - or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT) - or ($value =~ EMPTY_DATETIME_REGEX - and $field->type == FIELD_TYPE_DATETIME)) - { - ThrowUserError('required_field', { field => $field }); - } + $value = trim($value); + if ( !defined($value) + or $value eq "" + or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT) + or ($value =~ EMPTY_DATETIME_REGEX and $field->type == FIELD_TYPE_DATETIME)) + { + ThrowUserError('required_field', {field => $field}); + } } sub _check_date_field { - my ($invocant, $date) = @_; - return $invocant->_check_datetime_field($date, undef, {date_only => 1}); + my ($invocant, $date) = @_; + return $invocant->_check_datetime_field($date, undef, {date_only => 1}); } sub _check_datetime_field { - my ($invocant, $date_time, $field, $params) = @_; - - # Empty datetimes are empty strings or strings only containing - # 0's, whitespace, and punctuation. - if ($date_time =~ /^[\s0[:punct:]]*$/) { - return undef; - } - - $date_time = trim($date_time); - my ($date, $time) = split(' ', $date_time); - if ($date && !validate_date($date)) { - ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD' }); - } - if ($time && $params->{date_only}) { - ThrowUserError('illegal_date', { date => $date_time, - format => 'YYYY-MM-DD' }); - } - if ($time && !validate_time($time)) { - ThrowUserError('illegal_time', { 'time' => $time, - format => 'HH:MM:SS' }); - } - return $date_time + my ($invocant, $date_time, $field, $params) = @_; + + # Empty datetimes are empty strings or strings only containing + # 0's, whitespace, and punctuation. + if ($date_time =~ /^[\s0[:punct:]]*$/) { + return undef; + } + + $date_time = trim($date_time); + my ($date, $time) = split(' ', $date_time); + if ($date && !validate_date($date)) { + ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'}); + } + if ($time && $params->{date_only}) { + ThrowUserError('illegal_date', {date => $date_time, format => 'YYYY-MM-DD'}); + } + if ($time && !validate_time($time)) { + ThrowUserError('illegal_time', {'time' => $time, format => 'HH:MM:SS'}); + } + return $date_time; } sub _check_default_field { return defined $_[1] ? trim($_[1]) : ''; } sub _check_freetext_field { - my ($invocant, $text, $field) = @_; + my ($invocant, $text, $field) = @_; - $text = (defined $text) ? trim($text) : ''; - if (length($text) > MAX_FREETEXT_LENGTH) { - ThrowUserError('freetext_too_long', - { field => $field, text => $text }); - } - return $text; + $text = (defined $text) ? trim($text) : ''; + if (length($text) > MAX_FREETEXT_LENGTH) { + ThrowUserError('freetext_too_long', {field => $field, text => $text}); + } + return $text; } sub _check_multi_select_field { - my ($invocant, $values, $field) = @_; - - # 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)]; - } + my ($invocant, $values, $field) = @_; - return [] if !$values; - my @checked_values; - foreach my $value (@$values) { - push(@checked_values, $invocant->_check_select_field($value, $field)); - } - return \@checked_values; + # 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 [] 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) = @_; - my $object = Bugzilla::Field::Choice->type($field)->check($value); - return $object->name; + my ($invocant, $value, $field) = @_; + my $object = Bugzilla::Field::Choice->type($field)->check($value); + return $object->name; } sub _check_bugid_field { - my ($invocant, $value, $field) = @_; - return undef if !$value; - - # check that the value is a valid, visible bug id - my $checked_id = $invocant->check($value, $field)->id; - - # check for loop (can't have a loop if this is a new bug) - if (ref $invocant) { - _check_relationship_loop($field, $invocant->bug_id, $checked_id); - } + my ($invocant, $value, $field) = @_; + return undef if !$value; + + # check that the value is a valid, visible bug id + my $checked_id = $invocant->check($value, $field)->id; + + # check for loop (can't have a loop if this is a new bug) + if (ref $invocant) { + _check_relationship_loop($field, $invocant->bug_id, $checked_id); + } - return $checked_id; + return $checked_id; } sub _check_textarea_field { - my ($invocant, $text, $field) = @_; + my ($invocant, $text, $field) = @_; - $text = (defined $text) ? trim($text) : ''; + $text = (defined $text) ? trim($text) : ''; - # Web browsers submit newlines as \r\n. - # Sanitize all input to match the web standard. - # XMLRPC input could be either \n or \r\n - $text =~ s/\r?\n/\r\n/g; + # Web browsers submit newlines as \r\n. + # Sanitize all input to match the web standard. + # XMLRPC input could be either \n or \r\n + $text =~ s/\r?\n/\r\n/g; - return $text; + return $text; } sub _check_integer_field { - my ($invocant, $value, $field) = @_; - $value = defined($value) ? trim($value) : ''; + my ($invocant, $value, $field) = @_; + $value = defined($value) ? trim($value) : ''; - if ($value eq '') { - return 0; - } + if ($value eq '') { + return 0; + } - my $orig_value = $value; - if (!detaint_signed($value)) { - ThrowUserError("number_not_integer", - {field => $field, num => $orig_value}); - } - elsif (abs($value) > MAX_INT_32) { - ThrowUserError("number_too_large", - {field => $field, num => $orig_value, max_num => MAX_INT_32}); - } + my $orig_value = $value; + if (!detaint_signed($value)) { + ThrowUserError("number_not_integer", {field => $field, num => $orig_value}); + } + elsif (abs($value) > MAX_INT_32) { + ThrowUserError("number_too_large", + {field => $field, num => $orig_value, max_num => MAX_INT_32}); + } - return $value; + return $value; } sub _check_relationship_loop { - # Generates a dependency tree for a given bug. Calls itself recursively - # to generate sub-trees for the bug's dependencies. - my ($field, $bug_id, $dep_id, $ids) = @_; - - # Don't do anything if this bug doesn't have any dependencies. - return unless defined($dep_id); - - # Check whether we have seen this bug yet - $ids = {} unless defined $ids; - $ids->{$bug_id} = 1; - if ($ids->{$dep_id}) { - ThrowUserError("relationship_loop_single", { - 'bug_id' => $bug_id, - 'dep_id' => $dep_id, - 'field_name' => $field}); - } - - # Get this dependency's record from the database - my $dbh = Bugzilla->dbh; - my $next_dep_id = $dbh->selectrow_array( - "SELECT $field FROM bugs WHERE bug_id = ?", undef, $dep_id); - _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); + # Generates a dependency tree for a given bug. Calls itself recursively + # to generate sub-trees for the bug's dependencies. + my ($field, $bug_id, $dep_id, $ids) = @_; + + # Don't do anything if this bug doesn't have any dependencies. + return unless defined($dep_id); + + # Check whether we have seen this bug yet + $ids = {} unless defined $ids; + $ids->{$bug_id} = 1; + if ($ids->{$dep_id}) { + ThrowUserError("relationship_loop_single", + {'bug_id' => $bug_id, 'dep_id' => $dep_id, 'field_name' => $field}); + } + + # Get this dependency's record from the database + my $dbh = Bugzilla->dbh; + my $next_dep_id + = $dbh->selectrow_array("SELECT $field FROM bugs WHERE bug_id = ?", + undef, $dep_id); + + _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); } ##################################################################### @@ -2298,63 +2366,63 @@ sub _check_relationship_loop { ##################################################################### sub fields { - my $class = shift; - - my @fields = - ( - # Standard Fields - # Keep this ordering in sync with bugzilla.dtd. - qw(bug_id alias creation_ts short_desc delta_ts - reporter_accessible cclist_accessible - classification_id classification - product component version rep_platform op_sys - bug_status resolution dup_id see_also - bug_file_loc status_whiteboard keywords - priority bug_severity target_milestone - dependson blocked everconfirmed - reporter assigned_to cc estimated_time - remaining_time actual_time deadline), - - # Conditional Fields - Bugzilla->params->{'useqacontact'} ? "qa_contact" : (), - # Custom Fields - map { $_->name } Bugzilla->active_custom_fields - ); - Bugzilla::Hook::process('bug_fields', {'fields' => \@fields} ); - - return @fields; + my $class = shift; + + my @fields = ( + + # Standard Fields + # Keep this ordering in sync with bugzilla.dtd. + qw(bug_id alias creation_ts short_desc delta_ts + reporter_accessible cclist_accessible + classification_id classification + product component version rep_platform op_sys + bug_status resolution dup_id see_also + bug_file_loc status_whiteboard keywords + priority bug_severity target_milestone + dependson blocked everconfirmed + reporter assigned_to cc estimated_time + remaining_time actual_time deadline), + + # Conditional Fields + Bugzilla->params->{'useqacontact'} ? "qa_contact" : (), + + # Custom Fields + map { $_->name } Bugzilla->active_custom_fields + ); + Bugzilla::Hook::process('bug_fields', {'fields' => \@fields}); + + return @fields; } ##################################################################### -# Mutators +# Mutators ##################################################################### # To run check_can_change_field. sub _set_global_validator { - my ($self, $value, $field) = @_; - my $current = $self->$field; - my $privs; - - if (ref $current && ref($current) ne 'ARRAY' - && $current->isa('Bugzilla::Object')) { - $current = $current->id ; - } - if (ref $value && ref($value) ne 'ARRAY' - && $value->isa('Bugzilla::Object')) { - $value = $value->id ; - } - my $can = $self->check_can_change_field($field, $current, $value, \$privs); - if (!$can) { - if ($field eq 'assigned_to' || $field eq 'qa_contact') { - $value = Bugzilla::User->new($value)->login; - $current = Bugzilla::User->new($current)->login; - } - ThrowUserError('illegal_change', { field => $field, - oldvalue => $current, - newvalue => $value, - privs => $privs }); - } - $self->_check_field_is_mandatory($value, $field); + my ($self, $value, $field) = @_; + my $current = $self->$field; + my $privs; + + if ( ref $current + && ref($current) ne 'ARRAY' + && $current->isa('Bugzilla::Object')) + { + $current = $current->id; + } + if (ref $value && ref($value) ne 'ARRAY' && $value->isa('Bugzilla::Object')) { + $value = $value->id; + } + my $can = $self->check_can_change_field($field, $current, $value, \$privs); + if (!$can) { + if ($field eq 'assigned_to' || $field eq 'qa_contact') { + $value = Bugzilla::User->new($value)->login; + $current = Bugzilla::User->new($current)->login; + } + ThrowUserError('illegal_change', + {field => $field, oldvalue => $current, newvalue => $value, privs => $privs}); + } + $self->_check_field_is_mandatory($value, $field); } @@ -2365,359 +2433,384 @@ sub _set_global_validator { # Note that if you are changing multiple bugs at once, you must pass # other_bugs to set_all in order for it to behave properly. sub set_all { - my $self = shift; - my ($input_params) = @_; - - # Clone the data as we are going to alter it, and this would affect - # subsequent bugs when calling set_all() again, as some fields would - # be modified or no longer defined. - my $params = {}; - %$params = %$input_params; - - # You cannot mark bugs as duplicate when changing several bugs at once - # (because currently there is no way to check for duplicate loops in that - # situation). You also cannot set the alias of several bugs at once. - if ($params->{other_bugs} and scalar @{ $params->{other_bugs} } > 1) { - ThrowUserError('dupe_not_allowed') if exists $params->{dup_id}; - ThrowUserError('multiple_alias_not_allowed') - if defined $params->{alias}; - } - - # For security purposes, and because lots of other checks depend on it, - # we set the product first before anything else. - my $product_changed; # Used only for strict_isolation checks. - if (exists $params->{'product'}) { - $product_changed = $self->_set_product($params->{'product'}, $params); - } - - # strict_isolation checks mean that we should set the groups - # immediately after changing the product. - $self->_add_remove($params, 'groups'); - - if (exists $params->{'dependson'} or exists $params->{'blocked'}) { - my %set_deps; - foreach my $name (qw(dependson blocked)) { - my @dep_ids = @{ $self->$name }; - # If only one of the two fields was passed in, then we need to - # retain the current value for the other one. - if (!exists $params->{$name}) { - $set_deps{$name} = \@dep_ids; - next; - } - - # Explicitly setting them to a particular value overrides - # add/remove. - if (exists $params->{$name}->{set}) { - $set_deps{$name} = $params->{$name}->{set}; - next; - } - - foreach my $add (@{ $params->{$name}->{add} || [] }) { - push(@dep_ids, $add) if !grep($_ == $add, @dep_ids); - } - foreach my $remove (@{ $params->{$name}->{remove} || [] }) { - @dep_ids = grep($_ != $remove, @dep_ids); - } - $set_deps{$name} = \@dep_ids; - } - - $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'}); - } - - if (exists $params->{'keywords'}) { - # Sorting makes the order "add, remove, set", just like for other - # fields. - foreach my $action (sort keys %{ $params->{'keywords'} }) { - $self->modify_keywords($params->{'keywords'}->{$action}, $action); - } - } + my $self = shift; + my ($input_params) = @_; + + # Clone the data as we are going to alter it, and this would affect + # subsequent bugs when calling set_all() again, as some fields would + # be modified or no longer defined. + my $params = {}; + %$params = %$input_params; + + # You cannot mark bugs as duplicate when changing several bugs at once + # (because currently there is no way to check for duplicate loops in that + # situation). You also cannot set the alias of several bugs at once. + if ($params->{other_bugs} and scalar @{$params->{other_bugs}} > 1) { + ThrowUserError('dupe_not_allowed') if exists $params->{dup_id}; + ThrowUserError('multiple_alias_not_allowed') if defined $params->{alias}; + } + + # For security purposes, and because lots of other checks depend on it, + # we set the product first before anything else. + my $product_changed; # Used only for strict_isolation checks. + if (exists $params->{'product'}) { + $product_changed = $self->_set_product($params->{'product'}, $params); + } + + # strict_isolation checks mean that we should set the groups + # immediately after changing the product. + $self->_add_remove($params, 'groups'); + + if (exists $params->{'dependson'} or exists $params->{'blocked'}) { + my %set_deps; + foreach my $name (qw(dependson blocked)) { + my @dep_ids = @{$self->$name}; + + # If only one of the two fields was passed in, then we need to + # retain the current value for the other one. + if (!exists $params->{$name}) { + $set_deps{$name} = \@dep_ids; + next; + } + + # Explicitly setting them to a particular value overrides + # add/remove. + if (exists $params->{$name}->{set}) { + $set_deps{$name} = $params->{$name}->{set}; + next; + } + + foreach my $add (@{$params->{$name}->{add} || []}) { + push(@dep_ids, $add) if !grep($_ == $add, @dep_ids); + } + foreach my $remove (@{$params->{$name}->{remove} || []}) { + @dep_ids = grep($_ != $remove, @dep_ids); + } + $set_deps{$name} = \@dep_ids; + } + + $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'}); + } + + if (exists $params->{'keywords'}) { + + # Sorting makes the order "add, remove, set", just like for other + # fields. + foreach my $action (sort keys %{$params->{'keywords'}}) { + $self->modify_keywords($params->{'keywords'}->{$action}, $action); + } + } + + if (exists $params->{'comment'} or exists $params->{'work_time'}) { + + # Add a comment as needed to each bug. This is done early because + # there are lots of things that want to check if we added a comment. + $self->add_comment( + $params->{'comment'}->{'body'}, + { + isprivate => $params->{'comment'}->{'is_private'}, + work_time => $params->{'work_time'} + } + ); + } - if (exists $params->{'comment'} or exists $params->{'work_time'}) { - # Add a comment as needed to each bug. This is done early because - # there are lots of things that want to check if we added a comment. - $self->add_comment($params->{'comment'}->{'body'}, - { isprivate => $params->{'comment'}->{'is_private'}, - work_time => $params->{'work_time'} }); - } + if (exists $params->{alias} && $params->{alias}{set}) { + my ($removed_aliases, $added_aliases) + = diff_arrays($self->alias, $params->{alias}{set}); + $params->{alias} = {add => $added_aliases, remove => $removed_aliases,}; + } - if (exists $params->{alias} && $params->{alias}{set}) { - my ($removed_aliases, $added_aliases) = diff_arrays( - $self->alias, $params->{alias}{set}); - $params->{alias} = { - add => $added_aliases, - remove => $removed_aliases, - }; - } + my %normal_set_all; + foreach my $name (keys %$params) { - my %normal_set_all; - foreach my $name (keys %$params) { - # These are handled separately below. - if ($self->can("set_$name")) { - $normal_set_all{$name} = $params->{$name}; - } + # These are handled separately below. + if ($self->can("set_$name")) { + $normal_set_all{$name} = $params->{$name}; } - $self->SUPER::set_all(\%normal_set_all); + } + $self->SUPER::set_all(\%normal_set_all); - $self->reset_assigned_to if $params->{'reset_assigned_to'}; - $self->reset_qa_contact if $params->{'reset_qa_contact'}; + $self->reset_assigned_to if $params->{'reset_assigned_to'}; + $self->reset_qa_contact if $params->{'reset_qa_contact'}; - $self->_add_remove($params, 'see_also'); + $self->_add_remove($params, 'see_also'); - # And set custom fields. - my @custom_fields = Bugzilla->active_custom_fields; - foreach my $field (@custom_fields) { - my $fname = $field->name; - if (exists $params->{$fname}) { - $self->set_custom_field($field, $params->{$fname}); - } + # And set custom fields. + my @custom_fields = Bugzilla->active_custom_fields; + foreach my $field (@custom_fields) { + my $fname = $field->name; + if (exists $params->{$fname}) { + $self->set_custom_field($field, $params->{$fname}); } + } - $self->_add_remove($params, 'cc'); - $self->_add_remove($params, 'alias'); + $self->_add_remove($params, 'cc'); + $self->_add_remove($params, 'alias'); - # Theoretically you could move a product without ever specifying - # a new assignee or qa_contact, or adding/removing any CCs. So, - # we have to check that the current assignee, qa, and CCs are still - # valid if we've switched products, under strict_isolation. We can only - # do that here, because if they *did* change the assignee, qa, or CC, - # then we don't want to check the original ones, only the new ones. - $self->_check_strict_isolation() if $product_changed; + # Theoretically you could move a product without ever specifying + # a new assignee or qa_contact, or adding/removing any CCs. So, + # we have to check that the current assignee, qa, and CCs are still + # valid if we've switched products, under strict_isolation. We can only + # do that here, because if they *did* change the assignee, qa, or CC, + # then we don't want to check the original ones, only the new ones. + $self->_check_strict_isolation() if $product_changed; } # Helper for set_all that helps with fields that have an "add/remove" # pattern instead of a "set_" pattern. sub _add_remove { - my ($self, $params, $name) = @_; - my @add = @{ $params->{$name}->{add} || [] }; - my @remove = @{ $params->{$name}->{remove} || [] }; - $name =~ s/s$// if $name ne 'alias'; - my $add_method = "add_$name"; - my $remove_method = "remove_$name"; - $self->$add_method($_) foreach @add; - $self->$remove_method($_) foreach @remove; + my ($self, $params, $name) = @_; + my @add = @{$params->{$name}->{add} || []}; + my @remove = @{$params->{$name}->{remove} || []}; + $name =~ s/s$// if $name ne 'alias'; + my $add_method = "add_$name"; + my $remove_method = "remove_$name"; + $self->$add_method($_) foreach @add; + $self->$remove_method($_) foreach @remove; } sub set_assigned_to { - my ($self, $value) = @_; - $self->set('assigned_to', $value); - # Store the old assignee. check_can_change_field() needs it. - $self->{'_old_assigned_to'} = $self->{'assigned_to_obj'}->id; - delete $self->{'assigned_to_obj'}; + my ($self, $value) = @_; + $self->set('assigned_to', $value); + + # Store the old assignee. check_can_change_field() needs it. + $self->{'_old_assigned_to'} = $self->{'assigned_to_obj'}->id; + delete $self->{'assigned_to_obj'}; } + sub reset_assigned_to { - my $self = shift; - my $comp = $self->component_obj; - $self->set_assigned_to($comp->default_assignee); + my $self = shift; + my $comp = $self->component_obj; + $self->set_assigned_to($comp->default_assignee); } sub set_bug_ignored { $_[0]->set('bug_ignored', $_[1]); } sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); } sub set_comment_is_private { - my ($self, $comments, $isprivate) = @_; - $self->{comment_isprivate} ||= []; - my $is_insider = Bugzilla->user->is_insider; + my ($self, $comments, $isprivate) = @_; + $self->{comment_isprivate} ||= []; + my $is_insider = Bugzilla->user->is_insider; - $comments = { $comments => $isprivate } unless ref $comments; + $comments = {$comments => $isprivate} unless ref $comments; - foreach my $comment (@{$self->comments}) { - # Skip unmodified comment privacy. - next unless exists $comments->{$comment->id}; + foreach my $comment (@{$self->comments}) { - my $isprivate = delete $comments->{$comment->id} ? 1 : 0; - if ($isprivate != $comment->is_private) { - ThrowUserError('user_not_insider') unless $is_insider; - $comment->set_is_private($isprivate); - push @{$self->{comment_isprivate}}, $comment; - } + # Skip unmodified comment privacy. + next unless exists $comments->{$comment->id}; + + my $isprivate = delete $comments->{$comment->id} ? 1 : 0; + if ($isprivate != $comment->is_private) { + ThrowUserError('user_not_insider') unless $is_insider; + $comment->set_is_private($isprivate); + push @{$self->{comment_isprivate}}, $comment; } + } - # If there are still entries in $comments, then they are illegal. - ThrowUserError('comment_invalid_isprivate', { id => join(', ', keys %$comments) }) - if scalar keys %$comments; - - # If no comment privacy has been modified, remove this key. - delete $self->{comment_isprivate} unless scalar @{$self->{comment_isprivate}}; -} - -sub set_component { - my ($self, $name) = @_; - my $old_comp = $self->component_obj; - my $component = $self->_check_component($name); - if ($old_comp->id != $component->id) { - $self->{component_id} = $component->id; - $self->{component} = $component->name; - $self->{component_obj} = $component; - # For update() - $self->{_old_component_name} = $old_comp->name; - # Add in the Default CC of the new Component; - foreach my $cc (@{$component->initial_cc}) { - $self->add_cc($cc); - } + # If there are still entries in $comments, then they are illegal. + ThrowUserError('comment_invalid_isprivate', {id => join(', ', keys %$comments)}) + if scalar keys %$comments; + + # If no comment privacy has been modified, remove this key. + delete $self->{comment_isprivate} unless scalar @{$self->{comment_isprivate}}; +} + +sub set_component { + my ($self, $name) = @_; + my $old_comp = $self->component_obj; + my $component = $self->_check_component($name); + if ($old_comp->id != $component->id) { + $self->{component_id} = $component->id; + $self->{component} = $component->name; + $self->{component_obj} = $component; + + # For update() + $self->{_old_component_name} = $old_comp->name; + + # Add in the Default CC of the new Component; + foreach my $cc (@{$component->initial_cc}) { + $self->add_cc($cc); } + } } + sub set_custom_field { - my ($self, $field, $value) = @_; + my ($self, $field, $value) = @_; - if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { - $value = $value->[0]; - } - ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom; - $self->set($field->name, $value); + if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { + $value = $value->[0]; + } + ThrowCodeError('field_not_custom', {field => $field}) if !$field->custom; + $self->set($field->name, $value); } sub set_deadline { $_[0]->set('deadline', $_[1]); } + sub set_dependencies { - my ($self, $dependson, $blocked) = @_; - my %extra = ( blocked => $blocked ); - $dependson = $self->_check_dependencies($dependson, 'dependson', \%extra); - $blocked = $extra{blocked}; - # These may already be detainted, but all setters are supposed to - # detaint their input if they've run a validator (just as though - # we had used Bugzilla::Object::set), so we do that here. - detaint_natural($_) foreach (@$dependson, @$blocked); - $self->{'dependson'} = $dependson; - $self->{'blocked'} = $blocked; - delete $self->{depends_on_obj}; - delete $self->{blocks_obj}; + my ($self, $dependson, $blocked) = @_; + my %extra = (blocked => $blocked); + $dependson = $self->_check_dependencies($dependson, 'dependson', \%extra); + $blocked = $extra{blocked}; + + # These may already be detainted, but all setters are supposed to + # detaint their input if they've run a validator (just as though + # we had used Bugzilla::Object::set), so we do that here. + detaint_natural($_) foreach (@$dependson, @$blocked); + $self->{'dependson'} = $dependson; + $self->{'blocked'} = $blocked; + delete $self->{depends_on_obj}; + delete $self->{blocks_obj}; } sub _clear_dup_id { $_[0]->{dup_id} = undef; } + sub set_dup_id { - my ($self, $dup_id) = @_; - my $old = $self->dup_id || 0; - $self->set('dup_id', $dup_id); - my $new = $self->dup_id; - return if $old == $new; - - # Make sure that we have the DUPLICATE resolution. This is needed - # if somebody calls set_dup_id without calling set_bug_status or - # set_resolution. - if ($self->resolution ne 'DUPLICATE') { - # Even if the current status is VERIFIED, we change it back to - # RESOLVED (or whatever the duplicate_or_move_bug_status is) here, - # because that's the same thing the UI does when you click on the - # "Mark as Duplicate" link. If people really want to retain their - # current status, they can use set_bug_status and set the DUPLICATE - # resolution before getting here. - $self->set_bug_status( - Bugzilla->params->{'duplicate_or_move_bug_status'}, - { resolution => 'DUPLICATE' }); - } - - # Update the other bug. - my $dupe_of = new Bugzilla::Bug($self->dup_id); - if (delete $self->{_add_dup_cc}) { - $dupe_of->add_cc($self->reporter); - } - $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.) - if ($self->{added_comments}) { - my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL } - @{ $self->{added_comments} }; - # Turn the last one into a dup comment. - $normal[-1]->{type} = CMT_DUPE_OF; - $normal[-1]->{extra_data} = $self->dup_id; - } - else { - $self->add_comment('', { type => CMT_DUPE_OF, - extra_data => $self->dup_id }); - } + my ($self, $dup_id) = @_; + my $old = $self->dup_id || 0; + $self->set('dup_id', $dup_id); + my $new = $self->dup_id; + return if $old == $new; + + # Make sure that we have the DUPLICATE resolution. This is needed + # if somebody calls set_dup_id without calling set_bug_status or + # set_resolution. + if ($self->resolution ne 'DUPLICATE') { + + # Even if the current status is VERIFIED, we change it back to + # RESOLVED (or whatever the duplicate_or_move_bug_status is) here, + # because that's the same thing the UI does when you click on the + # "Mark as Duplicate" link. If people really want to retain their + # current status, they can use set_bug_status and set the DUPLICATE + # resolution before getting here. + $self->set_bug_status(Bugzilla->params->{'duplicate_or_move_bug_status'}, + {resolution => 'DUPLICATE'}); + } + + # Update the other bug. + my $dupe_of = new Bugzilla::Bug($self->dup_id); + if (delete $self->{_add_dup_cc}) { + $dupe_of->add_cc($self->reporter); + } + $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.) + if ($self->{added_comments}) { + my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL } + @{$self->{added_comments}}; + + # Turn the last one into a dup comment. + $normal[-1]->{type} = CMT_DUPE_OF; + $normal[-1]->{extra_data} = $self->dup_id; + } + else { + $self->add_comment('', {type => CMT_DUPE_OF, extra_data => $self->dup_id}); + } } sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); } -sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); } +sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); } + sub set_flags { - my ($self, $flags, $new_flags) = @_; + my ($self, $flags, $new_flags) = @_; - Bugzilla::Flag->set_flag($self, $_) foreach (@$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]); } +sub set_op_sys { $_[0]->set('op_sys', $_[1]); } +sub set_platform { $_[0]->set('rep_platform', $_[1]); } +sub set_priority { $_[0]->set('priority', $_[1]); } + # For security reasons, you have to use set_all to change the product. # See the strict_isolation check in set_all for an explanation. 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; - $self->{product} = $product->name; - $self->{product_obj} = $product; - # For update() - $self->{_old_product_name} = $old_product->name; - # Delete fields that depend upon the old Product value. - delete $self->{choices}; - $product_changed = 1; - } + my ($self, $name, $params) = @_; + my $old_product = $self->product_obj; + my $product = $self->_check_product($name); - $params ||= {}; - # We delete these so that they're not set again later in set_all. - my $comp_name = delete $params->{component} || $self->component; - my $vers_name = delete $params->{version} || $self->version; - my $tm_name = delete $params->{target_milestone}; - # This way, if usetargetmilestone is off and we've changed products, - # set_target_milestone will reset our target_milestone to - # $product->default_milestone. But if we haven't changed products, - # we don't reset anything. - if (!defined $tm_name - && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed)) - { - $tm_name = $self->target_milestone; - } + my $product_changed = 0; + if ($old_product->id != $product->id) { + $self->{product_id} = $product->id; + $self->{product} = $product->name; + $self->{product_obj} = $product; - if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # Try to set each value with the new product. - # Have to set error_mode because Throw*Error calls exit() otherwise. - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $component_ok = eval { $self->set_component($comp_name); 1; }; - my $version_ok = eval { $self->set_version($vers_name); 1; }; - my $milestone_ok = 1; - # Reporters can move bugs between products but not set the TM. - if ($self->check_can_change_field('target_milestone', 0, 1)) { - $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; }; - } - else { - # Have to set this directly to bypass the validators. - $self->{target_milestone} = $product->default_milestone; - } - # If there were any errors thrown, make sure we don't mess up any - # other part of Bugzilla that checks $@. - undef $@; - Bugzilla->error_mode($old_error_mode); - - my $verified = $params->{product_change_confirmed}; - my %vars; - if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) { - $vars{defaults} = { - # 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, - milestone => $milestone_ok ? $self->target_milestone - : $product->default_milestone - }; - $vars{components} = [map { $_->name } grep($_->is_active, @{$product->components})]; - $vars{milestones} = [map { $_->name } grep($_->is_active, @{$product->milestones})]; - $vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})]; - } + # For update() + $self->{_old_product_name} = $old_product->name; - if (!$verified) { - $vars{verify_bug_groups} = 1; - my $dbh = Bugzilla->dbh; - my @idlist = ($self->id); - push(@idlist, map {$_->id} @{ $params->{other_bugs} }) - if $params->{other_bugs}; - @idlist = uniq @idlist; - # Get the ID of groups which are no longer valid in the new product. - my $gids = $dbh->selectcol_arrayref( - 'SELECT bgm.group_id + # Delete fields that depend upon the old Product value. + delete $self->{choices}; + $product_changed = 1; + } + + $params ||= {}; + + # We delete these so that they're not set again later in set_all. + my $comp_name = delete $params->{component} || $self->component; + my $vers_name = delete $params->{version} || $self->version; + my $tm_name = delete $params->{target_milestone}; + + # This way, if usetargetmilestone is off and we've changed products, + # set_target_milestone will reset our target_milestone to + # $product->default_milestone. But if we haven't changed products, + # we don't reset anything. + if (!defined $tm_name + && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed)) + { + $tm_name = $self->target_milestone; + } + + if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + + # Try to set each value with the new product. + # Have to set error_mode because Throw*Error calls exit() otherwise. + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $component_ok = eval { $self->set_component($comp_name); 1; }; + my $version_ok = eval { $self->set_version($vers_name); 1; }; + my $milestone_ok = 1; + + # Reporters can move bugs between products but not set the TM. + if ($self->check_can_change_field('target_milestone', 0, 1)) { + $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; }; + } + else { + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; + } + + # If there were any errors thrown, make sure we don't mess up any + # other part of Bugzilla that checks $@. + undef $@; + Bugzilla->error_mode($old_error_mode); + + my $verified = $params->{product_change_confirmed}; + my %vars; + if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) { + $vars{defaults} = { + + # 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, + milestone => $milestone_ok + ? $self->target_milestone + : $product->default_milestone + }; + $vars{components} + = [map { $_->name } grep($_->is_active, @{$product->components})]; + $vars{milestones} + = [map { $_->name } grep($_->is_active, @{$product->milestones})]; + $vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})]; + } + + if (!$verified) { + $vars{verify_bug_groups} = 1; + my $dbh = Bugzilla->dbh; + my @idlist = ($self->id); + push(@idlist, map { $_->id } @{$params->{other_bugs}}) if $params->{other_bugs}; + @idlist = uniq @idlist; + + # Get the ID of groups which are no longer valid in the new product. + my $gids = $dbh->selectcol_arrayref( + 'SELECT bgm.group_id FROM bug_group_map AS bgm WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ') AND bgm.group_id NOT IN @@ -2726,159 +2819,172 @@ sub _set_product { WHERE gcm.product_id = ? AND ( (gcm.membercontrol != ? AND gcm.group_id IN (' - . Bugzilla->user->groups_as_string . ')) - OR gcm.othercontrol != ?) )', - undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA)); - $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids); - - # Did we come here from editing multiple bugs? (affects how we - # show optional group changes) - $vars{multiple_bugs} = (@idlist > 1) ? 1 : 0; - } - - if (%vars) { - $vars{product} = $product; - $vars{bug} = $self; - my $template = Bugzilla->template; - $template->process("bug/process/verify-new-product.html.tmpl", - \%vars) || ThrowTemplateError($template->error()); - exit; - } + . Bugzilla->user->groups_as_string . ')) + OR gcm.othercontrol != ?) )', undef, + (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA) + ); + $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids); + + # Did we come here from editing multiple bugs? (affects how we + # show optional group changes) + $vars{multiple_bugs} = (@idlist > 1) ? 1 : 0; + } + + if (%vars) { + $vars{product} = $product; + $vars{bug} = $self; + my $template = Bugzilla->template; + $template->process("bug/process/verify-new-product.html.tmpl", \%vars) + || ThrowTemplateError($template->error()); + exit; + } + } + else { + # When we're not in the browser (or we didn't change the product), we + # just die if any of these are invalid. + $self->set_component($comp_name); + $self->set_version($vers_name); + if ($product_changed + and !$self->check_can_change_field('target_milestone', 0, 1)) + { + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; } else { - # When we're not in the browser (or we didn't change the product), we - # just die if any of these are invalid. - $self->set_component($comp_name); - $self->set_version($vers_name); - if ($product_changed - and !$self->check_can_change_field('target_milestone', 0, 1)) - { - # Have to set this directly to bypass the validators. - $self->{target_milestone} = $product->default_milestone; - } - else { - $self->set_target_milestone($tm_name); - } + $self->set_target_milestone($tm_name); } + } - if ($product_changed) { - # Remove groups that can't be set in the new product. - # We copy this array because the original array is modified while we're - # working, and that confuses "foreach". - my @current_groups = @{$self->groups_in}; - foreach my $group (@current_groups) { - if (!$product->group_is_valid($group)) { - $self->remove_group($group); - } - } + if ($product_changed) { - # Make sure the bug is in all the mandatory groups for the new product. - foreach my $group (@{$product->groups_mandatory}) { - $self->add_group($group); - } + # Remove groups that can't be set in the new product. + # We copy this array because the original array is modified while we're + # working, and that confuses "foreach". + my @current_groups = @{$self->groups_in}; + foreach my $group (@current_groups) { + if (!$product->group_is_valid($group)) { + $self->remove_group($group); + } } - - return $product_changed; + + # Make sure the bug is in all the mandatory groups for the new product. + foreach my $group (@{$product->groups_mandatory}) { + $self->add_group($group); + } + } + + return $product_changed; } sub set_qa_contact { - my ($self, $value) = @_; - $self->set('qa_contact', $value); - # Store the old QA contact. check_can_change_field() needs it. - if ($self->{'qa_contact_obj'}) { - $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id; - } - delete $self->{'qa_contact_obj'}; + my ($self, $value) = @_; + $self->set('qa_contact', $value); + + # Store the old QA contact. check_can_change_field() needs it. + if ($self->{'qa_contact_obj'}) { + $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id; + } + delete $self->{'qa_contact_obj'}; } + sub reset_qa_contact { - my $self = shift; - my $comp = $self->component_obj; - $self->set_qa_contact($comp->default_qa_contact); + my $self = shift; + my $comp = $self->component_obj; + $self->set_qa_contact($comp->default_qa_contact); } sub set_remaining_time { $_[0]->set('remaining_time', $_[1]); } + # Used only when closing a bug or moving between closed states. sub _zero_remaining_time { $_[0]->{'remaining_time'} = 0; } 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; + my ($self, $value, $params) = @_; - if ($new_res ne $old_res) { - # Clear the dup_id if we're leaving the dup resolution. - if ($old_res eq 'DUPLICATE') { - $self->_clear_dup_id(); - } - # Duplicates should have no remaining time left. - elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) { - $self->_zero_remaining_time(); - } + my $old_res = $self->resolution; + $self->set('resolution', $value); + delete $self->{choices}; + my $new_res = $self->resolution; + + if ($new_res ne $old_res) { + + # Clear the dup_id if we're leaving the dup resolution. + if ($old_res eq 'DUPLICATE') { + $self->_clear_dup_id(); } - - # 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 - # when going between different closed states. - if ($self->resolution eq 'DUPLICATE') { - if (my $dup_id = $params->{dup_id}) { - $self->set_dup_id($dup_id); - } - elsif (!$self->dup_id) { - ThrowUserError('dupe_id_required'); - } + + # Duplicates should have no remaining time left. + elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) { + $self->_zero_remaining_time(); } + } - # This method has handled dup_id, so set_all doesn't have to worry - # about it now. - delete $params->{dup_id}; + # 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 + # when going between different closed states. + if ($self->resolution eq 'DUPLICATE') { + if (my $dup_id = $params->{dup_id}) { + $self->set_dup_id($dup_id); + } + elsif (!$self->dup_id) { + ThrowUserError('dupe_id_required'); + } + } + + # This method has handled dup_id, so set_all doesn't have to worry + # about it now. + delete $params->{dup_id}; } + sub clear_resolution { - my $self = shift; - if (!$self->status->is_open) { - ThrowUserError('resolution_cant_clear', { bug_id => $self->id }); - } - $self->{'resolution'} = ''; - $self->_clear_dup_id; + my $self = shift; + if (!$self->status->is_open) { + ThrowUserError('resolution_cant_clear', {bug_id => $self->id}); + } + $self->{'resolution'} = ''; + $self->_clear_dup_id; } -sub set_severity { $_[0]->set('bug_severity', $_[1]); } +sub set_severity { $_[0]->set('bug_severity', $_[1]); } + sub set_bug_status { - my ($self, $status, $params) = @_; - 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); - $self->clear_resolution(); - # Calling clear_resolution handled the "resolution" and "dup_id" - # setting, so set_all doesn't have to worry about them. - delete $params->{resolution}; - delete $params->{dup_id}; + my ($self, $status, $params) = @_; + 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); + $self->clear_resolution(); + + # Calling clear_resolution handled the "resolution" and "dup_id" + # setting, so set_all doesn't have to worry about them. + delete $params->{resolution}; + delete $params->{dup_id}; + } + else { + # We do this here so that we can make sure closed statuses have + # resolutions. + my $resolution = $self->resolution; + + # We need to check "defined" to prevent people from passing + # a blank resolution in the WebService, which would otherwise fail + # silently. + if (defined $params->{resolution}) { + $resolution = delete $params->{resolution}; } - else { - # We do this here so that we can make sure closed statuses have - # resolutions. - my $resolution = $self->resolution; - # We need to check "defined" to prevent people from passing - # a blank resolution in the WebService, which would otherwise fail - # silently. - if (defined $params->{resolution}) { - $resolution = delete $params->{resolution}; - } - $self->set_resolution($resolution, $params); + $self->set_resolution($resolution, $params); - # Changing between closed statuses zeros the remaining time. - if ($new_status->id != $old_status->id && $self->remaining_time != 0) { - $self->_zero_remaining_time(); - } + # Changing between closed statuses zeros the remaining time. + if ($new_status->id != $old_status->id && $self->remaining_time != 0) { + $self->_zero_remaining_time(); } + } } sub set_status_whiteboard { $_[0]->set('status_whiteboard', $_[1]); } sub set_summary { $_[0]->set('short_desc', $_[1]); } @@ -2895,373 +3001,390 @@ sub set_version { $_[0]->set('version', $_[1]); } # Accepts a User object or a username. Adds the user only if they # don't already exist as a CC on the bug. sub add_cc { - my ($self, $user_or_name) = @_; - return if !$user_or_name; - my $user = ref $user_or_name ? $user_or_name - : Bugzilla::User->check($user_or_name); - $self->_check_strict_isolation_for_user($user); - my $cc_users = $self->cc_users; - push(@$cc_users, $user) if !grep($_->id == $user->id, @$cc_users); + my ($self, $user_or_name) = @_; + return if !$user_or_name; + my $user + = ref $user_or_name ? $user_or_name : Bugzilla::User->check($user_or_name); + $self->_check_strict_isolation_for_user($user); + my $cc_users = $self->cc_users; + push(@$cc_users, $user) if !grep($_->id == $user->id, @$cc_users); } # Accepts a User object or a username. Removes the User if they exist # in the list, but doesn't throw an error if they don't exist. sub remove_cc { - my ($self, $user_or_name) = @_; - my $user = ref $user_or_name ? $user_or_name - : Bugzilla::User->check($user_or_name); - my $currentUser = Bugzilla->user; - if (!$self->user->{'canedit'} && $user->id != $currentUser->id) { - ThrowUserError('cc_remove_denied'); - } - my $cc_users = $self->cc_users; - @$cc_users = grep { $_->id != $user->id } @$cc_users; + my ($self, $user_or_name) = @_; + my $user + = ref $user_or_name ? $user_or_name : Bugzilla::User->check($user_or_name); + my $currentUser = Bugzilla->user; + if (!$self->user->{'canedit'} && $user->id != $currentUser->id) { + ThrowUserError('cc_remove_denied'); + } + my $cc_users = $self->cc_users; + @$cc_users = grep { $_->id != $user->id } @$cc_users; } sub add_alias { - my ($self, $alias) = @_; - return if !$alias; - my $aliases = $self->_check_alias($alias); - $alias = $aliases->[0]; - my @new_aliases; - my $found = 0; - foreach my $old_alias (@{ $self->alias }) { - if (lc($old_alias) eq lc($alias)) { - push(@new_aliases, $alias); - $found = 1; - } - else { - push(@new_aliases, $old_alias); - } + my ($self, $alias) = @_; + return if !$alias; + my $aliases = $self->_check_alias($alias); + $alias = $aliases->[0]; + my @new_aliases; + my $found = 0; + foreach my $old_alias (@{$self->alias}) { + if (lc($old_alias) eq lc($alias)) { + push(@new_aliases, $alias); + $found = 1; + } + else { + push(@new_aliases, $old_alias); } - push(@new_aliases, $alias) if !$found; - $self->{alias} = \@new_aliases; + } + push(@new_aliases, $alias) if !$found; + $self->{alias} = \@new_aliases; } sub remove_alias { - my ($self, $alias) = @_; - my $bug_aliases = $self->alias; - @$bug_aliases = grep { $_ ne $alias } @$bug_aliases; + my ($self, $alias) = @_; + my $bug_aliases = $self->alias; + @$bug_aliases = grep { $_ ne $alias } @$bug_aliases; } # $bug->add_comment("comment", {isprivate => 1, work_time => 10.5, # type => CMT_NORMAL, extra_data => $data}); sub add_comment { - my ($self, $comment, $params) = @_; + my ($self, $comment, $params) = @_; - $params ||= {}; + $params ||= {}; - # Fill out info that doesn't change and callers may not pass in - $params->{'bug_id'} = $self; - $params->{'thetext'} = defined($comment) ? $comment : ''; + # Fill out info that doesn't change and callers may not pass in + $params->{'bug_id'} = $self; + $params->{'thetext'} = defined($comment) ? $comment : ''; - # Validate all the entered data - Bugzilla::Comment->check_required_create_fields($params); - $params = Bugzilla::Comment->run_create_validators($params); + # Validate all the entered data + Bugzilla::Comment->check_required_create_fields($params); + $params = Bugzilla::Comment->run_create_validators($params); - # This makes it so we won't create new comments when there is nothing - # to add - if ($params->{'thetext'} eq '' - && !($params->{type} || abs($params->{work_time} || 0))) - { - return; - } + # This makes it so we won't create new comments when there is nothing + # to add + if ($params->{'thetext'} eq '' + && !($params->{type} || abs($params->{work_time} || 0))) + { + return; + } - # If the user has explicitly set remaining_time, this will be overridden - # later in set_all. But if they haven't, this keeps remaining_time - # up-to-date. - if ($params->{work_time}) { - $self->set_remaining_time(max($self->remaining_time - $params->{work_time}, 0)); - } + # If the user has explicitly set remaining_time, this will be overridden + # later in set_all. But if they haven't, this keeps remaining_time + # up-to-date. + if ($params->{work_time}) { + $self->set_remaining_time(max($self->remaining_time - $params->{work_time}, 0)); + } - $self->{added_comments} ||= []; + $self->{added_comments} ||= []; - push(@{$self->{added_comments}}, $params); + push(@{$self->{added_comments}}, $params); } sub modify_keywords { - my ($self, $keywords, $action) = @_; + my ($self, $keywords, $action) = @_; - if (!$action || !grep { $action eq $_ } qw(add remove set)) { - $action = 'set'; - } + if (!$action || !grep { $action eq $_ } qw(add remove set)) { + $action = 'set'; + } - $keywords = $self->_check_keywords($keywords); - my @old_keywords = @{ $self->keyword_objects }; - my @result; + $keywords = $self->_check_keywords($keywords); + my @old_keywords = @{$self->keyword_objects}; + my @result; - if ($action eq 'set') { - @result = @$keywords; + if ($action eq 'set') { + @result = @$keywords; + } + else { + # We're adding or deleting specific keywords. + my %keys = map { $_->id => $_ } @old_keywords; + if ($action eq 'add') { + $keys{$_->id} = $_ foreach @$keywords; } else { - # We're adding or deleting specific keywords. - my %keys = map { $_->id => $_ } @old_keywords; - if ($action eq 'add') { - $keys{$_->id} = $_ foreach @$keywords; - } - else { - delete $keys{$_->id} foreach @$keywords; - } - @result = values %keys; + delete $keys{$_->id} foreach @$keywords; } + @result = values %keys; + } - # Check if anything was added or removed. - my @old_ids = map { $_->id } @old_keywords; - my @new_ids = map { $_->id } @result; - my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids); - my $any_changes = scalar @$removed || scalar @$added; + # Check if anything was added or removed. + my @old_ids = map { $_->id } @old_keywords; + my @new_ids = map { $_->id } @result; + my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids); + my $any_changes = scalar @$removed || scalar @$added; - # Make sure we retain the sort order. - @result = sort {lc($a->name) cmp lc($b->name)} @result; + # 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)); - my $check = $self->check_can_change_field('keywords', 0, 1, \$privs) - || ThrowUserError('illegal_change', { field => 'keywords', - oldvalue => $self->keywords, - newvalue => $new, - privs => $privs }); - } - - $self->{'keyword_objects'} = \@result; + if ($any_changes) { + my $privs; + my $new = join(', ', (map { $_->name } @result)); + my $check + = $self->check_can_change_field('keywords', 0, 1, \$privs) || ThrowUserError( + 'illegal_change', + { + field => 'keywords', + oldvalue => $self->keywords, + newvalue => $new, + privs => $privs + } + ); + } + + $self->{'keyword_objects'} = \@result; } sub add_group { - my ($self, $group) = @_; + my ($self, $group) = @_; - # If the user enters "FoO" but the DB has "Foo", $group->name would - # return "Foo" and thus revealing the existence of the group name. - # So we have to store and pass the name as entered by the user to - # the error message, if we have it. - my $group_name = blessed($group) ? $group->name : $group; - my $args = { name => $group_name, product => $self->product, - bug_id => $self->id, action => 'add' }; + # If the user enters "FoO" but the DB has "Foo", $group->name would + # return "Foo" and thus revealing the existence of the group name. + # So we have to store and pass the name as entered by the user to + # the error message, if we have it. + my $group_name = blessed($group) ? $group->name : $group; + my $args = { + name => $group_name, + product => $self->product, + bug_id => $self->id, + action => 'add' + }; - $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; + $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; - # If the bug is already in this group, then there is nothing to do. - return if $self->in_group($group); + # If the bug is already in this group, then there is nothing to do. + return if $self->in_group($group); - # Make sure that bugs in this product can actually be restricted - # to this group by the current user. - $self->product_obj->group_is_settable($group) - || ThrowUserError('group_restriction_not_allowed', $args); + # Make sure that bugs in this product can actually be restricted + # to this group by the current user. + $self->product_obj->group_is_settable($group) + || ThrowUserError('group_restriction_not_allowed', $args); - # OtherControl people can add groups only during a product change, - # and only when the group is not NA for them. - if (!Bugzilla->user->in_group($group->name)) { - my $controls = $self->product_obj->group_controls->{$group->id}; - if (!$self->{_old_product_name} - || $controls->{othercontrol} == CONTROLMAPNA) - { - ThrowUserError('group_restriction_not_allowed', $args); - } + # OtherControl people can add groups only during a product change, + # and only when the group is not NA for them. + if (!Bugzilla->user->in_group($group->name)) { + my $controls = $self->product_obj->group_controls->{$group->id}; + if (!$self->{_old_product_name} || $controls->{othercontrol} == CONTROLMAPNA) { + ThrowUserError('group_restriction_not_allowed', $args); } + } - my $current_groups = $self->groups_in; - push(@$current_groups, $group); + my $current_groups = $self->groups_in; + push(@$current_groups, $group); } sub remove_group { - my ($self, $group) = @_; + my ($self, $group) = @_; - # See add_group() for the reason why we store the user input. - my $group_name = blessed($group) ? $group->name : $group; - my $args = { name => $group_name, product => $self->product, - bug_id => $self->id, action => 'remove' }; + # See add_group() for the reason why we store the user input. + my $group_name = blessed($group) ? $group->name : $group; + my $args = { + name => $group_name, + product => $self->product, + bug_id => $self->id, + action => 'remove' + }; - $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; + $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; - # If the bug isn't in this group, then either the name is misspelled, - # or the group really doesn't exist. Let the user know about this problem. - $self->in_group($group) || ThrowUserError('group_invalid_removal', $args); + # If the bug isn't in this group, then either the name is misspelled, + # or the group really doesn't exist. Let the user know about this problem. + $self->in_group($group) || ThrowUserError('group_invalid_removal', $args); - # Check if this is a valid group for this product. You can *always* - # remove a group that is not valid for this product (set_product does this). - # This particularly happens when we're moving a bug to a new product. - # You still have to be a member of an inactive group to remove it. - if ($self->product_obj->group_is_valid($group)) { - my $controls = $self->product_obj->group_controls->{$group->id}; + # Check if this is a valid group for this product. You can *always* + # remove a group that is not valid for this product (set_product does this). + # This particularly happens when we're moving a bug to a new product. + # You still have to be a member of an inactive group to remove it. + if ($self->product_obj->group_is_valid($group)) { + my $controls = $self->product_obj->group_controls->{$group->id}; - # Nobody can ever remove a Mandatory group, unless it became inactive. - if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) { - ThrowUserError('group_invalid_removal', $args); - } + # Nobody can ever remove a Mandatory group, unless it became inactive. + if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) { + ThrowUserError('group_invalid_removal', $args); + } - # OtherControl people can remove groups only during a product change, - # and only when they are non-Mandatory and non-NA. - if (!Bugzilla->user->in_group($group->name)) { - if (!$self->{_old_product_name} - || $controls->{othercontrol} == CONTROLMAPMANDATORY - || $controls->{othercontrol} == CONTROLMAPNA) - { - ThrowUserError('group_invalid_removal', $args); - } - } + # OtherControl people can remove groups only during a product change, + # and only when they are non-Mandatory and non-NA. + if (!Bugzilla->user->in_group($group->name)) { + if (!$self->{_old_product_name} + || $controls->{othercontrol} == CONTROLMAPMANDATORY + || $controls->{othercontrol} == CONTROLMAPNA) + { + ThrowUserError('group_invalid_removal', $args); + } } + } - my $current_groups = $self->groups_in; - @$current_groups = grep { $_->id != $group->id } @$current_groups; + my $current_groups = $self->groups_in; + @$current_groups = grep { $_->id != $group->id } @$current_groups; } sub add_see_also { - my ($self, $input, $skip_recursion) = @_; + my ($self, $input, $skip_recursion) = @_; - # This is needed by xt/search.t. - $input = $input->name if blessed($input); + # This is needed by xt/search.t. + $input = $input->name if blessed($input); - $input = trim($input); - return if !$input; + $input = trim($input); + return if !$input; - my ($class, $uri) = Bugzilla::BugUrl->class_for($input); + my ($class, $uri) = Bugzilla::BugUrl->class_for($input); - my $params = { value => $uri, bug_id => $self, class => $class }; - $class->check_required_create_fields($params); + my $params = {value => $uri, bug_id => $self, class => $class}; + $class->check_required_create_fields($params); - my $field_values = $class->run_create_validators($params); - my $value = $field_values->{value}->as_string; - trick_taint($value); - $field_values->{value} = $value; - - # We only add the new URI if it hasn't been added yet. URIs are - # case-sensitive, but most of our DBs are case-insensitive, so we do - # this check case-insensitively. - if (!grep { lc($_->name) eq lc($value) } @{ $self->see_also }) { - my $privs; - my $can = $self->check_can_change_field('see_also', '', $value, \$privs); - if (!$can) { - ThrowUserError('illegal_change', { field => 'see_also', - newvalue => $value, - privs => $privs }); - } - # If this is a link to a local bug then save the - # ref bug id for sending changes email. - my $ref_bug = delete $field_values->{ref_bug}; - if ($class->isa('Bugzilla::BugUrl::Bugzilla::Local') - and !$skip_recursion - and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs)) - { - $ref_bug->add_see_also($self->id, 'skip_recursion'); - push @{ $self->{_update_ref_bugs} }, $ref_bug; - push @{ $self->{see_also_changes} }, $ref_bug->id; - } - push @{ $self->{see_also} }, bless ($field_values, $class); - } -} - -sub remove_see_also { - my ($self, $url, $skip_recursion) = @_; - my $see_also = $self->see_also; - - # This is needed by xt/search.t. - $url = $url->name if blessed($url); - - my ($removed_bug_url, $new_see_also) = - part { lc($_->name) ne lc($url) } @$see_also; + my $field_values = $class->run_create_validators($params); + my $value = $field_values->{value}->as_string; + trick_taint($value); + $field_values->{value} = $value; + # We only add the new URI if it hasn't been added yet. URIs are + # case-sensitive, but most of our DBs are case-insensitive, so we do + # this check case-insensitively. + if (!grep { lc($_->name) eq lc($value) } @{$self->see_also}) { my $privs; - my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also, \$privs); + my $can = $self->check_can_change_field('see_also', '', $value, \$privs); if (!$can) { - ThrowUserError('illegal_change', { field => 'see_also', - oldvalue => $url, - privs => $privs }); + ThrowUserError('illegal_change', + {field => 'see_also', newvalue => $value, privs => $privs}); } - # Since we remove also the url from the referenced bug, - # we need to notify changes for that bug too. - $removed_bug_url = $removed_bug_url->[0]; - if (!$skip_recursion and $removed_bug_url - and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local') - and $removed_bug_url->ref_bug_url) + # If this is a link to a local bug then save the + # ref bug id for sending changes email. + my $ref_bug = delete $field_values->{ref_bug}; + if ( $class->isa('Bugzilla::BugUrl::Bugzilla::Local') + and !$skip_recursion + and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs)) { - my $ref_bug - = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id); + $ref_bug->add_see_also($self->id, 'skip_recursion'); + push @{$self->{_update_ref_bugs}}, $ref_bug; + push @{$self->{see_also_changes}}, $ref_bug->id; + } + push @{$self->{see_also}}, bless($field_values, $class); + } +} - if (Bugzilla->user->can_edit_product($ref_bug->product_id) - and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs)) - { - my $self_url = $removed_bug_url->local_uri($self->id); - $ref_bug->remove_see_also($self_url, 'skip_recursion'); - push @{ $self->{_update_ref_bugs} }, $ref_bug; - push @{ $self->{see_also_changes} }, $ref_bug->id; - } +sub remove_see_also { + my ($self, $url, $skip_recursion) = @_; + my $see_also = $self->see_also; + + # This is needed by xt/search.t. + $url = $url->name if blessed($url); + + my ($removed_bug_url, $new_see_also) + = part { lc($_->name) ne lc($url) } @$see_also; + + my $privs; + my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also, + \$privs); + if (!$can) { + ThrowUserError('illegal_change', + {field => 'see_also', oldvalue => $url, privs => $privs}); + } + + # Since we remove also the url from the referenced bug, + # we need to notify changes for that bug too. + $removed_bug_url = $removed_bug_url->[0]; + if ( !$skip_recursion + and $removed_bug_url + and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local') + and $removed_bug_url->ref_bug_url) + { + my $ref_bug = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id); + + if (Bugzilla->user->can_edit_product($ref_bug->product_id) + and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs)) + { + my $self_url = $removed_bug_url->local_uri($self->id); + $ref_bug->remove_see_also($self_url, 'skip_recursion'); + push @{$self->{_update_ref_bugs}}, $ref_bug; + push @{$self->{see_also_changes}}, $ref_bug->id; } + } - $self->{see_also} = $new_see_also || []; + $self->{see_also} = $new_see_also || []; } sub add_tag { - my ($self, $tag) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - $tag = $self->_check_tag_name($tag); - - my $tag_id = $user->tags->{$tag}->{id}; - # If this tag doesn't exist for this user yet, create it. - if (!$tag_id) { - $dbh->do('INSERT INTO tag (user_id, name) VALUES (?, ?)', - undef, ($user->id, $tag)); - - $tag_id = $dbh->selectrow_array('SELECT id FROM tag - WHERE name = ? AND user_id = ?', - undef, ($tag, $user->id)); - # The list has changed. - delete $user->{tags}; - } - # Do nothing if this tag is already set for this bug. - return if grep { $_ eq $tag } @{$self->tags}; + my ($self, $tag) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + $tag = $self->_check_tag_name($tag); + + my $tag_id = $user->tags->{$tag}->{id}; + + # If this tag doesn't exist for this user yet, create it. + if (!$tag_id) { + $dbh->do('INSERT INTO tag (user_id, name) VALUES (?, ?)', + undef, ($user->id, $tag)); + + $tag_id = $dbh->selectrow_array( + 'SELECT id FROM tag + WHERE name = ? AND user_id = ?', undef, + ($tag, $user->id) + ); - # Increment the counter. Do it before the SQL call below, - # to not count the tag twice. - $user->tags->{$tag}->{bug_count}++; + # The list has changed. + delete $user->{tags}; + } - $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)', - undef, ($self->id, $tag_id)); + # Do nothing if this tag is already set for this bug. + return if grep { $_ eq $tag } @{$self->tags}; - push(@{$self->{tags}}, $tag); + # Increment the counter. Do it before the SQL call below, + # to not count the tag twice. + $user->tags->{$tag}->{bug_count}++; + + $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)', + undef, ($self->id, $tag_id)); + + push(@{$self->{tags}}, $tag); } sub remove_tag { - my ($self, $tag) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - $tag = $self->_check_tag_name($tag); + my ($self, $tag) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + $tag = $self->_check_tag_name($tag); - my $tag_id = exists $user->tags->{$tag} ? $user->tags->{$tag}->{id} : undef; - # Do nothing if the user doesn't use this tag, or didn't set it for this bug. - return unless ($tag_id && grep { $_ eq $tag } @{$self->tags}); + my $tag_id = exists $user->tags->{$tag} ? $user->tags->{$tag}->{id} : undef; - $dbh->do('DELETE FROM bug_tag WHERE bug_id = ? AND tag_id = ?', - undef, ($self->id, $tag_id)); + # Do nothing if the user doesn't use this tag, or didn't set it for this bug. + return unless ($tag_id && grep { $_ eq $tag } @{$self->tags}); - $self->{tags} = [grep { $_ ne $tag } @{$self->tags}]; + $dbh->do('DELETE FROM bug_tag WHERE bug_id = ? AND tag_id = ?', + undef, ($self->id, $tag_id)); - # Decrement the counter, and delete the tag if no bugs are using it anymore. - if (!--$user->tags->{$tag}->{bug_count}) { - $dbh->do('DELETE FROM tag WHERE name = ? AND user_id = ?', - undef, ($tag, $user->id)); + $self->{tags} = [grep { $_ ne $tag } @{$self->tags}]; - # The list has changed. - delete $user->{tags}; - } + # Decrement the counter, and delete the tag if no bugs are using it anymore. + if (!--$user->tags->{$tag}->{bug_count}) { + $dbh->do('DELETE FROM tag WHERE name = ? AND user_id = ?', + undef, ($tag, $user->id)); + + # The list has changed. + delete $user->{tags}; + } } sub tags { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # This method doesn't support several users using the same bug object. - if (!exists $self->{tags}) { - $self->{tags} = $dbh->selectcol_arrayref( - 'SELECT name FROM bug_tag + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # This method doesn't support several users using the same bug object. + if (!exists $self->{tags}) { + $self->{tags} = $dbh->selectcol_arrayref( + 'SELECT name FROM bug_tag INNER JOIN tag ON tag.id = bug_tag.tag_id - WHERE bug_id = ? AND user_id = ?', - undef, ($self->id, $user->id)); - } - return $self->{tags}; + WHERE bug_id = ? AND user_id = ?', undef, ($self->id, $user->id) + ); + } + return $self->{tags}; } ##################################################################### @@ -3271,30 +3394,30 @@ sub tags { # These are accessors that don't need to access the database. # Keep them in alphabetical order. -sub bug_file_loc { return $_[0]->{bug_file_loc} } -sub bug_id { return $_[0]->{bug_id} } -sub bug_severity { return $_[0]->{bug_severity} } -sub bug_status { return $_[0]->{bug_status} } -sub cclist_accessible { return $_[0]->{cclist_accessible} } -sub component_id { return $_[0]->{component_id} } -sub creation_ts { return $_[0]->{creation_ts} } -sub estimated_time { return $_[0]->{estimated_time} } -sub deadline { return $_[0]->{deadline} } -sub delta_ts { return $_[0]->{delta_ts} } -sub error { return $_[0]->{error} } -sub everconfirmed { return $_[0]->{everconfirmed} } -sub lastdiffed { return $_[0]->{lastdiffed} } -sub op_sys { return $_[0]->{op_sys} } -sub priority { return $_[0]->{priority} } -sub product_id { return $_[0]->{product_id} } -sub remaining_time { return $_[0]->{remaining_time} } +sub bug_file_loc { return $_[0]->{bug_file_loc} } +sub bug_id { return $_[0]->{bug_id} } +sub bug_severity { return $_[0]->{bug_severity} } +sub bug_status { return $_[0]->{bug_status} } +sub cclist_accessible { return $_[0]->{cclist_accessible} } +sub component_id { return $_[0]->{component_id} } +sub creation_ts { return $_[0]->{creation_ts} } +sub estimated_time { return $_[0]->{estimated_time} } +sub deadline { return $_[0]->{deadline} } +sub delta_ts { return $_[0]->{delta_ts} } +sub error { return $_[0]->{error} } +sub everconfirmed { return $_[0]->{everconfirmed} } +sub lastdiffed { return $_[0]->{lastdiffed} } +sub op_sys { return $_[0]->{op_sys} } +sub priority { return $_[0]->{priority} } +sub product_id { return $_[0]->{product_id} } +sub remaining_time { return $_[0]->{remaining_time} } sub reporter_accessible { return $_[0]->{reporter_accessible} } -sub rep_platform { return $_[0]->{rep_platform} } -sub resolution { return $_[0]->{resolution} } -sub short_desc { return $_[0]->{short_desc} } -sub status_whiteboard { return $_[0]->{status_whiteboard} } -sub target_milestone { return $_[0]->{target_milestone} } -sub version { return $_[0]->{version} } +sub rep_platform { return $_[0]->{rep_platform} } +sub resolution { return $_[0]->{resolution} } +sub short_desc { return $_[0]->{short_desc} } +sub status_whiteboard { return $_[0]->{status_whiteboard} } +sub target_milestone { return $_[0]->{target_milestone} } +sub version { return $_[0]->{version} } ##################################################################### # Complex Accessors @@ -3313,674 +3436,713 @@ sub version { return $_[0]->{version} } # security holes. sub dup_id { - my ($self) = @_; - return $self->{'dup_id'} if exists $self->{'dup_id'}; + my ($self) = @_; + return $self->{'dup_id'} if exists $self->{'dup_id'}; - $self->{'dup_id'} = undef; - return if $self->{'error'}; + $self->{'dup_id'} = undef; + return if $self->{'error'}; - if ($self->{'resolution'} eq 'DUPLICATE') { - my $dbh = Bugzilla->dbh; - $self->{'dup_id'} = - $dbh->selectrow_array(q{SELECT dupe_of + if ($self->{'resolution'} eq 'DUPLICATE') { + my $dbh = Bugzilla->dbh; + $self->{'dup_id'} = $dbh->selectrow_array( + q{SELECT dupe_of FROM duplicates - WHERE dupe = ?}, - undef, - $self->{'bug_id'}); - } - return $self->{'dup_id'}; + WHERE dupe = ?}, undef, $self->{'bug_id'} + ); + } + return $self->{'dup_id'}; } sub _resolve_ultimate_dup_id { - my ($bug_id, $dupe_of, $loops_are_an_error) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?'); - - my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id); - my $last_dup = $bug_id; - - my %dupes; - while ($this_dup) { - if ($this_dup == $bug_id) { - if ($loops_are_an_error) { - ThrowUserError('dupe_loop_detected', { bug_id => $bug_id, - dupe_of => $dupe_of }); - } - else { - return $last_dup; - } - } - # If $dupes{$this_dup} is already set to 1, then a loop - # already exists which does not involve this bug. - # As the user is not responsible for this loop, do not - # prevent them from marking this bug as a duplicate. - return $last_dup if exists $dupes{$this_dup}; - $dupes{$this_dup} = 1; - $last_dup = $this_dup; - $this_dup = $dbh->selectrow_array($sth, undef, $this_dup); + my ($bug_id, $dupe_of, $loops_are_an_error) = @_; + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?'); + + my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id); + my $last_dup = $bug_id; + + my %dupes; + while ($this_dup) { + if ($this_dup == $bug_id) { + if ($loops_are_an_error) { + ThrowUserError('dupe_loop_detected', {bug_id => $bug_id, dupe_of => $dupe_of}); + } + else { + return $last_dup; + } } - return $last_dup; + # If $dupes{$this_dup} is already set to 1, then a loop + # already exists which does not involve this bug. + # As the user is not responsible for this loop, do not + # prevent them from marking this bug as a duplicate. + return $last_dup if exists $dupes{$this_dup}; + $dupes{$this_dup} = 1; + $last_dup = $this_dup; + $this_dup = $dbh->selectrow_array($sth, undef, $this_dup); + } + + return $last_dup; } sub actual_time { - my ($self) = @_; - return $self->{'actual_time'} if exists $self->{'actual_time'}; + my ($self) = @_; + return $self->{'actual_time'} if exists $self->{'actual_time'}; - if ( $self->{'error'} || !Bugzilla->user->is_timetracker ) { - $self->{'actual_time'} = undef; - return $self->{'actual_time'}; - } + if ($self->{'error'} || !Bugzilla->user->is_timetracker) { + $self->{'actual_time'} = undef; + return $self->{'actual_time'}; + } - my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time) + my $sth = Bugzilla->dbh->prepare( + "SELECT SUM(work_time) FROM longdescs - WHERE longdescs.bug_id=?"); - $sth->execute($self->{bug_id}); - $self->{'actual_time'} = $sth->fetchrow_array(); - return $self->{'actual_time'}; + WHERE longdescs.bug_id=?" + ); + $sth->execute($self->{bug_id}); + $self->{'actual_time'} = $sth->fetchrow_array(); + return $self->{'actual_time'}; } sub alias { - my ($self) = @_; - return $self->{'alias'} if exists $self->{'alias'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'alias'} if exists $self->{'alias'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - $self->{'alias'} = $dbh->selectcol_arrayref( - q{SELECT alias FROM bugs_aliases WHERE bug_id = ? ORDER BY alias}, - undef, $self->bug_id); + my $dbh = Bugzilla->dbh; + $self->{'alias'} + = $dbh->selectcol_arrayref( + q{SELECT alias FROM bugs_aliases WHERE bug_id = ? ORDER BY alias}, + undef, $self->bug_id); - return $self->{'alias'}; + return $self->{'alias'}; } sub any_flags_requesteeble { - my ($self) = @_; - return $self->{'any_flags_requesteeble'} - if exists $self->{'any_flags_requesteeble'}; - return 0 if $self->{'error'}; + my ($self) = @_; + return $self->{'any_flags_requesteeble'} + if exists $self->{'any_flags_requesteeble'}; + return 0 if $self->{'error'}; - 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; + my $any_flags_requesteeble + = grep { $_->is_requestable && $_->is_requesteeble } @{$self->flag_types}; - return $self->{'any_flags_requesteeble'}; + # 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'}; } sub attachments { - my ($self) = @_; - return $self->{'attachments'} if exists $self->{'attachments'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'attachments'} if exists $self->{'attachments'}; + return [] if $self->{'error'}; - $self->{'attachments'} = - Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); - $_->object_cache_set() foreach @{ $self->{'attachments'} }; - return $self->{'attachments'}; + $self->{'attachments'} + = Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); + $_->object_cache_set() foreach @{$self->{'attachments'}}; + return $self->{'attachments'}; } sub assigned_to { - my ($self) = @_; - return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; - $self->{'assigned_to'} = 0 if $self->{'error'}; - $self->{'assigned_to_obj'} ||= new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 }); - return $self->{'assigned_to_obj'}; + my ($self) = @_; + return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; + $self->{'assigned_to'} = 0 if $self->{'error'}; + $self->{'assigned_to_obj'} + ||= new Bugzilla::User({id => $self->{'assigned_to'}, cache => 1}); + return $self->{'assigned_to_obj'}; } sub blocked { - my ($self) = @_; - return $self->{'blocked'} if exists $self->{'blocked'}; - return [] if $self->{'error'}; - $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id); - return $self->{'blocked'}; + my ($self) = @_; + return $self->{'blocked'} if exists $self->{'blocked'}; + return [] if $self->{'error'}; + $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id); + return $self->{'blocked'}; } sub blocks_obj { - my ($self) = @_; - $self->{blocks_obj} ||= $self->_bugs_in_order($self->blocked); - return $self->{blocks_obj}; + my ($self) = @_; + $self->{blocks_obj} ||= $self->_bugs_in_order($self->blocked); + return $self->{blocks_obj}; } sub bug_group { - my ($self) = @_; - return join(', ', (map { $_->name } @{$self->groups_in})); + my ($self) = @_; + return join(', ', (map { $_->name } @{$self->groups_in})); } sub related_bugs { - my ($self, $relationship) = @_; - return [] if $self->{'error'}; + my ($self, $relationship) = @_; + return [] if $self->{'error'}; - my $field_name = $relationship->name; - $self->{'related_bugs'}->{$field_name} ||= $self->match({$field_name => $self->id}); - return $self->{'related_bugs'}->{$field_name}; + my $field_name = $relationship->name; + $self->{'related_bugs'}->{$field_name} + ||= $self->match({$field_name => $self->id}); + return $self->{'related_bugs'}->{$field_name}; } sub cc { - my ($self) = @_; - return $self->{'cc'} if exists $self->{'cc'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'cc'} if exists $self->{'cc'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - $self->{'cc'} = $dbh->selectcol_arrayref( - q{SELECT profiles.login_name FROM cc, profiles + my $dbh = Bugzilla->dbh; + $self->{'cc'} = $dbh->selectcol_arrayref( + q{SELECT profiles.login_name FROM cc, profiles WHERE bug_id = ? AND cc.who = profiles.userid - ORDER BY profiles.login_name}, - undef, $self->bug_id); + ORDER BY profiles.login_name}, undef, $self->bug_id + ); - return $self->{'cc'}; + return $self->{'cc'}; } # XXX Eventually this will become the standard "cc" method used everywhere. 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); - $self->{'cc_users'} = Bugzilla::User->new_from_list($cc_ids); - return $self->{'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); + $self->{'cc_users'} = Bugzilla::User->new_from_list($cc_ids); + return $self->{'cc_users'}; } sub component { - my ($self) = @_; - return '' if $self->{error}; - $self->{component} //= $self->component_obj->name; - return $self->{component}; + my ($self) = @_; + return '' if $self->{error}; + $self->{component} //= $self->component_obj->name; + return $self->{component}; } # XXX Eventually this will replace component() sub component_obj { - my ($self) = @_; - return $self->{component_obj} if defined $self->{component_obj}; - return {} if $self->{error}; - $self->{component_obj} = - new Bugzilla::Component({ id => $self->{component_id}, cache => 1 }); - return $self->{component_obj}; + my ($self) = @_; + return $self->{component_obj} if defined $self->{component_obj}; + return {} if $self->{error}; + $self->{component_obj} + = new Bugzilla::Component({id => $self->{component_id}, cache => 1}); + return $self->{component_obj}; } sub classification_id { - my ($self) = @_; - return 0 if $self->{error}; - $self->{classification_id} //= $self->product_obj->classification_id; - return $self->{classification_id}; + my ($self) = @_; + return 0 if $self->{error}; + $self->{classification_id} //= $self->product_obj->classification_id; + return $self->{classification_id}; } sub classification { - my ($self) = @_; - return '' if $self->{error}; - $self->{classification} //= $self->product_obj->classification->name; - return $self->{classification}; + my ($self) = @_; + return '' if $self->{error}; + $self->{classification} //= $self->product_obj->classification->name; + return $self->{classification}; } sub default_bug_status { - my $class = shift; - # XXX This should just call new_bug_statuses when the UI accepts closed - # bug statuses instead of accepting them as a parameter. - my @statuses = @_; - - my $status; - if (scalar(@statuses) == 1) { - $status = $statuses[0]->name; - } - else { - $status = ($statuses[0]->name ne 'UNCONFIRMED') - ? $statuses[0]->name : $statuses[1]->name; - } + my $class = shift; + + # XXX This should just call new_bug_statuses when the UI accepts closed + # bug statuses instead of accepting them as a parameter. + my @statuses = @_; - return $status; + my $status; + if (scalar(@statuses) == 1) { + $status = $statuses[0]->name; + } + else { + $status + = ($statuses[0]->name ne 'UNCONFIRMED') + ? $statuses[0]->name + : $statuses[1]->name; + } + + return $status; } sub dependson { - my ($self) = @_; - return $self->{'dependson'} if exists $self->{'dependson'}; - return [] if $self->{'error'}; - $self->{'dependson'} = - EmitDependList("blocked", "dependson", $self->bug_id); - return $self->{'dependson'}; + my ($self) = @_; + return $self->{'dependson'} if exists $self->{'dependson'}; + return [] if $self->{'error'}; + $self->{'dependson'} = EmitDependList("blocked", "dependson", $self->bug_id); + return $self->{'dependson'}; } sub depends_on_obj { - my ($self) = @_; - $self->{depends_on_obj} ||= $self->_bugs_in_order($self->dependson); - return $self->{depends_on_obj}; + my ($self) = @_; + $self->{depends_on_obj} ||= $self->_bugs_in_order($self->dependson); + return $self->{depends_on_obj}; } sub duplicates { - my $self = shift; - return $self->{duplicates} if exists $self->{duplicates}; - return [] if $self->{error}; - $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids); - return $self->{duplicates}; + my $self = shift; + return $self->{duplicates} if exists $self->{duplicates}; + return [] if $self->{error}; + $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids); + return $self->{duplicates}; } sub duplicate_ids { - my $self = shift; - return $self->{duplicate_ids} if exists $self->{duplicate_ids}; - return [] if $self->{error}; + my $self = shift; + return $self->{duplicate_ids} if exists $self->{duplicate_ids}; + return [] if $self->{error}; - my $dbh = Bugzilla->dbh; - $self->{duplicate_ids} = - $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?', - undef, $self->id); - return $self->{duplicate_ids}; + my $dbh = Bugzilla->dbh; + $self->{duplicate_ids} + = $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?', + undef, $self->id); + return $self->{duplicate_ids}; } sub flag_types { - my ($self) = @_; - return $self->{'flag_types'} if exists $self->{'flag_types'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'flag_types'} if exists $self->{'flag_types'}; + return [] if $self->{'error'}; - my $vars = { target_type => 'bug', - product_id => $self->{product_id}, - component_id => $self->{component_id}, - bug_id => $self->bug_id }; + my $vars = { + target_type => 'bug', + product_id => $self->{product_id}, + component_id => $self->{component_id}, + bug_id => $self->bug_id + }; - $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); - return $self->{'flag_types'}; + $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); + return $self->{'flag_types'}; } sub flags { - my $self = shift; + 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}; + # 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; - unless (exists $self->{isopened}) { - $self->{isopened} = is_open_state($self->{bug_status}) ? 1 : 0; - } - return $self->{isopened}; + my $self = shift; + unless (exists $self->{isopened}) { + $self->{isopened} = is_open_state($self->{bug_status}) ? 1 : 0; + } + return $self->{isopened}; } sub keywords { - my ($self) = @_; - return join(', ', (map { $_->name } @{$self->keyword_objects})); + my ($self) = @_; + return join(', ', (map { $_->name } @{$self->keyword_objects})); } # XXX At some point, this should probably replace the normal "keywords" sub. sub keyword_objects { - my $self = shift; - return $self->{'keyword_objects'} if defined $self->{'keyword_objects'}; - return [] if $self->{'error'}; + my $self = shift; + return $self->{'keyword_objects'} if defined $self->{'keyword_objects'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - my $ids = $dbh->selectcol_arrayref( - "SELECT keywordid FROM keywords WHERE bug_id = ?", undef, $self->id); - $self->{'keyword_objects'} = Bugzilla::Keyword->new_from_list($ids); - return $self->{'keyword_objects'}; + my $dbh = Bugzilla->dbh; + my $ids + = $dbh->selectcol_arrayref("SELECT keywordid FROM keywords WHERE bug_id = ?", + undef, $self->id); + $self->{'keyword_objects'} = Bugzilla::Keyword->new_from_list($ids); + return $self->{'keyword_objects'}; } sub comments { - my ($self, $params) = @_; - return [] if $self->{'error'}; - $params ||= {}; - - if (!defined $self->{'comments'}) { - $self->{'comments'} = Bugzilla::Comment->match({ bug_id => $self->id }); - my $count = 0; - state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; - foreach my $comment (@{ $self->{'comments'} }) { - $comment->{count} = $count++; - $comment->{bug} = $self; - # XXX - hack for MySQL. Convert [U+....] back into its Unicode - # equivalent for characters above U+FFFF as MySQL older than 5.5.3 - # cannot store them, see Bugzilla::Comment::_check_thetext(). - if ($is_mysql) { - # Perl 5.13.8 and older complain about non-characters. - no warnings 'utf8'; - $comment->{thetext} =~ s/\x{FDD0}\[U\+((?:[1-9A-F]|10)[0-9A-F]{4})\]\x{FDD1}/chr(hex $1)/eg - } - } - # Some bugs may have no comments when upgrading old installations. - Bugzilla::Comment->preload($self->{'comments'}) if $count; - } - my @comments = @{ $self->{'comments'} }; - - my $order = $params->{order} - || Bugzilla->user->setting('comment_sort_order'); - 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; + my ($self, $params) = @_; + return [] if $self->{'error'}; + $params ||= {}; + + if (!defined $self->{'comments'}) { + $self->{'comments'} = Bugzilla::Comment->match({bug_id => $self->id}); + my $count = 0; + state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + foreach my $comment (@{$self->{'comments'}}) { + $comment->{count} = $count++; + $comment->{bug} = $self; + + # XXX - hack for MySQL. Convert [U+....] back into its Unicode + # equivalent for characters above U+FFFF as MySQL older than 5.5.3 + # cannot store them, see Bugzilla::Comment::_check_thetext(). + if ($is_mysql) { + + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $comment->{thetext} + =~ s/\x{FDD0}\[U\+((?:[1-9A-F]|10)[0-9A-F]{4})\]\x{FDD1}/chr(hex $1)/eg; + } + } + + # Some bugs may have no comments when upgrading old installations. + Bugzilla::Comment->preload($self->{'comments'}) if $count; + } + my @comments = @{$self->{'comments'}}; + + my $order = $params->{order} || Bugzilla->user->setting('comment_sort_order'); + 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 new_bug_statuses { - my ($class, $product) = @_; - my $user = Bugzilla->user; + my ($class, $product) = @_; + my $user = Bugzilla->user; - # Construct the list of allowable statuses. - my @statuses = @{ Bugzilla::Bug->statuses_available($product) }; + # Construct the list of allowable statuses. + my @statuses = @{Bugzilla::Bug->statuses_available($product)}; - # If the user has no privs... - unless ($user->in_group('editbugs', $product->id) - || $user->in_group('canconfirm', $product->id)) - { - # ... use UNCONFIRMED if available, else use the first status of the list. - my ($unconfirmed) = grep { $_->name eq 'UNCONFIRMED' } @statuses; - - # Because of an apparent Perl bug, "$unconfirmed || $statuses[0]" doesn't - # work, so we're using an "?:" operator. See bug 603314 for details. - @statuses = ($unconfirmed ? $unconfirmed : $statuses[0]); - } + # If the user has no privs... + unless ($user->in_group('editbugs', $product->id) + || $user->in_group('canconfirm', $product->id)) + { + # ... use UNCONFIRMED if available, else use the first status of the list. + my ($unconfirmed) = grep { $_->name eq 'UNCONFIRMED' } @statuses; + + # Because of an apparent Perl bug, "$unconfirmed || $statuses[0]" doesn't + # work, so we're using an "?:" operator. See bug 603314 for details. + @statuses = ($unconfirmed ? $unconfirmed : $statuses[0]); + } - return \@statuses; + return \@statuses; } # This is needed by xt/search.t. sub percentage_complete { - my $self = shift; - return undef if $self->{'error'} || !Bugzilla->user->is_timetracker; - my $remaining = $self->remaining_time; - my $actual = $self->actual_time; - my $total = $remaining + $actual; - return undef if $total == 0; - # Search.pm truncates this value to an integer, so we want to as well, - # since this is mostly used in a test where its value needs to be - # identical to what the database will return. - return int(100 * ($actual / $total)); + my $self = shift; + return undef if $self->{'error'} || !Bugzilla->user->is_timetracker; + my $remaining = $self->remaining_time; + my $actual = $self->actual_time; + my $total = $remaining + $actual; + return undef if $total == 0; + + # Search.pm truncates this value to an integer, so we want to as well, + # since this is mostly used in a test where its value needs to be + # identical to what the database will return. + return int(100 * ($actual / $total)); } sub product { - my ($self) = @_; - return '' if $self->{error}; - $self->{product} //= $self->product_obj->name; - return $self->{product}; + my ($self) = @_; + return '' if $self->{error}; + $self->{product} //= $self->product_obj->name; + return $self->{product}; } # XXX This should eventually replace the "product" subroutine. sub product_obj { - my $self = shift; - return {} if $self->{error}; - $self->{product_obj} ||= - new Bugzilla::Product({ id => $self->{product_id}, cache => 1 }); - return $self->{product_obj}; + my $self = shift; + return {} if $self->{error}; + $self->{product_obj} + ||= new Bugzilla::Product({id => $self->{product_id}, cache => 1}); + return $self->{product_obj}; } sub qa_contact { - my ($self) = @_; - return $self->{'qa_contact_obj'} if exists $self->{'qa_contact_obj'}; - return undef if $self->{'error'}; + my ($self) = @_; + return $self->{'qa_contact_obj'} if exists $self->{'qa_contact_obj'}; + return undef if $self->{'error'}; - if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) { - $self->{'qa_contact_obj'} = new Bugzilla::User({ id => $self->{'qa_contact'}, cache => 1 }); - } else { - $self->{'qa_contact_obj'} = undef; - } - return $self->{'qa_contact_obj'}; + if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) { + $self->{'qa_contact_obj'} + = new Bugzilla::User({id => $self->{'qa_contact'}, cache => 1}); + } + else { + $self->{'qa_contact_obj'} = undef; + } + return $self->{'qa_contact_obj'}; } sub reporter { - my ($self) = @_; - return $self->{'reporter'} if exists $self->{'reporter'}; - $self->{'reporter_id'} = 0 if $self->{'error'}; - $self->{'reporter'} = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 }); - return $self->{'reporter'}; + my ($self) = @_; + return $self->{'reporter'} if exists $self->{'reporter'}; + $self->{'reporter_id'} = 0 if $self->{'error'}; + $self->{'reporter'} + = new Bugzilla::User({id => $self->{'reporter_id'}, cache => 1}); + return $self->{'reporter'}; } sub see_also { - my ($self) = @_; - return [] if $self->{'error'}; - if (!exists $self->{see_also}) { - my $ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT id FROM bug_see_also WHERE bug_id = ?', - undef, $self->id); + my ($self) = @_; + return [] if $self->{'error'}; + if (!exists $self->{see_also}) { + my $ids + = Bugzilla->dbh->selectcol_arrayref( + 'SELECT id FROM bug_see_also WHERE bug_id = ?', + undef, $self->id); - my $bug_urls = Bugzilla::BugUrl->new_from_list($ids); + my $bug_urls = Bugzilla::BugUrl->new_from_list($ids); - $self->{see_also} = $bug_urls; - } - return $self->{see_also}; + $self->{see_also} = $bug_urls; + } + return $self->{see_also}; } sub status { - my $self = shift; - return undef if $self->{'error'}; + my $self = shift; + return undef if $self->{'error'}; - $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}}); - return $self->{'status'}; + $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}}); + return $self->{'status'}; } sub statuses_available { - my ($invocant, $product) = @_; + my ($invocant, $product) = @_; - my @statuses; + my @statuses; - if (ref $invocant) { - return [] if $invocant->{'error'}; + if (ref $invocant) { + return [] if $invocant->{'error'}; - return $invocant->{'statuses_available'} - if defined $invocant->{'statuses_available'}; + return $invocant->{'statuses_available'} + if defined $invocant->{'statuses_available'}; - @statuses = @{ $invocant->status->can_change_to }; - $product = $invocant->product_obj; - } else { - @statuses = @{ Bugzilla::Status->can_change_to }; - } + @statuses = @{$invocant->status->can_change_to}; + $product = $invocant->product_obj; + } + else { + @statuses = @{Bugzilla::Status->can_change_to}; + } - # UNCONFIRMED is only a valid status if it is enabled in this product. - if (!$product->allows_unconfirmed) { - @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; - } + # UNCONFIRMED is only a valid status if it is enabled in this product. + if (!$product->allows_unconfirmed) { + @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; + } - if (ref $invocant) { - my $available = $invocant->_refine_available_statuses(@statuses); - $invocant->{'statuses_available'} = $available; - return $available; - } + if (ref $invocant) { + my $available = $invocant->_refine_available_statuses(@statuses); + $invocant->{'statuses_available'} = $available; + return $available; + } - return \@statuses; + return \@statuses; } sub _refine_available_statuses { - my $self = shift; - my @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); - } + my $self = shift; + my @statuses = @_; - # 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); - } - - return \@available; + 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); + } + + return \@available; } sub show_attachment_flags { - my ($self) = @_; - return $self->{'show_attachment_flags'} - if exists $self->{'show_attachment_flags'}; - return 0 if $self->{'error'}; + my ($self) = @_; + return $self->{'show_attachment_flags'} + if exists $self->{'show_attachment_flags'}; + return 0 if $self->{'error'}; - # The number of types of flags that can be set on attachments to this bug - # and the number of flags on those attachments. One of these counts must be - # greater than zero in order for the "flags" column to appear in the table - # of attachments. - my $num_attachment_flag_types = Bugzilla::FlagType::count( - { 'target_type' => 'attachment', - 'product_id' => $self->{'product_id'}, - 'component_id' => $self->{'component_id'} }); - my $num_attachment_flags = Bugzilla::Flag->count( - { 'target_type' => 'attachment', - 'bug_id' => $self->bug_id }); + # The number of types of flags that can be set on attachments to this bug + # and the number of flags on those attachments. One of these counts must be + # greater than zero in order for the "flags" column to appear in the table + # of attachments. + my $num_attachment_flag_types = Bugzilla::FlagType::count({ + 'target_type' => 'attachment', + 'product_id' => $self->{'product_id'}, + 'component_id' => $self->{'component_id'} + }); + my $num_attachment_flags = Bugzilla::Flag->count( + {'target_type' => 'attachment', 'bug_id' => $self->bug_id}); - $self->{'show_attachment_flags'} = - ($num_attachment_flag_types || $num_attachment_flags); + $self->{'show_attachment_flags'} + = ($num_attachment_flag_types || $num_attachment_flags); - return $self->{'show_attachment_flags'}; + return $self->{'show_attachment_flags'}; } sub groups { - my $self = shift; - return $self->{'groups'} if exists $self->{'groups'}; - return [] if $self->{'error'}; + my $self = shift; + return $self->{'groups'} if exists $self->{'groups'}; + return [] if $self->{'error'}; + + my $dbh = Bugzilla->dbh; + my @groups; + + # Some of this stuff needs to go into Bugzilla::User + + # For every group, we need to know if there is ANY bug_group_map + # record putting the current bug in that group and if there is ANY + # user_group_map record putting the user in that group. + # The LEFT JOINs are checking for record existence. + # + my $grouplist = Bugzilla->user->groups_as_string; + my $sth + = $dbh->prepare("SELECT DISTINCT groups.id, name, description," + . " CASE WHEN bug_group_map.group_id IS NOT NULL" + . " THEN 1 ELSE 0 END," + . " CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," + . " isactive, membercontrol, othercontrol" + . " FROM groups" + . " LEFT JOIN bug_group_map" + . " ON bug_group_map.group_id = groups.id" + . " AND bug_id = ?" + . " LEFT JOIN group_control_map" + . " ON group_control_map.group_id = groups.id" + . " AND group_control_map.product_id = ? " + . " WHERE isbuggroup = 1" + . " ORDER BY description"); + $sth->execute($self->{'bug_id'}, $self->{'product_id'}); + + while ( + my ( + $groupid, $name, $description, $ison, + $ingroup, $isactive, $membercontrol, $othercontrol + ) + = $sth->fetchrow_array() + ) + { + + $membercontrol ||= 0; + + # For product groups, we only want to use the group if either + # (1) The bit is set and not required, or + # (2) The group is Shown or Default for members and + # the user is a member of the group. + if ( + $ison + || ( $isactive + && $ingroup + && ( ($membercontrol == CONTROLMAPDEFAULT) + || ($membercontrol == CONTROLMAPSHOWN))) + ) + { + my $ismandatory = $isactive && ($membercontrol == CONTROLMAPMANDATORY); - my $dbh = Bugzilla->dbh; - my @groups; - - # Some of this stuff needs to go into Bugzilla::User - - # For every group, we need to know if there is ANY bug_group_map - # record putting the current bug in that group and if there is ANY - # user_group_map record putting the user in that group. - # The LEFT JOINs are checking for record existence. - # - my $grouplist = Bugzilla->user->groups_as_string; - my $sth = $dbh->prepare( - "SELECT DISTINCT groups.id, name, description," . - " CASE WHEN bug_group_map.group_id IS NOT NULL" . - " THEN 1 ELSE 0 END," . - " CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," . - " isactive, membercontrol, othercontrol" . - " FROM groups" . - " LEFT JOIN bug_group_map" . - " ON bug_group_map.group_id = groups.id" . - " AND bug_id = ?" . - " LEFT JOIN group_control_map" . - " ON group_control_map.group_id = groups.id" . - " AND group_control_map.product_id = ? " . - " WHERE isbuggroup = 1" . - " ORDER BY description"); - $sth->execute($self->{'bug_id'}, - $self->{'product_id'}); - - while (my ($groupid, $name, $description, $ison, $ingroup, $isactive, - $membercontrol, $othercontrol) = $sth->fetchrow_array()) { - - $membercontrol ||= 0; - - # For product groups, we only want to use the group if either - # (1) The bit is set and not required, or - # (2) The group is Shown or Default for members and - # the user is a member of the group. - if ($ison || - ($isactive && $ingroup - && (($membercontrol == CONTROLMAPDEFAULT) - || ($membercontrol == CONTROLMAPSHOWN)) - )) + push( + @groups, { - my $ismandatory = $isactive - && ($membercontrol == CONTROLMAPMANDATORY); - - push (@groups, { "bit" => $groupid, - "name" => $name, - "ison" => $ison, - "ingroup" => $ingroup, - "mandatory" => $ismandatory, - "description" => $description }); + "bit" => $groupid, + "name" => $name, + "ison" => $ison, + "ingroup" => $ingroup, + "mandatory" => $ismandatory, + "description" => $description } + ); } + } - $self->{'groups'} = \@groups; + $self->{'groups'} = \@groups; - return $self->{'groups'}; + return $self->{'groups'}; } sub groups_in { - my $self = shift; - return $self->{'groups_in'} if exists $self->{'groups_in'}; - return [] if $self->{'error'}; - my $group_ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT group_id FROM bug_group_map WHERE bug_id = ?', - undef, $self->id); - $self->{'groups_in'} = Bugzilla::Group->new_from_list($group_ids); - return $self->{'groups_in'}; + my $self = shift; + return $self->{'groups_in'} if exists $self->{'groups_in'}; + return [] if $self->{'error'}; + my $group_ids + = Bugzilla->dbh->selectcol_arrayref( + 'SELECT group_id FROM bug_group_map WHERE bug_id = ?', + undef, $self->id); + $self->{'groups_in'} = Bugzilla::Group->new_from_list($group_ids); + return $self->{'groups_in'}; } sub in_group { - my ($self, $group) = @_; - return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0; + my ($self, $group) = @_; + return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0; } sub user { - my $self = shift; - return $self->{'user'} if exists $self->{'user'}; - return {} if $self->{'error'}; - - my $user = Bugzilla->user; - my $prod_id = $self->{'product_id'}; - - my $editbugs = $user->in_group('editbugs', $prod_id); - my $is_reporter = $user->id == $self->{reporter_id} ? 1 : 0; - my $is_assignee = $user->id == $self->{'assigned_to'} ? 1 : 0; - my $is_qa_contact = Bugzilla->params->{'useqacontact'} - && $self->{'qa_contact'} - && $user->id == $self->{'qa_contact'} ? 1 : 0; - - my $canedit = $editbugs || $is_assignee || $is_qa_contact; - my $canconfirm = $editbugs || $user->in_group('canconfirm', $prod_id); - my $has_any_role = $is_reporter || $is_assignee || $is_qa_contact; - - $self->{'user'} = {canconfirm => $canconfirm, - canedit => $canedit, - isreporter => $is_reporter, - has_any_role => $has_any_role}; - return $self->{'user'}; + my $self = shift; + return $self->{'user'} if exists $self->{'user'}; + return {} if $self->{'error'}; + + my $user = Bugzilla->user; + my $prod_id = $self->{'product_id'}; + + my $editbugs = $user->in_group('editbugs', $prod_id); + my $is_reporter = $user->id == $self->{reporter_id} ? 1 : 0; + my $is_assignee = $user->id == $self->{'assigned_to'} ? 1 : 0; + my $is_qa_contact + = Bugzilla->params->{'useqacontact'} + && $self->{'qa_contact'} + && $user->id == $self->{'qa_contact'} ? 1 : 0; + + my $canedit = $editbugs || $is_assignee || $is_qa_contact; + my $canconfirm = $editbugs || $user->in_group('canconfirm', $prod_id); + my $has_any_role = $is_reporter || $is_assignee || $is_qa_contact; + + $self->{'user'} = { + canconfirm => $canconfirm, + canedit => $canedit, + isreporter => $is_reporter, + has_any_role => $has_any_role + }; + 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; - - 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 (!grep($_->name eq $self->product_obj->name, @products)) { - unshift(@products, $self->product_obj); - } - my %class_ids = map { $_->classification_id => 1 } @products; - my $classifications = - Bugzilla::Classification->new_from_list([keys %class_ids]); - - my %choices = ( - bug_status => $self->statuses_available, - classification => $classifications, - product => \@products, - component => $self->product_obj->components, - version => $self->product_obj->versions, - target_milestone => $self->product_obj->milestones, - ); - - 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 }); - $choices{'resolution'} = \@resolutions; - - foreach my $key (keys %choices) { - my $value = $self->$key; - $choices{$key} = [grep { $_->is_active || $_->name eq $value } @{ $choices{$key} }]; - } - - $self->{'choices'} = \%choices; - return $self->{'choices'}; + my $self = shift; + return $self->{'choices'} if exists $self->{'choices'}; + return {} if $self->{'error'}; + my $user = Bugzilla->user; + + 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 (!grep($_->name eq $self->product_obj->name, @products)) { + unshift(@products, $self->product_obj); + } + my %class_ids = map { $_->classification_id => 1 } @products; + my $classifications + = Bugzilla::Classification->new_from_list([keys %class_ids]); + + my %choices = ( + bug_status => $self->statuses_available, + classification => $classifications, + product => \@products, + component => $self->product_obj->components, + version => $self->product_obj->versions, + target_milestone => $self->product_obj->milestones, + ); + + 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}); + $choices{'resolution'} = \@resolutions; + + foreach my $key (keys %choices) { + my $value = $self->$key; + $choices{$key} + = [grep { $_->is_active || $_->name eq $value } @{$choices{$key}}]; + } + + $self->{'choices'} = \%choices; + return $self->{'choices'}; } # Convenience Function. If you need speed, use this. If you need @@ -3989,11 +4151,11 @@ sub choices { # Queries the database for the bug with a given alias, and returns # the ID of the bug if it exists or the undefined value if it doesn't. sub bug_alias_to_id { - my ($alias) = @_; - my $dbh = Bugzilla->dbh; - trick_taint($alias); - return $dbh->selectrow_array( - "SELECT bug_id FROM bugs_aliases WHERE alias = ?", undef, $alias); + my ($alias) = @_; + my $dbh = Bugzilla->dbh; + trick_taint($alias); + return $dbh->selectrow_array("SELECT bug_id FROM bugs_aliases WHERE alias = ?", + undef, $alias); } ##################################################################### @@ -4003,21 +4165,26 @@ sub bug_alias_to_id { # Returns a list of currently active and editable bug fields, # including multi-select fields. sub editable_bug_fields { - my @fields = Bugzilla->dbh->bz_table_columns('bugs'); - # Add multi-select fields - push(@fields, map { $_->name } @{Bugzilla->fields({obsolete => 0, - type => FIELD_TYPE_MULTI_SELECT})}); - # Obsolete custom fields are not editable. - my @obsolete_fields = @{ Bugzilla->fields({obsolete => 1, custom => 1}) }; - @obsolete_fields = map { $_->name } @obsolete_fields; - foreach my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", - "lastdiffed", @obsolete_fields) - { - my $location = firstidx { $_ eq $remove } @fields; - # Ensure field exists before attempting to remove it. - splice(@fields, $location, 1) if ($location > -1); - } - return @fields; + my @fields = Bugzilla->dbh->bz_table_columns('bugs'); + + # Add multi-select fields + push(@fields, + map { $_->name } + @{Bugzilla->fields({obsolete => 0, type => FIELD_TYPE_MULTI_SELECT})}); + + # Obsolete custom fields are not editable. + my @obsolete_fields = @{Bugzilla->fields({obsolete => 1, custom => 1})}; + @obsolete_fields = map { $_->name } @obsolete_fields; + foreach + my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", "lastdiffed", + @obsolete_fields) + { + my $location = firstidx { $_ eq $remove } @fields; + + # Ensure field exists before attempting to remove it. + splice(@fields, $location, 1) if ($location > -1); + } + return @fields; } # XXX - When Bug::update() will be implemented, we should make this routine @@ -4025,103 +4192,107 @@ sub editable_bug_fields { # Join with bug_status and bugs tables to show bugs with open statuses first, # and then the others sub EmitDependList { - my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_; - my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {}; + my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_; + my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {}; - my $dbh = Bugzilla->dbh; - $exclude_resolved = $exclude_resolved ? 1 : 0; - my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : ''; + my $dbh = Bugzilla->dbh; + $exclude_resolved = $exclude_resolved ? 1 : 0; + my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : ''; - $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare( - "SELECT $target_field + $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare( + "SELECT $target_field FROM dependencies INNER JOIN bugs ON dependencies.$target_field = bugs.bug_id INNER JOIN bug_status ON bugs.bug_status = bug_status.value WHERE $my_field = ? $is_open_clause - ORDER BY is_open DESC, $target_field"); + ORDER BY is_open DESC, $target_field" + ); - return $dbh->selectcol_arrayref( - $cache->{"${target_field}_sth_$exclude_resolved"}, - undef, $bug_id); + return $dbh->selectcol_arrayref( + $cache->{"${target_field}_sth_$exclude_resolved"}, + undef, $bug_id); } # Creates a lot of bug objects in the same order as the input array. sub _bugs_in_order { - my ($self, $bug_ids) = @_; - return [] unless @$bug_ids; + my ($self, $bug_ids) = @_; + return [] unless @$bug_ids; - my %bug_map; - my $dbh = Bugzilla->dbh; + my %bug_map; + my $dbh = Bugzilla->dbh; - # there's no need to load bugs from the database if they are already in the - # object-cache - my @missing_ids; - foreach my $bug_id (@$bug_ids) { - if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) { - $bug_map{$bug_id} = $bug; - } - else { - push @missing_ids, $bug_id; - } + # there's no need to load bugs from the database if they are already in the + # object-cache + my @missing_ids; + foreach my $bug_id (@$bug_ids) { + if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) { + $bug_map{$bug_id} = $bug; } - if (@missing_ids) { - my $bugs = Bugzilla::Bug->new_from_list(\@missing_ids); - $bug_map{$_->id} = $_ foreach @$bugs; + else { + push @missing_ids, $bug_id; } + } + if (@missing_ids) { + my $bugs = Bugzilla::Bug->new_from_list(\@missing_ids); + $bug_map{$_->id} = $_ foreach @$bugs; + } - # Dependencies are often displayed using their aliases instead of their - # bug ID. Load them all at once. - my $rows = $dbh->selectall_arrayref( - 'SELECT bug_id, alias FROM bugs_aliases WHERE ' . - $dbh->sql_in('bug_id', $bug_ids) . ' ORDER BY alias'); + # Dependencies are often displayed using their aliases instead of their + # bug ID. Load them all at once. + my $rows + = $dbh->selectall_arrayref('SELECT bug_id, alias FROM bugs_aliases WHERE ' + . $dbh->sql_in('bug_id', $bug_ids) + . ' ORDER BY alias'); - foreach my $row (@$rows) { - my ($bug_id, $alias) = @$row; - $bug_map{$bug_id}->{alias} ||= []; - push @{ $bug_map{$bug_id}->{alias} }, $alias; - } - # Make sure all bugs have their alias attribute set. - $bug_map{$_}->{alias} ||= [] foreach @$bug_ids; + foreach my $row (@$rows) { + my ($bug_id, $alias) = @$row; + $bug_map{$bug_id}->{alias} ||= []; + push @{$bug_map{$bug_id}->{alias}}, $alias; + } + + # Make sure all bugs have their alias attribute set. + $bug_map{$_}->{alias} ||= [] foreach @$bug_ids; - return [ map { $bug_map{$_} } @$bug_ids ]; + return [map { $bug_map{$_} } @$bug_ids]; } # Get the activity of a bug, starting from $starttime (if given). # This routine assumes Bugzilla::Bug->check has been previously called. sub get_activity { - my ($self, $attach_id, $starttime, $include_comment_tags) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # Arguments passed to the SQL query. - my @args = ($self->id); - - # Only consider changes since $starttime, if given. - my $datepart = ""; - if (defined $starttime) { - trick_taint($starttime); - push (@args, $starttime); - $datepart = "AND bug_when > ?"; - } - - my $attachpart = ""; - if ($attach_id) { - push(@args, $attach_id); - $attachpart = "AND bugs_activity.attach_id = ?"; - } - - # Only includes attachments the user is allowed to see. - my $suppjoins = ""; - my $suppwhere = ""; - if (!$user->is_insider) { - $suppjoins = "LEFT JOIN attachments + my ($self, $attach_id, $starttime, $include_comment_tags) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Arguments passed to the SQL query. + my @args = ($self->id); + + # Only consider changes since $starttime, if given. + my $datepart = ""; + if (defined $starttime) { + trick_taint($starttime); + push(@args, $starttime); + $datepart = "AND bug_when > ?"; + } + + my $attachpart = ""; + if ($attach_id) { + push(@args, $attach_id); + $attachpart = "AND bugs_activity.attach_id = ?"; + } + + # Only includes attachments the user is allowed to see. + my $suppjoins = ""; + my $suppwhere = ""; + if (!$user->is_insider) { + $suppjoins = "LEFT JOIN attachments ON attachments.attach_id = bugs_activity.attach_id"; - $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; - } + $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; + } - my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " . - $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') . - " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name, + my $query + = "SELECT fielddefs.name, bugs_activity.attach_id, " + . $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') + . " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name, bugs_activity.comment_id FROM bugs_activity $suppjoins @@ -4134,24 +4305,26 @@ sub get_activity { $attachpart $suppwhere "; - if (Bugzilla->params->{'comment_taggers_group'} - && $include_comment_tags - && !$attach_id) - { - # Only includes comment tag activity for comments the user is allowed to see. - $suppjoins = ""; - $suppwhere = ""; - if (!Bugzilla->user->is_insider) { - $suppjoins = "INNER JOIN longdescs + if ( Bugzilla->params->{'comment_taggers_group'} + && $include_comment_tags + && !$attach_id) + { + # Only includes comment tag activity for comments the user is allowed to see. + $suppjoins = ""; + $suppwhere = ""; + if (!Bugzilla->user->is_insider) { + $suppjoins = "INNER JOIN longdescs ON longdescs.comment_id = longdescs_tags_activity.comment_id"; - $suppwhere = "AND longdescs.isprivate = 0"; - } + $suppwhere = "AND longdescs.isprivate = 0"; + } - $query .= " + $query .= " UNION ALL SELECT 'comment_tag' AS name, - NULL AS attach_id," . - $dbh->sql_date_format('longdescs_tags_activity.bug_when', '%Y.%m.%d %H:%i:%s') . " AS bug_when, + NULL AS attach_id," + . $dbh->sql_date_format('longdescs_tags_activity.bug_when', + '%Y.%m.%d %H:%i:%s') + . " AS bug_when, longdescs_tags_activity.removed, longdescs_tags_activity.added, profiles.login_name, @@ -4163,168 +4336,177 @@ sub get_activity { $datepart $suppwhere "; - push @args, $self->id; - push @args, $starttime if defined $starttime; - } - - $query .= "ORDER BY bug_when, comment_id"; + push @args, $self->id; + push @args, $starttime if defined $starttime; + } - my $list = $dbh->selectall_arrayref($query, undef, @args); + $query .= "ORDER BY bug_when, comment_id"; - my @operations; - my $operation = {}; - my $changes = []; - my $incomplete_data = 0; + my $list = $dbh->selectall_arrayref($query, undef, @args); - foreach my $entry (@$list) { - my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) = @$entry; - my %change; - my $activity_visible = 1; + my @operations; + my $operation = {}; + my $changes = []; + my $incomplete_data = 0; - # check if the user should see this field's activity - if (grep { $fieldname eq $_ } TIMETRACKING_FIELDS) { - $activity_visible = $user->is_timetracker; - } - elsif ($fieldname eq 'longdescs.isprivate' - && !$user->is_insider && $added) - { - $activity_visible = 0; - } - else { - $activity_visible = 1; - } + foreach my $entry (@$list) { + my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) + = @$entry; + my %change; + my $activity_visible = 1; - if ($activity_visible) { - # Check for the results of an old Bugzilla data corruption bug - if (($added eq '?' && $removed eq '?') - || ($added =~ /^\? / || $removed =~ /^\? /)) { - $incomplete_data = 1; - } - - # An operation, done by 'who' at time 'when', has a number of - # 'changes' associated with it. - # If this is the start of a new operation, store the data from the - # previous one, and set up the new one. - if ($operation->{'who'} - && ($who ne $operation->{'who'} - || $when ne $operation->{'when'})) - { - $operation->{'changes'} = $changes; - push (@operations, $operation); - - # Create new empty anonymous data structures. - $operation = {}; - $changes = []; - } - - # If this is the same field as the previous item, then concatenate - # the data into the same change. - if ($operation->{'who'} && $who eq $operation->{'who'} - && $when eq $operation->{'when'} - && $fieldname eq $operation->{'fieldname'} - && ($comment_id || 0) == ($operation->{'comment_id'} || 0) - && ($attachid || 0) == ($operation->{'attachid'} || 0)) - { - my $old_change = pop @$changes; - $removed = join_activity_entries($fieldname, $old_change->{'removed'}, $removed); - $added = join_activity_entries($fieldname, $old_change->{'added'}, $added); - } - $operation->{'who'} = $who; - $operation->{'when'} = $when; - $operation->{'fieldname'} = $change{'fieldname'} = $fieldname; - $operation->{'attachid'} = $change{'attachid'} = $attachid; - $change{'removed'} = $removed; - $change{'added'} = $added; - - if ($comment_id) { - $operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id); - } - - push (@$changes, \%change); - } + # check if the user should see this field's activity + if (grep { $fieldname eq $_ } TIMETRACKING_FIELDS) { + $activity_visible = $user->is_timetracker; } - - if ($operation->{'who'}) { - $operation->{'changes'} = $changes; - push (@operations, $operation); + elsif ($fieldname eq 'longdescs.isprivate' && !$user->is_insider && $added) { + $activity_visible = 0; } + else { + $activity_visible = 1; + } + + if ($activity_visible) { - return(\@operations, $incomplete_data); + # Check for the results of an old Bugzilla data corruption bug + if ( ($added eq '?' && $removed eq '?') + || ($added =~ /^\? / || $removed =~ /^\? /)) + { + $incomplete_data = 1; + } + + # An operation, done by 'who' at time 'when', has a number of + # 'changes' associated with it. + # If this is the start of a new operation, store the data from the + # previous one, and set up the new one. + if ($operation->{'who'} + && ($who ne $operation->{'who'} || $when ne $operation->{'when'})) + { + $operation->{'changes'} = $changes; + push(@operations, $operation); + + # Create new empty anonymous data structures. + $operation = {}; + $changes = []; + } + + # If this is the same field as the previous item, then concatenate + # the data into the same change. + if ( $operation->{'who'} + && $who eq $operation->{'who'} + && $when eq $operation->{'when'} + && $fieldname eq $operation->{'fieldname'} + && ($comment_id || 0) == ($operation->{'comment_id'} || 0) + && ($attachid || 0) == ($operation->{'attachid'} || 0)) + { + my $old_change = pop @$changes; + $removed + = join_activity_entries($fieldname, $old_change->{'removed'}, $removed); + $added = join_activity_entries($fieldname, $old_change->{'added'}, $added); + } + $operation->{'who'} = $who; + $operation->{'when'} = $when; + $operation->{'fieldname'} = $change{'fieldname'} = $fieldname; + $operation->{'attachid'} = $change{'attachid'} = $attachid; + $change{'removed'} = $removed; + $change{'added'} = $added; + + if ($comment_id) { + $operation->{comment_id} = $change{'comment'} + = Bugzilla::Comment->new($comment_id); + } + + push(@$changes, \%change); + } + } + + if ($operation->{'who'}) { + $operation->{'changes'} = $changes; + push(@operations, $operation); + } + + return (\@operations, $incomplete_data); } # Update the bugs_activity table to reflect changes made in bugs. sub LogActivityEntry { - my ($bug_id, $field, $removed, $added, $user_id, $timestamp, $comment_id, - $attach_id) = @_; - my $sth = Bugzilla->dbh->prepare_cached( - 'INSERT INTO bugs_activity + my ($bug_id, $field, $removed, $added, $user_id, $timestamp, $comment_id, + $attach_id) + = @_; + my $sth = Bugzilla->dbh->prepare_cached( + 'INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)'); - - # in the case of CCs, deps, and keywords, there's a possibility that someone - # might try to add or remove a lot of them at once, which might take more - # space than the activity table allows. We'll solve this by splitting it - # into multiple entries if it's too long. - while ($removed || $added) { - my ($removestr, $addstr) = ($removed, $added); - if (length($removestr) > MAX_LINE_LENGTH) { - my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH); - $removestr = substr($removed, 0, $commaposition); - $removed = substr($removed, $commaposition); - } else { - $removed = ""; # no more entries - } - if (length($addstr) > MAX_LINE_LENGTH) { - my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH); - $addstr = substr($added, 0, $commaposition); - $added = substr($added, $commaposition); - } else { - $added = ""; # no more entries - } - trick_taint($addstr); - trick_taint($removestr); - my $fieldid = get_field_id($field); - $sth->execute($bug_id, $user_id, $timestamp, $fieldid, $removestr, - $addstr, $comment_id, $attach_id); + VALUES (?, ?, ?, ?, ?, ?, ?, ?)' + ); + + # in the case of CCs, deps, and keywords, there's a possibility that someone + # might try to add or remove a lot of them at once, which might take more + # space than the activity table allows. We'll solve this by splitting it + # into multiple entries if it's too long. + while ($removed || $added) { + my ($removestr, $addstr) = ($removed, $added); + if (length($removestr) > MAX_LINE_LENGTH) { + my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH); + $removestr = substr($removed, 0, $commaposition); + $removed = substr($removed, $commaposition); } + else { + $removed = ""; # no more entries + } + if (length($addstr) > MAX_LINE_LENGTH) { + my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH); + $addstr = substr($added, 0, $commaposition); + $added = substr($added, $commaposition); + } + else { + $added = ""; # no more entries + } + trick_taint($addstr); + trick_taint($removestr); + my $fieldid = get_field_id($field); + $sth->execute( + $bug_id, $user_id, $timestamp, $fieldid, + $removestr, $addstr, $comment_id, $attach_id + ); + } } # Update bug_user_last_visit table sub update_user_last_visit { - my ($self, $user, $last_visit_ts) = @_; - my $lv = Bugzilla::BugUserLastVisit->match({ bug_id => $self->id, - user_id => $user->id })->[0]; + my ($self, $user, $last_visit_ts) = @_; + my $lv = Bugzilla::BugUserLastVisit->match( + {bug_id => $self->id, user_id => $user->id})->[0]; - if ($lv) { - $lv->set(last_visit_ts => $last_visit_ts); - $lv->update; - } - else { - Bugzilla::BugUserLastVisit->create({ bug_id => $self->id, - user_id => $user->id, - last_visit_ts => $last_visit_ts }); - } + if ($lv) { + $lv->set(last_visit_ts => $last_visit_ts); + $lv->update; + } + else { + Bugzilla::BugUserLastVisit->create( + {bug_id => $self->id, user_id => $user->id, last_visit_ts => $last_visit_ts}); + } } # Convert WebService API and email_in.pl field names to internal DB field # names. sub map_fields { - my ($params, $except) = @_; - - my %field_values; - foreach my $field (keys %$params) { - # Don't allow setting private fields via email_in or the WebService. - next if $field =~ /^_/; - my $field_name; - if ($except->{$field}) { - $field_name = $field; - } - else { - $field_name = FIELD_MAP->{$field} || $field; - } - $field_values{$field_name} = $params->{$field}; + my ($params, $except) = @_; + + my %field_values; + foreach my $field (keys %$params) { + + # Don't allow setting private fields via email_in or the WebService. + next if $field =~ /^_/; + my $field_name; + if ($except->{$field}) { + $field_name = $field; + } + else { + $field_name = FIELD_MAP->{$field} || $field; } - return \%field_values; + $field_values{$field_name} = $params->{$field}; + } + return \%field_values; } ################################################################################ @@ -4344,164 +4526,187 @@ sub map_fields { # $PrivilegesRequired - return the reason of the failure, if any ################################################################################ sub check_can_change_field { - my $self = shift; - my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_); - my $user = Bugzilla->user; + my $self = shift; + my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_); + my $user = Bugzilla->user; + + $oldvalue = defined($oldvalue) ? $oldvalue : ''; + $newvalue = defined($newvalue) ? $newvalue : ''; + + # Return true if they haven't changed this field at all. + if ($oldvalue eq $newvalue) { + return 1; + } + elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') { + my ($removed, $added) = diff_arrays($oldvalue, $newvalue); + return 1 if !scalar(@$removed) && !scalar(@$added); + } + elsif (trim($oldvalue) eq trim($newvalue)) { + return 1; - $oldvalue = defined($oldvalue) ? $oldvalue : ''; - $newvalue = defined($newvalue) ? $newvalue : ''; - - # Return true if they haven't changed this field at all. - if ($oldvalue eq $newvalue) { - return 1; - } elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') { - my ($removed, $added) = diff_arrays($oldvalue, $newvalue); - return 1 if !scalar(@$removed) && !scalar(@$added); - } elsif (trim($oldvalue) eq trim($newvalue)) { - return 1; # numeric fields need to be compared using == - } elsif (($field eq 'estimated_time' || $field eq 'remaining_time' - || $field eq 'work_time') - && $oldvalue == $newvalue) + } + elsif ( + ( + $field eq 'estimated_time' + || $field eq 'remaining_time' + || $field eq 'work_time' + ) + && $oldvalue == $newvalue + ) + { + return 1; + } + + my @priv_results; + Bugzilla::Hook::process( + 'bug_check_can_change_field', { - return 1; + bug => $self, + field => $field, + new_value => $newvalue, + old_value => $oldvalue, + priv_results => \@priv_results + } + ); + if (my $priv_required = first { $_ > 0 } @priv_results) { + $$PrivilegesRequired = $priv_required; + return 0; + } + my $allow_found = first { $_ == 0 } @priv_results; + if (defined $allow_found) { + return 1; + } + + # Allow anyone to change comments, or set flags + if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { + return 1; + } + +# If the user isn't allowed to change a field, we must tell them who can. +# We store the required permission set into the $PrivilegesRequired +# variable which gets passed to the error template. +# +# $PrivilegesRequired = PRIVILEGES_REQUIRED_NONE : no privileges required; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER : the reporter, assignee or an empowered user; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE : the assignee or an empowered user; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. + + # Only users in the time-tracking group can change time-tracking fields, + # including the deadline. + if (grep { $_ eq $field } (TIMETRACKING_FIELDS, 'deadline')) { + if (!$user->is_timetracker) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; + return 0; + } + } + + # Allow anyone with (product-specific) "editbugs" privs to change anything. + if ($user->in_group('editbugs', $self->{'product_id'})) { + return 1; + } + + # *Only* users with (product-specific) "canconfirm" privs can confirm bugs. + if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; + return $user->in_group('canconfirm', $self->{'product_id'}); + } + + # Make sure that a valid bug ID has been given. + if (!$self->{'error'}) { + + # Allow the assignee to change anything else. + if ( $self->{'assigned_to'} == $user->id + || $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id) + { + return 1; } - my @priv_results; - Bugzilla::Hook::process('bug_check_can_change_field', - { bug => $self, field => $field, - new_value => $newvalue, old_value => $oldvalue, - priv_results => \@priv_results }); - if (my $priv_required = first { $_ > 0 } @priv_results) { - $$PrivilegesRequired = $priv_required; - return 0; - } - my $allow_found = first { $_ == 0 } @priv_results; - if (defined $allow_found) { - return 1; + # Allow the QA contact to change anything else. + if ( + Bugzilla->params->{'useqacontact'} + && ( ($self->{'qa_contact'} && $self->{'qa_contact'} == $user->id) + || ($self->{'_old_qa_contact'} && $self->{'_old_qa_contact'} == $user->id)) + ) + { + return 1; } + } - # Allow anyone to change comments, or set flags - if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { - return 1; - } + # At this point, the user is either the reporter or an + # unprivileged user. We first check for fields the reporter + # is not allowed to change. - # If the user isn't allowed to change a field, we must tell them who can. - # We store the required permission set into the $PrivilegesRequired - # variable which gets passed to the error template. - # - # $PrivilegesRequired = PRIVILEGES_REQUIRED_NONE : no privileges required; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER : the reporter, assignee or an empowered user; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE : the assignee or an empowered user; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. - - # Only users in the time-tracking group can change time-tracking fields, - # including the deadline. - if (grep { $_ eq $field } (TIMETRACKING_FIELDS, 'deadline')) { - if (!$user->is_timetracker) { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; - return 0; - } - } - - # Allow anyone with (product-specific) "editbugs" privs to change anything. - if ($user->in_group('editbugs', $self->{'product_id'})) { - return 1; - } + # The reporter may not: + # - reassign bugs, unless the bugs are assigned to them; + # in that case we will have already returned 1 above + # when checking for the assignee of the bug. + if ($field eq 'assigned_to') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # *Only* users with (product-specific) "canconfirm" privs can confirm bugs. - if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; - return $user->in_group('canconfirm', $self->{'product_id'}); - } + # - change the QA contact + if ($field eq 'qa_contact') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # Make sure that a valid bug ID has been given. - if (!$self->{'error'}) { - # Allow the assignee to change anything else. - if ($self->{'assigned_to'} == $user->id - || $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id) - { - return 1; - } + # - change the target milestone + if ($field eq 'target_milestone') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # Allow the QA contact to change anything else. - if (Bugzilla->params->{'useqacontact'} - && (($self->{'qa_contact'} && $self->{'qa_contact'} == $user->id) - || ($self->{'_old_qa_contact'} && $self->{'_old_qa_contact'} == $user->id))) - { - return 1; - } - } + # - change the priority (unless they could have set it originally) + if ($field eq 'priority' && !Bugzilla->params->{'letsubmitterchoosepriority'}) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # At this point, the user is either the reporter or an - # unprivileged user. We first check for fields the reporter - # is not allowed to change. - - # The reporter may not: - # - reassign bugs, unless the bugs are assigned to them; - # in that case we will have already returned 1 above - # when checking for the assignee of the bug. - if ($field eq 'assigned_to') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the QA contact - if ($field eq 'qa_contact') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the target milestone - if ($field eq 'target_milestone') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the priority (unless they could have set it originally) - if ($field eq 'priority' - && !Bugzilla->params->{'letsubmitterchoosepriority'}) - { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - unconfirm bugs (confirming them is handled above) - if ($field eq 'everconfirmed') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the status from one open state to another - if ($field eq 'bug_status' - && is_open_state($oldvalue) && is_open_state($newvalue)) - { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } + # - unconfirm bugs (confirming them is handled above) + if ($field eq 'everconfirmed') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } + + # - change the status from one open state to another + if ( $field eq 'bug_status' + && is_open_state($oldvalue) + && is_open_state($newvalue)) + { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # The reporter is allowed to change anything else. - if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) { - return 1; - } + # The reporter is allowed to change anything else. + if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) { + return 1; + } - # If we haven't returned by this point, then the user doesn't - # have the necessary permissions to change this field. - $$PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER; - return 0; + # If we haven't returned by this point, then the user doesn't + # have the necessary permissions to change this field. + $$PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER; + 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'); - } + 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'; } - return 0; + 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; } # @@ -4510,72 +4715,77 @@ sub _changes_everconfirmed { # Validate and return a hash of dependencies sub ValidateDependencies { - my $fields = {}; - # These can be arrayrefs or they can be strings. - $fields->{'dependson'} = shift; - $fields->{'blocked'} = shift; - my $id = shift || 0; - - unless (defined($fields->{'dependson'}) - || defined($fields->{'blocked'})) - { - return; - } - - my $dbh = Bugzilla->dbh; - my %deps; - my %deptree; - my %sth; - $sth{dependson} = $dbh->prepare('SELECT dependson FROM dependencies WHERE blocked = ?'); - $sth{blocked} = $dbh->prepare('SELECT blocked FROM dependencies WHERE dependson = ?'); - - foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { - my ($me, $target) = @{$pair}; - $deptree{$target} = []; - $deps{$target} = []; - next unless $fields->{$target}; - - my %seen; - my $target_array = ref($fields->{$target}) ? $fields->{$target} - : [split(/[\s,]+/, $fields->{$target})]; - foreach my $i (@$target_array) { - if ($id == $i) { - ThrowUserError("dependency_loop_single"); - } - if (!exists $seen{$i}) { - push(@{$deptree{$target}}, $i); - $seen{$i} = 1; - } - } - # populate $deps{$target} as first-level deps only. - # and find remainder of dependency tree in $deptree{$target} - @{$deps{$target}} = @{$deptree{$target}}; - my @stack = @{$deps{$target}}; - while (@stack) { - my $i = shift @stack; - my $dep_list = $dbh->selectcol_arrayref($sth{$target}, undef, $i); - foreach my $t (@$dep_list) { - # ignore any _current_ dependencies involving this bug, - # as they will be overwritten with data from the form. - if ($t != $id && !exists $seen{$t}) { - push(@{$deptree{$target}}, $t); - push @stack, $t; - $seen{$t} = 1; - } - } + my $fields = {}; + + # These can be arrayrefs or they can be strings. + $fields->{'dependson'} = shift; + $fields->{'blocked'} = shift; + my $id = shift || 0; + + unless (defined($fields->{'dependson'}) || defined($fields->{'blocked'})) { + return; + } + + my $dbh = Bugzilla->dbh; + my %deps; + my %deptree; + my %sth; + $sth{dependson} + = $dbh->prepare('SELECT dependson FROM dependencies WHERE blocked = ?'); + $sth{blocked} + = $dbh->prepare('SELECT blocked FROM dependencies WHERE dependson = ?'); + + foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { + my ($me, $target) = @{$pair}; + $deptree{$target} = []; + $deps{$target} = []; + next unless $fields->{$target}; + + my %seen; + my $target_array + = ref($fields->{$target}) + ? $fields->{$target} + : [split(/[\s,]+/, $fields->{$target})]; + foreach my $i (@$target_array) { + if ($id == $i) { + ThrowUserError("dependency_loop_single"); + } + if (!exists $seen{$i}) { + push(@{$deptree{$target}}, $i); + $seen{$i} = 1; + } + } + + # populate $deps{$target} as first-level deps only. + # and find remainder of dependency tree in $deptree{$target} + @{$deps{$target}} = @{$deptree{$target}}; + my @stack = @{$deps{$target}}; + while (@stack) { + my $i = shift @stack; + my $dep_list = $dbh->selectcol_arrayref($sth{$target}, undef, $i); + foreach my $t (@$dep_list) { + + # ignore any _current_ dependencies involving this bug, + # as they will be overwritten with data from the form. + if ($t != $id && !exists $seen{$t}) { + push(@{$deptree{$target}}, $t); + push @stack, $t; + $seen{$t} = 1; } + } } + } - my @deps = @{$deptree{'dependson'}}; - my @blocks = @{$deptree{'blocked'}}; - my %union = (); - my %isect = (); - foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ } - my @isect = keys %isect; - if (scalar(@isect) > 0) { - ThrowUserError("dependency_loop_multi", {'deps' => \@isect}); - } - return %deps; + my @deps = @{$deptree{'dependson'}}; + my @blocks = @{$deptree{'blocked'}}; + my %union = (); + my %isect = (); + foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ } + my @isect = keys %isect; + if (scalar(@isect) > 0) { + ThrowUserError("dependency_loop_multi", {'deps' => \@isect}); + } + return %deps; } @@ -4584,51 +4794,52 @@ sub ValidateDependencies { ##################################################################### sub _create_cf_accessors { - my ($invocant) = @_; - my $class = ref($invocant) || $invocant; - return if Bugzilla->request_cache->{"${class}_cf_accessors_created"}; - - my $fields = Bugzilla->fields({ custom => 1 }); - foreach my $field (@$fields) { - my $accessor = $class->_accessor_for($field); - my $name = "${class}::" . $field->name; - { - no strict 'refs'; - next if defined *{$name}; - *{$name} = $accessor; - } + my ($invocant) = @_; + my $class = ref($invocant) || $invocant; + return if Bugzilla->request_cache->{"${class}_cf_accessors_created"}; + + my $fields = Bugzilla->fields({custom => 1}); + foreach my $field (@$fields) { + my $accessor = $class->_accessor_for($field); + my $name = "${class}::" . $field->name; + { + no strict 'refs'; + next if defined *{$name}; + *{$name} = $accessor; } + } - Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; + Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; } sub _accessor_for { - my ($class, $field) = @_; - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - return $class->_multi_select_accessor($field->name); - } - return $class->_cf_accessor($field->name); + my ($class, $field) = @_; + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + return $class->_multi_select_accessor($field->name); + } + return $class->_cf_accessor($field->name); } sub _cf_accessor { - my ($class, $field) = @_; - my $accessor = sub { - my ($self) = @_; - return $self->{$field}; - }; - return $accessor; + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + return $self->{$field}; + }; + return $accessor; } sub _multi_select_accessor { - my ($class, $field) = @_; - my $accessor = sub { - my ($self) = @_; - $self->{$field} ||= Bugzilla->dbh->selectcol_arrayref( - "SELECT value FROM bug_$field WHERE bug_id = ? ORDER BY value", - undef, $self->id); - return $self->{$field}; - }; - return $accessor; + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + $self->{$field} + ||= Bugzilla->dbh->selectcol_arrayref( + "SELECT value FROM bug_$field WHERE bug_id = ? ORDER BY value", + undef, $self->id); + return $self->{$field}; + }; + return $accessor; } 1; diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm index 110a1ffaf..90eed0d8e 100644 --- a/Bugzilla/BugMail.pm +++ b/Bugzilla/BugMail.pm @@ -27,16 +27,17 @@ use Scalar::Util qw(blessed); use List::MoreUtils qw(uniq); use Storable qw(dclone); -use constant BIT_DIRECT => 1; -use constant BIT_WATCHING => 2; +use constant BIT_DIRECT => 1; +use constant BIT_WATCHING => 2; sub relationships { - my $ref = RELATIONSHIPS; - # Clone it so that we don't modify the constant; - my %relationships = %$ref; - Bugzilla::Hook::process('bugmail_relationships', - { relationships => \%relationships }); - return %relationships; + my $ref = RELATIONSHIPS; + + # Clone it so that we don't modify the constant; + my %relationships = %$ref; + Bugzilla::Hook::process('bugmail_relationships', + {relationships => \%relationships}); + return %relationships; } # args: bug_id, and an optional hash ref which may have keys for: @@ -46,452 +47,482 @@ sub relationships { # All the names are email addresses, not userids # values are scalars, except for cc, which is a list sub Send { - my ($id, $forced, $params) = @_; - $params ||= {}; - - my $dbh = Bugzilla->dbh; - my $bug = new Bugzilla::Bug($id); - - my $start = $bug->lastdiffed; - my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - # Bugzilla::User objects of people in various roles. More than one person - # can 'have' a role, if the person in that role has changed, or people are - # watching. - my @assignees = ($bug->assigned_to); - my @qa_contacts = $bug->qa_contact || (); - - my @ccs = @{ $bug->cc_users }; - # Include the people passed in as being in particular roles. - # This can include people who used to hold those roles. - # At this point, we don't care if there are duplicates in these arrays. - my $changer = $forced->{'changer'}; - if ($forced->{'owner'}) { - push (@assignees, Bugzilla::User->check($forced->{'owner'})); - } - - if ($forced->{'qacontact'}) { - push (@qa_contacts, Bugzilla::User->check($forced->{'qacontact'})); - } - - if ($forced->{'cc'}) { - foreach my $cc (@{$forced->{'cc'}}) { - push(@ccs, Bugzilla::User->check($cc)); - } + my ($id, $forced, $params) = @_; + $params ||= {}; + + my $dbh = Bugzilla->dbh; + my $bug = new Bugzilla::Bug($id); + + my $start = $bug->lastdiffed; + my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + # Bugzilla::User objects of people in various roles. More than one person + # can 'have' a role, if the person in that role has changed, or people are + # watching. + my @assignees = ($bug->assigned_to); + my @qa_contacts = $bug->qa_contact || (); + + my @ccs = @{$bug->cc_users}; + + # Include the people passed in as being in particular roles. + # This can include people who used to hold those roles. + # At this point, we don't care if there are duplicates in these arrays. + my $changer = $forced->{'changer'}; + if ($forced->{'owner'}) { + push(@assignees, Bugzilla::User->check($forced->{'owner'})); + } + + if ($forced->{'qacontact'}) { + push(@qa_contacts, Bugzilla::User->check($forced->{'qacontact'})); + } + + if ($forced->{'cc'}) { + foreach my $cc (@{$forced->{'cc'}}) { + push(@ccs, Bugzilla::User->check($cc)); } - my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); + } + my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); + + my @diffs; + if (!$start) { + @diffs = _get_new_bugmail_fields($bug); + } + + my $comments = []; + + if ($params->{dep_only}) { + push( + @diffs, + { + field_name => 'bug_status', + old => $params->{changes}->{bug_status}->[0], + new => $params->{changes}->{bug_status}->[1], + login_name => $changer->login, + who => $changer, + blocker => $params->{blocker} + }, + { + field_name => 'resolution', + old => $params->{changes}->{resolution}->[0], + new => $params->{changes}->{resolution}->[1], + login_name => $changer->login, + who => $changer, + blocker => $params->{blocker} + } + ); + } + else { + push(@diffs, _get_diffs($bug, $end, \%user_cache)); - my @diffs; - if (!$start) { - @diffs = _get_new_bugmail_fields($bug); - } + $comments = $bug->comments({after => $start, to => $end}); - my $comments = []; - - if ($params->{dep_only}) { - push(@diffs, { field_name => 'bug_status', - old => $params->{changes}->{bug_status}->[0], - new => $params->{changes}->{bug_status}->[1], - login_name => $changer->login, - who => $changer, - blocker => $params->{blocker} }, - { field_name => 'resolution', - old => $params->{changes}->{resolution}->[0], - new => $params->{changes}->{resolution}->[1], - login_name => $changer->login, - who => $changer, - blocker => $params->{blocker} }); - } - else { - push(@diffs, _get_diffs($bug, $end, \%user_cache)); + # Skip empty comments. + @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; - $comments = $bug->comments({ after => $start, to => $end }); - # Skip empty comments. - @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; + # If no changes have been made, there is no need to process further. + return {'sent' => []} unless scalar(@diffs) || scalar(@$comments); + } - # If no changes have been made, there is no need to process further. - return {'sent' => []} unless scalar(@diffs) || scalar(@$comments); - } + ########################################################################### + # Start of email filtering code + ########################################################################### - ########################################################################### - # Start of email filtering code - ########################################################################### - - # A user_id => roles hash to keep track of people. - my %recipients; - my %watching; - - # We also record bugs that are referenced - my @referenced_bug_ids = (); - - # Now we work out all the people involved with this bug, and note all of - # the relationships in a hash. The keys are userids, the values are an - # array of role constants. - - # CCs - $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs); - - # Reporter (there's only ever one) - $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT; - - # QA Contact - if (Bugzilla->params->{'useqacontact'}) { - foreach (@qa_contacts) { - # QA Contact can be blank; ignore it if so. - $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_; - } - } + # A user_id => roles hash to keep track of people. + my %recipients; + my %watching; - # Assignee - $recipients{$_->id}->{+REL_ASSIGNEE} = BIT_DIRECT foreach (@assignees); - - # The last relevant set of people are those who are being removed from - # their roles in this change. We get their names out of the diffs. - foreach my $change (@diffs) { - if ($change->{old}) { - # You can't stop being the reporter, so we don't check that - # relationship here. - # Ignore people whose user account has been deleted or renamed. - if ($change->{field_name} eq 'cc') { - foreach my $cc_user (split(/[\s,]+/, $change->{old})) { - my $uid = login_to_id($cc_user); - $recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid; - } - } - elsif ($change->{field_name} eq 'qa_contact') { - my $uid = login_to_id($change->{old}); - $recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid; - } - elsif ($change->{field_name} eq 'assigned_to') { - my $uid = login_to_id($change->{old}); - $recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid; - } - } + # We also record bugs that are referenced + my @referenced_bug_ids = (); - if ($change->{field_name} eq 'dependson' || $change->{field_name} eq 'blocked') { - push @referenced_bug_ids, split(/[\s,]+/, $change->{old} // ''); - push @referenced_bug_ids, split(/[\s,]+/, $change->{new} // ''); - } - } + # Now we work out all the people involved with this bug, and note all of + # the relationships in a hash. The keys are userids, the values are an + # array of role constants. + + # CCs + $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs); + + # Reporter (there's only ever one) + $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT; - my $referenced_bugs = scalar(@referenced_bug_ids) - ? Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]) - : []; + # QA Contact + if (Bugzilla->params->{'useqacontact'}) { + foreach (@qa_contacts) { - # Make sure %user_cache has every user in it so far referenced - foreach my $user_id (keys %recipients) { - $user_cache{$user_id} ||= new Bugzilla::User($user_id); + # QA Contact can be blank; ignore it if so. + $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_; } - - Bugzilla::Hook::process('bugmail_recipients', - { bug => $bug, recipients => \%recipients, - users => \%user_cache, diffs => \@diffs }); - - # We should not assume %recipients to have any entries. - if (scalar keys %recipients) { - # Find all those user-watching anyone on the current list, who is not - # on it already themselves. - my $involved = join(",", keys %recipients); - - my $userwatchers = - $dbh->selectall_arrayref("SELECT watcher, watched FROM watch - WHERE watched IN ($involved)"); - - # Mark these people as having the role of the person they are watching - foreach my $watch (@$userwatchers) { - while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) { - $recipients{$watch->[0]}->{$role} |= BIT_WATCHING - if $bits & BIT_DIRECT; - } - push(@{$watching{$watch->[0]}}, $watch->[1]); + } + + # Assignee + $recipients{$_->id}->{+REL_ASSIGNEE} = BIT_DIRECT foreach (@assignees); + + # The last relevant set of people are those who are being removed from + # their roles in this change. We get their names out of the diffs. + foreach my $change (@diffs) { + if ($change->{old}) { + + # You can't stop being the reporter, so we don't check that + # relationship here. + # Ignore people whose user account has been deleted or renamed. + if ($change->{field_name} eq 'cc') { + foreach my $cc_user (split(/[\s,]+/, $change->{old})) { + my $uid = login_to_id($cc_user); + $recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid; } + } + elsif ($change->{field_name} eq 'qa_contact') { + my $uid = login_to_id($change->{old}); + $recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid; + } + elsif ($change->{field_name} eq 'assigned_to') { + my $uid = login_to_id($change->{old}); + $recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid; + } } - # Global watcher - my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'}); - foreach (@watchers) { - my $watcher_id = login_to_id($_); - next unless $watcher_id; - $recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT; + if ($change->{field_name} eq 'dependson' || $change->{field_name} eq 'blocked') + { + push @referenced_bug_ids, split(/[\s,]+/, $change->{old} // ''); + push @referenced_bug_ids, split(/[\s,]+/, $change->{new} // ''); } - - # We now have a complete set of all the users, and their relationships to - # the bug in question. However, we are not necessarily going to mail them - # all - there are preferences, permissions checks and all sorts to do yet. - my @sent; - - # The email client will display the Date: header in the desired timezone, - # so we can always use UTC here. - my $date = $params->{dep_only} ? $end : $bug->delta_ts; - $date = format_time($date, '%a, %d %b %Y %T %z', 'UTC'); - - foreach my $user_id (keys %recipients) { - my %rels_which_want; - my $user = $user_cache{$user_id} ||= new Bugzilla::User($user_id); - # Deleted users must be excluded. - next unless $user; - - # If email notifications are disabled for this account, or the bug - # is ignored, there is no need to do additional checks. - next if ($user->email_disabled || $user->is_bug_ignored($id)); - - if ($user->can_see_bug($id)) { - # Go through each role the user has and see if they want mail in - # that role. - foreach my $relationship (keys %{$recipients{$user_id}}) { - if ($user->wants_bug_mail($bug, - $relationship, - $start ? \@diffs : [], - $comments, - $params->{dep_only}, - $changer)) - { - $rels_which_want{$relationship} = - $recipients{$user_id}->{$relationship}; - } - } - } - - if (scalar(%rels_which_want)) { - # 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? - - # We shouldn't send mail if this is a dependency mail and the - # depending bug is not visible to the user. - # This is to avoid leaking the summary of a confidential bug. - my $dep_ok = 1; - if ($params->{dep_only}) { - $dep_ok = $user->can_see_bug($params->{blocker}->id) ? 1 : 0; - } - - # Email the user if the dep check passed. - if ($dep_ok) { - my $sent_mail = sendMail( - { to => $user, - bug => $bug, - comments => $comments, - date => $date, - changer => $changer, - watchers => exists $watching{$user_id} ? - $watching{$user_id} : undef, - diffs => \@diffs, - rels_which_want => \%rels_which_want, - dep_only => $params->{dep_only}, - referenced_bugs => $referenced_bugs, - }); - push(@sent, $user->login) if $sent_mail; - } - } + } + + my $referenced_bugs + = scalar(@referenced_bug_ids) + ? Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]) + : []; + + # Make sure %user_cache has every user in it so far referenced + foreach my $user_id (keys %recipients) { + $user_cache{$user_id} ||= new Bugzilla::User($user_id); + } + + Bugzilla::Hook::process( + 'bugmail_recipients', + { + bug => $bug, + recipients => \%recipients, + users => \%user_cache, + diffs => \@diffs } + ); - # When sending bugmail about a blocker being reopened or resolved, - # we say nothing about changes in the bug being blocked, so we must - # not update lastdiffed in this case. - if (!$params->{dep_only}) { - $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?', - undef, ($end, $id)); - $bug->{lastdiffed} = $end; - } + # We should not assume %recipients to have any entries. + if (scalar keys %recipients) { - return {'sent' => \@sent}; -} + # Find all those user-watching anyone on the current list, who is not + # on it already themselves. + my $involved = join(",", keys %recipients); -sub sendMail { - my $params = shift; - - my $user = $params->{to}; - my $bug = $params->{bug}; - my @send_comments = @{ $params->{comments} }; - my $date = $params->{date}; - my $changer = $params->{changer}; - my $watchingRef = $params->{watchers}; - my @diffs = @{ $params->{diffs} }; - my $relRef = $params->{rels_which_want}; - my $dep_only = $params->{dep_only}; - my $referenced_bugs = $params->{referenced_bugs}; - - # Only display changes the user is allowed see. - my @display_diffs; - - foreach my $diff (@diffs) { - my $add_diff = 0; - - if (grep { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) { - $add_diff = 1 if $user->is_timetracker; - } - elsif (!$diff->{isprivate} || $user->is_insider) { - $add_diff = 1; - } - push(@display_diffs, $diff) if $add_diff; - } + my $userwatchers = $dbh->selectall_arrayref( + "SELECT watcher, watched FROM watch + WHERE watched IN ($involved)" + ); - if (!$user->is_insider) { - @send_comments = grep { !$_->is_private } @send_comments; + # Mark these people as having the role of the person they are watching + foreach my $watch (@$userwatchers) { + while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) { + $recipients{$watch->[0]}->{$role} |= BIT_WATCHING if $bits & BIT_DIRECT; + } + push(@{$watching{$watch->[0]}}, $watch->[1]); } - - if (!scalar(@display_diffs) && !scalar(@send_comments)) { - # Whoops, no differences! - return 0; + } + + # Global watcher + my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'}); + foreach (@watchers) { + my $watcher_id = login_to_id($_); + next unless $watcher_id; + $recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT; + } + + # We now have a complete set of all the users, and their relationships to + # the bug in question. However, we are not necessarily going to mail them + # all - there are preferences, permissions checks and all sorts to do yet. + my @sent; + + # The email client will display the Date: header in the desired timezone, + # so we can always use UTC here. + my $date = $params->{dep_only} ? $end : $bug->delta_ts; + $date = format_time($date, '%a, %d %b %Y %T %z', 'UTC'); + + foreach my $user_id (keys %recipients) { + my %rels_which_want; + my $user = $user_cache{$user_id} ||= new Bugzilla::User($user_id); + + # Deleted users must be excluded. + next unless $user; + + # If email notifications are disabled for this account, or the bug + # is ignored, there is no need to do additional checks. + next if ($user->email_disabled || $user->is_bug_ignored($id)); + + if ($user->can_see_bug($id)) { + + # Go through each role the user has and see if they want mail in + # that role. + foreach my $relationship (keys %{$recipients{$user_id}}) { + if ($user->wants_bug_mail( + $bug, $relationship, $start ? \@diffs : [], + $comments, $params->{dep_only}, $changer + )) + { + $rels_which_want{$relationship} = $recipients{$user_id}->{$relationship}; + } + } } - my (@reasons, @reasons_watch); - while (my ($relationship, $bits) = each %{$relRef}) { - push(@reasons, $relationship) if ($bits & BIT_DIRECT); - push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING); + if (scalar(%rels_which_want)) { + + # 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? + + # We shouldn't send mail if this is a dependency mail and the + # depending bug is not visible to the user. + # This is to avoid leaking the summary of a confidential bug. + my $dep_ok = 1; + if ($params->{dep_only}) { + $dep_ok = $user->can_see_bug($params->{blocker}->id) ? 1 : 0; + } + + # Email the user if the dep check passed. + if ($dep_ok) { + my $sent_mail = sendMail({ + to => $user, + bug => $bug, + comments => $comments, + date => $date, + changer => $changer, + watchers => exists $watching{$user_id} ? $watching{$user_id} : undef, + diffs => \@diffs, + rels_which_want => \%rels_which_want, + dep_only => $params->{dep_only}, + referenced_bugs => $referenced_bugs, + }); + push(@sent, $user->login) if $sent_mail; + } } + } - my %relationships = relationships(); - my @headerrel = map { $relationships{$_} } @reasons; - my @watchingrel = map { $relationships{$_} } @reasons_watch; - push(@headerrel, 'None') unless @headerrel; - push(@watchingrel, 'None') unless @watchingrel; - push @watchingrel, map { Bugzilla::User->new($_)->login } @$watchingRef; + # When sending bugmail about a blocker being reopened or resolved, + # we say nothing about changes in the bug being blocked, so we must + # not update lastdiffed in this case. + if (!$params->{dep_only}) { + $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?', undef, ($end, $id)); + $bug->{lastdiffed} = $end; + } - my @changedfields = uniq map { $_->{field_name} } @display_diffs; + return {'sent' => \@sent}; +} - # Add attachments.created to changedfields if one or more - # comments contain information about a new attachment - if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) { - push(@changedfields, 'attachments.created'); +sub sendMail { + my $params = shift; + + my $user = $params->{to}; + my $bug = $params->{bug}; + my @send_comments = @{$params->{comments}}; + my $date = $params->{date}; + my $changer = $params->{changer}; + my $watchingRef = $params->{watchers}; + my @diffs = @{$params->{diffs}}; + my $relRef = $params->{rels_which_want}; + my $dep_only = $params->{dep_only}; + my $referenced_bugs = $params->{referenced_bugs}; + + # Only display changes the user is allowed see. + my @display_diffs; + + foreach my $diff (@diffs) { + my $add_diff = 0; + + if (grep { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) { + $add_diff = 1 if $user->is_timetracker; } - - my $bugmailtype = "changed"; - $bugmailtype = "new" if !$bug->lastdiffed; - $bugmailtype = "dep_changed" if $dep_only; - - my $vars = { - date => $date, - to_user => $user, - bug => $bug, - reasons => \@reasons, - reasons_watch => \@reasons_watch, - reasonsheader => join(" ", @headerrel), - reasonswatchheader => join(" ", @watchingrel), - changer => $changer, - diffs => \@display_diffs, - changedfields => \@changedfields, - referenced_bugs => $user->visible_bugs($referenced_bugs), - new_comments => \@send_comments, - threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), - bugmailtype => $bugmailtype, - }; - if (Bugzilla->params->{'use_mailer_queue'}) { - enqueue($vars); - } else { - MessageToMTA(_generate_bugmail($vars)); + elsif (!$diff->{isprivate} || $user->is_insider) { + $add_diff = 1; } - - return 1; + push(@display_diffs, $diff) if $add_diff; + } + + if (!$user->is_insider) { + @send_comments = grep { !$_->is_private } @send_comments; + } + + if (!scalar(@display_diffs) && !scalar(@send_comments)) { + + # Whoops, no differences! + return 0; + } + + my (@reasons, @reasons_watch); + while (my ($relationship, $bits) = each %{$relRef}) { + push(@reasons, $relationship) if ($bits & BIT_DIRECT); + push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING); + } + + my %relationships = relationships(); + my @headerrel = map { $relationships{$_} } @reasons; + my @watchingrel = map { $relationships{$_} } @reasons_watch; + push(@headerrel, 'None') unless @headerrel; + push(@watchingrel, 'None') unless @watchingrel; + push @watchingrel, map { Bugzilla::User->new($_)->login } @$watchingRef; + + my @changedfields = uniq map { $_->{field_name} } @display_diffs; + + # Add attachments.created to changedfields if one or more + # comments contain information about a new attachment + if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) { + push(@changedfields, 'attachments.created'); + } + + my $bugmailtype = "changed"; + $bugmailtype = "new" if !$bug->lastdiffed; + $bugmailtype = "dep_changed" if $dep_only; + + my $vars = { + date => $date, + to_user => $user, + bug => $bug, + reasons => \@reasons, + reasons_watch => \@reasons_watch, + reasonsheader => join(" ", @headerrel), + reasonswatchheader => join(" ", @watchingrel), + changer => $changer, + diffs => \@display_diffs, + changedfields => \@changedfields, + referenced_bugs => $user->visible_bugs($referenced_bugs), + new_comments => \@send_comments, + threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), + bugmailtype => $bugmailtype, + }; + if (Bugzilla->params->{'use_mailer_queue'}) { + enqueue($vars); + } + else { + MessageToMTA(_generate_bugmail($vars)); + } + + return 1; } sub enqueue { - my ($vars) = @_; - # we need to flatten all objects to a hash before pushing to the job queue. - # the hashes need to be inflated in the dequeue method. - $vars->{bug} = _flatten_object($vars->{bug}); - $vars->{to_user} = _flatten_object($vars->{to_user}); - $vars->{changer} = _flatten_object($vars->{changer}); - $vars->{new_comments} = [ map { _flatten_object($_) } @{ $vars->{new_comments} } ]; - foreach my $diff (@{ $vars->{diffs} }) { - $diff->{who} = _flatten_object($diff->{who}); - if (exists $diff->{blocker}) { - $diff->{blocker} = _flatten_object($diff->{blocker}); - } + my ($vars) = @_; + + # we need to flatten all objects to a hash before pushing to the job queue. + # the hashes need to be inflated in the dequeue method. + $vars->{bug} = _flatten_object($vars->{bug}); + $vars->{to_user} = _flatten_object($vars->{to_user}); + $vars->{changer} = _flatten_object($vars->{changer}); + $vars->{new_comments} = [map { _flatten_object($_) } @{$vars->{new_comments}}]; + foreach my $diff (@{$vars->{diffs}}) { + $diff->{who} = _flatten_object($diff->{who}); + if (exists $diff->{blocker}) { + $diff->{blocker} = _flatten_object($diff->{blocker}); } - Bugzilla->job_queue->insert('bug_mail', { vars => $vars }); + } + Bugzilla->job_queue->insert('bug_mail', {vars => $vars}); } sub dequeue { - my ($payload) = @_; - # clone the payload so we can modify it without impacting TheSchwartz's - # ability to process the job when we've finished - my $vars = dclone($payload); - # inflate objects - $vars->{bug} = Bugzilla::Bug->new_from_hash($vars->{bug}); - $vars->{to_user} = Bugzilla::User->new_from_hash($vars->{to_user}); - $vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer}); - $vars->{new_comments} = [ map { Bugzilla::Comment->new_from_hash($_) } @{ $vars->{new_comments} } ]; - foreach my $diff (@{ $vars->{diffs} }) { - $diff->{who} = Bugzilla::User->new_from_hash($diff->{who}); - if (exists $diff->{blocker}) { - $diff->{blocker} = Bugzilla::Bug->new_from_hash($diff->{blocker}); - } + my ($payload) = @_; + + # clone the payload so we can modify it without impacting TheSchwartz's + # ability to process the job when we've finished + my $vars = dclone($payload); + + # inflate objects + $vars->{bug} = Bugzilla::Bug->new_from_hash($vars->{bug}); + $vars->{to_user} = Bugzilla::User->new_from_hash($vars->{to_user}); + $vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer}); + $vars->{new_comments} + = [map { Bugzilla::Comment->new_from_hash($_) } @{$vars->{new_comments}}]; + foreach my $diff (@{$vars->{diffs}}) { + $diff->{who} = Bugzilla::User->new_from_hash($diff->{who}); + if (exists $diff->{blocker}) { + $diff->{blocker} = Bugzilla::Bug->new_from_hash($diff->{blocker}); } - # generate bugmail and send - MessageToMTA(_generate_bugmail($vars), 1); + } + + # generate bugmail and send + MessageToMTA(_generate_bugmail($vars), 1); } sub _flatten_object { - my ($object) = @_; - # nothing to do if it's already flattened - return $object unless blessed($object); - # the same objects are used for each recipient, so cache the flattened hash - my $cache = Bugzilla->request_cache->{bugmail_flat_objects} ||= {}; - my $key = blessed($object) . '-' . $object->id; - return $cache->{$key} ||= $object->flatten_to_hash; + my ($object) = @_; + + # nothing to do if it's already flattened + return $object unless blessed($object); + + # the same objects are used for each recipient, so cache the flattened hash + my $cache = Bugzilla->request_cache->{bugmail_flat_objects} ||= {}; + my $key = blessed($object) . '-' . $object->id; + return $cache->{$key} ||= $object->flatten_to_hash; } sub _generate_bugmail { - my ($vars) = @_; - my $user = $vars->{to_user}; - my $template = Bugzilla->template_inner($user->setting('lang')); - my ($msg_text, $msg_html, $msg_header); - state $use_utf8 = Bugzilla->params->{'utf8'}; - - $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header) - || ThrowTemplateError($template->error()); - $template->process("email/bugmail.txt.tmpl", $vars, \$msg_text) - || ThrowTemplateError($template->error()); - - my @parts = ( - Bugzilla::MIME->create( - attributes => { - content_type => 'text/plain', - charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', - encoding => 'quoted-printable', - }, - body_str => $msg_text, - ) - ); - if ($user->setting('email_format') eq 'html') { - $template->process("email/bugmail.html.tmpl", $vars, \$msg_html) - || ThrowTemplateError($template->error()); - push @parts, Bugzilla::MIME->create( - attributes => { - content_type => 'text/html', - charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', - encoding => 'quoted-printable', - }, - body_str => $msg_html, - ); - } - - my $email = Bugzilla::MIME->new($msg_header); - if (scalar(@parts) == 1) { - $email->content_type_set($parts[0]->content_type); - } else { - $email->content_type_set('multipart/alternative'); - # Some mail clients need same encoding for each part, even empty ones. - $email->charset_set('UTF-8') if $use_utf8; - } - $email->parts_set(\@parts); - return $email; + my ($vars) = @_; + my $user = $vars->{to_user}; + my $template = Bugzilla->template_inner($user->setting('lang')); + my ($msg_text, $msg_html, $msg_header); + state $use_utf8 = Bugzilla->params->{'utf8'}; + + $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header) + || ThrowTemplateError($template->error()); + $template->process("email/bugmail.txt.tmpl", $vars, \$msg_text) + || ThrowTemplateError($template->error()); + + my @parts = (Bugzilla::MIME->create( + attributes => { + content_type => 'text/plain', + charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', + encoding => 'quoted-printable', + }, + body_str => $msg_text, + )); + + if ($user->setting('email_format') eq 'html') { + $template->process("email/bugmail.html.tmpl", $vars, \$msg_html) + || ThrowTemplateError($template->error()); + push @parts, + Bugzilla::MIME->create( + attributes => { + content_type => 'text/html', + charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', + encoding => 'quoted-printable', + }, + body_str => $msg_html, + ); + } + + my $email = Bugzilla::MIME->new($msg_header); + if (scalar(@parts) == 1) { + $email->content_type_set($parts[0]->content_type); + } + else { + $email->content_type_set('multipart/alternative'); + + # Some mail clients need same encoding for each part, even empty ones. + $email->charset_set('UTF-8') if $use_utf8; + } + $email->parts_set(\@parts); + return $email; } sub _get_diffs { - my ($bug, $end, $user_cache) = @_; - my $dbh = Bugzilla->dbh; - - my @args = ($bug->id); - # If lastdiffed is NULL, then we don't limit the search on time. - my $when_restriction = ''; - if ($bug->lastdiffed) { - $when_restriction = ' AND bug_when > ? AND bug_when <= ?'; - push @args, ($bug->lastdiffed, $end); - } + my ($bug, $end, $user_cache) = @_; + my $dbh = Bugzilla->dbh; - my $diffs = $dbh->selectall_arrayref( - "SELECT fielddefs.name AS field_name, + my @args = ($bug->id); + + # If lastdiffed is NULL, then we don't limit the search on time. + my $when_restriction = ''; + if ($bug->lastdiffed) { + $when_restriction = ' AND bug_when > ? AND bug_when <= ?'; + push @args, ($bug->lastdiffed, $end); + } + + my $diffs = $dbh->selectall_arrayref( + "SELECT fielddefs.name AS field_name, bugs_activity.bug_when, bugs_activity.removed AS old, bugs_activity.added AS new, bugs_activity.attach_id, bugs_activity.comment_id, bugs_activity.who @@ -500,89 +531,92 @@ sub _get_diffs { ON fielddefs.id = bugs_activity.fieldid WHERE bugs_activity.bug_id = ? $when_restriction - ORDER BY bugs_activity.bug_when, bugs_activity.id", - {Slice=>{}}, @args); - - foreach my $diff (@$diffs) { - $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who}); - $diff->{who} = $user_cache->{$diff->{who}}; - if ($diff->{attach_id}) { - $diff->{isprivate} = $dbh->selectrow_array( - 'SELECT isprivate FROM attachments WHERE attach_id = ?', - undef, $diff->{attach_id}); - } - if ($diff->{field_name} eq 'longdescs.isprivate') { - my $comment = Bugzilla::Comment->new($diff->{comment_id}); - $diff->{num} = $comment->count; - $diff->{isprivate} = $diff->{new}; - } + ORDER BY bugs_activity.bug_when, bugs_activity.id", {Slice => {}}, @args + ); + + foreach my $diff (@$diffs) { + $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who}); + $diff->{who} = $user_cache->{$diff->{who}}; + if ($diff->{attach_id}) { + $diff->{isprivate} + = $dbh->selectrow_array( + 'SELECT isprivate FROM attachments WHERE attach_id = ?', + undef, $diff->{attach_id}); } - - my @changes = (); - foreach my $diff (@$diffs) { - # If this is the same field as the previous item, then concatenate - # the data into the same change. - if (scalar(@changes) - && $diff->{field_name} eq $changes[-1]->{field_name} - && $diff->{bug_when} eq $changes[-1]->{bug_when} - && $diff->{who} eq $changes[-1]->{who} - && ($diff->{attach_id} // 0) == ($changes[-1]->{attach_id} // 0) - && ($diff->{comment_id} // 0) == ($changes[-1]->{comment_id} // 0) - ) { - my $old_change = pop @changes; - $diff->{old} = join_activity_entries($diff->{field_name}, $old_change->{old}, $diff->{old}); - $diff->{new} = join_activity_entries($diff->{field_name}, $old_change->{new}, $diff->{new}); - } - push @changes, $diff; + if ($diff->{field_name} eq 'longdescs.isprivate') { + my $comment = Bugzilla::Comment->new($diff->{comment_id}); + $diff->{num} = $comment->count; + $diff->{isprivate} = $diff->{new}; + } + } + + my @changes = (); + foreach my $diff (@$diffs) { + + # If this is the same field as the previous item, then concatenate + # the data into the same change. + if ( scalar(@changes) + && $diff->{field_name} eq $changes[-1]->{field_name} + && $diff->{bug_when} eq $changes[-1]->{bug_when} + && $diff->{who} eq $changes[-1]->{who} + && ($diff->{attach_id} // 0) == ($changes[-1]->{attach_id} // 0) + && ($diff->{comment_id} // 0) == ($changes[-1]->{comment_id} // 0)) + { + my $old_change = pop @changes; + $diff->{old} = join_activity_entries($diff->{field_name}, $old_change->{old}, + $diff->{old}); + $diff->{new} = join_activity_entries($diff->{field_name}, $old_change->{new}, + $diff->{new}); } + push @changes, $diff; + } - return @changes; + return @changes; } sub _get_new_bugmail_fields { - my $bug = shift; - my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) }; - my @diffs; - my $params = Bugzilla->params; - - foreach my $field (@fields) { - my $name = $field->name; - my $value = $bug->$name; - - next if !$field->is_visible_on_bug($bug) - || ($name eq 'classification' && !$params->{'useclassification'}) - || ($name eq 'status_whiteboard' && !$params->{'usestatuswhiteboard'}) - || ($name eq 'qa_contact' && !$params->{'useqacontact'}) - || ($name eq 'target_milestone' && !$params->{'usetargetmilestone'}); - - if (ref $value eq 'ARRAY') { - $value = join(', ', @$value); - } - elsif (blessed($value) && $value->isa('Bugzilla::User')) { - $value = $value->login; - } - elsif (blessed($value) && $value->isa('Bugzilla::Object')) { - $value = $value->name; - } - elsif ($name eq 'estimated_time') { - # "0.00" (which is what we get from the DB) is true, - # so we explicitly do a numerical comparison with 0. - $value = 0 if $value == 0; - } - elsif ($name eq 'deadline') { - $value = time2str("%Y-%m-%d", str2time($value)) if $value; - } - - # If there isn't anything to show, don't include this header. - next unless $value; + my $bug = shift; + my @fields = @{Bugzilla->fields({obsolete => 0, in_new_bugmail => 1})}; + my @diffs; + my $params = Bugzilla->params; + + foreach my $field (@fields) { + my $name = $field->name; + my $value = $bug->$name; + + next + if !$field->is_visible_on_bug($bug) + || ($name eq 'classification' && !$params->{'useclassification'}) + || ($name eq 'status_whiteboard' && !$params->{'usestatuswhiteboard'}) + || ($name eq 'qa_contact' && !$params->{'useqacontact'}) + || ($name eq 'target_milestone' && !$params->{'usetargetmilestone'}); + + if (ref $value eq 'ARRAY') { + $value = join(', ', @$value); + } + elsif (blessed($value) && $value->isa('Bugzilla::User')) { + $value = $value->login; + } + elsif (blessed($value) && $value->isa('Bugzilla::Object')) { + $value = $value->name; + } + elsif ($name eq 'estimated_time') { - push(@diffs, { - field_name => $name, - who => $bug->reporter, - new => $value}); + # "0.00" (which is what we get from the DB) is true, + # so we explicitly do a numerical comparison with 0. + $value = 0 if $value == 0; } + elsif ($name eq 'deadline') { + $value = time2str("%Y-%m-%d", str2time($value)) if $value; + } + + # If there isn't anything to show, don't include this header. + next unless $value; + + push(@diffs, {field_name => $name, who => $bug->reporter, new => $value}); + } - return @diffs; + return @diffs; } 1; diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm index 1fe8b3d0c..b7a11b120 100644 --- a/Bugzilla/BugUrl.pm +++ b/Bugzilla/BugUrl.pm @@ -28,49 +28,50 @@ use URI::QueryParam; use constant DB_TABLE => 'bug_see_also'; use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'id'; + # See Also is tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - bug_id - value - class + id + bug_id + value + class ); # This must be strings with the names of the validations, # instead of coderefs, because subclasses override these # validators with their own. use constant VALIDATORS => { - value => '_check_value', - bug_id => '_check_bug_id', - class => \&_check_class, + value => '_check_value', + bug_id => '_check_bug_id', + class => \&_check_class, }; # This is the order we go through all of subclasses and # pick the first one that should handle the url. New # subclasses should be added at the end of the list. use constant SUB_CLASSES => qw( - Bugzilla::BugUrl::Bugzilla::Local - Bugzilla::BugUrl::Bugzilla - Bugzilla::BugUrl::Launchpad - Bugzilla::BugUrl::Flyspray - Bugzilla::BugUrl::Google - Bugzilla::BugUrl::Debian - Bugzilla::BugUrl::JIRA - Bugzilla::BugUrl::Trac - Bugzilla::BugUrl::MantisBT - Bugzilla::BugUrl::SourceForge - Bugzilla::BugUrl::GitHub + Bugzilla::BugUrl::Bugzilla::Local + Bugzilla::BugUrl::Bugzilla + Bugzilla::BugUrl::Launchpad + Bugzilla::BugUrl::Flyspray + Bugzilla::BugUrl::Google + Bugzilla::BugUrl::Debian + Bugzilla::BugUrl::JIRA + Bugzilla::BugUrl::Trac + Bugzilla::BugUrl::MantisBT + Bugzilla::BugUrl::SourceForge + Bugzilla::BugUrl::GitHub ); ############################### #### Accessors ###### ############################### -sub class { return $_[0]->{class} } +sub class { return $_[0]->{class} } sub bug_id { return $_[0]->{bug_id} } ############################### @@ -78,130 +79,127 @@ sub bug_id { return $_[0]->{bug_id} } ############################### sub new { - my $class = shift; - my $param = shift; - - if (ref $param) { - my $bug_id = $param->{bug_id}; - my $name = $param->{name} || $param->{value}; - if (!defined $bug_id) { - ThrowCodeError('bad_arg', - { argument => 'bug_id', - function => "${class}::new" }); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - { argument => 'name', - function => "${class}::new" }); - } - - my $condition = 'bug_id = ? AND value = ?'; - my @values = ($bug_id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + + if (ref $param) { + my $bug_id = $param->{bug_id}; + my $name = $param->{name} || $param->{value}; + if (!defined $bug_id) { + ThrowCodeError('bad_arg', {argument => 'bug_id', function => "${class}::new"}); + } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - unshift @_, $param; - return $class->SUPER::new(@_); + my $condition = 'bug_id = ? AND value = ?'; + my @values = ($bug_id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + return $class->SUPER::new(@_); } sub _do_list_select { - my $class = shift; - my $objects = $class->SUPER::_do_list_select(@_); + my $class = shift; + my $objects = $class->SUPER::_do_list_select(@_); - foreach my $object (@$objects) { - eval "use " . $object->class; - # If the class cannot be loaded, then we build a generic object. - bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class); - } + foreach my $object (@$objects) { + eval "use " . $object->class; + + # If the class cannot be loaded, then we build a generic object. + bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class); + } - return $objects + return $objects; } # This is an abstract method. It must be overridden # in every subclass. sub should_handle { - my ($class, $input) = @_; - ThrowCodeError('unknown_method', - { method => "${class}::should_handle" }); + my ($class, $input) = @_; + ThrowCodeError('unknown_method', {method => "${class}::should_handle"}); } sub class_for { - my ($class, $value) = @_; - - my @sub_classes = $class->SUB_CLASSES; - Bugzilla::Hook::process("bug_url_sub_classes", - { sub_classes => \@sub_classes }); - - my $uri = URI->new($value); - foreach my $subclass (@sub_classes) { - eval "use $subclass"; - die $@ if $@; - return wantarray ? ($subclass, $uri) : $subclass - if $subclass->should_handle($uri); - } + my ($class, $value) = @_; - ThrowUserError('bug_url_invalid', { url => $value }); + my @sub_classes = $class->SUB_CLASSES; + Bugzilla::Hook::process("bug_url_sub_classes", {sub_classes => \@sub_classes}); + + my $uri = URI->new($value); + foreach my $subclass (@sub_classes) { + eval "use $subclass"; + die $@ if $@; + return wantarray ? ($subclass, $uri) : $subclass + if $subclass->should_handle($uri); + } + + ThrowUserError('bug_url_invalid', {url => $value}); } sub _check_class { - my ($class, $subclass) = @_; - eval "use $subclass"; die $@ if $@; - return $subclass; + my ($class, $subclass) = @_; + eval "use $subclass"; + die $@ if $@; + return $subclass; } sub _check_bug_id { - my ($class, $bug_id) = @_; + my ($class, $bug_id) = @_; - my $bug; - if (blessed $bug_id) { - # We got a bug object passed in, use it - $bug = $bug_id; - $bug->check_is_visible; - } - else { - # We got a bug id passed in, check it and get the bug object - $bug = Bugzilla::Bug->check({ id => $bug_id }); - } + my $bug; + if (blessed $bug_id) { + + # We got a bug object passed in, use it + $bug = $bug_id; + $bug->check_is_visible; + } + else { + # We got a bug id passed in, check it and get the bug object + $bug = Bugzilla::Bug->check({id => $bug_id}); + } - return $bug->id; + return $bug->id; } sub _check_value { - my ($class, $uri) = @_; - - my $value = $uri->as_string; - - if (!$value) { - ThrowCodeError('param_required', - { function => 'add_see_also', param => '$value' }); - } - - # We assume that the URL is an HTTP URL if there is no (something):// - # in front. - if (!$uri->scheme) { - # This works better than setting $uri->scheme('http'), because - # that creates URLs like "http:domain.com" and doesn't properly - # differentiate the path from the domain. - $uri = new URI("http://$value"); - } - elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') { - ThrowUserError('bug_url_invalid', { url => $value, reason => 'http' }); - } - - # This stops the following edge cases from being accepted: - # * show_bug.cgi?id=1 - # * /show_bug.cgi?id=1 - # * http:///show_bug.cgi?id=1 - if (!$uri->authority or $uri->path !~ m{/}) { - ThrowUserError('bug_url_invalid', - { url => $value, reason => 'path_only' }); - } - - if (length($uri->path) > MAX_BUG_URL_LENGTH) { - ThrowUserError('bug_url_too_long', { url => $uri->path }); - } - - return $uri; + my ($class, $uri) = @_; + + my $value = $uri->as_string; + + if (!$value) { + ThrowCodeError('param_required', + {function => 'add_see_also', param => '$value'}); + } + + # We assume that the URL is an HTTP URL if there is no (something):// + # in front. + if (!$uri->scheme) { + + # This works better than setting $uri->scheme('http'), because + # that creates URLs like "http:domain.com" and doesn't properly + # differentiate the path from the domain. + $uri = new URI("http://$value"); + } + elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'http'}); + } + + # This stops the following edge cases from being accepted: + # * show_bug.cgi?id=1 + # * /show_bug.cgi?id=1 + # * http:///show_bug.cgi?id=1 + if (!$uri->authority or $uri->path !~ m{/}) { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'path_only'}); + } + + if (length($uri->path) > MAX_BUG_URL_LENGTH) { + ThrowUserError('bug_url_too_long', {url => $uri->path}); + } + + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Bugzilla.pm b/Bugzilla/BugUrl/Bugzilla.pm index 402ff1509..9d036100f 100644 --- a/Bugzilla/BugUrl/Bugzilla.pm +++ b/Bugzilla/BugUrl/Bugzilla.pm @@ -21,37 +21,39 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0; + my ($class, $uri) = @_; + return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); - - my $bug_id = $uri->query_param('id'); - # We don't currently allow aliases, because we can't check to see - # if somebody's putting both an alias link and a numeric ID link. - # When we start validating the URL by accessing the other Bugzilla, - # we can allow aliases. - detaint_natural($bug_id); - if (!$bug_id) { - my $value = $uri->as_string; - ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' }); - } - - # Make sure that "id" is the only query parameter. - $uri->query("id=$bug_id"); - # And remove any # part if there is one. - $uri->fragment(undef); - - return $uri; + my ($class, $uri) = @_; + + $uri = $class->SUPER::_check_value($uri); + + my $bug_id = $uri->query_param('id'); + + # We don't currently allow aliases, because we can't check to see + # if somebody's putting both an alias link and a numeric ID link. + # When we start validating the URL by accessing the other Bugzilla, + # we can allow aliases. + detaint_natural($bug_id); + if (!$bug_id) { + my $value = $uri->as_string; + ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'}); + } + + # Make sure that "id" is the only query parameter. + $uri->query("id=$bug_id"); + + # And remove any # part if there is one. + $uri->fragment(undef); + + return $uri; } sub target_bug_id { - my ($self) = @_; - return new URI($self->name)->query_param('id'); + my ($self) = @_; + return new URI($self->name)->query_param('id'); } 1; diff --git a/Bugzilla/BugUrl/Bugzilla/Local.pm b/Bugzilla/BugUrl/Bugzilla/Local.pm index 7b9cb6a4f..3fe0fcb5d 100644 --- a/Bugzilla/BugUrl/Bugzilla/Local.pm +++ b/Bugzilla/BugUrl/Bugzilla/Local.pm @@ -20,77 +20,75 @@ use Bugzilla::Util; #### Initialization #### ############################### -use constant VALIDATOR_DEPENDENCIES => { - value => ['bug_id'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['bug_id'],}; ############################### #### Methods #### ############################### sub ref_bug_url { - my $self = shift; - - if (!exists $self->{ref_bug_url}) { - my $ref_bug_id = new URI($self->name)->query_param('id'); - my $ref_bug = Bugzilla::Bug->check($ref_bug_id); - my $ref_value = $self->local_uri($self->bug_id); - $self->{ref_bug_url} = - new Bugzilla::BugUrl::Bugzilla::Local({ bug_id => $ref_bug->id, - value => $ref_value }); - } - return $self->{ref_bug_url}; + my $self = shift; + + if (!exists $self->{ref_bug_url}) { + my $ref_bug_id = new URI($self->name)->query_param('id'); + my $ref_bug = Bugzilla::Bug->check($ref_bug_id); + my $ref_value = $self->local_uri($self->bug_id); + $self->{ref_bug_url} = new Bugzilla::BugUrl::Bugzilla::Local( + {bug_id => $ref_bug->id, value => $ref_value}); + } + return $self->{ref_bug_url}; } sub should_handle { - my ($class, $uri) = @_; - - # Check if it is either a bug id number or an alias. - return 1 if $uri->as_string =~ m/^\w+$/; - - # Check if it is a local Bugzilla uri and call - # Bugzilla::BugUrl::Bugzilla to check if it's a valid Bugzilla - # see also url. - my $canonical_local = URI->new($class->local_uri)->canonical; - if ($canonical_local->authority eq $uri->canonical->authority - and $canonical_local->path eq $uri->canonical->path) - { - return $class->SUPER::should_handle($uri); - } - - return 0; + my ($class, $uri) = @_; + + # Check if it is either a bug id number or an alias. + return 1 if $uri->as_string =~ m/^\w+$/; + + # Check if it is a local Bugzilla uri and call + # Bugzilla::BugUrl::Bugzilla to check if it's a valid Bugzilla + # see also url. + my $canonical_local = URI->new($class->local_uri)->canonical; + if ( $canonical_local->authority eq $uri->canonical->authority + and $canonical_local->path eq $uri->canonical->path) + { + return $class->SUPER::should_handle($uri); + } + + return 0; } sub _check_value { - my ($class, $uri, undef, $params) = @_; - - # At this point we are going to treat any word as a - # bug id/alias to the local Bugzilla. - my $value = $uri->as_string; - if ($value =~ m/^\w+$/) { - $uri = new URI($class->local_uri($value)); - } else { - # It's not a word, then we have to check - # if it's a valid Bugzilla url. - $uri = $class->SUPER::_check_value($uri); - } - - my $ref_bug_id = $uri->query_param('id'); - my $ref_bug = Bugzilla::Bug->check($ref_bug_id); - my $self_bug_id = $params->{bug_id}; - $params->{ref_bug} = $ref_bug; - - if ($ref_bug->id == $self_bug_id) { - ThrowUserError('see_also_self_reference'); - } - - return $uri; + my ($class, $uri, undef, $params) = @_; + + # At this point we are going to treat any word as a + # bug id/alias to the local Bugzilla. + my $value = $uri->as_string; + if ($value =~ m/^\w+$/) { + $uri = new URI($class->local_uri($value)); + } + else { + # It's not a word, then we have to check + # if it's a valid Bugzilla url. + $uri = $class->SUPER::_check_value($uri); + } + + my $ref_bug_id = $uri->query_param('id'); + my $ref_bug = Bugzilla::Bug->check($ref_bug_id); + my $self_bug_id = $params->{bug_id}; + $params->{ref_bug} = $ref_bug; + + if ($ref_bug->id == $self_bug_id) { + ThrowUserError('see_also_self_reference'); + } + + return $uri; } sub local_uri { - my ($self, $bug_id) = @_; - $bug_id ||= ''; - return correct_urlbase() . "show_bug.cgi?id=$bug_id"; + my ($self, $bug_id) = @_; + $bug_id ||= ''; + return correct_urlbase() . "show_bug.cgi?id=$bug_id"; } 1; diff --git a/Bugzilla/BugUrl/Debian.pm b/Bugzilla/BugUrl/Debian.pm index b726b0b5a..c4e4f459b 100644 --- a/Bugzilla/BugUrl/Debian.pm +++ b/Bugzilla/BugUrl/Debian.pm @@ -18,31 +18,35 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # Debian BTS URLs can look like various things: - # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234 - # http://bugs.debian.org/1234 - # https://debbugs.gnu.org/cgi/bugreport.cgi?bug=123 - # https://debbugs.gnu.org/123 - return ((lc($uri->authority) eq 'bugs.debian.org' - or lc($uri->authority) eq 'debbugs.gnu.org') - and (($uri->path =~ /bugreport\.cgi$/ - and $uri->query_param('bug') =~ m|^\d+$|) - or $uri->path =~ m|^/\d+$|)) ? 1 : 0; + my ($class, $uri) = @_; + + # Debian BTS URLs can look like various things: + # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234 + # http://bugs.debian.org/1234 + # https://debbugs.gnu.org/cgi/bugreport.cgi?bug=123 + # https://debbugs.gnu.org/123 + return ( + ( + lc($uri->authority) eq 'bugs.debian.org' + or lc($uri->authority) eq 'debbugs.gnu.org' + ) + and + (($uri->path =~ /bugreport\.cgi$/ and $uri->query_param('bug') =~ m|^\d+$|) + or $uri->path =~ m|^/\d+$|) + ) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # This is the shortest standard URL form for Debian BTS URLs, - # and so we reduce all URLs to this. - $uri->path =~ m|^/(\d+)$| || $uri->query_param('bug') =~ m|^(\d+)$|; - $uri = new URI('https://' . $uri->authority . '/' . $1); + # This is the shortest standard URL form for Debian BTS URLs, + # and so we reduce all URLs to this. + $uri->path =~ m|^/(\d+)$| || $uri->query_param('bug') =~ m|^(\d+)$|; + $uri = new URI('https://' . $uri->authority . '/' . $1); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Flyspray.pm b/Bugzilla/BugUrl/Flyspray.pm index 86583fc4b..0edf55141 100644 --- a/Bugzilla/BugUrl/Flyspray.pm +++ b/Bugzilla/BugUrl/Flyspray.pm @@ -14,23 +14,23 @@ use base qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # Flyspray URLs look like the following: - # https://bugs.flyspray.org/task/1237 - # https://bugs.archlinux.org/task/44825 - return ($uri->path_query =~ m|/task/\d+$|) ? 1 : 0; + # Flyspray URLs look like the following: + # https://bugs.flyspray.org/task/1237 + # https://bugs.archlinux.org/task/44825 + return ($uri->path_query =~ m|/task/\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Remove any # part if there is one. - $uri->fragment(undef); + # Remove any # part if there is one. + $uri->fragment(undef); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm index f14f1d6b0..583837a60 100644 --- a/Bugzilla/BugUrl/GitHub.pm +++ b/Bugzilla/BugUrl/GitHub.pm @@ -18,25 +18,25 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # GitHub issue URLs have only one form: - # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111 - # GitHub pull request URLs have only one form: - # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/pull/111 - return (lc($uri->authority) eq 'github.com' - and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0; + my ($class, $uri) = @_; + +# GitHub issue URLs have only one form: +# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111 +# GitHub pull request URLs have only one form: +# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/pull/111 + return (lc($uri->authority) eq 'github.com' + and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme. - $uri->scheme('https'); + # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme. + $uri->scheme('https'); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Google.pm b/Bugzilla/BugUrl/Google.pm index 71a9c46fb..106425302 100644 --- a/Bugzilla/BugUrl/Google.pm +++ b/Bugzilla/BugUrl/Google.pm @@ -18,27 +18,27 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # Google Code URLs only have one form: - # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234 - return (lc($uri->authority) eq 'code.google.com' - and $uri->path =~ m|^/p/[^/]+/issues/detail$| - and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0; + # Google Code URLs only have one form: + # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234 + return (lc($uri->authority) eq 'code.google.com' + and $uri->path =~ m|^/p/[^/]+/issues/detail$| + and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); + my ($class, $uri) = @_; - # While Google Code URLs can be either HTTP or HTTPS, - # always go with the HTTP scheme, as that's the default. - if ($uri->scheme eq 'https') { - $uri->scheme('http'); - } + $uri = $class->SUPER::_check_value($uri); - return $uri; + # While Google Code URLs can be either HTTP or HTTPS, + # always go with the HTTP scheme, as that's the default. + if ($uri->scheme eq 'https') { + $uri->scheme('http'); + } + + return $uri; } 1; diff --git a/Bugzilla/BugUrl/JIRA.pm b/Bugzilla/BugUrl/JIRA.pm index e9d2a2d2a..b42b1decc 100644 --- a/Bugzilla/BugUrl/JIRA.pm +++ b/Bugzilla/BugUrl/JIRA.pm @@ -18,25 +18,26 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # JIRA URLs have only one basic form (but the jira is optional): - # https://issues.apache.org/jira/browse/KEY-1234 - # http://issues.example.com/browse/KEY-1234 - return ($uri->path =~ m|/browse/[A-Z][A-Z]+-\d+$|) ? 1 : 0; + # JIRA URLs have only one basic form (but the jira is optional): + # https://issues.apache.org/jira/browse/KEY-1234 + # http://issues.example.com/browse/KEY-1234 + return ($uri->path =~ m|/browse/[A-Z][A-Z]+-\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Make sure there are no query parameters. - $uri->query(undef); - # And remove any # part if there is one. - $uri->fragment(undef); + # Make sure there are no query parameters. + $uri->query(undef); - return $uri; + # And remove any # part if there is one. + $uri->fragment(undef); + + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Launchpad.pm b/Bugzilla/BugUrl/Launchpad.pm index 0362747a2..5be8088d1 100644 --- a/Bugzilla/BugUrl/Launchpad.pm +++ b/Bugzilla/BugUrl/Launchpad.pm @@ -18,27 +18,28 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # Launchpad bug URLs can look like various things: - # https://bugs.launchpad.net/ubuntu/+bug/1234 - # https://launchpad.net/bugs/1234 - # All variations end with either "/bugs/1234" or "/+bug/1234" - return ($uri->authority =~ /launchpad\.net$/ - and $uri->path =~ m|bugs?/\d+$|) ? 1 : 0; + my ($class, $uri) = @_; + + # Launchpad bug URLs can look like various things: + # https://bugs.launchpad.net/ubuntu/+bug/1234 + # https://launchpad.net/bugs/1234 + # All variations end with either "/bugs/1234" or "/+bug/1234" + return ($uri->authority =~ /launchpad\.net$/ and $uri->path =~ m|bugs?/\d+$|) + ? 1 + : 0; } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - # This is the shortest standard URL form for Launchpad bugs, - # and so we reduce all URLs to this. - $uri->path =~ m|bugs?/(\d+)$|; - $uri = new URI("https://launchpad.net/bugs/$1"); + # This is the shortest standard URL form for Launchpad bugs, + # and so we reduce all URLs to this. + $uri->path =~ m|bugs?/(\d+)$|; + $uri = new URI("https://launchpad.net/bugs/$1"); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/MantisBT.pm b/Bugzilla/BugUrl/MantisBT.pm index 60d3b578e..742ae1a47 100644 --- a/Bugzilla/BugUrl/MantisBT.pm +++ b/Bugzilla/BugUrl/MantisBT.pm @@ -18,22 +18,22 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # MantisBT URLs look like the following ('bugs' directory is optional): - # http://www.mantisbt.org/bugs/view.php?id=1234 - return ($uri->path_query =~ m|view\.php\?id=\d+$|) ? 1 : 0; + # MantisBT URLs look like the following ('bugs' directory is optional): + # http://www.mantisbt.org/bugs/view.php?id=1234 + return ($uri->path_query =~ m|view\.php\?id=\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Remove any # part if there is one. - $uri->fragment(undef); + # Remove any # part if there is one. + $uri->fragment(undef); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/SourceForge.pm b/Bugzilla/BugUrl/SourceForge.pm index acba0df28..ffdde42f4 100644 --- a/Bugzilla/BugUrl/SourceForge.pm +++ b/Bugzilla/BugUrl/SourceForge.pm @@ -18,27 +18,27 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # SourceForge tracker URLs have only one form: - # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111 - return (lc($uri->authority) eq 'sourceforge.net' - and $uri->path =~ m|/tracker/| - and $uri->query_param('func') eq 'detail' - and $uri->query_param('aid') - and $uri->query_param('group_id') - and $uri->query_param('atid')) ? 1 : 0; + my ($class, $uri) = @_; + + # SourceForge tracker URLs have only one form: + # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111 + return (lc($uri->authority) eq 'sourceforge.net' + and $uri->path =~ m|/tracker/| + and $uri->query_param('func') eq 'detail' + and $uri->query_param('aid') + and $uri->query_param('group_id') + and $uri->query_param('atid')) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Remove any # part if there is one. - $uri->fragment(undef); + # Remove any # part if there is one. + $uri->fragment(undef); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Trac.pm b/Bugzilla/BugUrl/Trac.pm index fe74abf33..22418a1df 100644 --- a/Bugzilla/BugUrl/Trac.pm +++ b/Bugzilla/BugUrl/Trac.pm @@ -18,25 +18,26 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # Trac URLs can look like various things: - # http://dev.mutt.org/trac/ticket/1234 - # http://trac.roundcube.net/ticket/1484130 - return ($uri->path =~ m|/ticket/\d+$|) ? 1 : 0; + # Trac URLs can look like various things: + # http://dev.mutt.org/trac/ticket/1234 + # http://trac.roundcube.net/ticket/1484130 + return ($uri->path =~ m|/ticket/\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Make sure there are no query parameters. - $uri->query(undef); - # And remove any # part if there is one. - $uri->fragment(undef); + # Make sure there are no query parameters. + $uri->query(undef); - return $uri; + # And remove any # part if there is one. + $uri->fragment(undef); + + return $uri; } 1; diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm index d043b121a..d1c351959 100644 --- a/Bugzilla/BugUserLastVisit.pm +++ b/Bugzilla/BugUserLastVisit.pm @@ -25,25 +25,27 @@ use constant LIST_ORDER => 'id'; use constant NAME_FIELD => 'id'; # turn off auditing and exclude these objects from memcached -use constant { AUDIT_CREATES => 0, - AUDIT_UPDATES => 0, - AUDIT_REMOVES => 0, - USE_MEMCACHED => 0 }; +use constant { + AUDIT_CREATES => 0, + AUDIT_UPDATES => 0, + AUDIT_REMOVES => 0, + USE_MEMCACHED => 0 +}; ##################################################################### # Provide accessors for our columns ##################################################################### -sub id { return $_[0]->{id} } -sub bug_id { return $_[0]->{bug_id} } -sub user_id { return $_[0]->{user_id} } +sub id { return $_[0]->{id} } +sub bug_id { return $_[0]->{bug_id} } +sub user_id { return $_[0]->{user_id} } sub last_visit_ts { return $_[0]->{last_visit_ts} } sub user { - my $self = shift; + my $self = shift; - $self->{user} //= Bugzilla::User->new({ id => $self->user_id, cache => 1 }); - return $self->{user}; + $self->{user} //= Bugzilla::User->new({id => $self->user_id, cache => 1}); + return $self->{user}; } 1; diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 30c530b8c..527b54996 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -22,280 +22,301 @@ use Bugzilla::Search::Recent; use File::Basename; sub _init_bz_cgi_globals { - my $invocant = shift; - # We need to disable output buffering - see bug 179174 - $| = 1; - - # Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes - # their browser window while a script is running, the web server sends these - # signals, and we don't want to die half way through a write. - $SIG{TERM} = 'IGNORE'; - $SIG{PIPE} = 'IGNORE'; - - # We don't precompile any functions here, that's done specially in - # mod_perl code. - $invocant->_setup_symbols(qw(:no_xhtml :oldstyle_urls :private_tempfiles - :unique_headers)); + my $invocant = shift; + + # We need to disable output buffering - see bug 179174 + $| = 1; + + # Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes + # their browser window while a script is running, the web server sends these + # signals, and we don't want to die half way through a write. + $SIG{TERM} = 'IGNORE'; + $SIG{PIPE} = 'IGNORE'; + + # We don't precompile any functions here, that's done specially in + # mod_perl code. + $invocant->_setup_symbols( + qw(:no_xhtml :oldstyle_urls :private_tempfiles + :unique_headers) + ); } BEGIN { __PACKAGE__->_init_bz_cgi_globals() if i_am_cgi(); } sub new { - my ($invocant, @args) = @_; - my $class = ref($invocant) || $invocant; - - # Under mod_perl, CGI's global variables get reset on each request, - # so we need to set them up again every time. - $class->_init_bz_cgi_globals() if $ENV{MOD_PERL}; - - my $self = $class->SUPER::new(@args); - - # Make sure our outgoing cookie list is empty on each invocation - $self->{Bugzilla_cookie_list} = []; - - # Path-Info is of no use for Bugzilla and interacts badly with IIS. - # Moreover, it causes unexpected behaviors, such as totally breaking - # the rendering of pages. - my $script = basename($0); - if (my $path_info = $self->path_info) { - my @whitelist = ("rest.cgi"); - Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist }); - if (!grep($_ eq $script, @whitelist)) { - # IIS includes the full path to the script in PATH_INFO, - # so we have to extract the real PATH_INFO from it, - # else we will be redirected outside Bugzilla. - my $script_name = $self->script_name; - $path_info =~ s/^\Q$script_name\E//; - if ($script_name && $path_info) { - print $self->redirect($self->url(-path => 0, -query => 1)); - } - } - } - - # Send appropriate charset - $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : ''); - - # Redirect to urlbase/sslbase if we are not viewing an attachment. - if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { - $self->redirect_to_urlbase(); - } - - # Check for errors - # All of the Bugzilla code wants to do this, so do it here instead of - # in each script - - my $err = $self->cgi_error; - - if ($err) { - # Note that this error block is only triggered by CGI.pm for malformed - # multipart requests, and so should never happen unless there is a - # browser bug. - - print $self->header(-status => $err); - - # ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi - # which creates a new Bugzilla::CGI object, which fails again, which - # ends up here, and calls ThrowCodeError, and then recurses forever. - # So don't use it. - # In fact, we can't use templates at all, because we need a CGI object - # to determine the template lang as well as the current url (from the - # template) - # Since this is an internal error which indicates a severe browser bug, - # just die. - die "CGI parsing error: $err"; - } - - return $self; + my ($invocant, @args) = @_; + my $class = ref($invocant) || $invocant; + + # Under mod_perl, CGI's global variables get reset on each request, + # so we need to set them up again every time. + $class->_init_bz_cgi_globals() if $ENV{MOD_PERL}; + + my $self = $class->SUPER::new(@args); + + # Make sure our outgoing cookie list is empty on each invocation + $self->{Bugzilla_cookie_list} = []; + + # Path-Info is of no use for Bugzilla and interacts badly with IIS. + # Moreover, it causes unexpected behaviors, such as totally breaking + # the rendering of pages. + my $script = basename($0); + if (my $path_info = $self->path_info) { + my @whitelist = ("rest.cgi"); + Bugzilla::Hook::process('path_info_whitelist', {whitelist => \@whitelist}); + if (!grep($_ eq $script, @whitelist)) { + + # IIS includes the full path to the script in PATH_INFO, + # so we have to extract the real PATH_INFO from it, + # else we will be redirected outside Bugzilla. + my $script_name = $self->script_name; + $path_info =~ s/^\Q$script_name\E//; + if ($script_name && $path_info) { + print $self->redirect($self->url(-path => 0, -query => 1)); + } + } + } + + # Send appropriate charset + $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : ''); + + # Redirect to urlbase/sslbase if we are not viewing an attachment. + if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { + $self->redirect_to_urlbase(); + } + + # Check for errors + # All of the Bugzilla code wants to do this, so do it here instead of + # in each script + + my $err = $self->cgi_error; + + if ($err) { + + # Note that this error block is only triggered by CGI.pm for malformed + # multipart requests, and so should never happen unless there is a + # browser bug. + + print $self->header(-status => $err); + + # ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi + # which creates a new Bugzilla::CGI object, which fails again, which + # ends up here, and calls ThrowCodeError, and then recurses forever. + # So don't use it. + # In fact, we can't use templates at all, because we need a CGI object + # to determine the template lang as well as the current url (from the + # template) + # Since this is an internal error which indicates a severe browser bug, + # just die. + die "CGI parsing error: $err"; + } + + return $self; } # We want this sorted plus the ability to exclude certain params sub canonicalise_query { - my ($self, @exclude) = @_; + my ($self, @exclude) = @_; - # Reconstruct the URL by concatenating the sorted param=value pairs - my @parameters; - foreach my $key (sort($self->param())) { - # Leave this key out if it's in the exclude list - next if grep { $_ eq $key } @exclude; + # Reconstruct the URL by concatenating the sorted param=value pairs + my @parameters; + foreach my $key (sort($self->param())) { - # Remove the Boolean Charts for standard query.cgi fields - # They are listed in the query URL already - next if $key =~ /^(field|type|value)(-\d+){3}$/; + # Leave this key out if it's in the exclude list + next if grep { $_ eq $key } @exclude; - my $esc_key = url_quote($key); + # Remove the Boolean Charts for standard query.cgi fields + # They are listed in the query URL already + next if $key =~ /^(field|type|value)(-\d+){3}$/; - foreach my $value ($self->param($key)) { - # Omit params with an empty value - if (defined($value) && $value ne '') { - my $esc_value = url_quote($value); + my $esc_key = url_quote($key); - push(@parameters, "$esc_key=$esc_value"); - } - } + foreach my $value ($self->param($key)) { + + # Omit params with an empty value + if (defined($value) && $value ne '') { + my $esc_value = url_quote($value); + + push(@parameters, "$esc_key=$esc_value"); + } } + } - return join("&", @parameters); + return join("&", @parameters); } sub clean_search_url { - my $self = shift; - # Delete any empty URL parameter. - my @cgi_params = $self->param; - - foreach my $param (@cgi_params) { - if (defined $self->param($param) && $self->param($param) eq '') { - $self->delete($param); - $self->delete("${param}_type"); - } - - # Custom Search stuff is empty if it's "noop". We also keep around - # the old Boolean Chart syntax for backwards-compatibility. - if (($param =~ /\d-\d-\d/ || $param =~ /^[[:alpha:]]\d+$/) - && defined $self->param($param) && $self->param($param) eq 'noop') - { - $self->delete($param); - } - - # Any "join" for custom search that's an AND can be removed, because - # that's the default. - if (($param =~ /^j\d+$/ || $param eq 'j_top') - && $self->param($param) eq 'AND') - { - $self->delete($param); - } - } + my $self = shift; - # Delete leftovers from the login form - $self->delete('Bugzilla_remember', 'GoAheadAndLogIn'); + # Delete any empty URL parameter. + my @cgi_params = $self->param; - # Delete the token if we're not performing an action which needs it - unless ((defined $self->param('remtype') - && ($self->param('remtype') eq 'asdefault' - || $self->param('remtype') eq 'asnamed')) - || (defined $self->param('remaction') - && $self->param('remaction') eq 'forget')) - { - $self->delete("token"); - } - - foreach my $num (1,2,3) { - # If there's no value in the email field, delete the related fields. - if (!$self->param("email$num")) { - foreach my $field (qw(type assigned_to reporter qa_contact cc longdesc)) { - $self->delete("email$field$num"); - } - } + foreach my $param (@cgi_params) { + if (defined $self->param($param) && $self->param($param) eq '') { + $self->delete($param); + $self->delete("${param}_type"); } - # chfieldto is set to "Now" by default in query.cgi. But if none - # of the other chfield parameters are set, it's meaningless. - if (!defined $self->param('chfieldfrom') && !$self->param('chfield') - && !defined $self->param('chfieldvalue') && $self->param('chfieldto') - && lc($self->param('chfieldto')) eq 'now') + # Custom Search stuff is empty if it's "noop". We also keep around + # the old Boolean Chart syntax for backwards-compatibility. + if ( ($param =~ /\d-\d-\d/ || $param =~ /^[[:alpha:]]\d+$/) + && defined $self->param($param) + && $self->param($param) eq 'noop') { - $self->delete('chfieldto'); + $self->delete($param); } - # cmdtype "doit" is the default from query.cgi, but it's only meaningful - # if there's a remtype parameter. - if (defined $self->param('cmdtype') && $self->param('cmdtype') eq 'doit' - && !defined $self->param('remtype')) + # Any "join" for custom search that's an AND can be removed, because + # that's the default. + if (($param =~ /^j\d+$/ || $param eq 'j_top') && $self->param($param) eq 'AND') { - $self->delete('cmdtype'); - } + $self->delete($param); + } + } + + # Delete leftovers from the login form + $self->delete('Bugzilla_remember', 'GoAheadAndLogIn'); + + # Delete the token if we're not performing an action which needs it + unless ( + ( + defined $self->param('remtype') + && ( $self->param('remtype') eq 'asdefault' + || $self->param('remtype') eq 'asnamed') + ) + || (defined $self->param('remaction') && $self->param('remaction') eq 'forget') + ) + { + $self->delete("token"); + } + + foreach my $num (1, 2, 3) { + + # If there's no value in the email field, delete the related fields. + if (!$self->param("email$num")) { + foreach my $field (qw(type assigned_to reporter qa_contact cc longdesc)) { + $self->delete("email$field$num"); + } + } + } + + # chfieldto is set to "Now" by default in query.cgi. But if none + # of the other chfield parameters are set, it's meaningless. + if ( !defined $self->param('chfieldfrom') + && !$self->param('chfield') + && !defined $self->param('chfieldvalue') + && $self->param('chfieldto') + && lc($self->param('chfieldto')) eq 'now') + { + $self->delete('chfieldto'); + } + + # cmdtype "doit" is the default from query.cgi, but it's only meaningful + # if there's a remtype parameter. + if ( defined $self->param('cmdtype') + && $self->param('cmdtype') eq 'doit' + && !defined $self->param('remtype')) + { + $self->delete('cmdtype'); + } + + # "Reuse same sort as last time" is actually the default, so we don't + # need it in the URL. + if ( $self->param('order') + && $self->param('order') eq 'Reuse same sort as last time') + { + $self->delete('order'); + } + + # list_id is added in buglist.cgi after calling clean_search_url, + # and doesn't need to be saved in saved searches. + $self->delete('list_id'); + + # no_redirect is used internally by redirect_search_url(). + $self->delete('no_redirect'); + + # And now finally, if query_format is our only parameter, that + # really means we have no parameters, so we should delete query_format. + if ($self->param('query_format') && scalar($self->param()) == 1) { + $self->delete('query_format'); + } +} - # "Reuse same sort as last time" is actually the default, so we don't - # need it in the URL. - if ($self->param('order') - && $self->param('order') eq 'Reuse same sort as last time') - { - $self->delete('order'); - } +sub check_etag { + my ($self, $valid_etag) = @_; - # list_id is added in buglist.cgi after calling clean_search_url, - # and doesn't need to be saved in saved searches. - $self->delete('list_id'); + # ETag support. + my $if_none_match = $self->http('If-None-Match'); + return if !$if_none_match; - # no_redirect is used internally by redirect_search_url(). - $self->delete('no_redirect'); + my @if_none = split(/[\s,]+/, $if_none_match); + foreach my $possible_etag (@if_none) { - # And now finally, if query_format is our only parameter, that - # really means we have no parameters, so we should delete query_format. - if ($self->param('query_format') && scalar($self->param()) == 1) { - $self->delete('query_format'); + # remove quotes from begin and end of the string + $possible_etag =~ s/^\"//g; + $possible_etag =~ s/\"$//g; + if ($possible_etag eq $valid_etag or $possible_etag eq '*') { + return 1; } -} + } -sub check_etag { - my ($self, $valid_etag) = @_; - - # ETag support. - my $if_none_match = $self->http('If-None-Match'); - return if !$if_none_match; - - my @if_none = split(/[\s,]+/, $if_none_match); - foreach my $possible_etag (@if_none) { - # remove quotes from begin and end of the string - $possible_etag =~ s/^\"//g; - $possible_etag =~ s/\"$//g; - if ($possible_etag eq $valid_etag or $possible_etag eq '*') { - return 1; - } - } - - return 0; + return 0; } # Have to add the cookies in. sub multipart_start { - my $self = shift; - - my %args = @_; - - # CGI.pm::multipart_start doesn't honour its own charset information, so - # we do it ourselves here - if (defined $self->charset() && defined $args{-type}) { - # Remove any existing charset specifier - $args{-type} =~ s/;.*$//; - # and add the specified one - $args{-type} .= '; charset=' . $self->charset(); - } - - my $headers = $self->SUPER::multipart_start(%args); - # Eliminate the one extra CRLF at the end. - $headers =~ s/$CGI::CRLF$//; - # Add the cookies. We have to do it this way instead of - # passing them to multpart_start, because CGI.pm's multipart_start - # doesn't understand a '-cookie' argument pointing to an arrayref. - foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) { - $headers .= "Set-Cookie: ${cookie}${CGI::CRLF}"; - } - $headers .= $CGI::CRLF; - $self->{_multipart_in_progress} = 1; - return $headers; + my $self = shift; + + my %args = @_; + + # CGI.pm::multipart_start doesn't honour its own charset information, so + # we do it ourselves here + if (defined $self->charset() && defined $args{-type}) { + + # Remove any existing charset specifier + $args{-type} =~ s/;.*$//; + + # and add the specified one + $args{-type} .= '; charset=' . $self->charset(); + } + + my $headers = $self->SUPER::multipart_start(%args); + + # Eliminate the one extra CRLF at the end. + $headers =~ s/$CGI::CRLF$//; + + # Add the cookies. We have to do it this way instead of + # passing them to multpart_start, because CGI.pm's multipart_start + # doesn't understand a '-cookie' argument pointing to an arrayref. + foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) { + $headers .= "Set-Cookie: ${cookie}${CGI::CRLF}"; + } + $headers .= $CGI::CRLF; + $self->{_multipart_in_progress} = 1; + return $headers; } sub close_standby_message { - my ($self, $contenttype, $disp, $disp_prefix, $extension) = @_; - $self->set_dated_content_disp($disp, $disp_prefix, $extension); - - if ($self->{_multipart_in_progress}) { - print $self->multipart_end(); - print $self->multipart_start(-type => $contenttype); - } - elsif (!$self->{_header_done}) { - print $self->header($contenttype); - } + my ($self, $contenttype, $disp, $disp_prefix, $extension) = @_; + $self->set_dated_content_disp($disp, $disp_prefix, $extension); + + if ($self->{_multipart_in_progress}) { + print $self->multipart_end(); + print $self->multipart_start(-type => $contenttype); + } + elsif (!$self->{_header_done}) { + print $self->header($contenttype); + } } our $ALLOW_UNSAFE_RESPONSE = 0; + # responding to text/plain or text/html is safe # responding to any request with a referer header is safe # some things need to have unsafe responses (attachment.cgi) # everything else should get a 403. sub _prevent_unsafe_response { - my ($self, $headers) = @_; - my $safe_content_type_re = qr{ + my ($self, $headers) = @_; + my $safe_content_type_re = qr{ ^ (*COMMIT) # COMMIT makes the regex faster # by preventing back-tracking. see also perldoc pelre. # application/x-javascript, xml, atom+xml, rdf+xml, xml-dtd, and json @@ -309,12 +330,13 @@ sub _prevent_unsafe_response { # used for HTTP push responses | multipart/x-mixed-replace) }sx; - my $safe_referer_re = do { - # Note that urlbase must end with a /. - # It almost certainly does, but let's be extra careful. - my $urlbase = correct_urlbase(); - $urlbase =~ s{/$}{}; - qr{ + my $safe_referer_re = do { + + # Note that urlbase must end with a /. + # It almost certainly does, but let's be extra careful. + my $urlbase = correct_urlbase(); + $urlbase =~ s{/$}{}; + qr{ # Begins with literal urlbase ^ (*COMMIT) \Q$urlbase\E @@ -322,374 +344,387 @@ sub _prevent_unsafe_response { (?: / | $ ) }sx - }; - - return if $ALLOW_UNSAFE_RESPONSE; - - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # Safe content types are ones that arn't images. - # For now let's assume plain text and html are not valid images. - my $content_type = $headers->{'-type'} // $headers->{'-content_type'} // 'text/html'; - my $is_safe_content_type = $content_type =~ $safe_content_type_re; - - # Safe referers are ones that begin with the urlbase. - my $referer = $self->referer; - my $is_safe_referer = $referer && $referer =~ $safe_referer_re; - - if (!$is_safe_referer && !$is_safe_content_type) { - print $self->SUPER::header(-type => 'text/html', -status => '403 Forbidden'); - if ($content_type ne 'text/html') { - print "Untrusted Referer Header\n"; - if ($ENV{MOD_PERL}) { - my $r = $self->r; - $r->rflush; - $r->status(200); - } - } - exit; + }; + + return if $ALLOW_UNSAFE_RESPONSE; + + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + + # Safe content types are ones that arn't images. + # For now let's assume plain text and html are not valid images. + my $content_type = $headers->{'-type'} // $headers->{'-content_type'} + // 'text/html'; + my $is_safe_content_type = $content_type =~ $safe_content_type_re; + + # Safe referers are ones that begin with the urlbase. + my $referer = $self->referer; + my $is_safe_referer = $referer && $referer =~ $safe_referer_re; + + if (!$is_safe_referer && !$is_safe_content_type) { + print $self->SUPER::header(-type => 'text/html', -status => '403 Forbidden'); + if ($content_type ne 'text/html') { + print "Untrusted Referer Header\n"; + if ($ENV{MOD_PERL}) { + my $r = $self->r; + $r->rflush; + $r->status(200); } + } + exit; } + } } # Override header so we can add the cookies in sub header { - my $self = shift; - - my %headers; - my $user = Bugzilla->user; - - # If there's only one parameter, then it's a Content-Type. - if (scalar(@_) == 1) { - %headers = ('-type' => shift(@_)); - } - else { - %headers = @_; - } - $self->_prevent_unsafe_response(\%headers); - - if ($self->{'_content_disp'}) { - $headers{'-content_disposition'} = $self->{'_content_disp'}; - } - - if (!$user->id && $user->authorizer->can_login - && !$self->cookie('Bugzilla_login_request_cookie')) - { - my %args; - $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect}; - - $self->send_cookie(-name => 'Bugzilla_login_request_cookie', - -value => generate_random_password(), - -httponly => 1, - %args); - } - - # Add the cookies in if we have any - if (scalar(@{$self->{Bugzilla_cookie_list}})) { - $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; - } + my $self = shift; + + my %headers; + my $user = Bugzilla->user; + + # If there's only one parameter, then it's a Content-Type. + if (scalar(@_) == 1) { + %headers = ('-type' => shift(@_)); + } + else { + %headers = @_; + } + $self->_prevent_unsafe_response(\%headers); + + if ($self->{'_content_disp'}) { + $headers{'-content_disposition'} = $self->{'_content_disp'}; + } + + if (!$user->id + && $user->authorizer->can_login + && !$self->cookie('Bugzilla_login_request_cookie')) + { + my %args; + $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect}; + + $self->send_cookie( + -name => 'Bugzilla_login_request_cookie', + -value => generate_random_password(), + -httponly => 1, + %args + ); + } + + # Add the cookies in if we have any + if (scalar(@{$self->{Bugzilla_cookie_list}})) { + $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; + } + + # Add Strict-Transport-Security (STS) header if this response + # is over SSL and the strict_transport_security param is turned on. + if ( $self->https + && !$self->url_is_attachment_base + && Bugzilla->params->{'strict_transport_security'} ne 'off') + { + my $sts_opts = 'max-age=' . MAX_STS_AGE; + if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') { + $sts_opts .= '; includeSubDomains'; + } + + $headers{'-strict_transport_security'} = $sts_opts; + } + + # Add X-Frame-Options & CSP headers to prevent framing and subsequent + # possible clickjacking problems. + unless ($self->url_is_attachment_base) { + $headers{'-x_frame_options'} = 'SAMEORIGIN'; + $headers{'-content_security_policy'} = "frame-ancestors 'self'"; + } + + # Add X-XSS-Protection header to prevent simple XSS attacks + # and enforce the blocking (rather than the rewriting) mode. + $headers{'-x_xss_protection'} = '1; mode=block'; + + # Add X-Content-Type-Options header to prevent browsers sniffing + # the MIME type away from the declared Content-Type. + $headers{'-x_content_type_options'} = 'nosniff'; + + Bugzilla::Hook::process('cgi_headers', {cgi => $self, headers => \%headers}); + $self->{_header_done} = 1; + + return $self->SUPER::header(%headers) || ""; +} - # Add Strict-Transport-Security (STS) header if this response - # is over SSL and the strict_transport_security param is turned on. - if ($self->https && !$self->url_is_attachment_base - && Bugzilla->params->{'strict_transport_security'} ne 'off') +sub param { + my $self = shift; + local $CGI::LIST_CONTEXT_WARN = 0; + + # When we are just requesting the value of a parameter... + if (scalar(@_) == 1) { + my @result = $self->SUPER::param(@_); + + # Also look at the URL parameters, after we look at the POST + # parameters. This is to allow things like login-form submissions + # with URL parameters in the form's "target" attribute. + if ( !scalar(@result) + && $self->request_method + && $self->request_method eq 'POST') { - my $sts_opts = 'max-age=' . MAX_STS_AGE; - if (Bugzilla->params->{'strict_transport_security'} - eq 'include_subdomains') - { - $sts_opts .= '; includeSubDomains'; - } - - $headers{'-strict_transport_security'} = $sts_opts; + @result = $self->url_param(@_); } - # Add X-Frame-Options & CSP headers to prevent framing and subsequent - # possible clickjacking problems. - unless ($self->url_is_attachment_base) { - $headers{'-x_frame_options'} = 'SAMEORIGIN'; - $headers{'-content_security_policy'} = "frame-ancestors 'self'"; + # Fix UTF-8-ness of input parameters. + if (Bugzilla->params->{'utf8'}) { + @result = map { _fix_utf8($_) } @result; } - # Add X-XSS-Protection header to prevent simple XSS attacks - # and enforce the blocking (rather than the rewriting) mode. - $headers{'-x_xss_protection'} = '1; mode=block'; - - # Add X-Content-Type-Options header to prevent browsers sniffing - # the MIME type away from the declared Content-Type. - $headers{'-x_content_type_options'} = 'nosniff'; - - Bugzilla::Hook::process('cgi_headers', - { cgi => $self, headers => \%headers } - ); - $self->{_header_done} = 1; - - return $self->SUPER::header(%headers) || ""; -} + return wantarray ? @result : $result[0]; + } -sub param { - my $self = shift; - local $CGI::LIST_CONTEXT_WARN = 0; - - # When we are just requesting the value of a parameter... - if (scalar(@_) == 1) { - my @result = $self->SUPER::param(@_); - - # Also look at the URL parameters, after we look at the POST - # parameters. This is to allow things like login-form submissions - # with URL parameters in the form's "target" attribute. - if (!scalar(@result) - && $self->request_method && $self->request_method eq 'POST') - { - @result = $self->url_param(@_); - } + # 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. + elsif (!scalar(@_) && $self->request_method && $self->request_method eq 'POST') + { + my @post_params = $self->SUPER::param; + my @url_params = $self->url_param; + my %params = map { $_ => 1 } (@post_params, @url_params); + return keys %params; + } - # Fix UTF-8-ness of input parameters. - if (Bugzilla->params->{'utf8'}) { - @result = map { _fix_utf8($_) } @result; - } - - 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. - elsif (!scalar(@_) && $self->request_method - && $self->request_method eq 'POST') - { - my @post_params = $self->SUPER::param; - my @url_params = $self->url_param; - my %params = map { $_ => 1 } (@post_params, @url_params); - return keys %params; - } - - return $self->SUPER::param(@_); + return $self->SUPER::param(@_); } sub url_param { - my $self = shift; - # Some servers fail to set the QUERY_STRING parameter, which - # causes undef issues - $ENV{'QUERY_STRING'} //= ''; - return $self->SUPER::url_param(@_); + my $self = shift; + + # Some servers fail to set the QUERY_STRING parameter, which + # causes undef issues + $ENV{'QUERY_STRING'} //= ''; + return $self->SUPER::url_param(@_); } sub _fix_utf8 { - my $input = shift; - # The is_utf8 is here in case CGI gets smart about utf8 someday. - utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input); - return $input; + my $input = shift; + + # The is_utf8 is here in case CGI gets smart about utf8 someday. + utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input); + return $input; } sub should_set { - my ($self, $param) = @_; - my $set = (defined $self->param($param) - or defined $self->param("defined_$param")) - ? 1 : 0; - return $set; + my ($self, $param) = @_; + my $set + = (defined $self->param($param) or defined $self->param("defined_$param")) + ? 1 + : 0; + return $set; } # The various parts of Bugzilla which create cookies don't want to have to # pass them around to all of the callers. Instead, store them locally here, # and then output as required from |header|. sub send_cookie { - my $self = shift; - - # Move the param list into a hash for easier handling. - my %paramhash; - my @paramlist; - my ($key, $value); - while ($key = shift) { - $value = shift; - $paramhash{$key} = $value; - } - - # Complain if -value is not given or empty (bug 268146). - if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { - ThrowCodeError('cookies_need_value'); - } - - # Add the default path and the domain in. - $paramhash{'-path'} = Bugzilla->params->{'cookiepath'}; - $paramhash{'-domain'} = Bugzilla->params->{'cookiedomain'} - if Bugzilla->params->{'cookiedomain'}; - - # Move the param list back into an array for the call to cookie(). - foreach (keys(%paramhash)) { - unshift(@paramlist, $_ => $paramhash{$_}); - } - - push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(@paramlist)); + my $self = shift; + + # Move the param list into a hash for easier handling. + my %paramhash; + my @paramlist; + my ($key, $value); + while ($key = shift) { + $value = shift; + $paramhash{$key} = $value; + } + + # Complain if -value is not given or empty (bug 268146). + if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { + ThrowCodeError('cookies_need_value'); + } + + # Add the default path and the domain in. + $paramhash{'-path'} = Bugzilla->params->{'cookiepath'}; + $paramhash{'-domain'} = Bugzilla->params->{'cookiedomain'} + if Bugzilla->params->{'cookiedomain'}; + + # Move the param list back into an array for the call to cookie(). + foreach (keys(%paramhash)) { + unshift(@paramlist, $_ => $paramhash{$_}); + } + + push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(@paramlist)); } # Cookies are removed by setting an expiry date in the past. # This method is a send_cookie wrapper doing exactly this. sub remove_cookie { - my $self = shift; - my ($cookiename) = (@_); - - # Expire the cookie, giving a non-empty dummy value (bug 268146). - $self->send_cookie('-name' => $cookiename, - '-expires' => 'Tue, 15-Sep-1998 21:49:00 GMT', - '-value' => 'X'); + my $self = shift; + my ($cookiename) = (@_); + + # Expire the cookie, giving a non-empty dummy value (bug 268146). + $self->send_cookie( + '-name' => $cookiename, + '-expires' => 'Tue, 15-Sep-1998 21:49:00 GMT', + '-value' => 'X' + ); } # This helps implement Bugzilla::Search::Recent, and also shortens search # URLs that get POSTed to buglist.cgi. sub redirect_search_url { - my $self = shift; - - # If there is no parameter, there is nothing to do. - return unless $self->param; - - # If we're retreiving an old list, we never need to redirect or - # do anything related to Bugzilla::Search::Recent. - return if $self->param('regetlastlist'); - - my $user = Bugzilla->user; - - if ($user->id) { - # There are two conditions that could happen here--we could get a URL - # with no list id, and we could get a URL with a list_id that isn't - # ours. - my $list_id = $self->param('list_id'); - if ($list_id) { - # If we have a valid list_id, no need to redirect or clean. - return if Bugzilla::Search::Recent->check_quietly( - { id => $list_id }); - } - } - elsif ($self->request_method ne 'POST') { - # Logged-out users who do a GET don't get a list_id, don't get - # their URLs cleaned, and don't get redirected. - return; - } + my $self = shift; - my $no_redirect = $self->param('no_redirect'); - $self->clean_search_url(); - - # Make sure we still have params still after cleaning otherwise we - # do not want to store a list_id for an empty search. - if ($user->id && $self->param) { - # Insert a placeholder Bugzilla::Search::Recent, so that we know what - # the id of the resulting search will be. This is then pulled out - # of the Referer header when viewing show_bug.cgi to know what - # bug list we came from. - my $recent_search = Bugzilla::Search::Recent->create_placeholder; - $self->param('list_id', $recent_search->id); - } + # If there is no parameter, there is nothing to do. + return unless $self->param; + + # If we're retreiving an old list, we never need to redirect or + # do anything related to Bugzilla::Search::Recent. + return if $self->param('regetlastlist'); + + my $user = Bugzilla->user; + + if ($user->id) { - # Browsers which support history.replaceState do not need to be - # redirected. We can fix the URL on the fly. - return if $no_redirect; + # There are two conditions that could happen here--we could get a URL + # with no list id, and we could get a URL with a list_id that isn't + # ours. + my $list_id = $self->param('list_id'); + if ($list_id) { - # GET requests that lacked a list_id are always redirected. POST requests - # are only redirected if they're under the CGI_URI_LIMIT though. - my $self_url = $self->self_url(); - if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) { - print $self->redirect(-url => $self_url); - exit; + # If we have a valid list_id, no need to redirect or clean. + return if Bugzilla::Search::Recent->check_quietly({id => $list_id}); } + } + elsif ($self->request_method ne 'POST') { + + # Logged-out users who do a GET don't get a list_id, don't get + # their URLs cleaned, and don't get redirected. + return; + } + + my $no_redirect = $self->param('no_redirect'); + $self->clean_search_url(); + + # Make sure we still have params still after cleaning otherwise we + # do not want to store a list_id for an empty search. + if ($user->id && $self->param) { + + # Insert a placeholder Bugzilla::Search::Recent, so that we know what + # the id of the resulting search will be. This is then pulled out + # of the Referer header when viewing show_bug.cgi to know what + # bug list we came from. + my $recent_search = Bugzilla::Search::Recent->create_placeholder; + $self->param('list_id', $recent_search->id); + } + + # Browsers which support history.replaceState do not need to be + # redirected. We can fix the URL on the fly. + return if $no_redirect; + + # GET requests that lacked a list_id are always redirected. POST requests + # are only redirected if they're under the CGI_URI_LIMIT though. + my $self_url = $self->self_url(); + if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) { + print $self->redirect(-url => $self_url); + exit; + } } 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; + 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. sub redirect_to_urlbase { - my $self = shift; - my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); - print $self->redirect('-location' => correct_urlbase() . $path); - exit; + my $self = shift; + my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); + print $self->redirect('-location' => correct_urlbase() . $path); + 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->url =~ $regex) ? 1 : 0; + 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->url =~ $regex) ? 1 : 0; } sub set_dated_content_disp { - my ($self, $type, $prefix, $ext) = @_; + my ($self, $type, $prefix, $ext) = @_; - my @time = localtime(time()); - my $date = sprintf "%04d-%02d-%02d", 1900+$time[5], $time[4]+1, $time[3]; - my $filename = "$prefix-$date.$ext"; + my @time = localtime(time()); + my $date = sprintf "%04d-%02d-%02d", 1900 + $time[5], $time[4] + 1, $time[3]; + my $filename = "$prefix-$date.$ext"; - $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering - $filename =~ s/\\/_/g; # Remove backslashes as well - $filename =~ s/"/\\"/g; # escape quotes + $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering + $filename =~ s/\\/_/g; # Remove backslashes as well + $filename =~ s/"/\\"/g; # escape quotes - my $disposition = "$type; filename=\"$filename\""; + my $disposition = "$type; filename=\"$filename\""; - $self->{'_content_disp'} = $disposition; + $self->{'_content_disp'} = $disposition; } ########################## # Vars TIEHASH Interface # ########################## -# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept +# 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(@_); + 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; + 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 +# 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; + my ($self, $param) = @_; + my $value = $self->FETCH($param); + $self->delete($param); + return $value; } 1; diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm index 3c69006aa..3aee1aafb 100644 --- a/Bugzilla/Chart.pm +++ b/Bugzilla/Chart.pm @@ -26,405 +26,424 @@ use Date::Parse; use List::Util qw(max); sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - - # Create a ref to an empty hash and bless it - my $self = {}; - bless($self, $class); - - if ($#_ == 0) { - # Construct from a CGI object. - $self->init($_[0]); - } - else { - die("CGI object not passed in - invalid number of args \($#_\)($_)"); - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + # Create a ref to an empty hash and bless it + my $self = {}; + bless($self, $class); + + if ($#_ == 0) { - return $self; + # Construct from a CGI object. + $self->init($_[0]); + } + else { + die("CGI object not passed in - invalid number of args \($#_\)($_)"); + } + + return $self; } sub init { - my $self = shift; - my $cgi = shift; - - # The data structure is a list of lists (lines) of Series objects. - # There is a separate list for the labels. - # - # The URL encoding is: - # line0=67&line0=73&line1=81&line2=67... - # &label0=B+/+R+/+CONFIRMED&label1=... - # &select0=1&select3=1... - # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html... - # >=1&labelgt=Grand+Total - foreach my $param ($cgi->param()) { - # Store all the lines - if ($param =~ /^line(\d+)$/) { - foreach my $series_id ($cgi->param($param)) { - detaint_natural($series_id) - || ThrowCodeError("invalid_series_id"); - my $series = new Bugzilla::Series($series_id); - push(@{$self->{'lines'}[$1]}, $series) if $series; - } - } - - # Store all the labels - if ($param =~ /^label(\d+)$/) { - $self->{'labels'}[$1] = $cgi->param($param); - } - } - - # Store the miscellaneous metadata - $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0; - $self->{'gt'} = $cgi->param('gt') ? 1 : 0; - $self->{'labelgt'} = $cgi->param('labelgt'); - $self->{'datefrom'} = $cgi->param('datefrom'); - $self->{'dateto'} = $cgi->param('dateto'); - - # If we are cumulating, a grand total makes no sense - $self->{'gt'} = 0 if $self->{'cumulate'}; - - # Make sure the dates are ones we are able to interpret - foreach my $date ('datefrom', 'dateto') { - if ($self->{$date}) { - $self->{$date} = str2time($self->{$date}) - || ThrowUserError("illegal_date", { date => $self->{$date}}); - } + my $self = shift; + my $cgi = shift; + + # The data structure is a list of lists (lines) of Series objects. + # There is a separate list for the labels. + # + # The URL encoding is: + # line0=67&line0=73&line1=81&line2=67... + # &label0=B+/+R+/+CONFIRMED&label1=... + # &select0=1&select3=1... + # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html... + # >=1&labelgt=Grand+Total + foreach my $param ($cgi->param()) { + + # Store all the lines + if ($param =~ /^line(\d+)$/) { + foreach my $series_id ($cgi->param($param)) { + detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); + my $series = new Bugzilla::Series($series_id); + push(@{$self->{'lines'}[$1]}, $series) if $series; + } } - # datefrom can't be after dateto - if ($self->{'datefrom'} && $self->{'dateto'} && - $self->{'datefrom'} > $self->{'dateto'}) - { - ThrowUserError('misarranged_dates', { 'datefrom' => scalar $cgi->param('datefrom'), - 'dateto' => scalar $cgi->param('dateto') }); + # Store all the labels + if ($param =~ /^label(\d+)$/) { + $self->{'labels'}[$1] = $cgi->param($param); } + } + + # Store the miscellaneous metadata + $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0; + $self->{'gt'} = $cgi->param('gt') ? 1 : 0; + $self->{'labelgt'} = $cgi->param('labelgt'); + $self->{'datefrom'} = $cgi->param('datefrom'); + $self->{'dateto'} = $cgi->param('dateto'); + + # If we are cumulating, a grand total makes no sense + $self->{'gt'} = 0 if $self->{'cumulate'}; + + # Make sure the dates are ones we are able to interpret + foreach my $date ('datefrom', 'dateto') { + if ($self->{$date}) { + $self->{$date} = str2time($self->{$date}) + || ThrowUserError("illegal_date", {date => $self->{$date}}); + } + } + + # datefrom can't be after dateto + if ( $self->{'datefrom'} + && $self->{'dateto'} + && $self->{'datefrom'} > $self->{'dateto'}) + { + ThrowUserError( + 'misarranged_dates', + { + 'datefrom' => scalar $cgi->param('datefrom'), + 'dateto' => scalar $cgi->param('dateto') + } + ); + } } # Alter Chart so that the selected series are added to it. sub add { - my $self = shift; - my @series_ids = @_; - - # Get the current size of the series; required for adding Grand Total later - my $current_size = scalar($self->getSeriesIDs()); - - # Count the number of added series - my $added = 0; - # Create new Series and push them on to the list of lines. - # Note that new lines have no label; the display template is responsible - # for inventing something sensible. - foreach my $series_id (@series_ids) { - my $series = new Bugzilla::Series($series_id); - if ($series) { - push(@{$self->{'lines'}}, [$series]); - push(@{$self->{'labels'}}, ""); - $added++; - } + my $self = shift; + my @series_ids = @_; + + # Get the current size of the series; required for adding Grand Total later + my $current_size = scalar($self->getSeriesIDs()); + + # Count the number of added series + my $added = 0; + + # Create new Series and push them on to the list of lines. + # Note that new lines have no label; the display template is responsible + # for inventing something sensible. + foreach my $series_id (@series_ids) { + my $series = new Bugzilla::Series($series_id); + if ($series) { + push(@{$self->{'lines'}}, [$series]); + push(@{$self->{'labels'}}, ""); + $added++; } - - # If we are going from < 2 to >= 2 series, add the Grand Total line. - if (!$self->{'gt'}) { - if ($current_size < 2 && - $current_size + $added >= 2) - { - $self->{'gt'} = 1; - } + } + + # If we are going from < 2 to >= 2 series, add the Grand Total line. + if (!$self->{'gt'}) { + if ($current_size < 2 && $current_size + $added >= 2) { + $self->{'gt'} = 1; } + } } # Alter Chart so that the selections are removed from it. sub remove { - my $self = shift; - my @line_ids = @_; - - foreach my $line_id (@line_ids) { - if ($line_id == 65536) { - # Magic value - delete Grand Total. - $self->{'gt'} = 0; - } - else { - delete($self->{'lines'}->[$line_id]); - delete($self->{'labels'}->[$line_id]); - } + my $self = shift; + my @line_ids = @_; + + foreach my $line_id (@line_ids) { + if ($line_id == 65536) { + + # Magic value - delete Grand Total. + $self->{'gt'} = 0; + } + else { + delete($self->{'lines'}->[$line_id]); + delete($self->{'labels'}->[$line_id]); } + } } # Alter Chart so that the selections are summed. sub sum { - my $self = shift; - my @line_ids = @_; - - # We can't add the Grand Total to things. - @line_ids = grep(!/^65536$/, @line_ids); - - # We can't add less than two things. - return if scalar(@line_ids) < 2; - - my @series; - my $label = ""; - my $biggestlength = 0; - - # We rescue the Series objects of all the series involved in the sum. - foreach my $line_id (@line_ids) { - my @line = @{$self->{'lines'}->[$line_id]}; - - foreach my $series (@line) { - push(@series, $series); - } - - # We keep the label that labels the line with the most series. - if (scalar(@line) > $biggestlength) { - $biggestlength = scalar(@line); - $label = $self->{'labels'}->[$line_id]; - } + my $self = shift; + my @line_ids = @_; + + # We can't add the Grand Total to things. + @line_ids = grep(!/^65536$/, @line_ids); + + # We can't add less than two things. + return if scalar(@line_ids) < 2; + + my @series; + my $label = ""; + my $biggestlength = 0; + + # We rescue the Series objects of all the series involved in the sum. + foreach my $line_id (@line_ids) { + my @line = @{$self->{'lines'}->[$line_id]}; + + foreach my $series (@line) { + push(@series, $series); + } + + # We keep the label that labels the line with the most series. + if (scalar(@line) > $biggestlength) { + $biggestlength = scalar(@line); + $label = $self->{'labels'}->[$line_id]; } + } - $self->remove(@line_ids); + $self->remove(@line_ids); - push(@{$self->{'lines'}}, \@series); - push(@{$self->{'labels'}}, $label); + push(@{$self->{'lines'}}, \@series); + push(@{$self->{'labels'}}, $label); } sub data { - my $self = shift; - $self->{'_data'} ||= $self->readData(); - return $self->{'_data'}; + my $self = shift; + $self->{'_data'} ||= $self->readData(); + return $self->{'_data'}; } # Convert the Chart's data into a plottable form in $self->{'_data'}. sub readData { - my $self = shift; - my @data; - my @maxvals; - - # Note: you get a bad image if getSeriesIDs returns nothing - # We need to handle errors better. - my $series_ids = join(",", $self->getSeriesIDs()); - - return [] unless $series_ids; - - # Work out the date boundaries for our data. - my $dbh = Bugzilla->dbh; - - # The date used is the one given if it's in a sensible range; otherwise, - # it's the earliest or latest date in the database as appropriate. - my $datefrom = $dbh->selectrow_array("SELECT MIN(series_date) " . - "FROM series_data " . - "WHERE series_id IN ($series_ids)"); - $datefrom = str2time($datefrom); - - if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) { - $datefrom = $self->{'datefrom'}; - } - - my $dateto = $dbh->selectrow_array("SELECT MAX(series_date) " . - "FROM series_data " . - "WHERE series_id IN ($series_ids)"); - $dateto = str2time($dateto); - - if ($self->{'dateto'} && $self->{'dateto'} < $dateto) { - $dateto = $self->{'dateto'}; - } - - # Convert UNIX times back to a date format usable for SQL queries. - my $sql_from = time2str('%Y-%m-%d', $datefrom); - my $sql_to = time2str('%Y-%m-%d', $dateto); - - # Prepare the query which retrieves the data for each series - my $query = "SELECT " . $dbh->sql_to_days('series_date') . " - " . - $dbh->sql_to_days('?') . ", series_value " . - "FROM series_data " . - "WHERE series_id = ? " . - "AND series_date >= ?"; - if ($dateto) { - $query .= " AND series_date <= ?"; - } - - my $sth = $dbh->prepare($query); - - my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef; - my $line_index = 0; - - $maxvals[$gt_index] = 0 if $gt_index; - - my @datediff_total; - - foreach my $line (@{$self->{'lines'}}) { - # Even if we end up with no data, we need an empty arrayref to prevent - # errors in the PNG-generating code - $data[$line_index] = []; - $maxvals[$line_index] = 0; - - foreach my $series (@$line) { - - # Get the data for this series and add it on - if ($dateto) { - $sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to); - } - else { - $sth->execute($sql_from, $series->{'series_id'}, $sql_from); - } - my $points = $sth->fetchall_arrayref(); - - foreach my $point (@$points) { - my ($datediff, $value) = @$point; - $data[$line_index][$datediff] ||= 0; - $data[$line_index][$datediff] += $value; - if ($data[$line_index][$datediff] > $maxvals[$line_index]) { - $maxvals[$line_index] = $data[$line_index][$datediff]; - } - - $datediff_total[$datediff] += $value; - - # Add to the grand total, if we are doing that - if ($gt_index) { - $data[$gt_index][$datediff] ||= 0; - $data[$gt_index][$datediff] += $value; - if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) { - $maxvals[$gt_index] = $data[$gt_index][$datediff]; - } - } - } + my $self = shift; + my @data; + my @maxvals; + + # Note: you get a bad image if getSeriesIDs returns nothing + # We need to handle errors better. + my $series_ids = join(",", $self->getSeriesIDs()); + + return [] unless $series_ids; + + # Work out the date boundaries for our data. + my $dbh = Bugzilla->dbh; + + # The date used is the one given if it's in a sensible range; otherwise, + # it's the earliest or latest date in the database as appropriate. + my $datefrom + = $dbh->selectrow_array("SELECT MIN(series_date) " + . "FROM series_data " + . "WHERE series_id IN ($series_ids)"); + $datefrom = str2time($datefrom); + + if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) { + $datefrom = $self->{'datefrom'}; + } + + my $dateto + = $dbh->selectrow_array("SELECT MAX(series_date) " + . "FROM series_data " + . "WHERE series_id IN ($series_ids)"); + $dateto = str2time($dateto); + + if ($self->{'dateto'} && $self->{'dateto'} < $dateto) { + $dateto = $self->{'dateto'}; + } + + # Convert UNIX times back to a date format usable for SQL queries. + my $sql_from = time2str('%Y-%m-%d', $datefrom); + my $sql_to = time2str('%Y-%m-%d', $dateto); + + # Prepare the query which retrieves the data for each series + my $query + = "SELECT " + . $dbh->sql_to_days('series_date') . " - " + . $dbh->sql_to_days('?') + . ", series_value " + . "FROM series_data " + . "WHERE series_id = ? " + . "AND series_date >= ?"; + if ($dateto) { + $query .= " AND series_date <= ?"; + } + + my $sth = $dbh->prepare($query); + + my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef; + my $line_index = 0; + + $maxvals[$gt_index] = 0 if $gt_index; + + my @datediff_total; + + foreach my $line (@{$self->{'lines'}}) { + + # Even if we end up with no data, we need an empty arrayref to prevent + # errors in the PNG-generating code + $data[$line_index] = []; + $maxvals[$line_index] = 0; + + foreach my $series (@$line) { + + # Get the data for this series and add it on + if ($dateto) { + $sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to); + } + else { + $sth->execute($sql_from, $series->{'series_id'}, $sql_from); + } + my $points = $sth->fetchall_arrayref(); + + foreach my $point (@$points) { + my ($datediff, $value) = @$point; + $data[$line_index][$datediff] ||= 0; + $data[$line_index][$datediff] += $value; + if ($data[$line_index][$datediff] > $maxvals[$line_index]) { + $maxvals[$line_index] = $data[$line_index][$datediff]; } - # We are done with the series making up this line, go to the next one - $line_index++; - } + $datediff_total[$datediff] += $value; - # calculate maximum y value - if ($self->{'cumulate'}) { - # Make sure we do not try to take the max of an array with undef values - my @processed_datediff; - while (@datediff_total) { - my $datediff = shift @datediff_total; - push @processed_datediff, $datediff if defined($datediff); + # Add to the grand total, if we are doing that + if ($gt_index) { + $data[$gt_index][$datediff] ||= 0; + $data[$gt_index][$datediff] += $value; + if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) { + $maxvals[$gt_index] = $data[$gt_index][$datediff]; + } } - $self->{'y_max_value'} = max(@processed_datediff); - } - else { - $self->{'y_max_value'} = max(@maxvals); - } - $self->{'y_max_value'} |= 1; # For log() - - # Align the max y value: - # For one- or two-digit numbers, increase y_max_value until divisible by 8 - # For larger numbers, see the comments below to figure out what's going on - if ($self->{'y_max_value'} < 100) { - do { - ++$self->{'y_max_value'}; - } while ($self->{'y_max_value'} % 8 != 0); - } - else { - # First, get the # of digits in the y_max_value - my $num_digits = 1+int(log($self->{'y_max_value'})/log(10)); - - # We want to zero out all but the top 2 digits - my $mask_length = $num_digits - 2; - $self->{'y_max_value'} /= 10**$mask_length; - $self->{'y_max_value'} = int($self->{'y_max_value'}); - $self->{'y_max_value'} *= 10**$mask_length; - - # Add 10^$mask_length to the max value - # Continue to increase until it's divisible by 8 * 10^($mask_length-1) - # (Throwing in the -1 keeps at least the smallest digit at zero) - do { - $self->{'y_max_value'} += 10**$mask_length; - } while ($self->{'y_max_value'} % (8*(10**($mask_length-1))) != 0); + } } - - # Add the x-axis labels into the data structure - my $date_progression = generateDateProgression($datefrom, $dateto); - unshift(@data, $date_progression); + # We are done with the series making up this line, go to the next one + $line_index++; + } - if ($self->{'gt'}) { - # Add Grand Total to label list - push(@{$self->{'labels'}}, $self->{'labelgt'}); + # calculate maximum y value + if ($self->{'cumulate'}) { - $data[$gt_index] ||= []; + # Make sure we do not try to take the max of an array with undef values + my @processed_datediff; + while (@datediff_total) { + my $datediff = shift @datediff_total; + push @processed_datediff, $datediff if defined($datediff); } - - return \@data; + $self->{'y_max_value'} = max(@processed_datediff); + } + else { + $self->{'y_max_value'} = max(@maxvals); + } + $self->{'y_max_value'} |= 1; # For log() + + # Align the max y value: + # For one- or two-digit numbers, increase y_max_value until divisible by 8 + # For larger numbers, see the comments below to figure out what's going on + if ($self->{'y_max_value'} < 100) { + do { + ++$self->{'y_max_value'}; + } while ($self->{'y_max_value'} % 8 != 0); + } + else { + # First, get the # of digits in the y_max_value + my $num_digits = 1 + int(log($self->{'y_max_value'}) / log(10)); + + # We want to zero out all but the top 2 digits + my $mask_length = $num_digits - 2; + $self->{'y_max_value'} /= 10**$mask_length; + $self->{'y_max_value'} = int($self->{'y_max_value'}); + $self->{'y_max_value'} *= 10**$mask_length; + + # Add 10^$mask_length to the max value + # Continue to increase until it's divisible by 8 * 10^($mask_length-1) + # (Throwing in the -1 keeps at least the smallest digit at zero) + do { + $self->{'y_max_value'} += 10**$mask_length; + } while ($self->{'y_max_value'} % (8 * (10**($mask_length - 1))) != 0); + } + + + # Add the x-axis labels into the data structure + my $date_progression = generateDateProgression($datefrom, $dateto); + unshift(@data, $date_progression); + + if ($self->{'gt'}) { + + # Add Grand Total to label list + push(@{$self->{'labels'}}, $self->{'labelgt'}); + + $data[$gt_index] ||= []; + } + + return \@data; } # Flatten the data structure into a list of series_ids sub getSeriesIDs { - my $self = shift; - my @series_ids; + my $self = shift; + my @series_ids; - foreach my $line (@{$self->{'lines'}}) { - foreach my $series (@$line) { - push(@series_ids, $series->{'series_id'}); - } + foreach my $line (@{$self->{'lines'}}) { + foreach my $series (@$line) { + push(@series_ids, $series->{'series_id'}); } + } - return @series_ids; + return @series_ids; } # Class method to get the data necessary to populate the "select series" # widgets on various pages. sub getVisibleSeries { - my %cats; - - my $grouplist = Bugzilla->user->groups_as_string; - - # Get all visible series - my $dbh = Bugzilla->dbh; - my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " . - "series.name, series.series_id " . - "FROM series " . - "INNER JOIN series_categories AS cc1 " . - " ON series.category = cc1.id " . - "INNER JOIN series_categories AS cc2 " . - " ON series.subcategory = cc2.id " . - "LEFT JOIN category_group_map AS cgm " . - " ON series.category = cgm.category_id " . - " AND cgm.group_id NOT IN($grouplist) " . - "WHERE creator = ? OR (is_public = 1 AND cgm.category_id IS NULL) " . - $dbh->sql_group_by('series.series_id', 'cc1.name, cc2.name, ' . - 'series.name'), - undef, Bugzilla->user->id); - foreach my $series (@$serieses) { - my ($cat, $subcat, $name, $series_id) = @$series; - $cats{$cat}{$subcat}{$name} = $series_id; - } - - return \%cats; + my %cats; + + my $grouplist = Bugzilla->user->groups_as_string; + + # Get all visible series + my $dbh = Bugzilla->dbh; + my $serieses = $dbh->selectall_arrayref( + "SELECT cc1.name, cc2.name, " + . "series.name, series.series_id " + . "FROM series " + . "INNER JOIN series_categories AS cc1 " + . " ON series.category = cc1.id " + . "INNER JOIN series_categories AS cc2 " + . " ON series.subcategory = cc2.id " + . "LEFT JOIN category_group_map AS cgm " + . " ON series.category = cgm.category_id " + . " AND cgm.group_id NOT IN($grouplist) " + . "WHERE creator = ? OR (is_public = 1 AND cgm.category_id IS NULL) " + . $dbh->sql_group_by( + 'series.series_id', 'cc1.name, cc2.name, ' . 'series.name' + ), + undef, + Bugzilla->user->id + ); + foreach my $series (@$serieses) { + my ($cat, $subcat, $name, $series_id) = @$series; + $cats{$cat}{$subcat}{$name} = $series_id; + } + + return \%cats; } sub generateDateProgression { - my ($datefrom, $dateto) = @_; - my @progression; - - $dateto = $dateto || time(); - my $oneday = 60 * 60 * 24; - - # When the from and to dates are converted by str2time(), you end up with - # a time figure representing midnight at the beginning of that day. We - # adjust the times by 1/3 and 2/3 of a day respectively to prevent - # edge conditions in time2str(). - $datefrom += $oneday / 3; - $dateto += (2 * $oneday) / 3; - - while ($datefrom < $dateto) { - push (@progression, time2str("%Y-%m-%d", $datefrom)); - $datefrom += $oneday; - } + my ($datefrom, $dateto) = @_; + my @progression; + + $dateto = $dateto || time(); + my $oneday = 60 * 60 * 24; + + # When the from and to dates are converted by str2time(), you end up with + # a time figure representing midnight at the beginning of that day. We + # adjust the times by 1/3 and 2/3 of a day respectively to prevent + # edge conditions in time2str(). + $datefrom += $oneday / 3; + $dateto += (2 * $oneday) / 3; - return \@progression; + while ($datefrom < $dateto) { + push(@progression, time2str("%Y-%m-%d", $datefrom)); + $datefrom += $oneday; + } + + return \@progression; } sub dump { - my $self = shift; - - # Make sure we've read in our data - my $data = $self->data; - - require Data::Dumper; - say "<pre>Bugzilla::Chart object:"; - print html_quote(Data::Dumper::Dumper($self)); - print "</pre>"; + my $self = shift; + + # Make sure we've read in our data + my $data = $self->data; + + require Data::Dumper; + say "<pre>Bugzilla::Chart object:"; + print html_quote(Data::Dumper::Dumper($self)); + print "</pre>"; } 1; diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm index 09f71baaf..1ea86f592 100644 --- a/Bugzilla/Classification.pm +++ b/Bugzilla/Classification.pm @@ -26,26 +26,26 @@ use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter); use constant IS_CONFIG => 1; -use constant DB_TABLE => 'classifications'; +use constant DB_TABLE => 'classifications'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - sortkey + id + name + description + sortkey ); use constant UPDATE_COLUMNS => qw( - name - description - sortkey + name + description + sortkey ); use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - sortkey => \&_check_sortkey, + name => \&_check_name, + description => \&_check_description, + sortkey => \&_check_sortkey, }; ############################### @@ -53,29 +53,31 @@ use constant VALIDATORS => { ############################### sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - ThrowUserError("classification_not_deletable") if ($self->id == 1); + ThrowUserError("classification_not_deletable") if ($self->id == 1); - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Reclassify products to the default classification, if needed. - my $product_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM products WHERE classification_id = ?', undef, $self->id); - - if (@$product_ids) { - $dbh->do('UPDATE products SET classification_id = 1 WHERE ' - . $dbh->sql_in('id', $product_ids)); - foreach my $id (@$product_ids) { - Bugzilla->memcached->clear({ table => 'products', id => $id }); - } - Bugzilla->memcached->clear_config(); + # Reclassify products to the default classification, if needed. + my $product_ids + = $dbh->selectcol_arrayref( + 'SELECT id FROM products WHERE classification_id = ?', + undef, $self->id); + + if (@$product_ids) { + $dbh->do('UPDATE products SET classification_id = 1 WHERE ' + . $dbh->sql_in('id', $product_ids)); + foreach my $id (@$product_ids) { + Bugzilla->memcached->clear({table => 'products', id => $id}); } + Bugzilla->memcached->clear_config(); + } - $self->SUPER::remove_from_db(); + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } @@ -84,38 +86,41 @@ sub remove_from_db { ############################### sub _check_name { - my ($invocant, $name) = @_; - - $name = trim($name); - $name || ThrowUserError('classification_not_specified'); - - if (length($name) > MAX_CLASSIFICATION_SIZE) { - ThrowUserError('classification_name_too_long', {'name' => $name}); - } - - my $classification = new Bugzilla::Classification({name => $name}); - if ($classification && (!ref $invocant || $classification->id != $invocant->id)) { - ThrowUserError("classification_already_exists", { name => $classification->name }); - } - return $name; + my ($invocant, $name) = @_; + + $name = trim($name); + $name || ThrowUserError('classification_not_specified'); + + if (length($name) > MAX_CLASSIFICATION_SIZE) { + ThrowUserError('classification_name_too_long', {'name' => $name}); + } + + my $classification = new Bugzilla::Classification({name => $name}); + if ($classification && (!ref $invocant || $classification->id != $invocant->id)) + { + ThrowUserError("classification_already_exists", + {name => $classification->name}); + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description || ''); - return $description; + $description = trim($description || ''); + return $description; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - - $sortkey ||= 0; - my $stored_sortkey = $sortkey; - if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) { - ThrowUserError('classification_invalid_sortkey', { 'sortkey' => $stored_sortkey }); - } - return $sortkey; + my ($invocant, $sortkey) = @_; + + $sortkey ||= 0; + my $stored_sortkey = $sortkey; + if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) { + ThrowUserError('classification_invalid_sortkey', + {'sortkey' => $stored_sortkey}); + } + return $sortkey; } ##################################### @@ -124,41 +129,45 @@ sub _check_sortkey { use constant FIELD_NAME => 'classification'; use constant is_default => 0; -use constant is_active => 1; +use constant is_active => 1; ############################### #### Methods #### ############################### -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } sub product_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'product_count'}) { - $self->{'product_count'} = $dbh->selectrow_array(q{ + if (!defined $self->{'product_count'}) { + $self->{'product_count'} = $dbh->selectrow_array( + q{ SELECT COUNT(*) FROM products - WHERE classification_id = ?}, undef, $self->id) || 0; - } - return $self->{'product_count'}; + WHERE classification_id = ?}, undef, $self->id + ) || 0; + } + return $self->{'product_count'}; } sub products { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!$self->{'products'}) { - my $product_ids = $dbh->selectcol_arrayref(q{ + if (!$self->{'products'}) { + my $product_ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM products WHERE classification_id = ? - ORDER BY name}, undef, $self->id); + ORDER BY name}, undef, $self->id + ); - $self->{'products'} = Bugzilla::Product->new_from_list($product_ids); - } - return $self->{'products'}; + $self->{'products'} = Bugzilla::Product->new_from_list($product_ids); + } + return $self->{'products'}; } ############################### @@ -166,7 +175,7 @@ sub products { ############################### sub description { return $_[0]->{'description'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } ############################### @@ -177,27 +186,32 @@ sub sortkey { return $_[0]->{'sortkey'}; } # in global/choose-product.html.tmpl. sub sort_products_by_classification { - my $products = shift; - my $list; - - if (Bugzilla->params->{'useclassification'}) { - my $class = {}; - # Get all classifications with at least one product. - foreach my $product (@$products) { - $class->{$product->classification_id}->{'object'} ||= - new Bugzilla::Classification($product->classification_id); - # Nice way to group products per classification, without querying - # the DB again. - push(@{$class->{$product->classification_id}->{'products'}}, $product); - } - $list = [sort {$a->{'object'}->sortkey <=> $b->{'object'}->sortkey - || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)} - (values %$class)]; - } - else { - $list = [{object => undef, products => $products}]; + my $products = shift; + my $list; + + if (Bugzilla->params->{'useclassification'}) { + my $class = {}; + + # Get all classifications with at least one product. + foreach my $product (@$products) { + $class->{$product->classification_id}->{'object'} + ||= new Bugzilla::Classification($product->classification_id); + + # Nice way to group products per classification, without querying + # the DB again. + push(@{$class->{$product->classification_id}->{'products'}}, $product); } - return $list; + $list = [ + sort { + $a->{'object'}->sortkey <=> $b->{'object'}->sortkey + || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) + } (values %$class) + ]; + } + else { + $list = [{object => undef, products => $products}]; + } + return $list; } 1; diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm index b036907d7..02a044ff5 100644 --- a/Bugzilla/Comment.pm +++ b/Bugzilla/Comment.pm @@ -33,47 +33,48 @@ use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant DB_COLUMNS => qw( - comment_id - bug_id - who - bug_when - work_time - thetext - isprivate - already_wrapped - type - extra_data + comment_id + bug_id + who + bug_when + work_time + thetext + isprivate + already_wrapped + type + extra_data ); use constant UPDATE_COLUMNS => qw( - isprivate - type - extra_data + isprivate + type + extra_data ); use constant DB_TABLE => 'longdescs'; use constant ID_FIELD => 'comment_id'; + # In some rare cases, two comments can have identical timestamps. If # this happens, we want to be sure that the comment added later shows up # later in the sequence. use constant LIST_ORDER => 'bug_when, comment_id'; use constant VALIDATORS => { - bug_id => \&_check_bug_id, - who => \&_check_who, - bug_when => \&_check_bug_when, - work_time => \&_check_work_time, - thetext => \&_check_thetext, - isprivate => \&_check_isprivate, - extra_data => \&_check_extra_data, - type => \&_check_type, + bug_id => \&_check_bug_id, + who => \&_check_who, + bug_when => \&_check_bug_when, + work_time => \&_check_work_time, + thetext => \&_check_thetext, + isprivate => \&_check_isprivate, + extra_data => \&_check_extra_data, + type => \&_check_type, }; use constant VALIDATOR_DEPENDENCIES => { - extra_data => ['type'], - bug_id => ['who'], - work_time => ['who', 'bug_id'], - isprivate => ['who'], + extra_data => ['type'], + bug_id => ['who'], + work_time => ['who', 'bug_id'], + isprivate => ['who'], }; ######################### @@ -81,95 +82,100 @@ use constant VALIDATOR_DEPENDENCIES => { ######################### sub update { - my $self = shift; - my ($changes, $old_comment) = $self->SUPER::update(@_); - - if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) { - $self->bug->_sync_fulltext( update_comments => 1); - } - - my @old_tags = @{ $old_comment->tags }; - my @new_tags = @{ $self->tags }; - my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags); - - if (@$removed_tags || @$added_tags) { - my $dbh = Bugzilla->dbh; - my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)"); - my $sth_delete = $dbh->prepare( - "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?" - ); - my $sth_insert = $dbh->prepare( - "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)" - ); - my $sth_activity = $dbh->prepare( - "INSERT INTO longdescs_tags_activity + my $self = shift; + my ($changes, $old_comment) = $self->SUPER::update(@_); + + if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) { + $self->bug->_sync_fulltext(update_comments => 1); + } + + my @old_tags = @{$old_comment->tags}; + my @new_tags = @{$self->tags}; + my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags); + + if (@$removed_tags || @$added_tags) { + my $dbh = Bugzilla->dbh; + my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)"); + my $sth_delete = $dbh->prepare( + "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"); + my $sth_insert + = $dbh->prepare("INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"); + my $sth_activity = $dbh->prepare( + "INSERT INTO longdescs_tags_activity (bug_id, comment_id, who, bug_when, added, removed) VALUES (?, ?, ?, ?, ?, ?)" - ); - - foreach my $tag (@$removed_tags) { - my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag }); - if ($weighted) { - if ($weighted->weight == 1) { - $weighted->remove_from_db(); - } else { - $weighted->set_weight($weighted->weight - 1); - $weighted->update(); - } - } - trick_taint($tag); - $sth_delete->execute($self->id, $tag); - $sth_activity->execute( - $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag); - } + ); - foreach my $tag (@$added_tags) { - my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag }); - if ($weighted) { - $weighted->set_weight($weighted->weight + 1); - $weighted->update(); - } else { - Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 }); - } - trick_taint($tag); - $sth_insert->execute($self->id, $tag); - $sth_activity->execute( - $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, ''); + foreach my $tag (@$removed_tags) { + my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag}); + if ($weighted) { + if ($weighted->weight == 1) { + $weighted->remove_from_db(); } + else { + $weighted->set_weight($weighted->weight - 1); + $weighted->update(); + } + } + trick_taint($tag); + $sth_delete->execute($self->id, $tag); + $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, '', + $tag); } - return $changes; + foreach my $tag (@$added_tags) { + my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag}); + if ($weighted) { + $weighted->set_weight($weighted->weight + 1); + $weighted->update(); + } + else { + Bugzilla::Comment::TagWeights->create({tag => $tag, weight => 1}); + } + trick_taint($tag); + $sth_insert->execute($self->id, $tag); + $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, + $tag, ''); + } + } + + return $changes; } # Speeds up displays of comment lists by loading all author objects and tags at # once for a whole list. sub preload { - my ($class, $comments) = @_; - # Author - 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}}; - } - # Tags - if (Bugzilla->params->{'comment_taggers_group'}) { - my $dbh = Bugzilla->dbh; - my @comment_ids = map { $_->id } @$comments; - my %comment_map = map { $_->id => $_ } @$comments; - my $rows = $dbh->selectall_arrayref( - "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . " + my ($class, $comments) = @_; + + # Author + 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}}; + } + + # Tags + if (Bugzilla->params->{'comment_taggers_group'}) { + my $dbh = Bugzilla->dbh; + my @comment_ids = map { $_->id } @$comments; + my %comment_map = map { $_->id => $_ } @$comments; + my $rows = $dbh->selectall_arrayref( + "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . " FROM longdescs_tags - WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . ' ' . - $dbh->sql_group_by('comment_id')); - foreach my $row (@$rows) { - $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ]; - } - # Also sets the 'tags' attribute for comments which have no entry - # in the longdescs_tags table, else calling $comment->tags will - # trigger another SQL query again. - $comment_map{$_}->{tags} ||= [] foreach @comment_ids; + WHERE " + . $dbh->sql_in('comment_id', \@comment_ids) . ' ' + . $dbh->sql_group_by('comment_id') + ); + foreach my $row (@$rows) { + $comment_map{$row->[0]}->{tags} = [split(/,/, $row->[1])]; } + + # Also sets the 'tags' attribute for comments which have no entry + # in the longdescs_tags table, else calling $comment->tags will + # trigger another SQL query again. + $comment_map{$_}->{tags} ||= [] foreach @comment_ids; + } } ############################### @@ -177,130 +183,132 @@ sub preload { ############################### 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 { - # Work time is returned as a string (see bug 607909) - return 0 if $_[0]->{'work_time'} + 0 == 0; - return $_[0]->{'work_time'}; +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 { + + # Work time is returned as a string (see bug 607909) + return 0 if $_[0]->{'work_time'} + 0 == 0; + return $_[0]->{'work_time'}; } -sub type { return $_[0]->{'type'}; } -sub extra_data { return $_[0]->{'extra_data'} } +sub type { return $_[0]->{'type'}; } +sub extra_data { return $_[0]->{'extra_data'} } sub tags { - my ($self) = @_; - state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; - return [] unless $comment_taggers_group; - $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref( - "SELECT tag + my ($self) = @_; + state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; + return [] unless $comment_taggers_group; + $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref( + "SELECT tag FROM longdescs_tags WHERE comment_id = ? - ORDER BY tag", - undef, $self->id); - return $self->{'tags'}; + ORDER BY tag", undef, $self->id + ); + return $self->{'tags'}; } sub collapsed { - my ($self) = @_; - state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; - return 0 unless $comment_taggers_group; - return $self->{collapsed} if exists $self->{collapsed}; - - state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'}; - $self->{collapsed} = 0; - Bugzilla->request_cache->{comment_tags_collapsed} - ||= [ split(/\s*,\s*/, $collapsed_comment_tags) ]; - my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} }; - foreach my $my_tag (@{ $self->tags }) { - $my_tag = lc($my_tag); - foreach my $collapsed_tag (@collapsed_tags) { - if ($my_tag eq lc($collapsed_tag)) { - $self->{collapsed} = 1; - last; - } - } - last if $self->{collapsed}; + my ($self) = @_; + state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; + return 0 unless $comment_taggers_group; + return $self->{collapsed} if exists $self->{collapsed}; + + state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'}; + $self->{collapsed} = 0; + Bugzilla->request_cache->{comment_tags_collapsed} + ||= [split(/\s*,\s*/, $collapsed_comment_tags)]; + my @collapsed_tags = @{Bugzilla->request_cache->{comment_tags_collapsed}}; + foreach my $my_tag (@{$self->tags}) { + $my_tag = lc($my_tag); + foreach my $collapsed_tag (@collapsed_tags) { + if ($my_tag eq lc($collapsed_tag)) { + $self->{collapsed} = 1; + last; + } } - return $self->{collapsed}; + last if $self->{collapsed}; + } + return $self->{collapsed}; } sub bug { - my $self = shift; - require Bugzilla::Bug; - $self->{bug} ||= new Bugzilla::Bug($self->bug_id); - return $self->{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; + 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({ id => $self->extra_data, cache => 1 }); - return $self->{attachment}; + my ($self) = @_; + return undef if not $self->is_about_attachment; + $self->{attachment} + ||= new Bugzilla::Attachment({id => $self->extra_data, cache => 1}); + return $self->{attachment}; } -sub author { - my $self = shift; - $self->{'author'} - ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 }); - return $self->{'author'}; +sub author { + my $self = shift; + $self->{'author'} ||= new Bugzilla::User({id => $self->{'who'}, cache => 1}); + 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; + 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_is_private { $_[0]->set('isprivate', $_[1]); } -sub set_type { $_[0]->set('type', $_[1]); } -sub set_extra_data { $_[0]->set('extra_data', $_[1]); } +sub set_is_private { $_[0]->set('isprivate', $_[1]); } +sub set_type { $_[0]->set('type', $_[1]); } +sub set_extra_data { $_[0]->set('extra_data', $_[1]); } sub add_tag { - my ($self, $tag) = @_; - $tag = $self->_check_tag($tag); + my ($self, $tag) = @_; + $tag = $self->_check_tag($tag); - my $tags = $self->tags; - return if grep { lc($tag) eq lc($_) } @$tags; - push @$tags, $tag; - $self->{'tags'} = [ sort @$tags ]; + my $tags = $self->tags; + return if grep { lc($tag) eq lc($_) } @$tags; + push @$tags, $tag; + $self->{'tags'} = [sort @$tags]; } sub remove_tag { - my ($self, $tag) = @_; - $tag = $self->_check_tag($tag); + my ($self, $tag) = @_; + $tag = $self->_check_tag($tag); - my $tags = $self->tags; - my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1; - return unless defined $index; - splice(@$tags, $index, 1); + my $tags = $self->tags; + my $index = first { lc($tags->[$_]) eq lc($tag) } 0 .. scalar(@$tags) - 1; + return unless defined $index; + splice(@$tags, $index, 1); } ############## @@ -308,180 +316,180 @@ sub remove_tag { ############## sub run_create_validators { - my $self = shift; - my $params = $self->SUPER::run_create_validators(@_); - # Sometimes this run_create_validators is called with parameters that - # skip bug_id validation, so it might not exist in the resulting hash. - if (defined $params->{bug_id}) { - $params->{bug_id} = $params->{bug_id}->id; - } - return $params; + my $self = shift; + my $params = $self->SUPER::run_create_validators(@_); + + # Sometimes this run_create_validators is called with parameters that + # skip bug_id validation, so it might not exist in the resulting hash. + if (defined $params->{bug_id}) { + $params->{bug_id} = $params->{bug_id}->id; + } + return $params; } sub _check_extra_data { - my ($invocant, $extra_data, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; + my ($invocant, $extra_data, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; - if ($type == CMT_NORMAL) { - if (defined $extra_data) { - ThrowCodeError('comment_extra_data_not_allowed', - { type => $type, extra_data => $extra_data }); - } + if ($type == CMT_NORMAL) { + 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}); + } + elsif ($type == CMT_ATTACHMENT_CREATED or $type == CMT_ATTACHMENT_UPDATED) { + my $attachment = Bugzilla::Attachment->check({id => $extra_data}); + $extra_data = $attachment->id; } else { - if (!defined $extra_data) { - ThrowCodeError('comment_extra_data_required', { type => $type }); - } - 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 }); - } + my $original = $extra_data; + detaint_natural($extra_data) + or ThrowCodeError('comment_extra_data_not_numeric', + {type => $type, extra_data => $original}); } + } - return $extra_data; + 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; + my ($invocant, $type) = @_; + $type ||= CMT_NORMAL; + my $original = $type; + detaint_natural($type) + or ThrowCodeError('comment_type_invalid', {type => $original}); + return $type; } sub _check_bug_id { - my ($invocant, $bug_id) = @_; - - ThrowCodeError('param_required', {function => 'Bugzilla::Comment->create', - param => 'bug_id'}) unless $bug_id; - - my $bug; - if (blessed $bug_id) { - # We got a bug object passed in, use it - $bug = $bug_id; - $bug->check_is_visible; - } - else { - # We got a bug id passed in, check it and get the bug object - $bug = Bugzilla::Bug->check({ id => $bug_id }); - } - - # Make sure the user can edit the product - Bugzilla->user->can_edit_product($bug->{product_id}); - - # Make sure the user can comment - my $privs; - $bug->check_can_change_field('longdesc', 0, 1, \$privs) - || ThrowUserError('illegal_change', - { field => 'longdesc', privs => $privs }); - return $bug; + my ($invocant, $bug_id) = @_; + + ThrowCodeError('param_required', + {function => 'Bugzilla::Comment->create', param => 'bug_id'}) + unless $bug_id; + + my $bug; + if (blessed $bug_id) { + + # We got a bug object passed in, use it + $bug = $bug_id; + $bug->check_is_visible; + } + else { + # We got a bug id passed in, check it and get the bug object + $bug = Bugzilla::Bug->check({id => $bug_id}); + } + + # Make sure the user can edit the product + Bugzilla->user->can_edit_product($bug->{product_id}); + + # Make sure the user can comment + my $privs; + $bug->check_can_change_field('longdesc', 0, 1, \$privs) + || ThrowUserError('illegal_change', {field => 'longdesc', privs => $privs}); + return $bug; } sub _check_who { - my ($invocant, $who) = @_; - Bugzilla->login(LOGIN_REQUIRED); - return Bugzilla->user->id; + my ($invocant, $who) = @_; + Bugzilla->login(LOGIN_REQUIRED); + return Bugzilla->user->id; } sub _check_bug_when { - my ($invocant, $when) = @_; + my ($invocant, $when) = @_; - # Make sure the timestamp is defined, default to a timestamp from the db - if (!defined $when) { - $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - } + # Make sure the timestamp is defined, default to a timestamp from the db + if (!defined $when) { + $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + } - # Make sure the timestamp parses - if (!datetime_from($when)) { - ThrowCodeError('invalid_timestamp', { timestamp => $when }); - } + # Make sure the timestamp parses + if (!datetime_from($when)) { + ThrowCodeError('invalid_timestamp', {timestamp => $when}); + } - return $when; + return $when; } sub _check_work_time { - my ($invocant, $value_in, $field, $params) = @_; - - # Call down to Bugzilla::Object, letting it know negative - # values are ok - my $time = $invocant->check_time($value_in, $field, $params, 1); - my $privs; - $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs) - || ThrowUserError('illegal_change', - { field => 'work_time', privs => $privs }); - return $time; + my ($invocant, $value_in, $field, $params) = @_; + + # Call down to Bugzilla::Object, letting it know negative + # values are ok + my $time = $invocant->check_time($value_in, $field, $params, 1); + my $privs; + $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs) + || ThrowUserError('illegal_change', {field => 'work_time', privs => $privs}); + return $time; } sub _check_thetext { - my ($invocant, $thetext) = @_; - - ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create', - param => 'thetext'}) unless defined $thetext; - - # Remove any trailing whitespace. Leading whitespace could be - # a valid part of the comment. - $thetext =~ s/\s*$//s; - $thetext =~ s/\r\n?/\n/g; # Get rid of \r. - - # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they - # require the new utf8mb4 character set. Other DB servers are handling them - # without any problem. So we need to replace these characters if we use MySQL, - # else the comment is truncated. - # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away. - state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; - if ($is_mysql) { - # Perl 5.13.8 and older complain about non-characters. - no warnings 'utf8'; - $thetext =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; - } - - ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH; - return $thetext; + my ($invocant, $thetext) = @_; + + ThrowCodeError('param_required', + {function => 'Bugzilla::Comment->create', param => 'thetext'}) + unless defined $thetext; + + # Remove any trailing whitespace. Leading whitespace could be + # a valid part of the comment. + $thetext =~ s/\s*$//s; + $thetext =~ s/\r\n?/\n/g; # Get rid of \r. + + # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they + # require the new utf8mb4 character set. Other DB servers are handling them + # without any problem. So we need to replace these characters if we use MySQL, + # else the comment is truncated. + # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away. + state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + if ($is_mysql) { + + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $thetext + =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; + } + + ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH; + return $thetext; } sub _check_isprivate { - my ($invocant, $isprivate) = @_; - if ($isprivate && !Bugzilla->user->is_insider) { - ThrowUserError('user_not_insider'); - } - return $isprivate ? 1 : 0; + my ($invocant, $isprivate) = @_; + if ($isprivate && !Bugzilla->user->is_insider) { + ThrowUserError('user_not_insider'); + } + return $isprivate ? 1 : 0; } sub _check_tag { - my ($invocant, $tag) = @_; - length($tag) < MIN_COMMENT_TAG_LENGTH - and ThrowUserError('comment_tag_too_short', { tag => $tag }); - length($tag) > MAX_COMMENT_TAG_LENGTH - and ThrowUserError('comment_tag_too_long', { tag => $tag }); - $tag =~ /^[\w\d\._-]+$/ - or ThrowUserError('comment_tag_invalid', { tag => $tag }); - return $tag; + my ($invocant, $tag) = @_; + length($tag) < MIN_COMMENT_TAG_LENGTH + and ThrowUserError('comment_tag_too_short', {tag => $tag}); + length($tag) > MAX_COMMENT_TAG_LENGTH + and ThrowUserError('comment_tag_too_long', {tag => $tag}); + $tag =~ /^[\w\d\._-]+$/ or ThrowUserError('comment_tag_invalid', {tag => $tag}); + return $tag; } sub count { - my ($self) = @_; + my ($self) = @_; - return $self->{'count'} if defined $self->{'count'}; + return $self->{'count'} if defined $self->{'count'}; - my $dbh = Bugzilla->dbh; - ($self->{'count'}) = $dbh->selectrow_array( - "SELECT COUNT(*) + my $dbh = Bugzilla->dbh; + ($self->{'count'}) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM longdescs WHERE bug_id = ? - AND bug_when <= ?", - undef, $self->bug_id, $self->creation_ts); + AND bug_when <= ?", undef, $self->bug_id, $self->creation_ts + ); - return --$self->{'count'}; + return --$self->{'count'}; } 1; diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm index 7dba53e34..5355cad7f 100644 --- a/Bugzilla/Comment/TagWeights.pm +++ b/Bugzilla/Comment/TagWeights.pm @@ -21,20 +21,20 @@ use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - tag - weight + id + tag + weight ); use constant UPDATE_COLUMNS => qw( - weight + weight ); use constant DB_TABLE => 'longdescs_tags_weights'; use constant ID_FIELD => 'id'; use constant NAME_FIELD => 'tag'; use constant LIST_ORDER => 'weight DESC'; -use constant VALIDATORS => { }; +use constant VALIDATORS => {}; # There's no gain to caching these objects use constant USE_MEMCACHED => 0; diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm index d5a6ece5d..3cdee9d63 100644 --- a/Bugzilla/Component.pm +++ b/Bugzilla/Component.pm @@ -27,150 +27,147 @@ use Scalar::Util qw(blessed); ############################### use constant DB_TABLE => 'components'; + # This is mostly for the editfields.cgi case where ->get_all is called. use constant LIST_ORDER => 'product_id, name'; use constant DB_COLUMNS => qw( - id - name - product_id - initialowner - initialqacontact - description - isactive + id + name + product_id + initialowner + initialqacontact + description + isactive ); use constant UPDATE_COLUMNS => qw( - name - initialowner - initialqacontact - description - isactive + name + initialowner + initialqacontact + description + isactive ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; use constant VALIDATORS => { - create_series => \&Bugzilla::Object::check_boolean, - product => \&_check_product, - initialowner => \&_check_initialowner, - initialqacontact => \&_check_initialqacontact, - description => \&_check_description, - initial_cc => \&_check_cc_list, - name => \&_check_name, - isactive => \&Bugzilla::Object::check_boolean, + create_series => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + initialowner => \&_check_initialowner, + initialqacontact => \&_check_initialqacontact, + description => \&_check_description, + initial_cc => \&_check_cc_list, + name => \&_check_name, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - name => ['product'], -}; +use constant VALIDATOR_DEPENDENCIES => {name => ['product'],}; ############################### sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param and !defined $param->{id}) { - $product = $param->{product}; - my $name = $param->{name}; - if (!defined $product) { - ThrowCodeError('bad_arg', - {argument => 'product', - function => "${class}::new"}); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - - my $condition = 'product_id = ? AND name = ?'; - my @values = ($product->id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $product; + if (ref $param and !defined $param->{id}) { + $product = $param->{product}; + my $name = $param->{name}; + if (!defined $product) { + ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"}); } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); + } + + my $condition = 'product_id = ? AND name = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } - unshift @_, $param; - my $component = $class->SUPER::new(@_); - # Add the product object as attribute only if the component exists. - $component->{product} = $product if ($component && $product); - return $component; + unshift @_, $param; + my $component = $class->SUPER::new(@_); + + # Add the product object as attribute only if the component exists. + $component->{product} = $product if ($component && $product); + return $component; } sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; + my $class = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - $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 $product = delete $params->{product}; - $params->{product_id} = $product->id; + $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 $product = delete $params->{product}; + $params->{product_id} = $product->id; - my $component = $class->insert_create_data($params); - $component->{product} = $product; + my $component = $class->insert_create_data($params); + $component->{product} = $product; - # We still have to fill the component_cc table. - $component->_update_cc_list($cc_list) if $cc_list; + # We still have to fill the component_cc table. + $component->_update_cc_list($cc_list) if $cc_list; - # Create series for the new component. - $component->_create_series() if $create_series; + # Create series for the new component. + $component->_create_series() if $create_series; - $dbh->bz_commit_transaction(); - return $component; + $dbh->bz_commit_transaction(); + return $component; } sub update { - my $self = shift; - my $changes = $self->SUPER::update(@_); - - # Update the component_cc table if necessary. - if (defined $self->{cc_ids}) { - my $diff = $self->_update_cc_list($self->{cc_ids}); - $changes->{cc_list} = $diff if defined $diff; - } - return $changes; + my $self = shift; + my $changes = $self->SUPER::update(@_); + + # Update the component_cc table if necessary. + if (defined $self->{cc_ids}) { + my $diff = $self->_update_cc_list($self->{cc_ids}); + $changes->{cc_list} = $diff if defined $diff; + } + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $self->_check_if_controller(); # From ChoiceInterface + $self->_check_if_controller(); # From ChoiceInterface - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Products must have at least one component. - my @components = @{ $self->product->components }; - if (scalar(@components) == 1) { - ThrowUserError('component_is_last', { comp => $self }); - } + # Products must have at least one component. + my @components = @{$self->product->components}; + if (scalar(@components) == 1) { + ThrowUserError('component_is_last', {comp => $self}); + } + + if ($self->bug_count) { + if (Bugzilla->params->{'allowbugdeletion'}) { + require Bugzilla::Bug; + foreach my $bug_id (@{$self->bug_ids}) { - if ($self->bug_count) { - if (Bugzilla->params->{'allowbugdeletion'}) { - require Bugzilla::Bug; - foreach my $bug_id (@{$self->bug_ids}) { - # Note: We allow admins to delete bugs even if they can't - # see them, as long as they can see the product. - my $bug = new Bugzilla::Bug($bug_id); - $bug->remove_from_db(); - } - } else { - ThrowUserError('component_has_bugs', {nb => $self->bug_count}); - } + # Note: We allow admins to delete bugs even if they can't + # see them, as long as they can see the product. + my $bug = new Bugzilla::Bug($bug_id); + $bug->remove_from_db(); + } } - # Update the list of components in the product object. - $self->product->{components} = [grep { $_->id != $self->id } @components]; - $self->SUPER::remove_from_db(); + else { + ThrowUserError('component_has_bugs', {nb => $self->bug_count}); + } + } + + # Update the list of components in the product object. + $self->product->{components} = [grep { $_->id != $self->id } @components]; + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } ################################ @@ -178,69 +175,70 @@ sub remove_from_db { ################################ sub _check_name { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('component_blank_name'); - - if (length($name) > MAX_COMPONENT_SIZE) { - ThrowUserError('component_name_too_long', {'name' => $name}); - } - - my $component = new Bugzilla::Component({product => $product, name => $name}); - if ($component && (!ref $invocant || $component->id != $invocant->id)) { - ThrowUserError('component_already_exists', { name => $component->name, - product => $product }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('component_blank_name'); + + if (length($name) > MAX_COMPONENT_SIZE) { + ThrowUserError('component_name_too_long', {'name' => $name}); + } + + my $component = new Bugzilla::Component({product => $product, name => $name}); + if ($component && (!ref $invocant || $component->id != $invocant->id)) { + ThrowUserError('component_already_exists', + {name => $component->name, product => $product}); + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('component_blank_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('component_blank_description'); + return $description; } sub _check_initialowner { - my ($invocant, $owner) = @_; + my ($invocant, $owner) = @_; - $owner || ThrowUserError('component_need_initialowner'); - my $owner_id = Bugzilla::User->check($owner)->id; - return $owner_id; + $owner || ThrowUserError('component_need_initialowner'); + my $owner_id = Bugzilla::User->check($owner)->id; + return $owner_id; } sub _check_initialqacontact { - my ($invocant, $qa_contact) = @_; - - my $qa_contact_id; - if (Bugzilla->params->{'useqacontact'}) { - $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact; - } - elsif (ref $invocant) { - $qa_contact_id = $invocant->{initialqacontact}; - } - return $qa_contact_id; + my ($invocant, $qa_contact) = @_; + + my $qa_contact_id; + if (Bugzilla->params->{'useqacontact'}) { + $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact; + } + elsif (ref $invocant) { + $qa_contact_id = $invocant->{initialqacontact}; + } + return $qa_contact_id; } sub _check_product { - my ($invocant, $product) = @_; - $product || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'product' }); - return Bugzilla->user->check_can_admin_product($product->name); + my ($invocant, $product) = @_; + $product + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'product'}); + return Bugzilla->user->check_can_admin_product($product->name); } sub _check_cc_list { - my ($invocant, $cc_list) = @_; - - my %cc_ids; - foreach my $cc (@$cc_list) { - my $id = login_to_id($cc, THROW_ERROR); - $cc_ids{$id} = 1; - } - return [keys %cc_ids]; + my ($invocant, $cc_list) = @_; + + my %cc_ids; + foreach my $cc (@$cc_list) { + my $id = login_to_id($cc, THROW_ERROR); + $cc_ids{$id} = 1; + } + return [keys %cc_ids]; } ############################### @@ -248,156 +246,176 @@ sub _check_cc_list { ############################### sub _update_cc_list { - my ($self, $cc_list) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $cc_list) = @_; + my $dbh = Bugzilla->dbh; - my $old_cc_list = - $dbh->selectcol_arrayref('SELECT user_id FROM component_cc - WHERE component_id = ?', undef, $self->id); + my $old_cc_list = $dbh->selectcol_arrayref( + 'SELECT user_id FROM component_cc + WHERE component_id = ?', undef, $self->id + ); - my ($removed, $added) = diff_arrays($old_cc_list, $cc_list); - my $diff; - if (scalar @$removed || scalar @$added) { - $diff = [join(', ', @$removed), join(', ', @$added)]; - } + my ($removed, $added) = diff_arrays($old_cc_list, $cc_list); + my $diff; + if (scalar @$removed || scalar @$added) { + $diff = [join(', ', @$removed), join(', ', @$added)]; + } - $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id); + $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id); - my $sth = $dbh->prepare('INSERT INTO component_cc - (user_id, component_id) VALUES (?, ?)'); - $sth->execute($_, $self->id) foreach (@$cc_list); + my $sth = $dbh->prepare( + 'INSERT INTO component_cc + (user_id, component_id) VALUES (?, ?)' + ); + $sth->execute($_, $self->id) foreach (@$cc_list); - return $diff; + return $diff; } sub _create_series { - my $self = shift; - - # Insert default charting queries for this product. - # If they aren't using charting, this won't do any harm. - my $prodcomp = "&product=" . url_quote($self->product->name) . - "&component=" . url_quote($self->name); - - my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . - $prodcomp; - my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . - $prodcomp; - - my @series = ([get_text('series_all_open'), $open_query], - [get_text('series_all_closed'), $nonopen_query]); - - foreach my $sdata (@series) { - my $series = new Bugzilla::Series(undef, $self->product->name, - $self->name, $sdata->[0], - Bugzilla->user->id, 1, $sdata->[1], 1); - $series->writeToDatabase(); - } + my $self = shift; + + # Insert default charting queries for this product. + # If they aren't using charting, this won't do any harm. + my $prodcomp + = "&product=" + . url_quote($self->product->name) + . "&component=" + . url_quote($self->name); + + my $open_query + = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . $prodcomp; + my $nonopen_query + = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . $prodcomp; + + my @series = ( + [get_text('series_all_open'), $open_query], + [get_text('series_all_closed'), $nonopen_query] + ); + + foreach my $sdata (@series) { + my $series + = new Bugzilla::Series(undef, $self->product->name, $self->name, $sdata->[0], + Bugzilla->user->id, 1, $sdata->[1], 1); + $series->writeToDatabase(); + } } -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } + sub set_default_assignee { - my ($self, $owner) = @_; + my ($self, $owner) = @_; + + $self->set('initialowner', $owner); - $self->set('initialowner', $owner); - # Reset the default owner object. - delete $self->{default_assignee}; + # Reset the default owner object. + delete $self->{default_assignee}; } + sub set_default_qa_contact { - my ($self, $qa_contact) = @_; + my ($self, $qa_contact) = @_; + + $self->set('initialqacontact', $qa_contact); - $self->set('initialqacontact', $qa_contact); - # Reset the default QA contact object. - delete $self->{default_qa_contact}; + # Reset the default QA contact object. + delete $self->{default_qa_contact}; } + sub set_cc_list { - my ($self, $cc_list) = @_; + my ($self, $cc_list) = @_; + + $self->{cc_ids} = $self->_check_cc_list($cc_list); - $self->{cc_ids} = $self->_check_cc_list($cc_list); - # Reset the list of CC user objects. - delete $self->{initial_cc}; + # Reset the list of CC user objects. + delete $self->{initial_cc}; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(q{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + q{ SELECT COUNT(*) FROM bugs - WHERE component_id = ?}, undef, $self->id) || 0; - } - return $self->{'bug_count'}; + WHERE component_id = ?}, undef, $self->id + ) || 0; + } + return $self->{'bug_count'}; } sub bug_ids { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bugs_ids'}) { - $self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{ + if (!defined $self->{'bugs_ids'}) { + $self->{'bugs_ids'} = $dbh->selectcol_arrayref( + q{ SELECT bug_id FROM bugs - WHERE component_id = ?}, undef, $self->id); - } - return $self->{'bugs_ids'}; + WHERE component_id = ?}, undef, $self->id + ); + } + return $self->{'bugs_ids'}; } sub default_assignee { - my $self = shift; + my $self = shift; - return $self->{'default_assignee'} - ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 }); + return $self->{'default_assignee'} + ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1}); } sub default_qa_contact { - my $self = shift; + my $self = shift; - return unless $self->{'initialqacontact'}; - return $self->{'default_qa_contact'} - ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1 }); + return unless $self->{'initialqacontact'}; + return $self->{'default_qa_contact'} + ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1}); } sub flag_types { - my $self = shift; - - if (!defined $self->{'flag_types'}) { - my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id, - component_id => $self->id }); - - $self->{'flag_types'} = {}; - $self->{'flag_types'}->{'bug'} = - [grep { $_->target_type eq 'bug' } @$flagtypes]; - $self->{'flag_types'}->{'attachment'} = - [grep { $_->target_type eq 'attachment' } @$flagtypes]; - } - return $self->{'flag_types'}; + my $self = shift; + + if (!defined $self->{'flag_types'}) { + my $flagtypes = Bugzilla::FlagType::match( + {product_id => $self->product_id, component_id => $self->id}); + + $self->{'flag_types'} = {}; + $self->{'flag_types'}->{'bug'} + = [grep { $_->target_type eq 'bug' } @$flagtypes]; + $self->{'flag_types'}->{'attachment'} + = [grep { $_->target_type eq 'attachment' } @$flagtypes]; + } + return $self->{'flag_types'}; } sub initial_cc { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'initial_cc'}) { - # If set_cc_list() has been called but data are not yet written - # into the DB, we want the new values defined by it. - my $cc_ids = $self->{cc_ids} - || $dbh->selectcol_arrayref('SELECT user_id FROM component_cc - WHERE component_id = ?', - undef, $self->id); - - $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids); - } - return $self->{'initial_cc'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'initial_cc'}) { + + # If set_cc_list() has been called but data are not yet written + # into the DB, we want the new values defined by it. + my $cc_ids = $self->{cc_ids} || $dbh->selectcol_arrayref( + 'SELECT user_id FROM component_cc + WHERE component_id = ?', undef, + $self->id + ); + + $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids); + } + return $self->{'initial_cc'}; } sub product { - my $self = shift; - if (!defined $self->{'product'}) { - require Bugzilla::Product; # We cannot |use| it. - $self->{'product'} = new Bugzilla::Product($self->product_id); - } - return $self->{'product'}; + my $self = shift; + if (!defined $self->{'product'}) { + require Bugzilla::Product; # We cannot |use| it. + $self->{'product'} = new Bugzilla::Product($self->product_id); + } + return $self->{'product'}; } ############################### @@ -405,8 +423,8 @@ sub product { ############################### sub description { return $_[0]->{'description'}; } -sub product_id { return $_[0]->{'product_id'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub product_id { return $_[0]->{'product_id'}; } +sub is_active { return $_[0]->{'isactive'}; } ############################################## # Implement Bugzilla::Field::ChoiceInterface # @@ -416,11 +434,11 @@ use constant FIELD_NAME => 'component'; use constant is_default => 0; sub is_set_on_bug { - my ($self, $bug) = @_; - my $value = blessed($bug) ? $bug->component_id : $bug->{component}; - $value = $value->id if blessed($value); - return 0 unless $value; - return $value == $self->id ? 1 : 0; + my ($self, $bug) = @_; + my $value = blessed($bug) ? $bug->component_id : $bug->{component}; + $value = $value->id if blessed($value); + return 0 unless $value; + return $value == $self->id ? 1 : 0; } ############################### diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm index 458616701..1aa944985 100644 --- a/Bugzilla/Config.pm +++ b/Bugzilla/Config.pm @@ -25,316 +25,323 @@ use File::Basename; # Don't export localvars by default - people should have to explicitly # ask for it, as a (probably futile) attempt to stop code using it # when it shouldn't -%Bugzilla::Config::EXPORT_TAGS = - ( - admin => [qw(update_params SetParam write_params)], - ); +%Bugzilla::Config::EXPORT_TAGS + = (admin => [qw(update_params SetParam write_params)],); Exporter::export_ok_tags('admin'); # INITIALISATION CODE # Perl throws a warning if we use bz_locations() directly after do. our %params; + # Load in the param definitions sub _load_params { - my $panels = param_panels(); - my %hook_panels; - foreach my $panel (keys %$panels) { - my $module = $panels->{$panel}; - eval("require $module") || die $@; - my @new_param_list = $module->get_param_list(); - $hook_panels{lc($panel)} = { params => \@new_param_list }; - } - # This hook is also called in editparams.cgi. This call here is required - # to make SetParam work. - Bugzilla::Hook::process('config_modify_panels', - { panels => \%hook_panels }); - - foreach my $panel (keys %hook_panels) { - foreach my $item (@{$hook_panels{$panel}->{params}}) { - $params{$item->{'name'}} = $item; - } + my $panels = param_panels(); + my %hook_panels; + foreach my $panel (keys %$panels) { + my $module = $panels->{$panel}; + eval("require $module") || die $@; + my @new_param_list = $module->get_param_list(); + $hook_panels{lc($panel)} = {params => \@new_param_list}; + } + + # This hook is also called in editparams.cgi. This call here is required + # to make SetParam work. + Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels}); + + foreach my $panel (keys %hook_panels) { + foreach my $item (@{$hook_panels{$panel}->{params}}) { + $params{$item->{'name'}} = $item; } + } } + # END INIT CODE # Subroutines go here sub param_panels { - my $param_panels = {}; - my $libpath = bz_locations()->{'libpath'}; - foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) { - $item =~ m#/([^/]+)\.pm$#; - my $module = $1; - $param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common'; - } - # Now check for any hooked params - Bugzilla::Hook::process('config_add_panels', - { panel_modules => $param_panels }); - return $param_panels; + my $param_panels = {}; + my $libpath = bz_locations()->{'libpath'}; + foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) { + $item =~ m#/([^/]+)\.pm$#; + my $module = $1; + $param_panels->{$module} = "Bugzilla::Config::$module" + unless $module eq 'Common'; + } + + # Now check for any hooked params + Bugzilla::Hook::process('config_add_panels', {panel_modules => $param_panels}); + return $param_panels; } sub SetParam { - my ($name, $value) = @_; + my ($name, $value) = @_; - _load_params unless %params; - die "Unknown param $name" unless (exists $params{$name}); + _load_params unless %params; + die "Unknown param $name" unless (exists $params{$name}); - my $entry = $params{$name}; + my $entry = $params{$name}; - # sanity check the value + # sanity check the value - # XXX - This runs the checks. Which would be good, except that - # check_shadowdb creates the database as a side effect, and so the - # checker fails the second time around... - if ($name ne 'shadowdb' && exists $entry->{'checker'}) { - my $err = $entry->{'checker'}->($value, $entry); - die "Param $name is not valid: $err" unless $err eq ''; - } + # XXX - This runs the checks. Which would be good, except that + # check_shadowdb creates the database as a side effect, and so the + # checker fails the second time around... + if ($name ne 'shadowdb' && exists $entry->{'checker'}) { + my $err = $entry->{'checker'}->($value, $entry); + die "Param $name is not valid: $err" unless $err eq ''; + } - Bugzilla->params->{$name} = $value; + Bugzilla->params->{$name} = $value; } sub update_params { - my ($params) = @_; - my $answer = Bugzilla->installation_answers; - my $datadir = bz_locations()->{'datadir'}; - my $param; - - # If the old data/params file using Data::Dumper output still exists, - # read it. It will be deleted once the parameters are stored in the new - # data/params.json file. - my $old_file = "$datadir/params"; - - if (-e $old_file) { - require Safe; - my $s = new Safe; - - $s->rdo($old_file); - die "Error reading $old_file: $!" if $!; - die "Error evaluating $old_file: $@" if $@; - - # Now read the param back out from the sandbox. - $param = \%{ $s->varglob('param') }; + my ($params) = @_; + my $answer = Bugzilla->installation_answers; + my $datadir = bz_locations()->{'datadir'}; + my $param; + + # If the old data/params file using Data::Dumper output still exists, + # read it. It will be deleted once the parameters are stored in the new + # data/params.json file. + my $old_file = "$datadir/params"; + + if (-e $old_file) { + require Safe; + my $s = new Safe; + + $s->rdo($old_file); + die "Error reading $old_file: $!" if $!; + die "Error evaluating $old_file: $@" if $@; + + # Now read the param back out from the sandbox. + $param = \%{$s->varglob('param')}; + } + else { + # Rename params.js to params.json if checksetup.pl + # was executed with an earlier version of this change + rename "$old_file.js", "$old_file.json" + if -e "$old_file.js" && !-e "$old_file.json"; + + # Read the new data/params.json file. + $param = read_param_file(); + } + + my %new_params; + + # If we didn't return any param values, then this is a new installation. + my $new_install = !(keys %$param); + + # --- UPDATE OLD PARAMS --- + + # Change from usebrowserinfo to defaultplatform/defaultopsys combo + if (exists $param->{'usebrowserinfo'}) { + if (!$param->{'usebrowserinfo'}) { + if (!exists $param->{'defaultplatform'}) { + $new_params{'defaultplatform'} = 'Other'; + } + if (!exists $param->{'defaultopsys'}) { + $new_params{'defaultopsys'} = 'Other'; + } } - else { - # Rename params.js to params.json if checksetup.pl - # was executed with an earlier version of this change - rename "$old_file.js", "$old_file.json" - if -e "$old_file.js" && !-e "$old_file.json"; - - # Read the new data/params.json file. - $param = read_param_file(); + } + + # Change from a boolean for quips to multi-state + if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) { + $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off'; + } + + # Change from old product groups to controls for group_control_map + # 2002-10-14 bug 147275 bugreport@peshkin.net + if (exists $param->{'usebuggroups'} && !exists $param->{'makeproductgroups'}) { + $new_params{'makeproductgroups'} = $param->{'usebuggroups'}; + } + + # Modularise auth code + if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) { + $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB"; + } + + # set verify method to whatever loginmethod was + if (exists $param->{'loginmethod'} && !exists $param->{'user_verify_class'}) { + $new_params{'user_verify_class'} = $param->{'loginmethod'}; + } + + # Remove quip-display control from parameters + # and give it to users via User Settings (Bug 41972) + if (exists $param->{'enablequips'} + && !exists $param->{'quip_list_entry_control'}) + { + my $new_value; + ($param->{'enablequips'} eq 'on') && do { $new_value = 'open'; }; + ($param->{'enablequips'} eq 'approved') && do { $new_value = 'moderated'; }; + ($param->{'enablequips'} eq 'frozen') && do { $new_value = 'closed'; }; + ($param->{'enablequips'} eq 'off') && do { $new_value = 'closed'; }; + $new_params{'quip_list_entry_control'} = $new_value; + } + + # Old mail_delivery_method choices contained no uppercase characters + my $mta = $param->{'mail_delivery_method'}; + if ($mta) { + if ($mta !~ /[A-Z]/) { + my %translation = ( + 'sendmail' => 'Sendmail', + 'smtp' => 'SMTP', + 'qmail' => 'Qmail', + 'testfile' => 'Test', + 'none' => 'None' + ); + $param->{'mail_delivery_method'} = $translation{$mta}; } - my %new_params; - - # If we didn't return any param values, then this is a new installation. - my $new_install = !(keys %$param); - - # --- UPDATE OLD PARAMS --- - - # Change from usebrowserinfo to defaultplatform/defaultopsys combo - if (exists $param->{'usebrowserinfo'}) { - if (!$param->{'usebrowserinfo'}) { - if (!exists $param->{'defaultplatform'}) { - $new_params{'defaultplatform'} = 'Other'; - } - if (!exists $param->{'defaultopsys'}) { - $new_params{'defaultopsys'} = 'Other'; - } - } + # This will force the parameter to be reset to its default value. + delete $param->{'mail_delivery_method'} + if $param->{'mail_delivery_method'} eq 'Qmail'; + } + + # 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') { + $new_params{'ssl_redirect'} = 1; + } + +# "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria". + if (exists $param->{'specific_search_allow_empty_words'}) { + $new_params{'search_allow_no_criteria'} + = $param->{'specific_search_allow_empty_words'}; + } + + # --- DEFAULTS FOR NEW PARAMS --- + + _load_params unless %params; + foreach my $name (keys %params) { + my $item = $params{$name}; + unless (exists $param->{$name}) { + print "New parameter: $name\n" unless $new_install; + if (exists $new_params{$name}) { + $param->{$name} = $new_params{$name}; + } + elsif (exists $answer->{$name}) { + $param->{$name} = $answer->{$name}; + } + else { + $param->{$name} = $item->{'default'}; + } } + } - # Change from a boolean for quips to multi-state - if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) { - $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off'; - } + $param->{'utf8'} = 1 if $new_install; - # Change from old product groups to controls for group_control_map - # 2002-10-14 bug 147275 bugreport@peshkin.net - if (exists $param->{'usebuggroups'} && - !exists $param->{'makeproductgroups'}) - { - $new_params{'makeproductgroups'} = $param->{'usebuggroups'}; - } + # Bug 452525: OR based groups are on by default for new installations + $param->{'or_groups'} = 1 if $new_install; - # Modularise auth code - if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) { - $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB"; - } + # --- REMOVE OLD PARAMS --- - # set verify method to whatever loginmethod was - if (exists $param->{'loginmethod'} - && !exists $param->{'user_verify_class'}) - { - $new_params{'user_verify_class'} = $param->{'loginmethod'}; - } + my %oldparams; - # Remove quip-display control from parameters - # and give it to users via User Settings (Bug 41972) - if ( exists $param->{'enablequips'} - && !exists $param->{'quip_list_entry_control'}) - { - my $new_value; - ($param->{'enablequips'} eq 'on') && do {$new_value = 'open';}; - ($param->{'enablequips'} eq 'approved') && do {$new_value = 'moderated';}; - ($param->{'enablequips'} eq 'frozen') && do {$new_value = 'closed';}; - ($param->{'enablequips'} eq 'off') && do {$new_value = 'closed';}; - $new_params{'quip_list_entry_control'} = $new_value; + # Remove any old params + foreach my $item (keys %$param) { + if (!exists $params{$item}) { + $oldparams{$item} = delete $param->{$item}; } - - # Old mail_delivery_method choices contained no uppercase characters - my $mta = $param->{'mail_delivery_method'}; - if ($mta) { - if ($mta !~ /[A-Z]/) { - my %translation = ( - 'sendmail' => 'Sendmail', - 'smtp' => 'SMTP', - 'qmail' => 'Qmail', - 'testfile' => 'Test', - 'none' => 'None'); - $param->{'mail_delivery_method'} = $translation{$mta}; - } - # This will force the parameter to be reset to its default value. - delete $param->{'mail_delivery_method'} if $param->{'mail_delivery_method'} eq 'Qmail'; + } + + # Write any old parameters to old-params.txt + my $old_param_file = "$datadir/old-params.txt"; + if (scalar(keys %oldparams)) { + my $op_file = new IO::File($old_param_file, '>>', 0600) + || die "Couldn't create $old_param_file: $!"; + + print "The following parameters are no longer used in Bugzilla,", + " and so have been\nmoved from your parameters file into", + " $old_param_file:\n"; + + my $comma = ""; + foreach my $item (keys %oldparams) { + print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n"; + print "${comma}$item"; + $comma = ", "; } + print "\n"; + $op_file->close; + } - # 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') { - $new_params{'ssl_redirect'} = 1; - } + write_params($param); - # "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria". - if (exists $param->{'specific_search_allow_empty_words'}) { - $new_params{'search_allow_no_criteria'} = $param->{'specific_search_allow_empty_words'}; - } + if (-e $old_file) { + unlink $old_file; + say "$old_file has been converted into $old_file.json, using the JSON format."; + } - # --- DEFAULTS FOR NEW PARAMS --- - - _load_params unless %params; - foreach my $name (keys %params) { - my $item = $params{$name}; - unless (exists $param->{$name}) { - print "New parameter: $name\n" unless $new_install; - if (exists $new_params{$name}) { - $param->{$name} = $new_params{$name}; - } - elsif (exists $answer->{$name}) { - $param->{$name} = $answer->{$name}; - } - else { - $param->{$name} = $item->{'default'}; - } - } - } - - $param->{'utf8'} = 1 if $new_install; - - # Bug 452525: OR based groups are on by default for new installations - $param->{'or_groups'} = 1 if $new_install; - - # --- REMOVE OLD PARAMS --- - - my %oldparams; - # Remove any old params - foreach my $item (keys %$param) { - if (!exists $params{$item}) { - $oldparams{$item} = delete $param->{$item}; - } - } - - # Write any old parameters to old-params.txt - my $old_param_file = "$datadir/old-params.txt"; - if (scalar(keys %oldparams)) { - my $op_file = new IO::File($old_param_file, '>>', 0600) - || die "Couldn't create $old_param_file: $!"; - - print "The following parameters are no longer used in Bugzilla,", - " and so have been\nmoved from your parameters file into", - " $old_param_file:\n"; - - my $comma = ""; - foreach my $item (keys %oldparams) { - print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n"; - print "${comma}$item"; - $comma = ", "; - } - print "\n"; - $op_file->close; - } - - write_params($param); - - if (-e $old_file) { - unlink $old_file; - say "$old_file has been converted into $old_file.json, using the JSON format."; - } - - # Return deleted params and values so that checksetup.pl has a chance - # to convert old params to new data. - return %oldparams; + # Return deleted params and values so that checksetup.pl has a chance + # to convert old params to new data. + return %oldparams; } sub write_params { - my ($param_data) = @_; - $param_data ||= Bugzilla->params; - my $param_file = bz_locations()->{'datadir'} . '/params.json'; + my ($param_data) = @_; + $param_data ||= Bugzilla->params; + my $param_file = bz_locations()->{'datadir'} . '/params.json'; - my $json_data = JSON::XS->new->canonical->pretty->encode($param_data); - write_text($param_file, $json_data); + my $json_data = JSON::XS->new->canonical->pretty->encode($param_data); + write_text($param_file, $json_data); - # It's not common to edit parameters and loading - # Bugzilla::Install::Filesystem is slow. - require Bugzilla::Install::Filesystem; - Bugzilla::Install::Filesystem::fix_file_permissions($param_file); + # It's not common to edit parameters and loading + # Bugzilla::Install::Filesystem is slow. + require Bugzilla::Install::Filesystem; + Bugzilla::Install::Filesystem::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}; + # And now we have to reset the params cache so that Bugzilla will re-read + # them. + delete Bugzilla->request_cache->{params}; } sub read_param_file { - my %params; - my $file = bz_locations()->{'datadir'} . '/params.json'; - - if (-e $file) { - my $data = read_text($file); - trick_taint($data); - - # If params.json has been manually edited and e.g. some quotes are - # missing, we don't want JSON::XS to leak the content of the file - # to all users in its error message, so we have to eval'uate it. - %params = eval { %{JSON::XS->new->decode($data)} }; - if ($@) { - my $error_msg = (basename($0) eq 'checksetup.pl') ? - $@ : 'run checksetup.pl to see the details.'; - die "Error parsing $file: $error_msg"; - } - # JSON::XS doesn't detaint data for us. - foreach my $key (keys %params) { - if (ref($params{$key}) eq "ARRAY") { - foreach my $item (@{$params{$key}}) { - trick_taint($item); - } - } else { - trick_taint($params{$key}) if defined $params{$key}; - } - } + my %params; + my $file = bz_locations()->{'datadir'} . '/params.json'; + + if (-e $file) { + my $data = read_text($file); + trick_taint($data); + + # If params.json has been manually edited and e.g. some quotes are + # missing, we don't want JSON::XS to leak the content of the file + # to all users in its error message, so we have to eval'uate it. + %params = eval { %{JSON::XS->new->decode($data)} }; + if ($@) { + my $error_msg + = (basename($0) eq 'checksetup.pl') + ? $@ + : 'run checksetup.pl to see the details.'; + die "Error parsing $file: $error_msg"; } - elsif ($ENV{'SERVER_SOFTWARE'}) { - # We're in a CGI, but the params file doesn't exist. We can't - # Template Toolkit, or even install_string, since checksetup - # might not have thrown an error. Bugzilla::CGI->new - # hasn't even been called yet, so we manually use CGI::Carp here - # so that the user sees the error. - require CGI::Carp; - CGI::Carp->import('fatalsToBrowser'); - die "The $file file does not exist." - . ' You probably need to run checksetup.pl.', + + # JSON::XS doesn't detaint data for us. + foreach my $key (keys %params) { + if (ref($params{$key}) eq "ARRAY") { + foreach my $item (@{$params{$key}}) { + trick_taint($item); + } + } + else { + trick_taint($params{$key}) if defined $params{$key}; + } } - return \%params; + } + elsif ($ENV{'SERVER_SOFTWARE'}) { + + # We're in a CGI, but the params file doesn't exist. We can't + # Template Toolkit, or even install_string, since checksetup + # might not have thrown an error. Bugzilla::CGI->new + # hasn't even been called yet, so we manually use CGI::Carp here + # so that the user sees the error. + require CGI::Carp; + CGI::Carp->import('fatalsToBrowser'); + die "The $file file does not exist." + . ' You probably need to run checksetup.pl.',; + } + return \%params; } 1; diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm index 41d929298..fe19d7cf0 100644 --- a/Bugzilla/Config/Admin.pm +++ b/Bugzilla/Config/Admin.pm @@ -16,32 +16,21 @@ use Bugzilla::Config::Common; our $sortkey = 200; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'allowbugdeletion', - type => 'b', - default => 0 - }, - - { - name => 'allowemailchange', - type => 'b', - default => 1 - }, - - { - name => 'allowuserdeletion', - type => 'b', - default => 0 - }, - - { - name => 'last_visit_keep_days', - type => 't', - default => 10, - checker => \&check_numeric - }); + {name => 'allowbugdeletion', type => 'b', default => 0}, + + {name => 'allowemailchange', type => 'b', default => 1}, + + {name => 'allowuserdeletion', type => 'b', default => 0}, + + { + name => 'last_visit_keep_days', + type => 't', + default => 10, + checker => \&check_numeric + } + ); return @param_list; } diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm index 8356c3361..043a892d7 100644 --- a/Bugzilla/Config/Advanced.pm +++ b/Bugzilla/Config/Advanced.pm @@ -16,31 +16,18 @@ use Bugzilla::Config::Common; our $sortkey = 1700; use constant get_param_list => ( - { - name => 'cookiedomain', - type => 't', - default => '' - }, + {name => 'cookiedomain', type => 't', default => ''}, - { - name => 'inbound_proxies', - type => 't', - default => '', - checker => \&check_ip - }, + {name => 'inbound_proxies', type => 't', default => '', checker => \&check_ip}, - { - name => 'proxy_url', - type => 't', - default => '' - }, + {name => 'proxy_url', type => 't', default => ''}, { - name => 'strict_transport_security', - type => 's', - choices => ['off', 'this_domain_only', 'include_subdomains'], - default => 'off', - checker => \&check_multi + name => 'strict_transport_security', + type => 's', + choices => ['off', 'this_domain_only', 'include_subdomains'], + default => 'off', + checker => \&check_multi }, ); diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm index 580ec46d9..0cf4b768a 100644 --- a/Bugzilla/Config/Attachment.pm +++ b/Bugzilla/Config/Attachment.pm @@ -16,48 +16,41 @@ use Bugzilla::Config::Common; our $sortkey = 400; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'allow_attachment_display', - type => 'b', - default => 0 - }, - - { - name => 'attachment_base', - type => 't', - default => '', - checker => \&check_urlbase - }, - - { - name => 'allow_attachment_deletion', - type => 'b', - default => 0 - }, - - { - name => 'maxattachmentsize', - type => 't', - default => '1000', - checker => \&check_maxattachmentsize - }, - - # 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 => 'maxlocalattachment', - type => 't', - default => '0', - checker => \&check_numeric - } ); + {name => 'allow_attachment_display', type => 'b', default => 0}, + + { + name => 'attachment_base', + type => 't', + default => '', + checker => \&check_urlbase + }, + + {name => 'allow_attachment_deletion', type => 'b', default => 0}, + + { + name => 'maxattachmentsize', + type => 't', + default => '1000', + checker => \&check_maxattachmentsize + }, + + # 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 => 'maxlocalattachment', + type => 't', + default => '0', + checker => \&check_numeric + } + ); return @param_list; } diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm index 78d719b15..09e81339f 100644 --- a/Bugzilla/Config/Auth.pm +++ b/Bugzilla/Config/Auth.pm @@ -16,111 +16,85 @@ use Bugzilla::Config::Common; our $sortkey = 300; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'auth_env_id', - type => 't', - default => '', - }, - - { - name => 'auth_env_email', - type => 't', - default => '', - }, - - { - name => 'auth_env_realname', - type => 't', - default => '', - }, - - # XXX in the future: - # - # user_verify_class and user_info_class should have choices gathered from - # whatever sits in their respective directories - # - # rather than comma-separated lists, these two should eventually become - # arrays, but that requires alterations to editparams first - - { - name => 'user_info_class', - type => 's', - choices => [ 'CGI', 'Env', 'Env,CGI' ], - default => 'CGI', - checker => \&check_multi - }, - - { - name => 'user_verify_class', - type => 'o', - choices => [ 'DB', 'RADIUS', 'LDAP' ], - default => 'DB', - checker => \&check_user_verify_class - }, - - { - name => 'rememberlogin', - type => 's', - choices => ['on', 'defaulton', 'defaultoff', 'off'], - default => 'on', - checker => \&check_multi - }, - - { - name => 'requirelogin', - type => 'b', - default => '0' - }, - - { - name => 'webservice_email_filter', - type => 'b', - default => 0 - }, - - { - name => 'emailregexp', - type => 't', - default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:, - checker => \&check_regexp - }, - - { - name => 'emailregexpdesc', - type => 'l', - default => 'A legal address must contain exactly one \'@\', and at least ' . - 'one \'.\' after the @.' - }, - - { - name => 'emailsuffix', - type => 't', - default => '' - }, - - { - name => 'createemailregexp', - type => 't', - default => q:.*:, - checker => \&check_regexp - }, - - { - name => 'password_complexity', - type => 's', - choices => [ 'no_constraints', 'mixed_letters', 'letters_numbers', - 'letters_numbers_specialchars' ], - default => 'no_constraints', - checker => \&check_multi - }, - - { - name => 'password_check_on_login', - type => 'b', - default => '1' - }, + {name => 'auth_env_id', type => 't', default => '',}, + + {name => 'auth_env_email', type => 't', default => '',}, + + {name => 'auth_env_realname', type => 't', default => '',}, + + # XXX in the future: + # + # user_verify_class and user_info_class should have choices gathered from + # whatever sits in their respective directories + # + # rather than comma-separated lists, these two should eventually become + # arrays, but that requires alterations to editparams first + + { + name => 'user_info_class', + type => 's', + choices => ['CGI', 'Env', 'Env,CGI'], + default => 'CGI', + checker => \&check_multi + }, + + { + name => 'user_verify_class', + type => 'o', + choices => ['DB', 'RADIUS', 'LDAP'], + default => 'DB', + checker => \&check_user_verify_class + }, + + { + name => 'rememberlogin', + type => 's', + choices => ['on', 'defaulton', 'defaultoff', 'off'], + default => 'on', + checker => \&check_multi + }, + + {name => 'requirelogin', type => 'b', default => '0'}, + + {name => 'webservice_email_filter', type => 'b', default => 0}, + + { + name => 'emailregexp', + type => 't', + default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:, + checker => \&check_regexp + }, + + { + name => 'emailregexpdesc', + type => 'l', + default => 'A legal address must contain exactly one \'@\', and at least ' + . 'one \'.\' after the @.' + }, + + {name => 'emailsuffix', type => 't', default => ''}, + + { + name => 'createemailregexp', + type => 't', + default => q:.*:, + checker => \&check_regexp + }, + + { + name => 'password_complexity', + type => 's', + choices => [ + 'no_constraints', 'mixed_letters', + 'letters_numbers', 'letters_numbers_specialchars' + ], + default => 'no_constraints', + checker => \&check_multi + }, + + {name => 'password_check_on_login', type => 'b', default => '1'}, ); return @param_list; } diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm index 0acdc0ce4..ad1cafefc 100644 --- a/Bugzilla/Config/BugChange.pm +++ b/Bugzilla/Config/BugChange.pm @@ -26,55 +26,33 @@ sub get_param_list { # and bug_status.is_open is not yet defined (hence the eval), so we use # the bug statuses above as they are still hardcoded. eval { - my @current_closed_states = map {$_->name} closed_bug_statuses(); - # If no closed state was found, use the default list above. - @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states); + my @current_closed_states = map { $_->name } closed_bug_statuses(); + + # If no closed state was found, use the default list above. + @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states); }; my @param_list = ( - { - name => 'duplicate_or_move_bug_status', - type => 's', - choices => \@closed_bug_statuses, - default => $closed_bug_statuses[0], - checker => \&check_bug_status - }, - - { - name => 'letsubmitterchoosepriority', - type => 'b', - default => 1 - }, - - { - name => 'letsubmitterchoosemilestone', - type => 'b', - default => 1 - }, - - { - name => 'musthavemilestoneonaccept', - type => 'b', - default => 0 - }, - - { - name => 'commentonchange_resolution', - type => 'b', - default => 0 - }, - - { - name => 'commentonduplicate', - type => 'b', - default => 0 - }, - - { - name => 'noresolveonopenblockers', - type => 'b', - default => 0, - } ); + { + name => 'duplicate_or_move_bug_status', + type => 's', + choices => \@closed_bug_statuses, + default => $closed_bug_statuses[0], + checker => \&check_bug_status + }, + + {name => 'letsubmitterchoosepriority', type => 'b', default => 1}, + + {name => 'letsubmitterchoosemilestone', type => 'b', default => 1}, + + {name => 'musthavemilestoneonaccept', type => 'b', default => 0}, + + {name => 'commentonchange_resolution', type => 'b', default => 0}, + + {name => 'commentonduplicate', type => 'b', default => 0}, + + {name => 'noresolveonopenblockers', type => 'b', default => 0,} + ); return @param_list; } diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm index ef2faa64b..1659dc66a 100644 --- a/Bugzilla/Config/BugFields.pm +++ b/Bugzilla/Config/BugFields.pm @@ -25,73 +25,50 @@ sub get_param_list { my @legal_OS = @{get_legal_field_values('op_sys')}; my @param_list = ( - { - name => 'useclassification', - type => 'b', - default => 0 - }, - - { - name => 'usetargetmilestone', - type => 'b', - default => 0 - }, - - { - name => 'useqacontact', - type => 'b', - default => 0 - }, - - { - name => 'usestatuswhiteboard', - type => 'b', - default => 0 - }, - - { - name => 'use_see_also', - type => 'b', - default => 1 - }, - - { - name => 'defaultpriority', - type => 's', - choices => \@legal_priorities, - default => $legal_priorities[-1], - checker => \&check_priority - }, - - { - name => 'defaultseverity', - type => 's', - choices => \@legal_severities, - default => $legal_severities[-1], - checker => \&check_severity - }, - - { - name => 'defaultplatform', - type => 's', - choices => ['', @legal_platforms], - default => '', - checker => \&check_platform - }, - - { - name => 'defaultopsys', - type => 's', - choices => ['', @legal_OS], - default => '', - checker => \&check_opsys - }, - - { - name => 'collapsed_comment_tags', - type => 't', - default => 'obsolete, spam', - }); + {name => 'useclassification', type => 'b', default => 0}, + + {name => 'usetargetmilestone', type => 'b', default => 0}, + + {name => 'useqacontact', type => 'b', default => 0}, + + {name => 'usestatuswhiteboard', type => 'b', default => 0}, + + {name => 'use_see_also', type => 'b', default => 1}, + + { + name => 'defaultpriority', + type => 's', + choices => \@legal_priorities, + default => $legal_priorities[-1], + checker => \&check_priority + }, + + { + name => 'defaultseverity', + type => 's', + choices => \@legal_severities, + default => $legal_severities[-1], + checker => \&check_severity + }, + + { + name => 'defaultplatform', + type => 's', + choices => ['', @legal_platforms], + default => '', + checker => \&check_platform + }, + + { + name => 'defaultopsys', + type => 's', + choices => ['', @legal_OS], + default => '', + checker => \&check_opsys + }, + + {name => 'collapsed_comment_tags', type => 't', default => 'obsolete, spam',} + ); return @param_list; } diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm index bd9b0bf84..756dbb0dd 100644 --- a/Bugzilla/Config/Common.pm +++ b/Bugzilla/Config/Common.pm @@ -21,392 +21,406 @@ use Bugzilla::Group; use Bugzilla::Status; use parent qw(Exporter); -@Bugzilla::Config::Common::EXPORT = - 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_user_verify_class check_ip check_font_file - check_mail_delivery_method check_notification check_utf8 - check_bug_status check_smtp_auth check_theschwartz_available - check_maxattachmentsize check_email check_smtp_ssl - check_comment_taggers_group check_smtp_server +@Bugzilla::Config::Common::EXPORT + = 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_user_verify_class check_ip check_font_file + check_mail_delivery_method check_notification check_utf8 + check_bug_status check_smtp_auth check_theschwartz_available + check_maxattachmentsize check_email check_smtp_ssl + check_comment_taggers_group check_smtp_server ); # Checking functions for the various values sub check_multi { - my ($value, $param) = (@_); + my ($value, $param) = (@_); - if ($param->{'type'} eq "s") { - unless (scalar(grep {$_ eq $value} (@{$param->{'choices'}}))) { - return "Invalid choice '$value' for single-select list param '$param->{'name'}'"; - } - - return ""; + if ($param->{'type'} eq "s") { + unless (scalar(grep { $_ eq $value } (@{$param->{'choices'}}))) { + return + "Invalid choice '$value' for single-select list param '$param->{'name'}'"; } - elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') { - if (ref($value) ne "ARRAY") { - $value = [split(',', $value)] - } - foreach my $chkParam (@$value) { - unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) { - return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'"; - } - } - - return ""; + + return ""; + } + elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') { + if (ref($value) ne "ARRAY") { + $value = [split(',', $value)]; } - else { - return "Invalid param type '$param->{'type'}' for check_multi(); " . - "contact your Bugzilla administrator"; + foreach my $chkParam (@$value) { + unless (scalar(grep { $_ eq $chkParam } (@{$param->{'choices'}}))) { + return + "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'"; + } } + + return ""; + } + else { + return "Invalid param type '$param->{'type'}' for check_multi(); " + . "contact your Bugzilla administrator"; + } } sub check_numeric { - my ($value) = (@_); - if ($value !~ /^[0-9]+$/) { - return "must be a numeric value"; - } - return ""; + my ($value) = (@_); + if ($value !~ /^[0-9]+$/) { + return "must be a numeric value"; + } + return ""; } sub check_regexp { - my ($value) = (@_); - eval { qr/$value/ }; - return $@; + my ($value) = (@_); + eval {qr/$value/}; + return $@; } sub check_email { - my ($value) = @_; - if ($value !~ $Email::Address::mailbox) { - return "must be a valid email address."; - } - return ""; + my ($value) = @_; + if ($value !~ $Email::Address::mailbox) { + return "must be a valid email address."; + } + return ""; } sub check_sslbase { - my $url = shift; - if ($url ne '') { - if ($url !~ m#^https://([^/]+).*/$#) { - return "must be a legal URL, that starts with https and ends with a slash."; - } - my $host = $1; - # Fall back to port 443 if for some reason getservbyname() fails. - my $port = getservbyname('https', 'tcp') || 443; - if ($host =~ /^(.+):(\d+)$/) { - $host = $1; - $port = $2; - } - local *SOCK; - my $proto = getprotobyname('tcp'); - socket(SOCK, PF_INET, SOCK_STREAM, $proto); - my $iaddr = inet_aton($host) || return "The host $host cannot be resolved"; - my $sin = sockaddr_in($port, $iaddr); - if (!connect(SOCK, $sin)) { - return "Failed to connect to $host:$port ($!); unable to enable SSL"; - } - close(SOCK); - } - return ""; + my $url = shift; + if ($url ne '') { + if ($url !~ m#^https://([^/]+).*/$#) { + return "must be a legal URL, that starts with https and ends with a slash."; + } + my $host = $1; + + # Fall back to port 443 if for some reason getservbyname() fails. + my $port = getservbyname('https', 'tcp') || 443; + if ($host =~ /^(.+):(\d+)$/) { + $host = $1; + $port = $2; + } + local *SOCK; + my $proto = getprotobyname('tcp'); + socket(SOCK, PF_INET, SOCK_STREAM, $proto); + my $iaddr = inet_aton($host) || return "The host $host cannot be resolved"; + my $sin = sockaddr_in($port, $iaddr); + if (!connect(SOCK, $sin)) { + return "Failed to connect to $host:$port ($!); unable to enable SSL"; + } + close(SOCK); + } + return ""; } sub check_ip { - my $inbound_proxies = shift; - my @proxies = split(/[\s,]+/, $inbound_proxies); - foreach my $proxy (@proxies) { - validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address"; - } - return ""; + my $inbound_proxies = shift; + my @proxies = split(/[\s,]+/, $inbound_proxies); + foreach my $proxy (@proxies) { + validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address"; + } + return ""; } sub check_utf8 { - my $utf8 = shift; - # You cannot turn off the UTF-8 parameter if you've already converted - # your tables to utf-8. - my $dbh = Bugzilla->dbh; - if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) { - return "You cannot disable UTF-8 support, because your MySQL database" - . " is encoded in UTF-8"; - } - return ""; + my $utf8 = shift; + + # You cannot turn off the UTF-8 parameter if you've already converted + # your tables to utf-8. + my $dbh = Bugzilla->dbh; + if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) { + return "You cannot disable UTF-8 support, because your MySQL database" + . " is encoded in UTF-8"; + } + return ""; } sub check_priority { - my ($value) = (@_); - my $legal_priorities = get_legal_field_values('priority'); - if (!grep($_ eq $value, @$legal_priorities)) { - return "Must be a legal priority value: one of " . - join(", ", @$legal_priorities); - } - return ""; + my ($value) = (@_); + my $legal_priorities = get_legal_field_values('priority'); + if (!grep($_ eq $value, @$legal_priorities)) { + return "Must be a legal priority value: one of " + . join(", ", @$legal_priorities); + } + return ""; } sub check_severity { - my ($value) = (@_); - my $legal_severities = get_legal_field_values('bug_severity'); - if (!grep($_ eq $value, @$legal_severities)) { - return "Must be a legal severity value: one of " . - join(", ", @$legal_severities); - } - return ""; + my ($value) = (@_); + my $legal_severities = get_legal_field_values('bug_severity'); + if (!grep($_ eq $value, @$legal_severities)) { + return "Must be a legal severity value: one of " + . join(", ", @$legal_severities); + } + return ""; } sub check_platform { - my ($value) = (@_); - my $legal_platforms = get_legal_field_values('rep_platform'); - if (!grep($_ eq $value, '', @$legal_platforms)) { - return "Must be empty or a legal platform value: one of " . - join(", ", @$legal_platforms); - } - return ""; + my ($value) = (@_); + my $legal_platforms = get_legal_field_values('rep_platform'); + if (!grep($_ eq $value, '', @$legal_platforms)) { + return "Must be empty or a legal platform value: one of " + . join(", ", @$legal_platforms); + } + return ""; } sub check_opsys { - my ($value) = (@_); - my $legal_OS = get_legal_field_values('op_sys'); - if (!grep($_ eq $value, '', @$legal_OS)) { - return "Must be empty or a legal operating system value: one of " . - join(", ", @$legal_OS); - } - return ""; + my ($value) = (@_); + my $legal_OS = get_legal_field_values('op_sys'); + if (!grep($_ eq $value, '', @$legal_OS)) { + return "Must be empty or a legal operating system value: one of " + . join(", ", @$legal_OS); + } + return ""; } sub check_bug_status { - my $bug_status = shift; - my @closed_bug_statuses = map {$_->name} closed_bug_statuses(); - if (!grep($_ eq $bug_status, @closed_bug_statuses)) { - return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses); - } - return ""; + my $bug_status = shift; + my @closed_bug_statuses = map { $_->name } closed_bug_statuses(); + if (!grep($_ eq $bug_status, @closed_bug_statuses)) { + return "Must be a valid closed status: one of " + . join(', ', @closed_bug_statuses); + } + return ""; } sub check_group { - my $group_name = shift; - return "" unless $group_name; - my $group = new Bugzilla::Group({'name' => $group_name}); - unless (defined $group) { - return "Must be an existing group name"; - } - return ""; + my $group_name = shift; + return "" unless $group_name; + my $group = new Bugzilla::Group({'name' => $group_name}); + unless (defined $group) { + return "Must be an existing group name"; + } + return ""; } sub check_shadowdb { - my ($value) = (@_); - $value = trim($value); - if ($value eq "") { - return ""; - } + my ($value) = (@_); + $value = trim($value); + if ($value eq "") { + return ""; + } - if (!Bugzilla->params->{'shadowdbhost'}) { - return "You need to specify a host when using a shadow database"; - } + if (!Bugzilla->params->{'shadowdbhost'}) { + return "You need to specify a host when using a shadow database"; + } - # Can't test existence of this because ConnectToDatabase uses the param, - # but we can't set this before testing.... - # This can really only be fixed after we can use the DBI more openly - return ""; + # Can't test existence of this because ConnectToDatabase uses the param, + # but we can't set this before testing.... + # This can really only be fixed after we can use the DBI more openly + return ""; } sub check_urlbase { - my ($url) = (@_); - if ($url && $url !~ m:^http.*/$:) { - return "must be a legal URL, that starts with http and ends with a slash."; - } - return ""; + my ($url) = (@_); + if ($url && $url !~ m:^http.*/$:) { + return "must be a legal URL, that starts with http and ends with a slash."; + } + return ""; } sub check_url { - my ($url) = (@_); - return '' if $url eq ''; # Allow empty URLs - if ($url !~ m:/$:) { - return 'must be a legal URL, absolute or relative, ending with a slash.'; - } - return ''; + my ($url) = (@_); + return '' if $url eq ''; # Allow empty URLs + if ($url !~ m:/$:) { + return 'must be a legal URL, absolute or relative, ending with a slash.'; + } + return ''; } sub check_webdotbase { - my ($value) = (@_); - $value = trim($value); - if ($value eq "") { - return ""; - } - if($value !~ /^https?:/) { - if(! -x $value) { - return "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally."; - } - # Check .htaccess allows access to generated images - my $webdotdir = bz_locations()->{'webdotdir'}; - if(-e "$webdotdir/.htaccess") { - open HTACCESS, "<", "$webdotdir/.htaccess"; - if(! grep(/ \\\.png\$/,<HTACCESS>)) { - return "Dependency graph images are not accessible.\nAssuming that you have not modified the file, delete $webdotdir/.htaccess and re-run checksetup.pl to rectify.\n"; - } - close HTACCESS; - } - } + my ($value) = (@_); + $value = trim($value); + if ($value eq "") { return ""; + } + if ($value !~ /^https?:/) { + if (!-x $value) { + return + "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally."; + } + + # Check .htaccess allows access to generated images + my $webdotdir = bz_locations()->{'webdotdir'}; + if (-e "$webdotdir/.htaccess") { + open HTACCESS, "<", "$webdotdir/.htaccess"; + if (!grep(/ \\\.png\$/, <HTACCESS>)) { + return + "Dependency graph images are not accessible.\nAssuming that you have not modified the file, delete $webdotdir/.htaccess and re-run checksetup.pl to rectify.\n"; + } + close HTACCESS; + } + } + return ""; } sub check_font_file { - my ($font) = @_; - $font = trim($font); - return '' unless $font; - - if ($font !~ /\.(ttf|otf)$/) { - return "The file must point to a TrueType or OpenType font file (its extension must be .ttf or .otf)" - } - if (! -f $font) { - return "The file '$font' cannot be found. Make sure you typed the full path to the file" - } - return ''; + my ($font) = @_; + $font = trim($font); + return '' unless $font; + + if ($font !~ /\.(ttf|otf)$/) { + return + "The file must point to a TrueType or OpenType font file (its extension must be .ttf or .otf)"; + } + if (!-f $font) { + return + "The file '$font' cannot be found. Make sure you typed the full path to the file"; + } + 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 - # other params, it must be below that other one. So you can't have two - # params mutually dependent on each other. - # This means that if someone clears the LDAP config params after setting - # 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') { - 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') { - 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 ""; + + # 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 + # other params, it must be below that other one. So you can't have two + # params mutually dependent on each other. + # This means that if someone clears the LDAP config params after setting + # 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') { + 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') { + 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_mail_delivery_method { - my $check = check_multi(@_); - return $check if $check; - my $mailer = shift; - if ($mailer eq 'Sendmail' and ON_WINDOWS) { - # look for sendmail.exe - return "Failed to locate " . SENDMAIL_EXE - unless -e SENDMAIL_EXE; - } - return ""; + my $check = check_multi(@_); + return $check if $check; + my $mailer = shift; + if ($mailer eq 'Sendmail' and ON_WINDOWS) { + + # look for sendmail.exe + return "Failed to locate " . SENDMAIL_EXE unless -e SENDMAIL_EXE; + } + return ""; } sub check_maxattachmentsize { - my $check = check_numeric(@_); - return $check if $check; - my $size = shift; - my $dbh = Bugzilla->dbh; - if ($dbh->isa('Bugzilla::DB::Mysql')) { - my (undef, $max_packet) = $dbh->selectrow_array( - q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); - my $byte_size = $size * 1024; - if ($max_packet < $byte_size) { - return "You asked for a maxattachmentsize of $byte_size bytes," - . " but the max_allowed_packet setting in MySQL currently" - . " only allows packets up to $max_packet bytes"; - } - } - return ""; + my $check = check_numeric(@_); + return $check if $check; + my $size = shift; + my $dbh = Bugzilla->dbh; + if ($dbh->isa('Bugzilla::DB::Mysql')) { + my (undef, $max_packet) + = $dbh->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); + my $byte_size = $size * 1024; + if ($max_packet < $byte_size) { + return + "You asked for a maxattachmentsize of $byte_size bytes," + . " but the max_allowed_packet setting in MySQL currently" + . " only allows packets up to $max_packet bytes"; + } + } + return ""; } sub check_notification { - my $option = shift; - my @current_version = - (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); - if ($current_version[1] % 2 && $option eq 'stable_branch_release') { - return "You are currently running a development snapshot, and so your " . - "installation is not based on a branch. If you want to be notified " . - "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 ""; + my $option = shift; + my @current_version + = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + if ($current_version[1] % 2 && $option eq 'stable_branch_release') { + return + "You are currently running a development snapshot, and so your " + . "installation is not based on a branch. If you want to be notified " + . "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_server { - my $host = shift; - my $port; - - return '' unless $host; - - if ($host =~ /:/) { - ($host, $port) = split(/:/, $host, 2); - unless ($port && detaint_natural($port)) { - return "Invalid port. It must be an integer (typically 25, 465 or 587)"; - } - } - trick_taint($host); - # Let's first try to connect using SSL. If this fails, we fall back to - # an unencrypted connection. - foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) { - my ($class, $default_port) = @$method; - next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl'); - eval "require $class"; - my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5); - if ($smtp) { - # The connection works! - $smtp->quit; - return ''; - } - } - return "Cannot connect to $host" . ($port ? " using port $port" : ""); + my $host = shift; + my $port; + + return '' unless $host; + + if ($host =~ /:/) { + ($host, $port) = split(/:/, $host, 2); + unless ($port && detaint_natural($port)) { + return "Invalid port. It must be an integer (typically 25, 465 or 587)"; + } + } + trick_taint($host); + + # Let's first try to connect using SSL. If this fails, we fall back to + # an unencrypted connection. + foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) { + my ($class, $default_port) = @$method; + next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl'); + eval "require $class"; + my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5); + if ($smtp) { + + # The connection works! + $smtp->quit; + return ''; + } + } + return "Cannot connect to $host" . ($port ? " using port $port" : ""); } sub check_smtp_auth { - my $username = shift; - if ($username and !Bugzilla->feature('smtp_auth')) { - return "SMTP Authentication is not available. Run checksetup.pl for" - . " more details"; - } - return ""; + my $username = shift; + if ($username and !Bugzilla->feature('smtp_auth')) { + return "SMTP Authentication is not available. Run checksetup.pl for" + . " more details"; + } + return ""; } sub check_smtp_ssl { - my $use_ssl = shift; - if ($use_ssl && !Bugzilla->feature('smtp_ssl')) { - return "SSL support is not available. Run checksetup.pl for more details"; - } - return ""; + my $use_ssl = shift; + if ($use_ssl && !Bugzilla->feature('smtp_ssl')) { + return "SSL support is not available. Run checksetup.pl for more details"; + } + return ""; } sub check_theschwartz_available { - 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"; - } - return ""; + 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"; + } + return ""; } sub check_comment_taggers_group { - my $group_name = shift; - if ($group_name && !Bugzilla->feature('jsonrpc')) { - return "Comment tagging requires installation of the JSONRPC feature"; - } - return check_group($group_name); + my $group_name = shift; + if ($group_name && !Bugzilla->feature('jsonrpc')) { + return "Comment tagging requires installation of the JSONRPC feature"; + } + return check_group($group_name); } # OK, here are the parameter definitions themselves. @@ -467,13 +481,13 @@ sub check_comment_taggers_group { # } # # Here, 'b' is the default option, and 'a' and 'c' are other possible -# options, but only one at a time! +# options, but only one at a time! # # &check_multi should always be used as the param verification function # for list (single and multiple) parameter types. sub get_param_list { - return; + return; } 1; diff --git a/Bugzilla/Config/Core.pm b/Bugzilla/Config/Core.pm index 654e569ba..50af9a077 100644 --- a/Bugzilla/Config/Core.pm +++ b/Bugzilla/Config/Core.pm @@ -16,31 +16,13 @@ use Bugzilla::Config::Common; our $sortkey = 100; use constant get_param_list => ( - { - name => 'urlbase', - type => 't', - default => '', - checker => \&check_urlbase - }, - - { - name => 'ssl_redirect', - type => 'b', - default => 0 - }, - - { - name => 'sslbase', - type => 't', - default => '', - checker => \&check_sslbase - }, - - { - name => 'cookiepath', - type => 't', - default => '/' - }, + {name => 'urlbase', type => 't', default => '', checker => \&check_urlbase}, + + {name => 'ssl_redirect', type => 'b', default => 0}, + + {name => 'sslbase', type => 't', default => '', checker => \&check_sslbase}, + + {name => 'cookiepath', type => 't', default => '/'}, ); 1; diff --git a/Bugzilla/Config/DependencyGraph.pm b/Bugzilla/Config/DependencyGraph.pm index c815822f3..27bc9938d 100644 --- a/Bugzilla/Config/DependencyGraph.pm +++ b/Bugzilla/Config/DependencyGraph.pm @@ -16,21 +16,17 @@ use Bugzilla::Config::Common; our $sortkey = 800; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'webdotbase', - type => 't', - default => '', - checker => \&check_webdotbase - }, + { + name => 'webdotbase', + type => 't', + default => '', + checker => \&check_webdotbase + }, - { - name => 'font_file', - type => 't', - default => '', - checker => \&check_font_file - }); + {name => 'font_file', type => 't', default => '', checker => \&check_font_file} + ); return @param_list; } diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm index 380680590..322275aa0 100644 --- a/Bugzilla/Config/General.pm +++ b/Bugzilla/Config/General.pm @@ -17,39 +17,28 @@ our $sortkey = 150; use constant get_param_list => ( { - name => 'maintainer', - type => 't', - no_reset => '1', - default => '', - checker => \&check_email + name => 'maintainer', + type => 't', + no_reset => '1', + default => '', + checker => \&check_email }, - { - name => 'utf8', - type => 'b', - default => '0', - checker => \&check_utf8 - }, + {name => 'utf8', type => 'b', default => '0', checker => \&check_utf8}, - { - name => 'shutdownhtml', - type => 'l', - default => '' - }, + {name => 'shutdownhtml', type => 'l', default => ''}, - { - name => 'announcehtml', - 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 + name => 'upgrade_notification', + type => 's', + choices => [ + 'development_snapshot', 'latest_stable_release', + 'stable_branch_release', 'disabled' + ], + default => 'latest_stable_release', + checker => \&check_notification }, ); diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm index e827834a0..6602cdfea 100644 --- a/Bugzilla/Config/GroupSecurity.pm +++ b/Bugzilla/Config/GroupSecurity.pm @@ -20,84 +20,69 @@ sub get_param_list { my $class = shift; my @param_list = ( - { - name => 'makeproductgroups', - type => 'b', - default => 0 - }, - - { - name => 'chartgroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'insidergroup', - type => 's', - choices => \&_get_all_group_names, - default => '', - checker => \&check_group - }, - - { - name => 'timetrackinggroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'querysharegroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'comment_taggers_group', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_comment_taggers_group - }, - - { - name => 'debug_group', - type => 's', - choices => \&_get_all_group_names, - default => 'admin', - checker => \&check_group - }, - - { - name => 'usevisibilitygroups', - type => 'b', - default => 0 - }, - - { - name => 'strict_isolation', - type => 'b', - default => 0 - }, - - { - name => 'or_groups', - type => 'b', - default => 0 - } ); + {name => 'makeproductgroups', type => 'b', default => 0}, + + { + name => 'chartgroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'insidergroup', + type => 's', + choices => \&_get_all_group_names, + default => '', + checker => \&check_group + }, + + { + name => 'timetrackinggroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'querysharegroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'comment_taggers_group', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_comment_taggers_group + }, + + { + name => 'debug_group', + type => 's', + choices => \&_get_all_group_names, + default => 'admin', + checker => \&check_group + }, + + {name => 'usevisibilitygroups', type => 'b', default => 0}, + + {name => 'strict_isolation', type => 'b', default => 0}, + + {name => 'or_groups', type => 'b', default => 0} + ); return @param_list; } sub _get_all_group_names { - my @group_names = map {$_->name} Bugzilla::Group->get_all; - unshift(@group_names, ''); - return \@group_names; + my @group_names = map { $_->name } Bugzilla::Group->get_all; + unshift(@group_names, ''); + return \@group_names; } 1; diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm index 0bc8240df..75f58e141 100644 --- a/Bugzilla/Config/LDAP.pm +++ b/Bugzilla/Config/LDAP.pm @@ -16,49 +16,22 @@ use Bugzilla::Config::Common; our $sortkey = 1000; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'LDAPserver', - type => 't', - default => '' - }, + {name => 'LDAPserver', type => 't', default => ''}, - { - name => 'LDAPstarttls', - type => 'b', - default => 0 - }, + {name => 'LDAPstarttls', type => 'b', default => 0}, - { - name => 'LDAPbinddn', - type => 't', - default => '' - }, + {name => 'LDAPbinddn', type => 't', default => ''}, - { - name => 'LDAPBaseDN', - type => 't', - default => '' - }, + {name => 'LDAPBaseDN', type => 't', default => ''}, - { - name => 'LDAPuidattribute', - type => 't', - default => 'uid' - }, + {name => 'LDAPuidattribute', type => 't', default => 'uid'}, - { - name => 'LDAPmailattribute', - type => 't', - default => 'mail' - }, + {name => 'LDAPmailattribute', type => 't', default => 'mail'}, - { - name => 'LDAPfilter', - type => 't', - default => '', - } ); + {name => 'LDAPfilter', type => 't', default => '',} + ); return @param_list; } diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm index 467bdab3f..c7f8e5057 100644 --- a/Bugzilla/Config/MTA.pm +++ b/Bugzilla/Config/MTA.pm @@ -16,68 +16,43 @@ use Bugzilla::Config::Common; our $sortkey = 1200; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'mail_delivery_method', - type => 's', - choices => ['Sendmail', 'SMTP', 'Test', 'None'], - default => 'Sendmail', - checker => \&check_mail_delivery_method - }, - - { - name => 'mailfrom', - type => 't', - default => 'bugzilla-daemon' - }, - - { - name => 'use_mailer_queue', - type => 'b', - default => 0, - checker => \&check_theschwartz_available, - }, - - { - name => 'smtpserver', - type => 't', - default => 'localhost', - checker => \&check_smtp_server - }, - { - name => 'smtp_username', - type => 't', - default => '', - checker => \&check_smtp_auth - }, - { - name => 'smtp_password', - type => 'p', - default => '' - }, - { - name => 'smtp_ssl', - type => 'b', - default => 0, - checker => \&check_smtp_ssl - }, - { - name => 'smtp_debug', - type => 'b', - default => 0 - }, - { - name => 'whinedays', - type => 't', - default => 7, - checker => \&check_numeric - }, - { - name => 'globalwatchers', - type => 't', - default => '', - }, ); + { + name => 'mail_delivery_method', + type => 's', + choices => ['Sendmail', 'SMTP', 'Test', 'None'], + default => 'Sendmail', + checker => \&check_mail_delivery_method + }, + + {name => 'mailfrom', type => 't', default => 'bugzilla-daemon'}, + + { + name => 'use_mailer_queue', + type => 'b', + default => 0, + checker => \&check_theschwartz_available, + }, + + { + name => 'smtpserver', + type => 't', + default => 'localhost', + checker => \&check_smtp_server + }, + { + name => 'smtp_username', + type => 't', + default => '', + checker => \&check_smtp_auth + }, + {name => 'smtp_password', type => 'p', default => ''}, + {name => 'smtp_ssl', type => 'b', default => 0, checker => \&check_smtp_ssl}, + {name => 'smtp_debug', type => 'b', default => 0}, + {name => 'whinedays', type => 't', default => 7, checker => \&check_numeric}, + {name => 'globalwatchers', type => 't', default => '',}, + ); return @param_list; } diff --git a/Bugzilla/Config/Memcached.pm b/Bugzilla/Config/Memcached.pm index 292803d86..5ab3364f9 100644 --- a/Bugzilla/Config/Memcached.pm +++ b/Bugzilla/Config/Memcached.pm @@ -17,16 +17,8 @@ our $sortkey = 1550; sub get_param_list { return ( - { - name => 'memcached_servers', - type => 't', - default => '' - }, - { - name => 'memcached_namespace', - type => 't', - default => 'bugzilla:', - }, + {name => 'memcached_servers', type => 't', default => ''}, + {name => 'memcached_namespace', type => 't', default => 'bugzilla:',}, ); } diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm index f18bb90df..adfb4eaf4 100644 --- a/Bugzilla/Config/Query.pm +++ b/Bugzilla/Config/Query.pm @@ -16,47 +16,45 @@ use Bugzilla::Config::Common; our $sortkey = 1400; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'quip_list_entry_control', - type => 's', - choices => ['open', 'moderated', 'closed'], - default => 'open', - checker => \&check_multi - }, - - { - name => 'mybugstemplate', - type => 't', - default => 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%' - }, - - { - name => 'defaultquery', - type => 't', - default => 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring' - }, - - { - name => 'search_allow_no_criteria', - type => 'b', - default => 1 - }, - - { - name => 'default_search_limit', - type => 't', - default => '500', - checker => \&check_numeric - }, - - { - name => 'max_search_results', - type => 't', - default => '10000', - checker => \&check_numeric - }, + { + name => 'quip_list_entry_control', + type => 's', + choices => ['open', 'moderated', 'closed'], + default => 'open', + checker => \&check_multi + }, + + { + name => 'mybugstemplate', + type => 't', + default => + 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%' + }, + + { + name => 'defaultquery', + type => 't', + default => + 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring' + }, + + {name => 'search_allow_no_criteria', type => 'b', default => 1}, + + { + name => 'default_search_limit', + type => 't', + default => '500', + checker => \&check_numeric + }, + + { + name => 'max_search_results', + type => 't', + default => '10000', + checker => \&check_numeric + }, ); return @param_list; } diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm index 8e30b07a9..b0a5ddbf5 100644 --- a/Bugzilla/Config/RADIUS.pm +++ b/Bugzilla/Config/RADIUS.pm @@ -16,31 +16,15 @@ use Bugzilla::Config::Common; our $sortkey = 1100; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'RADIUS_server', - type => 't', - default => '' - }, - - { - name => 'RADIUS_secret', - type => 't', - default => '' - }, - - { - name => 'RADIUS_NAS_IP', - type => 't', - default => '' - }, - - { - name => 'RADIUS_email_suffix', - type => 't', - default => '' - }, + {name => 'RADIUS_server', type => 't', default => ''}, + + {name => 'RADIUS_secret', type => 't', default => ''}, + + {name => 'RADIUS_NAS_IP', type => 't', default => ''}, + + {name => 'RADIUS_email_suffix', type => 't', default => ''}, ); return @param_list; } diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm index 5dbbb5202..101e4678f 100644 --- a/Bugzilla/Config/ShadowDB.pm +++ b/Bugzilla/Config/ShadowDB.pm @@ -16,35 +16,23 @@ use Bugzilla::Config::Common; our $sortkey = 1500; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'shadowdbhost', - type => 't', - default => '', - }, - - { - name => 'shadowdbport', - type => 't', - default => '3306', - checker => \&check_numeric, - }, - - { - name => 'shadowdbsock', - type => 't', - default => '', - }, - - # This entry must be _after_ the shadowdb{host,port,sock} settings so that - # they can be used in the validation here - { - name => 'shadowdb', - type => 't', - default => '', - checker => \&check_shadowdb - } ); + {name => 'shadowdbhost', type => 't', default => '',}, + + { + name => 'shadowdbport', + type => 't', + default => '3306', + checker => \&check_numeric, + }, + + {name => 'shadowdbsock', type => 't', default => '',}, + + # This entry must be _after_ the shadowdb{host,port,sock} settings so that + # they can be used in the validation here + {name => 'shadowdb', type => 't', default => '', checker => \&check_shadowdb} + ); return @param_list; } diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm index 3f74a7c44..a1f8a3eb2 100644 --- a/Bugzilla/Config/UserMatch.pm +++ b/Bugzilla/Config/UserMatch.pm @@ -16,32 +16,21 @@ use Bugzilla::Config::Common; our $sortkey = 1600; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'usemenuforusers', - type => 'b', - default => '0' - }, - - { - name => 'ajax_user_autocompletion', - type => 'b', - default => '1', - }, - - { - name => 'maxusermatches', - type => 't', - default => '1000', - checker => \&check_numeric - }, - - { - name => 'confirmuniqueusermatch', - type => 'b', - default => 1, - } ); + {name => 'usemenuforusers', type => 'b', default => '0'}, + + {name => 'ajax_user_autocompletion', type => 'b', default => '1',}, + + { + name => 'maxusermatches', + type => 't', + default => '1000', + checker => \&check_numeric + }, + + {name => 'confirmuniqueusermatch', type => 'b', default => 1,} + ); return @param_list; } diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 7c387c82e..426a63b66 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -18,181 +18,181 @@ use File::Basename; use Memoize; @Bugzilla::Constants::EXPORT = qw( - BUGZILLA_VERSION - REST_DOC - - REMOTE_FILE - LOCAL_FILE + BUGZILLA_VERSION + REST_DOC - bz_locations - - CONCATENATE_ASSETS - - IS_NULL - NOT_NULL - - CONTROLMAPNA - CONTROLMAPSHOWN - CONTROLMAPDEFAULT - CONTROLMAPMANDATORY - - AUTH_OK - AUTH_NODATA - AUTH_ERROR - AUTH_LOGINFAILED - AUTH_DISABLED - AUTH_NO_SUCH_USER - AUTH_LOCKOUT - - USER_PASSWORD_MIN_LENGTH - - LOGIN_OPTIONAL - LOGIN_NORMAL - LOGIN_REQUIRED - - LOGOUT_ALL - LOGOUT_CURRENT - LOGOUT_KEEP_CURRENT - - GRANT_DIRECT - GRANT_REGEXP - - GROUP_MEMBERSHIP - GROUP_BLESS - GROUP_VISIBLE - - MAILTO_USER - MAILTO_GROUP - - DEFAULT_COLUMN_LIST - DEFAULT_QUERY_NAME - DEFAULT_MILESTONE - - SAVE_NUM_SEARCHES - - COMMENT_COLS - MAX_COMMENT_LENGTH - - MIN_COMMENT_TAG_LENGTH - MAX_COMMENT_TAG_LENGTH - - CMT_NORMAL - CMT_DUPE_OF - CMT_HAS_DUPE - CMT_ATTACHMENT_CREATED - CMT_ATTACHMENT_UPDATED - - THROW_ERROR - - RELATIONSHIPS - REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER - REL_ANY - - POS_EVENTS - 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 EVT_COMPONENT - - NEG_EVENTS - EVT_UNCONFIRMED EVT_CHANGED_BY_ME - - GLOBAL_EVENTS - EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG - - ADMIN_GROUP_NAME - PER_PRODUCT_PRIVILEGES - - SENDMAIL_EXE - SENDMAIL_PATH - - FIELD_TYPE_UNKNOWN - FIELD_TYPE_FREETEXT - FIELD_TYPE_SINGLE_SELECT - FIELD_TYPE_MULTI_SELECT - FIELD_TYPE_TEXTAREA - FIELD_TYPE_DATETIME - FIELD_TYPE_DATE - FIELD_TYPE_BUG_ID - FIELD_TYPE_BUG_URLS - FIELD_TYPE_KEYWORDS - FIELD_TYPE_INTEGER - FIELD_TYPE_HIGHEST_PLUS_ONE - - EMPTY_DATETIME_REGEX - - ABNORMAL_SELECTS - - TIMETRACKING_FIELDS - - USAGE_MODE_BROWSER - USAGE_MODE_CMDLINE - USAGE_MODE_XMLRPC - USAGE_MODE_EMAIL - USAGE_MODE_JSON - USAGE_MODE_TEST - USAGE_MODE_REST - - ERROR_MODE_WEBPAGE - ERROR_MODE_DIE - ERROR_MODE_DIE_SOAP_FAULT - ERROR_MODE_JSON_RPC - ERROR_MODE_TEST - ERROR_MODE_REST - - COLOR_ERROR - COLOR_SUCCESS - - INSTALLATION_MODE_INTERACTIVE - INSTALLATION_MODE_NON_INTERACTIVE - - DB_MODULE - ROOT_USER - ON_WINDOWS - ON_ACTIVESTATE - - MAX_TOKEN_AGE - MAX_LOGINCOOKIE_AGE - MAX_SUDO_TOKEN_AGE - MAX_LOGIN_ATTEMPTS - LOGIN_LOCKOUT_INTERVAL - ACCOUNT_CHANGE_INTERVAL - MAX_STS_AGE - - SAFE_PROTOCOLS - LEGAL_CONTENT_TYPES - - MIN_SMALLINT - MAX_SMALLINT - MAX_INT_32 - - MAX_LEN_QUERY_NAME - MAX_CLASSIFICATION_SIZE - MAX_PRODUCT_SIZE - MAX_MILESTONE_SIZE - MAX_COMPONENT_SIZE - MAX_FIELD_VALUE_SIZE - MAX_FIELD_LONG_DESC_LENGTH - MAX_FREETEXT_LENGTH - MAX_BUG_URL_LENGTH - MAX_POSSIBLE_DUPLICATES - MAX_ATTACH_FILENAME_LENGTH - MAX_QUIP_LENGTH - MAX_WEBDOT_BUGS - - PASSWORD_DIGEST_ALGORITHM - PASSWORD_SALT_LENGTH - - CGI_URI_LIMIT - - PRIVILEGES_REQUIRED_NONE - PRIVILEGES_REQUIRED_REPORTER - PRIVILEGES_REQUIRED_ASSIGNEE - PRIVILEGES_REQUIRED_EMPOWERED - - AUDIT_CREATE - AUDIT_REMOVE - - MOST_FREQUENT_THRESHOLD + REMOTE_FILE + LOCAL_FILE + + bz_locations + + CONCATENATE_ASSETS + + IS_NULL + NOT_NULL + + CONTROLMAPNA + CONTROLMAPSHOWN + CONTROLMAPDEFAULT + CONTROLMAPMANDATORY + + AUTH_OK + AUTH_NODATA + AUTH_ERROR + AUTH_LOGINFAILED + AUTH_DISABLED + AUTH_NO_SUCH_USER + AUTH_LOCKOUT + + USER_PASSWORD_MIN_LENGTH + + LOGIN_OPTIONAL + LOGIN_NORMAL + LOGIN_REQUIRED + + LOGOUT_ALL + LOGOUT_CURRENT + LOGOUT_KEEP_CURRENT + + GRANT_DIRECT + GRANT_REGEXP + + GROUP_MEMBERSHIP + GROUP_BLESS + GROUP_VISIBLE + + MAILTO_USER + MAILTO_GROUP + + DEFAULT_COLUMN_LIST + DEFAULT_QUERY_NAME + DEFAULT_MILESTONE + + SAVE_NUM_SEARCHES + + COMMENT_COLS + MAX_COMMENT_LENGTH + + MIN_COMMENT_TAG_LENGTH + MAX_COMMENT_TAG_LENGTH + + CMT_NORMAL + CMT_DUPE_OF + CMT_HAS_DUPE + CMT_ATTACHMENT_CREATED + CMT_ATTACHMENT_UPDATED + + THROW_ERROR + + RELATIONSHIPS + REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER + REL_ANY + + POS_EVENTS + 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 EVT_COMPONENT + + NEG_EVENTS + EVT_UNCONFIRMED EVT_CHANGED_BY_ME + + GLOBAL_EVENTS + EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG + + ADMIN_GROUP_NAME + PER_PRODUCT_PRIVILEGES + + SENDMAIL_EXE + SENDMAIL_PATH + + FIELD_TYPE_UNKNOWN + FIELD_TYPE_FREETEXT + FIELD_TYPE_SINGLE_SELECT + FIELD_TYPE_MULTI_SELECT + FIELD_TYPE_TEXTAREA + FIELD_TYPE_DATETIME + FIELD_TYPE_DATE + FIELD_TYPE_BUG_ID + FIELD_TYPE_BUG_URLS + FIELD_TYPE_KEYWORDS + FIELD_TYPE_INTEGER + FIELD_TYPE_HIGHEST_PLUS_ONE + + EMPTY_DATETIME_REGEX + + ABNORMAL_SELECTS + + TIMETRACKING_FIELDS + + USAGE_MODE_BROWSER + USAGE_MODE_CMDLINE + USAGE_MODE_XMLRPC + USAGE_MODE_EMAIL + USAGE_MODE_JSON + USAGE_MODE_TEST + USAGE_MODE_REST + + ERROR_MODE_WEBPAGE + ERROR_MODE_DIE + ERROR_MODE_DIE_SOAP_FAULT + ERROR_MODE_JSON_RPC + ERROR_MODE_TEST + ERROR_MODE_REST + + COLOR_ERROR + COLOR_SUCCESS + + INSTALLATION_MODE_INTERACTIVE + INSTALLATION_MODE_NON_INTERACTIVE + + DB_MODULE + ROOT_USER + ON_WINDOWS + ON_ACTIVESTATE + + MAX_TOKEN_AGE + MAX_LOGINCOOKIE_AGE + MAX_SUDO_TOKEN_AGE + MAX_LOGIN_ATTEMPTS + LOGIN_LOCKOUT_INTERVAL + ACCOUNT_CHANGE_INTERVAL + MAX_STS_AGE + + SAFE_PROTOCOLS + LEGAL_CONTENT_TYPES + + MIN_SMALLINT + MAX_SMALLINT + MAX_INT_32 + + MAX_LEN_QUERY_NAME + MAX_CLASSIFICATION_SIZE + MAX_PRODUCT_SIZE + MAX_MILESTONE_SIZE + MAX_COMPONENT_SIZE + MAX_FIELD_VALUE_SIZE + MAX_FIELD_LONG_DESC_LENGTH + MAX_FREETEXT_LENGTH + MAX_BUG_URL_LENGTH + MAX_POSSIBLE_DUPLICATES + MAX_ATTACH_FILENAME_LENGTH + MAX_QUIP_LENGTH + MAX_WEBDOT_BUGS + + PASSWORD_DIGEST_ALGORITHM + PASSWORD_SALT_LENGTH + + CGI_URI_LIMIT + + PRIVILEGES_REQUIRED_NONE + PRIVILEGES_REQUIRED_REPORTER + PRIVILEGES_REQUIRED_ASSIGNEE + PRIVILEGES_REQUIRED_EMPOWERED + + AUDIT_CREATE + AUDIT_REMOVE + + MOST_FREQUENT_THRESHOLD ); @Bugzilla::Constants::EXPORT_OK = qw(contenttypes); @@ -208,7 +208,7 @@ use constant REST_DOC => 'https://bugzilla.readthedocs.org/en/5.0/api/'; # Location of the remote and local XML files to track new releases. use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml'; -use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir. +use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir. # When true CSS and JavaScript assets will be concatanted and minified at # run-time, to reduce the number of requests required to render a page. @@ -229,9 +229,9 @@ use constant NOT_NULL => ' __NOT_NULL__ '; # # ControlMap constants for group_control_map. # membercontol:othercontrol => meaning -# Na:Na => Bugs in this product may not be restricted to this +# Na:Na => Bugs in this product may not be restricted to this # group. -# Shown:Na => Members of the group may restrict bugs +# Shown:Na => Members of the group may restrict bugs # in this product to this group. # Shown:Shown => Members of the group may restrict bugs # in this product to this group. @@ -253,46 +253,46 @@ use constant NOT_NULL => ' __NOT_NULL__ '; # Mandatory:Mandatory => Bug will be forced into this group regardless. # All other combinations are illegal. -use constant CONTROLMAPNA => 0; -use constant CONTROLMAPSHOWN => 1; -use constant CONTROLMAPDEFAULT => 2; +use constant CONTROLMAPNA => 0; +use constant CONTROLMAPSHOWN => 1; +use constant CONTROLMAPDEFAULT => 2; use constant CONTROLMAPMANDATORY => 3; # See Bugzilla::Auth for docs on AUTH_*, LOGIN_* and LOGOUT_* -use constant AUTH_OK => 0; -use constant AUTH_NODATA => 1; -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; +use constant AUTH_OK => 0; +use constant AUTH_NODATA => 1; +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 length a password must have. use constant USER_PASSWORD_MIN_LENGTH => 6; use constant LOGIN_OPTIONAL => 0; -use constant LOGIN_NORMAL => 1; +use constant LOGIN_NORMAL => 1; use constant LOGIN_REQUIRED => 2; -use constant LOGOUT_ALL => 0; -use constant LOGOUT_CURRENT => 1; +use constant LOGOUT_ALL => 0; +use constant LOGOUT_CURRENT => 1; use constant LOGOUT_KEEP_CURRENT => 2; use constant GRANT_DIRECT => 0; use constant GRANT_REGEXP => 2; use constant GROUP_MEMBERSHIP => 0; -use constant GROUP_BLESS => 1; -use constant GROUP_VISIBLE => 2; +use constant GROUP_BLESS => 1; +use constant GROUP_VISIBLE => 2; -use constant MAILTO_USER => 0; +use constant MAILTO_USER => 0; use constant MAILTO_GROUP => 1; # The default list of columns for buglist.cgi use constant DEFAULT_COLUMN_LIST => ( - "product", "component", "assigned_to", - "bug_status", "resolution", "short_desc", "changeddate" + "product", "component", "assigned_to", "bug_status", + "resolution", "short_desc", "changeddate" ); # Used by query.cgi and buglist.cgi as the named-query name @@ -307,6 +307,7 @@ use constant SAVE_NUM_SEARCHES => 10; # The column width for comment textareas and comments in bugmails. use constant COMMENT_COLS => 80; + # Used in _check_comment(). Gives the max length allowed for a comment. use constant MAX_COMMENT_LENGTH => 16384; @@ -319,9 +320,10 @@ use constant MIN_COMMENT_TAG_LENGTH => 3; use constant MAX_COMMENT_TAG_LENGTH => 24; # The type of bug comments. -use constant CMT_NORMAL => 0; -use constant CMT_DUPE_OF => 1; +use constant CMT_NORMAL => 0; +use constant CMT_DUPE_OF => 1; use constant CMT_HAS_DUPE => 2; + # Type 3 was CMT_POPULAR_VOTES, which moved to the Voting extension. # Type 4 was CMT_MOVED_TO, which moved to the OldBugMove extension. use constant CMT_ATTACHMENT_CREATED => 5; @@ -331,27 +333,26 @@ use constant CMT_ATTACHMENT_UPDATED => 6; # an error when the validation fails. use constant THROW_ERROR => 1; -use constant REL_ASSIGNEE => 0; -use constant REL_QA => 1; -use constant REL_REPORTER => 2; -use constant REL_CC => 3; +use constant REL_ASSIGNEE => 0; +use constant REL_QA => 1; +use constant REL_REPORTER => 2; +use constant REL_CC => 3; + # REL 4 was REL_VOTER, before it was moved ino an extension. -use constant REL_GLOBAL_WATCHER => 5; +use constant REL_GLOBAL_WATCHER => 5; # We need these strings for the X-Bugzilla-Reasons header # Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS. # This should be accessed through Bugzilla::BugMail::relationships() instead # of being accessed directly. use constant RELATIONSHIPS => { - REL_ASSIGNEE , "AssignedTo", - REL_REPORTER , "Reporter", - REL_QA , "QAcontact", - REL_CC , "CC", - REL_GLOBAL_WATCHER, "GlobalWatcher" + REL_ASSIGNEE, "AssignedTo", REL_REPORTER, "Reporter", + REL_QA, "QAcontact", REL_CC, "CC", + REL_GLOBAL_WATCHER, "GlobalWatcher" }; - + # Used for global events like EVT_FLAG_REQUESTED -use constant REL_ANY => 100; +use constant REL_ANY => 100; # There are two sorts of event - positive and negative. Positive events are # those for which the user says "I want mail if this happens." Negative events @@ -359,34 +360,34 @@ use constant REL_ANY => 100; # # Exactly when each event fires is defined in wants_bug_mail() in User.pm; I'm # not commenting them here in case the comments and the code get out of sync. -use constant EVT_OTHER => 0; -use constant EVT_ADDED_REMOVED => 1; -use constant EVT_COMMENT => 2; -use constant EVT_ATTACHMENT => 3; -use constant EVT_ATTACHMENT_DATA => 4; -use constant EVT_PROJ_MANAGEMENT => 5; -use constant EVT_OPENED_CLOSED => 6; -use constant EVT_KEYWORD => 7; -use constant EVT_CC => 8; -use constant EVT_DEPEND_BLOCK => 9; -use constant EVT_BUG_CREATED => 10; -use constant EVT_COMPONENT => 11; - -use constant POS_EVENTS => 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, - EVT_COMPONENT; - -use constant EVT_UNCONFIRMED => 50; -use constant EVT_CHANGED_BY_ME => 51; +use constant EVT_OTHER => 0; +use constant EVT_ADDED_REMOVED => 1; +use constant EVT_COMMENT => 2; +use constant EVT_ATTACHMENT => 3; +use constant EVT_ATTACHMENT_DATA => 4; +use constant EVT_PROJ_MANAGEMENT => 5; +use constant EVT_OPENED_CLOSED => 6; +use constant EVT_KEYWORD => 7; +use constant EVT_CC => 8; +use constant EVT_DEPEND_BLOCK => 9; +use constant EVT_BUG_CREATED => 10; +use constant EVT_COMPONENT => 11; + +use constant + POS_EVENTS => 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, EVT_COMPONENT; + +use constant EVT_UNCONFIRMED => 50; +use constant EVT_CHANGED_BY_ME => 51; use constant NEG_EVENTS => EVT_UNCONFIRMED, EVT_CHANGED_BY_ME; # These are the "global" flags, which aren't tied to a particular relationship. # and so use REL_ANY. -use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me -use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag +use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me +use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG; @@ -394,10 +395,12 @@ use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG; use constant ADMIN_GROUP_NAME => 'admin'; # Privileges which can be per-product. -use constant PER_PRODUCT_PRIVILEGES => ('editcomponents', 'editbugs', 'canconfirm'); +use constant PER_PRODUCT_PRIVILEGES => + ('editcomponents', 'editbugs', 'canconfirm'); # Path to sendmail.exe (Windows only) use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe'; + # Paths to search for the sendmail binary (non-Windows) use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib'; @@ -408,45 +411,46 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib'; # we do more than we would do for a standard integer type (f.e. we might # display a user picker). -use constant FIELD_TYPE_UNKNOWN => 0; -use constant FIELD_TYPE_FREETEXT => 1; +use constant FIELD_TYPE_UNKNOWN => 0; +use constant FIELD_TYPE_FREETEXT => 1; use constant FIELD_TYPE_SINGLE_SELECT => 2; -use constant FIELD_TYPE_MULTI_SELECT => 3; -use constant FIELD_TYPE_TEXTAREA => 4; -use constant FIELD_TYPE_DATETIME => 5; -use constant FIELD_TYPE_BUG_ID => 6; -use constant FIELD_TYPE_BUG_URLS => 7; -use constant FIELD_TYPE_KEYWORDS => 8; -use constant FIELD_TYPE_DATE => 9; -use constant FIELD_TYPE_INTEGER => 10; +use constant FIELD_TYPE_MULTI_SELECT => 3; +use constant FIELD_TYPE_TEXTAREA => 4; +use constant FIELD_TYPE_DATETIME => 5; +use constant FIELD_TYPE_BUG_ID => 6; +use constant FIELD_TYPE_BUG_URLS => 7; +use constant FIELD_TYPE_KEYWORDS => 8; +use constant FIELD_TYPE_DATE => 9; +use constant FIELD_TYPE_INTEGER => 10; + # Add new field types above this line, and change the below value in the # obvious fashion use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 11; -use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/; +use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/; # See the POD for Bugzilla::Field/is_abnormal to see why these are listed # here. -use constant ABNORMAL_SELECTS => { - classification => 1, - component => 1, - product => 1, -}; +use constant ABNORMAL_SELECTS => + {classification => 1, component => 1, product => 1,}; # 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); + qw(estimated_time remaining_time work_time actual_time percentage_complete); # 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; + # How many seconds (default is 6 hours) a sudo cookie remains valid. use constant MAX_SUDO_TOKEN_AGE => 21600; # 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; @@ -460,36 +464,39 @@ use constant ACCOUNT_CHANGE_INTERVAL => 10; use constant MAX_STS_AGE => 15768000; # Protocols which are considered as safe. -use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https', - 'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero', - 'telnet', 'view-source', 'wais'); +use constant SAFE_PROTOCOLS => ( + 'afs', 'cid', 'ftp', 'gopher', 'http', 'https', + 'irc', 'ircs', '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" , - "dtd" => "application/xml-dtd" , - "js" => "application/x-javascript" , - "json" => "application/json" , - "csv" => "text/csv" , - "png" => "image/png" , - "ics" => "text/calendar" , - }; +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", + "dtd" => "application/xml-dtd", + "js" => "application/x-javascript", + "json" => "application/json", + "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_XMLRPC => 2; -use constant USAGE_MODE_EMAIL => 3; -use constant USAGE_MODE_JSON => 4; -use constant USAGE_MODE_TEST => 5; -use constant USAGE_MODE_REST => 6; +use constant USAGE_MODE_BROWSER => 0; +use constant USAGE_MODE_CMDLINE => 1; +use constant USAGE_MODE_XMLRPC => 2; +use constant USAGE_MODE_EMAIL => 3; +use constant USAGE_MODE_JSON => 4; +use constant USAGE_MODE_TEST => 5; +use constant USAGE_MODE_REST => 6; # Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE # usually). Use with Bugzilla->error_mode. @@ -501,60 +508,76 @@ use constant ERROR_MODE_TEST => 4; use constant ERROR_MODE_REST => 5; # The ANSI colors of messages that command-line scripts use -use constant COLOR_ERROR => 'red'; +use constant COLOR_ERROR => 'red'; use constant COLOR_SUCCESS => 'green'; # The various modes that checksetup.pl can run in. -use constant INSTALLATION_MODE_INTERACTIVE => 0; +use constant INSTALLATION_MODE_INTERACTIVE => 0; use constant INSTALLATION_MODE_NON_INTERACTIVE => 1; # Data about what we require for different databases. use constant DB_MODULE => { - # MySQL 5.0.15 was the first production 5.0.x release. - 'mysql' => {db => 'Bugzilla::DB::Mysql', db_version => '5.0.15', - dbd => { - package => 'DBD-mysql', - module => 'DBD::mysql', - # Disallow development versions - blacklist => ['_'], - # For UTF-8 support. 4.001 makes sure that blobs aren't - # marked as UTF-8. - version => '4.001', - }, - name => 'MySQL'}, - # Also see Bugzilla::DB::Pg::bz_check_server_version, which has special - # code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above. - 'pg' => {db => 'Bugzilla::DB::Pg', db_version => '8.03.0000', - dbd => { - package => 'DBD-Pg', - module => 'DBD::Pg', - # 2.7.0 fixes a problem with quoting strings - # containing backslashes in them. - version => '2.7.0', - }, - name => 'PostgreSQL'}, - 'oracle'=> {db => 'Bugzilla::DB::Oracle', db_version => '10.02.0', - dbd => { - package => 'DBD-Oracle', - module => 'DBD::Oracle', - version => '1.19', - }, - name => 'Oracle'}, - # SQLite 3.6.22 fixes a WHERE clause problem that may affect us. - sqlite => {db => 'Bugzilla::DB::Sqlite', db_version => '3.6.22', - dbd => { - package => 'DBD-SQLite', - module => 'DBD::SQLite', - # 1.29 is the version that contains 3.6.22. - version => '1.29', - }, - name => 'SQLite'}, + + # MySQL 5.0.15 was the first production 5.0.x release. + 'mysql' => { + db => 'Bugzilla::DB::Mysql', + db_version => '5.0.15', + dbd => { + package => 'DBD-mysql', + module => 'DBD::mysql', + + # Disallow development versions + blacklist => ['_'], + + # For UTF-8 support. 4.001 makes sure that blobs aren't + # marked as UTF-8. + version => '4.001', + }, + name => 'MySQL' + }, + + # Also see Bugzilla::DB::Pg::bz_check_server_version, which has special + # code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above. + 'pg' => { + db => 'Bugzilla::DB::Pg', + db_version => '8.03.0000', + dbd => { + package => 'DBD-Pg', + module => 'DBD::Pg', + + # 2.7.0 fixes a problem with quoting strings + # containing backslashes in them. + version => '2.7.0', + }, + name => 'PostgreSQL' + }, + 'oracle' => { + db => 'Bugzilla::DB::Oracle', + db_version => '10.02.0', + dbd => {package => 'DBD-Oracle', module => 'DBD::Oracle', version => '1.19',}, + name => 'Oracle' + }, + + # SQLite 3.6.22 fixes a WHERE clause problem that may affect us. + sqlite => { + db => 'Bugzilla::DB::Sqlite', + db_version => '3.6.22', + dbd => { + package => 'DBD-SQLite', + module => 'DBD::SQLite', + + # 1.29 is the version that contains 3.6.22. + version => '1.29', + }, + name => 'SQLite' + }, }; # True if we're on Win32. use constant ON_WINDOWS => ($^O =~ /MSWin32/i) ? 1 : 0; + # True if we're using ActiveState Perl (as opposed to Strawberry) on Windows. -use constant ON_ACTIVESTATE => eval { &Win32::BuildNumber }; +use constant ON_ACTIVESTATE => eval {&Win32::BuildNumber}; # The user who should be considered "root" when we're giving # instructions to Bugzilla administrators. @@ -562,7 +585,7 @@ use constant ROOT_USER => ON_WINDOWS ? 'Administrator' : 'root'; use constant MIN_SMALLINT => -32768; use constant MAX_SMALLINT => 32767; -use constant MAX_INT_32 => 2147483647; +use constant MAX_INT_32 => 2147483647; # The longest that a saved search name can be. use constant MAX_LEN_QUERY_NAME => 64; @@ -611,6 +634,7 @@ use constant MAX_WEBDOT_BUGS => 2000; # Perl's "Digest" module. Note that if you change this, it won't take # effect until a user logs in or changes their password. use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256'; + # How long of a salt should we use? Note that if you change this, it # won't take effect until a user logs in or changes their password. use constant PASSWORD_SALT_LENGTH => 8; @@ -619,7 +643,9 @@ use constant PASSWORD_SALT_LENGTH => 8; # via POST such as buglist.cgi. This value determines whether the redirect # can be safely done or not based on the web server's URI length setting. # See http://support.microsoft.com/kb/208427 for why MSIE is different -use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ ? 2083 : 8000; +use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ + ? 2083 + : 8000; # If the user isn't allowed to change a field, we must tell them who can. # We store the required permission set into the $PrivilegesRequired @@ -640,72 +666,79 @@ use constant AUDIT_REMOVE => '__remove__'; use constant MOST_FREQUENT_THRESHOLD => 2; sub bz_locations { - # Force memoize() to re-compute data per project, to avoid - # sharing the same data across different installations. - return _bz_locations($ENV{'PROJECT'}); + + # Force memoize() to re-compute data per project, to avoid + # sharing the same data across different installations. + return _bz_locations($ENV{'PROJECT'}); } sub _bz_locations { - my $project = shift; - # We know that Bugzilla/Constants.pm must be in %INC at this point. - # So the only question is, what's the name of the directory - # above it? This is the most reliable way to get our current working - # directory under both mod_cgi and mod_perl. We call dirname twice - # to get the name of the directory above the "Bugzilla/" directory. - # - # Calling dirname twice like that won't work on VMS or AmigaOS - # but I doubt anybody runs Bugzilla on those. - # - # On mod_cgi this will be a relative path. On mod_perl it will be an - # absolute path. - my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'})); - # We have to detaint $libpath, but we can't use Bugzilla::Util here. - $libpath =~ /(.*)/; - $libpath = $1; - - my ($localconfig, $datadir); - if ($project && $project =~ /^(\w+)$/) { - $project = $1; - $localconfig = "localconfig.$project"; - $datadir = "data/$project"; - } else { - $project = undef; - $localconfig = "localconfig"; - $datadir = "data"; - } - - $datadir = "$libpath/$datadir"; - # We have to return absolute paths for mod_perl. - # That means that if you modify these paths, they must be absolute paths. - return { - 'libpath' => $libpath, - 'ext_libpath' => "$libpath/lib", - # If you put the libraries in a different location than the CGIs, - # make sure this still points to the CGIs. - 'cgi_path' => $libpath, - 'templatedir' => "$libpath/template", - 'template_cache' => "$datadir/template", - 'project' => $project, - 'localconfig' => "$libpath/$localconfig", - 'datadir' => $datadir, - 'attachdir' => "$datadir/attachments", - 'skinsdir' => "$libpath/skins", - 'graphsdir' => "$libpath/graphs", - # $webdotdir must be in the web server's tree somewhere. Even if you use a - # local dot, we output images to there. Also, if $webdotdir is - # not relative to the bugzilla root directory, you'll need to - # change showdependencygraph.cgi to set image_url to the correct - # location. - # The script should really generate these graphs directly... - 'webdotdir' => "$datadir/webdot", - 'extensionsdir' => "$libpath/extensions", - 'assetsdir' => "$datadir/assets", - }; + my $project = shift; + + # We know that Bugzilla/Constants.pm must be in %INC at this point. + # So the only question is, what's the name of the directory + # above it? This is the most reliable way to get our current working + # directory under both mod_cgi and mod_perl. We call dirname twice + # to get the name of the directory above the "Bugzilla/" directory. + # + # Calling dirname twice like that won't work on VMS or AmigaOS + # but I doubt anybody runs Bugzilla on those. + # + # On mod_cgi this will be a relative path. On mod_perl it will be an + # absolute path. + my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'})); + + # We have to detaint $libpath, but we can't use Bugzilla::Util here. + $libpath =~ /(.*)/; + $libpath = $1; + + my ($localconfig, $datadir); + if ($project && $project =~ /^(\w+)$/) { + $project = $1; + $localconfig = "localconfig.$project"; + $datadir = "data/$project"; + } + else { + $project = undef; + $localconfig = "localconfig"; + $datadir = "data"; + } + + $datadir = "$libpath/$datadir"; + + # We have to return absolute paths for mod_perl. + # That means that if you modify these paths, they must be absolute paths. + return { + 'libpath' => $libpath, + 'ext_libpath' => "$libpath/lib", + + # If you put the libraries in a different location than the CGIs, + # make sure this still points to the CGIs. + 'cgi_path' => $libpath, + 'templatedir' => "$libpath/template", + 'template_cache' => "$datadir/template", + 'project' => $project, + 'localconfig' => "$libpath/$localconfig", + 'datadir' => $datadir, + 'attachdir' => "$datadir/attachments", + 'skinsdir' => "$libpath/skins", + 'graphsdir' => "$libpath/graphs", + + # $webdotdir must be in the web server's tree somewhere. Even if you use a + # local dot, we output images to there. Also, if $webdotdir is + # not relative to the bugzilla root directory, you'll need to + # change showdependencygraph.cgi to set image_url to the correct + # location. + # The script should really generate these graphs directly... + 'webdotdir' => "$datadir/webdot", + 'extensionsdir' => "$libpath/extensions", + 'assetsdir' => "$datadir/assets", + }; } # This makes us not re-compute all the bz_locations data every time it's # called. -BEGIN { memoize('_bz_locations') }; +BEGIN { memoize('_bz_locations') } 1; diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 5bc83f9d6..029e05f03 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -33,7 +33,7 @@ use Storable qw(dclone); # Constants ##################################################################### -use constant BLOB_TYPE => DBI::SQL_BLOB; +use constant BLOB_TYPE => DBI::SQL_BLOB; use constant ISOLATION_LEVEL => 'REPEATABLE READ'; # Set default values for what used to be the enum types. These values @@ -46,14 +46,14 @@ use constant ISOLATION_LEVEL => 'REPEATABLE READ'; # Bugzilla with enums. After that, they are either controlled through # the Bugzilla UI or through the DB. use constant ENUM_DEFAULTS => { - bug_severity => ['blocker', 'critical', 'major', 'normal', - 'minor', 'trivial', 'enhancement'], - priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"], - op_sys => ["All","Windows","Mac OS","Linux","Other"], - rep_platform => ["All","PC","Macintosh","Other"], - bug_status => ["UNCONFIRMED","CONFIRMED","IN_PROGRESS","RESOLVED", - "VERIFIED"], - resolution => ["","FIXED","INVALID","WONTFIX", "DUPLICATE","WORKSFORME"], + bug_severity => + ['blocker', 'critical', 'major', 'normal', 'minor', 'trivial', 'enhancement'], + priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"], + op_sys => ["All", "Windows", "Mac OS", "Linux", "Other"], + rep_platform => ["All", "PC", "Macintosh", "Other"], + bug_status => + ["UNCONFIRMED", "CONFIRMED", "IN_PROGRESS", "RESOLVED", "VERIFIED"], + resolution => ["", "FIXED", "INVALID", "WONTFIX", "DUPLICATE", "WORKSFORME"], }; # The character that means "OR" in a boolean fulltext search. If empty, @@ -83,14 +83,14 @@ use constant WORD_END => '($|[^[:alnum:]])'; use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1; ##################################################################### -# Overridden Superclass Methods +# Overridden Superclass Methods ##################################################################### sub quote { - my $self = shift; - my $retval = $self->SUPER::quote(@_); - trick_taint($retval) if defined $retval; - return $retval; + my $self = shift; + my $retval = $self->SUPER::quote(@_); + trick_taint($retval) if defined $retval; + return $retval; } ##################################################################### @@ -98,95 +98,94 @@ sub quote { ##################################################################### sub connect_shadow { - my $params = Bugzilla->params; - die "Tried to connect to non-existent shadowdb" - unless $params->{'shadowdb'}; + my $params = Bugzilla->params; + die "Tried to connect to non-existent shadowdb" unless $params->{'shadowdb'}; - # Instead of just passing in a new hashref, we locally modify the - # values of "localconfig", because some drivers access it while - # connecting. - my %connect_params = %{ Bugzilla->localconfig }; - $connect_params{db_host} = $params->{'shadowdbhost'}; - $connect_params{db_name} = $params->{'shadowdb'}; - $connect_params{db_port} = $params->{'shadowdbport'}; - $connect_params{db_sock} = $params->{'shadowdbsock'}; + # Instead of just passing in a new hashref, we locally modify the + # values of "localconfig", because some drivers access it while + # connecting. + my %connect_params = %{Bugzilla->localconfig}; + $connect_params{db_host} = $params->{'shadowdbhost'}; + $connect_params{db_name} = $params->{'shadowdb'}; + $connect_params{db_port} = $params->{'shadowdbport'}; + $connect_params{db_sock} = $params->{'shadowdbsock'}; - return _connect(\%connect_params); + return _connect(\%connect_params); } sub connect_main { - my $lc = Bugzilla->localconfig; - return _connect(Bugzilla->localconfig); + my $lc = Bugzilla->localconfig; + return _connect(Bugzilla->localconfig); } sub _connect { - my ($params) = @_; + my ($params) = @_; - my $driver = $params->{db_driver}; - my $pkg_module = DB_MODULE->{lc($driver)}->{db}; + my $driver = $params->{db_driver}; + my $pkg_module = DB_MODULE->{lc($driver)}->{db}; - # do the actual import - eval ("require $pkg_module") - || die ("'$driver' is not a valid choice for \$db_driver in " - . " localconfig: " . $@); + # do the actual import + eval("require $pkg_module") + || die( + "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@); - # instantiate the correct DB specific module - my $dbh = $pkg_module->new($params); + # instantiate the correct DB specific module + my $dbh = $pkg_module->new($params); - return $dbh; + return $dbh; } sub _handle_error { - require Carp; + require Carp; - # Cut down the error string to a reasonable size - $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) - if length($_[0]) > 4000; - $_[0] = Carp::longmess($_[0]); - return 0; # Now let DBI handle raising the error + # Cut down the error string to a reasonable size + $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) + if length($_[0]) > 4000; + $_[0] = Carp::longmess($_[0]); + return 0; # Now let DBI handle raising the error } sub bz_check_requirements { - my ($output) = @_; + my ($output) = @_; - my $lc = Bugzilla->localconfig; - my $db = DB_MODULE->{lc($lc->{db_driver})}; + my $lc = Bugzilla->localconfig; + my $db = DB_MODULE->{lc($lc->{db_driver})}; - # Only certain values are allowed for $db_driver. - if (!defined $db) { - die "$lc->{db_driver} is not a valid choice for \$db_driver in" - . bz_locations()->{'localconfig'}; - } + # Only certain values are allowed for $db_driver. + if (!defined $db) { + die "$lc->{db_driver} is not a valid choice for \$db_driver in" + . bz_locations()->{'localconfig'}; + } - # Check the existence and version of the DBD that we need. - my $dbd = $db->{dbd}; - _bz_check_dbd($db, $output); + # Check the existence and version of the DBD that we need. + my $dbd = $db->{dbd}; + _bz_check_dbd($db, $output); - # We don't try to connect to the actual database if $db_check is - # disabled. - unless ($lc->{db_check}) { - print "\n" if $output; - return; - } + # We don't try to connect to the actual database if $db_check is + # disabled. + unless ($lc->{db_check}) { + print "\n" if $output; + return; + } - # And now check the version of the database server itself. - my $dbh = _get_no_db_connection(); - $dbh->bz_check_server_version($db, $output); + # And now check the version of the database server itself. + my $dbh = _get_no_db_connection(); + $dbh->bz_check_server_version($db, $output); - print "\n" if $output; + print "\n" if $output; } sub _bz_check_dbd { - my ($db, $output) = @_; + my ($db, $output) = @_; - my $dbd = $db->{dbd}; - unless (have_vers($dbd, $output)) { - my $sql_server = $db->{name}; - my $command = install_command($dbd); - my $root = ROOT_USER; - my $dbd_mod = $dbd->{module}; - my $dbd_ver = $dbd->{version}; - die <<EOT; + my $dbd = $db->{dbd}; + unless (have_vers($dbd, $output)) { + my $sql_server = $db->{name}; + my $command = install_command($dbd); + my $root = ROOT_USER; + my $dbd_mod = $dbd->{module}; + my $dbd_ver = $dbd->{version}; + die <<EOT; For $sql_server, Bugzilla requires that perl's $dbd_mod $dbd_ver or later be installed. To install this module, run the following command (as $root): @@ -194,103 +193,107 @@ installed. To install this module, run the following command (as $root): $command EOT - } + } } sub bz_check_server_version { - my ($self, $db, $output) = @_; + my ($self, $db, $output) = @_; - my $sql_vers = $self->bz_server_version; - $self->disconnect; + my $sql_vers = $self->bz_server_version; + $self->disconnect; - my $sql_want = $db->{db_version}; - my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; + my $sql_want = $db->{db_version}; + my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; - my $sql_server = $db->{name}; - if ($output) { - Bugzilla::Install::Requirements::_checking_for({ - package => $sql_server, wanted => $sql_want, - found => $sql_vers, ok => $version_ok }); - } + my $sql_server = $db->{name}; + if ($output) { + Bugzilla::Install::Requirements::_checking_for({ + package => $sql_server, + wanted => $sql_want, + found => $sql_vers, + ok => $version_ok + }); + } - # Check what version of the database server is installed and let - # the user know if the version is too old to be used with Bugzilla. - if (!$version_ok) { - die <<EOT; + # Check what version of the database server is installed and let + # the user know if the version is too old to be used with Bugzilla. + if (!$version_ok) { + die <<EOT; Your $sql_server v$sql_vers is too old. Bugzilla requires version $sql_want or later of $sql_server. Please download and install a newer version. EOT - } + } - # This is used by subclasses. - return $sql_vers; + # This is used by subclasses. + return $sql_vers; } # Note that this function requires that localconfig exist and # be valid. sub bz_create_database { - my $dbh; - # See if we can connect to the actual Bugzilla database. - my $conn_success = eval { $dbh = connect_main() }; - my $db_name = Bugzilla->localconfig->{db_name}; - - if (!$conn_success) { - $dbh = _get_no_db_connection(); - say "Creating database $db_name..."; - - # Try to create the DB, and if we fail print a friendly error. - my $success = eval { - my @sql = $dbh->_bz_schema->get_create_database_sql($db_name); - # This ends with 1 because this particular do doesn't always - # return something. - $dbh->do($_) foreach @sql; 1; - }; - if (!$success) { - my $error = $dbh->errstr || $@; - chomp($error); - die "The '$db_name' database could not be created.", - " The error returned was:\n\n $error\n\n", - _bz_connect_error_reasons(); - } + my $dbh; + + # See if we can connect to the actual Bugzilla database. + my $conn_success = eval { $dbh = connect_main() }; + my $db_name = Bugzilla->localconfig->{db_name}; + + if (!$conn_success) { + $dbh = _get_no_db_connection(); + say "Creating database $db_name..."; + + # Try to create the DB, and if we fail print a friendly error. + my $success = eval { + my @sql = $dbh->_bz_schema->get_create_database_sql($db_name); + + # This ends with 1 because this particular do doesn't always + # return something. + $dbh->do($_) foreach @sql; + 1; + }; + if (!$success) { + my $error = $dbh->errstr || $@; + chomp($error); + die "The '$db_name' database could not be created.", + " The error returned was:\n\n $error\n\n", _bz_connect_error_reasons(); } + } - $dbh->disconnect; + $dbh->disconnect; } # A helper for bz_create_database and bz_check_requirements. sub _get_no_db_connection { - my ($sql_server) = @_; - my $dbh; - my %connect_params = %{ Bugzilla->localconfig }; - $connect_params{db_name} = ''; - my $conn_success = eval { - $dbh = _connect(\%connect_params); - }; - if (!$conn_success) { - my $driver = $connect_params{db_driver}; - my $sql_server = DB_MODULE->{lc($driver)}->{name}; - # Can't use $dbh->errstr because $dbh is undef. - my $error = $DBI::errstr || $@; - chomp($error); - die "There was an error connecting to $sql_server:\n\n", - " $error\n\n", _bz_connect_error_reasons(), "\n"; - } - return $dbh; + my ($sql_server) = @_; + my $dbh; + my %connect_params = %{Bugzilla->localconfig}; + $connect_params{db_name} = ''; + my $conn_success = eval { $dbh = _connect(\%connect_params); }; + if (!$conn_success) { + my $driver = $connect_params{db_driver}; + my $sql_server = DB_MODULE->{lc($driver)}->{name}; + + # Can't use $dbh->errstr because $dbh is undef. + my $error = $DBI::errstr || $@; + chomp($error); + die "There was an error connecting to $sql_server:\n\n", " $error\n\n", + _bz_connect_error_reasons(), "\n"; + } + return $dbh; } # Just a helper because we have to re-use this text. # We don't use this in db_new because it gives away the database # username, and db_new errors can show up on CGIs. sub _bz_connect_error_reasons { - my $lc_file = bz_locations()->{'localconfig'}; - my $lc = Bugzilla->localconfig; - my $db = DB_MODULE->{lc($lc->{db_driver})}; - my $server = $db->{name}; + my $lc_file = bz_locations()->{'localconfig'}; + my $lc = Bugzilla->localconfig; + my $db = DB_MODULE->{lc($lc->{db_driver})}; + my $server = $db->{name}; -return <<EOT; + return <<EOT; This might have several reasons: * $server is not running. @@ -309,154 +312,157 @@ EOT # List of abstract methods we are checking the derived class implements our @_abstract_methods = qw(new sql_regexp sql_not_regexp sql_limit sql_to_days - sql_date_format sql_date_math bz_explain - sql_group_concat); + sql_date_format sql_date_math bz_explain + sql_group_concat); # This overridden import method will check implementation of inherited classes # for missing implementation of abstract methods # See http://perlmonks.thepen.com/44265.html sub import { - my $pkg = shift; - - # do not check this module - if ($pkg ne __PACKAGE__) { - # make sure all abstract methods are implemented - foreach my $meth (@_abstract_methods) { - $pkg->can($meth) - or die("Class $pkg does not define method $meth"); - } + my $pkg = shift; + + # do not check this module + if ($pkg ne __PACKAGE__) { + + # make sure all abstract methods are implemented + foreach my $meth (@_abstract_methods) { + $pkg->can($meth) or die("Class $pkg does not define method $meth"); } + } - # Now we want to call our superclass implementation. - # If our superclass is Exporter, which is using caller() to find - # a namespace to populate, we need to adjust for this extra call. - # All this can go when we stop using deprecated functions. - my $is_exporter = $pkg->isa('Exporter'); - $Exporter::ExportLevel++ if $is_exporter; - $pkg->SUPER::import(@_); - $Exporter::ExportLevel-- if $is_exporter; + # Now we want to call our superclass implementation. + # If our superclass is Exporter, which is using caller() to find + # a namespace to populate, we need to adjust for this extra call. + # All this can go when we stop using deprecated functions. + my $is_exporter = $pkg->isa('Exporter'); + $Exporter::ExportLevel++ if $is_exporter; + $pkg->SUPER::import(@_); + $Exporter::ExportLevel-- if $is_exporter; } sub sql_istrcmp { - my ($self, $left, $right, $op) = @_; - $op ||= "="; + my ($self, $left, $right, $op) = @_; + $op ||= "="; - return $self->sql_istring($left) . " $op " . $self->sql_istring($right); + return $self->sql_istring($left) . " $op " . $self->sql_istring($right); } sub sql_istring { - my ($self, $string) = @_; + my ($self, $string) = @_; - return "LOWER($string)"; + return "LOWER($string)"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - $fragment = $self->sql_istring($fragment); - $text = $self->sql_istring($text); - return $self->sql_position($fragment, $text); + my ($self, $fragment, $text) = @_; + $fragment = $self->sql_istring($fragment); + $text = $self->sql_istring($text); + return $self->sql_position($fragment, $text); } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "POSITION($fragment IN $text)"; + return "POSITION($fragment IN $text)"; } sub sql_like { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_position($quoted, $column) . " > 0"; + return $self->sql_position($quoted, $column) . " > 0"; } sub sql_ilike { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_iposition($quoted, $column) . " > 0"; + return $self->sql_iposition($quoted, $column) . " > 0"; } sub sql_not_ilike { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_iposition($quoted, $column) . " = 0"; + return $self->sql_iposition($quoted, $column) . " = 0"; } sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; + my ($self, $needed_columns, $optional_columns) = @_; - my $expression = "GROUP BY $needed_columns"; - $expression .= ", " . $optional_columns if $optional_columns; - - return $expression; + my $expression = "GROUP BY $needed_columns"; + $expression .= ", " . $optional_columns if $optional_columns; + + return $expression; } sub sql_string_concat { - my ($self, @params) = @_; - - return '(' . join(' || ', @params) . ')'; + my ($self, @params) = @_; + + return '(' . join(' || ', @params) . ')'; } sub sql_string_until { - my ($self, $string, $substring) = @_; + my ($self, $string, $substring) = @_; - my $position = $self->sql_position($substring, $string); - return "CASE WHEN $position != 0" - . " THEN SUBSTR($string, 1, $position - 1)" - . " ELSE $string END"; + my $position = $self->sql_position($substring, $string); + return + "CASE WHEN $position != 0" + . " THEN SUBSTR($string, 1, $position - 1)" + . " ELSE $string END"; } sub sql_in { - my ($self, $column_name, $in_list_ref, $negate) = @_; - return " $column_name " - . ($negate ? "NOT " : "") - . "IN (" . join(',', @$in_list_ref) . ") "; + my ($self, $column_name, $in_list_ref, $negate) = @_; + return + " $column_name " + . ($negate ? "NOT " : "") . "IN (" + . join(',', @$in_list_ref) . ") "; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - - # This is as close as we can get to doing full text search using - # standard ANSI SQL, without real full text search support. DB specific - # modules should override this, as this will be always much slower. - - # make the string lowercase to do case insensitive search - my $lower_text = lc($text); - - # split the text we're searching for into separate words. As a hack - # to allow quicksearch to work, if the field starts and ends with - # a double-quote, then we don't split it into words. We can't use - # Text::ParseWords here because it gets very confused by unbalanced - # quotes, which breaks searches like "don't try this" (because of the - # unbalanced single-quote in "don't"). - my @words; - if ($lower_text =~ /^"/ and $lower_text =~ /"$/) { - $lower_text =~ s/^"//; - $lower_text =~ s/"$//; - @words = ($lower_text); - } - else { - @words = split(/\s+/, $lower_text); - } - - # surround the words with wildcards and SQL quotes so we can use them - # in LIKE search clauses - @words = map($self->quote("\%$_\%"), @words); - - # untaint words, since they are safe to use now that we've quoted them - trick_taint($_) foreach @words; - - # turn the words into a set of LIKE search clauses - @words = map("LOWER($column) LIKE $_", @words); - - # search for occurrences of all specified words in the column - return join (" AND ", @words), "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END"; + my ($self, $column, $text) = @_; + + # This is as close as we can get to doing full text search using + # standard ANSI SQL, without real full text search support. DB specific + # modules should override this, as this will be always much slower. + + # make the string lowercase to do case insensitive search + my $lower_text = lc($text); + + # split the text we're searching for into separate words. As a hack + # to allow quicksearch to work, if the field starts and ends with + # a double-quote, then we don't split it into words. We can't use + # Text::ParseWords here because it gets very confused by unbalanced + # quotes, which breaks searches like "don't try this" (because of the + # unbalanced single-quote in "don't"). + my @words; + if ($lower_text =~ /^"/ and $lower_text =~ /"$/) { + $lower_text =~ s/^"//; + $lower_text =~ s/"$//; + @words = ($lower_text); + } + else { + @words = split(/\s+/, $lower_text); + } + + # surround the words with wildcards and SQL quotes so we can use them + # in LIKE search clauses + @words = map($self->quote("\%$_\%"), @words); + + # untaint words, since they are safe to use now that we've quoted them + trick_taint($_) foreach @words; + + # turn the words into a set of LIKE search clauses + @words = map("LOWER($column) LIKE $_", @words); + + # search for occurrences of all specified words in the column + return join(" AND ", @words), + "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END"; } ##################################################################### @@ -465,24 +471,27 @@ sub sql_fulltext_search { # XXX - Needs to be documented. sub bz_server_version { - my ($self) = @_; - return $self->get_info(18); # SQL_DBMS_VER + my ($self) = @_; + return $self->get_info(18); # SQL_DBMS_VER } sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - return $self->last_insert_id(Bugzilla->localconfig->{db_name}, undef, - $table, $column); + return $self->last_insert_id(Bugzilla->localconfig->{db_name}, + undef, $table, $column); } sub bz_check_regexp { - my ($self, $pattern) = @_; + my ($self, $pattern) = @_; - eval { $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)) }; + eval { + $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)); + }; - $@ && ThrowUserError('illegal_regexp', - { value => $pattern, dberror => $self->errstr }); + $@ + && ThrowUserError('illegal_regexp', + {value => $pattern, dberror => $self->errstr}); } ##################################################################### @@ -490,99 +499,100 @@ sub bz_check_regexp { ##################################################################### sub bz_setup_database { - my ($self) = @_; - - # If we haven't ever stored a serialized schema, - # set up the bz_schema table and store it. - $self->_bz_init_schema_storage(); - - # We don't use bz_table_list here, because that uses _bz_real_schema. - # We actually want the table list from the ABSTRACT_SCHEMA in - # Bugzilla::DB::Schema. - my @desired_tables = $self->_bz_schema->get_table_list(); - my $bugs_exists = $self->bz_table_info('bugs'); - if (!$bugs_exists) { - say install_string('db_table_setup'); - } + my ($self) = @_; - foreach my $table_name (@desired_tables) { - $self->bz_add_table($table_name, { silently => !$bugs_exists }); - } + # If we haven't ever stored a serialized schema, + # set up the bz_schema table and store it. + $self->_bz_init_schema_storage(); + + # We don't use bz_table_list here, because that uses _bz_real_schema. + # We actually want the table list from the ABSTRACT_SCHEMA in + # Bugzilla::DB::Schema. + my @desired_tables = $self->_bz_schema->get_table_list(); + my $bugs_exists = $self->bz_table_info('bugs'); + if (!$bugs_exists) { + say install_string('db_table_setup'); + } + + foreach my $table_name (@desired_tables) { + $self->bz_add_table($table_name, {silently => !$bugs_exists}); + } } # This really just exists to get overridden in Bugzilla::DB::Mysql. sub bz_enum_initial_values { - return ENUM_DEFAULTS; + return ENUM_DEFAULTS; } sub bz_populate_enum_tables { - my ($self) = @_; + my ($self) = @_; - my $any_severities = $self->selectrow_array( - 'SELECT 1 FROM bug_severity ' . $self->sql_limit(1)); - print install_string('db_enum_setup'), "\n " if !$any_severities; + my $any_severities + = $self->selectrow_array('SELECT 1 FROM bug_severity ' . $self->sql_limit(1)); + print install_string('db_enum_setup'), "\n " if !$any_severities; - my $enum_values = $self->bz_enum_initial_values(); - while (my ($table, $values) = each %$enum_values) { - $self->_bz_populate_enum_table($table, $values); - } + my $enum_values = $self->bz_enum_initial_values(); + while (my ($table, $values) = each %$enum_values) { + $self->_bz_populate_enum_table($table, $values); + } - print "\n" if !$any_severities; + print "\n" if !$any_severities; } sub bz_setup_foreign_keys { - my ($self) = @_; - - # profiles_activity was the first table to get foreign keys, - # so if it doesn't have them, then we're setting up FKs - # for the first time, and should be quieter about it. - my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid'); - my $any_fks = $activity_fk && $activity_fk->{created}; - if (!$any_fks) { - say get_text('install_fk_setup'); - } + my ($self) = @_; + + # profiles_activity was the first table to get foreign keys, + # so if it doesn't have them, then we're setting up FKs + # for the first time, and should be quieter about it. + my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid'); + my $any_fks = $activity_fk && $activity_fk->{created}; + if (!$any_fks) { + say get_text('install_fk_setup'); + } + + my @tables = $self->bz_table_list(); + foreach my $table (@tables) { + my @columns = $self->bz_table_columns($table); + my %add_fks; + foreach my $column (@columns) { - my @tables = $self->bz_table_list(); - foreach my $table (@tables) { - my @columns = $self->bz_table_columns($table); - my %add_fks; - foreach my $column (@columns) { - # First we check for any FKs that have created => 0, - # in the _bz_real_schema. This also picks up FKs with - # created => 1, but bz_add_fks will ignore those. - my $fk = $self->bz_fk_info($table, $column); - # Then we check the abstract schema to see if there - # should be an FK on this column, but one wasn't set in the - # _bz_real_schema for some reason. We do this to handle - # various problems caused by upgrading from versions - # prior to 4.2, and also to handle problems caused - # by enabling an extension pre-4.2, disabling it for - # the 4.2 upgrade, and then re-enabling it later. - unless ($fk && $fk->{created}) { - my $standard_def = - $self->_bz_schema->get_column_abstract($table, $column); - if (exists $standard_def->{REFERENCES}) { - $fk = dclone($standard_def->{REFERENCES}); - } - } - - $add_fks{$column} = $fk if $fk; + # First we check for any FKs that have created => 0, + # in the _bz_real_schema. This also picks up FKs with + # created => 1, but bz_add_fks will ignore those. + my $fk = $self->bz_fk_info($table, $column); + + # Then we check the abstract schema to see if there + # should be an FK on this column, but one wasn't set in the + # _bz_real_schema for some reason. We do this to handle + # various problems caused by upgrading from versions + # prior to 4.2, and also to handle problems caused + # by enabling an extension pre-4.2, disabling it for + # the 4.2 upgrade, and then re-enabling it later. + unless ($fk && $fk->{created}) { + my $standard_def = $self->_bz_schema->get_column_abstract($table, $column); + if (exists $standard_def->{REFERENCES}) { + $fk = dclone($standard_def->{REFERENCES}); } - $self->bz_add_fks($table, \%add_fks, { silently => !$any_fks }); + } + + $add_fks{$column} = $fk if $fk; } + $self->bz_add_fks($table, \%add_fks, {silently => !$any_fks}); + } } # This is used by contrib/bzdbcopy.pl, mostly. sub bz_drop_foreign_keys { - my ($self) = @_; + my ($self) = @_; - my @tables = $self->bz_table_list(); - foreach my $table (@tables) { - my @columns = $self->bz_table_columns($table); - foreach my $column (@columns) { - $self->bz_drop_fk($table, $column); - } + my @tables = $self->bz_table_list(); + foreach my $table (@tables) { + my @columns = $self->bz_table_columns($table); + foreach my $column (@columns) { + $self->bz_drop_fk($table, $column); } + } } ##################################################################### @@ -590,119 +600,121 @@ sub bz_drop_foreign_keys { ##################################################################### sub bz_add_column { - my ($self, $table, $name, $new_def, $init_value) = @_; - - # You can't add a NOT NULL column to a table with - # no DEFAULT statement, unless you have an init_value. - # SERIAL types are an exception, though, because they can - # auto-populate. - if ( $new_def->{NOTNULL} && !exists $new_def->{DEFAULT} - && !defined $init_value && $new_def->{TYPE} !~ /SERIAL/) - { - ThrowCodeError('column_not_null_without_default', - { name => "$table.$name" }); + my ($self, $table, $name, $new_def, $init_value) = @_; + + # You can't add a NOT NULL column to a table with + # no DEFAULT statement, unless you have an init_value. + # SERIAL types are an exception, though, because they can + # auto-populate. + if ( $new_def->{NOTNULL} + && !exists $new_def->{DEFAULT} + && !defined $init_value + && $new_def->{TYPE} !~ /SERIAL/) + { + ThrowCodeError('column_not_null_without_default', {name => "$table.$name"}); + } + + my $current_def = $self->bz_column_info($table, $name); + + if (!$current_def) { + + # REFERENCES need to happen later and not be created right away + my $trimmed_def = dclone($new_def); + delete $trimmed_def->{REFERENCES}; + my @statements + = $self->_bz_real_schema->get_add_column_ddl($table, $name, $trimmed_def, + defined $init_value ? $self->quote($init_value) : undef); + print get_text('install_column_add', {column => $name, table => $table}) . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + $self->do($sql); } - my $current_def = $self->bz_column_info($table, $name); - - if (!$current_def) { - # REFERENCES need to happen later and not be created right away - my $trimmed_def = dclone($new_def); - delete $trimmed_def->{REFERENCES}; - my @statements = $self->_bz_real_schema->get_add_column_ddl( - $table, $name, $trimmed_def, - defined $init_value ? $self->quote($init_value) : undef); - print get_text('install_column_add', - { column => $name, table => $table }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - $self->do($sql); - } - - # To make things easier for callers, if they don't specify - # a REFERENCES item, we pull it from the _bz_schema if the - # column exists there and has a REFERENCES item. - # bz_setup_foreign_keys will then add this FK at the end of - # Install::DB. - my $col_abstract = - $self->_bz_schema->get_column_abstract($table, $name); - if (exists $col_abstract->{REFERENCES}) { - my $new_fk = dclone($col_abstract->{REFERENCES}); - $new_fk->{created} = 0; - $new_def->{REFERENCES} = $new_fk; - } - - $self->_bz_real_schema->set_column($table, $name, $new_def); - $self->_bz_store_real_schema; + # To make things easier for callers, if they don't specify + # a REFERENCES item, we pull it from the _bz_schema if the + # column exists there and has a REFERENCES item. + # bz_setup_foreign_keys will then add this FK at the end of + # Install::DB. + my $col_abstract = $self->_bz_schema->get_column_abstract($table, $name); + if (exists $col_abstract->{REFERENCES}) { + my $new_fk = dclone($col_abstract->{REFERENCES}); + $new_fk->{created} = 0; + $new_def->{REFERENCES} = $new_fk; } + + $self->_bz_real_schema->set_column($table, $name, $new_def); + $self->_bz_store_real_schema; + } } sub bz_add_fk { - my ($self, $table, $column, $def) = @_; - $self->bz_add_fks($table, { $column => $def }); + my ($self, $table, $column, $def) = @_; + $self->bz_add_fks($table, {$column => $def}); } sub bz_add_fks { - my ($self, $table, $column_fks, $options) = @_; - - my %add_these; - foreach my $column (keys %$column_fks) { - my $current_fk = $self->bz_fk_info($table, $column); - next if ($current_fk and $current_fk->{created}); - my $new_fk = $column_fks->{$column}; - $self->_check_references($table, $column, $new_fk); - $add_these{$column} = $new_fk; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - and !$options->{silently}) - { - print get_text('install_fk_add', - { table => $table, column => $column, - fk => $new_fk }), "\n"; - } + my ($self, $table, $column_fks, $options) = @_; + + my %add_these; + foreach my $column (keys %$column_fks) { + my $current_fk = $self->bz_fk_info($table, $column); + next if ($current_fk and $current_fk->{created}); + my $new_fk = $column_fks->{$column}; + $self->_check_references($table, $column, $new_fk); + $add_these{$column} = $new_fk; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) { + print get_text( + 'install_fk_add', {table => $table, column => $column, fk => $new_fk} + ), + "\n"; } + } - return if !scalar(keys %add_these); + return if !scalar(keys %add_these); - my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these); - $self->do($_) foreach @sql; + my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these); + $self->do($_) foreach @sql; - foreach my $column (keys %add_these) { - my $fk_def = $add_these{$column}; - $fk_def->{created} = 1; - $self->_bz_real_schema->set_fk($table, $column, $fk_def); - } + foreach my $column (keys %add_these) { + my $fk_def = $add_these{$column}; + $fk_def->{created} = 1; + $self->_bz_real_schema->set_fk($table, $column, $fk_def); + } - $self->_bz_store_real_schema(); + $self->_bz_store_real_schema(); } sub bz_alter_column { - my ($self, $table, $name, $new_def, $set_nulls_to) = @_; + my ($self, $table, $name, $new_def, $set_nulls_to) = @_; - my $current_def = $self->bz_column_info($table, $name); + my $current_def = $self->bz_column_info($table, $name); - if (!$self->_bz_schema->columns_equal($current_def, $new_def)) { - # You can't change a column to be NOT NULL if you have no DEFAULT - # and no value for $set_nulls_to, if there are any NULL values - # in that column. - if ($new_def->{NOTNULL} && - !exists $new_def->{DEFAULT} && !defined $set_nulls_to) - { - # Check for NULLs - my $any_nulls = $self->selectrow_array( - "SELECT 1 FROM $table WHERE $name IS NULL"); - 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 (my $fk = $self->bz_fk_info($table, $name)) { - $new_def->{REFERENCES} = $fk; - } - $self->bz_alter_column_raw($table, $name, $new_def, $current_def, - $set_nulls_to); - $self->_bz_real_schema->set_column($table, $name, $new_def); - $self->_bz_store_real_schema; + if (!$self->_bz_schema->columns_equal($current_def, $new_def)) { + + # You can't change a column to be NOT NULL if you have no DEFAULT + # and no value for $set_nulls_to, if there are any NULL values + # in that column. + if ( $new_def->{NOTNULL} + && !exists $new_def->{DEFAULT} + && !defined $set_nulls_to) + { + # Check for NULLs + my $any_nulls + = $self->selectrow_array("SELECT 1 FROM $table WHERE $name IS NULL"); + 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 (my $fk = $self->bz_fk_info($table, $name)) { + $new_def->{REFERENCES} = $fk; } + $self->bz_alter_column_raw($table, $name, $new_def, $current_def, + $set_nulls_to); + $self->_bz_real_schema->set_column($table, $name, $new_def); + $self->_bz_store_real_schema; + } } @@ -728,39 +740,40 @@ sub bz_alter_column { # Returns: nothing # sub bz_alter_column_raw { - my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_; - my @statements = $self->_bz_real_schema->get_alter_column_ddl( - $table, $name, $new_def, - defined $set_nulls_to ? $self->quote($set_nulls_to) : undef); - my $new_ddl = $self->_bz_schema->get_type_ddl($new_def); - say "Updating column $name in table $table ..."; - if (defined $current_def) { - my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); - say "Old: $old_ddl"; - } - say "New: $new_ddl"; - $self->do($_) foreach (@statements); + my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_; + my @statements + = $self->_bz_real_schema->get_alter_column_ddl($table, $name, $new_def, + defined $set_nulls_to ? $self->quote($set_nulls_to) : undef); + my $new_ddl = $self->_bz_schema->get_type_ddl($new_def); + say "Updating column $name in table $table ..."; + if (defined $current_def) { + my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); + say "Old: $old_ddl"; + } + say "New: $new_ddl"; + $self->do($_) foreach (@statements); } sub bz_alter_fk { - my ($self, $table, $column, $fk_def) = @_; - my $current_fk = $self->bz_fk_info($table, $column); - ThrowCodeError('column_alter_nonexistent_fk', - { table => $table, column => $column }) if !$current_fk; - $self->bz_drop_fk($table, $column); - $self->bz_add_fk($table, $column, $fk_def); + my ($self, $table, $column, $fk_def) = @_; + my $current_fk = $self->bz_fk_info($table, $column); + ThrowCodeError('column_alter_nonexistent_fk', + {table => $table, column => $column}) + if !$current_fk; + $self->bz_drop_fk($table, $column); + $self->bz_add_fk($table, $column, $fk_def); } sub bz_add_index { - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - my $index_exists = $self->bz_index_info($table, $name); + my $index_exists = $self->bz_index_info($table, $name); - if (!$index_exists) { - $self->bz_add_index_raw($table, $name, $definition); - $self->_bz_real_schema->set_index($table, $name, $definition); - $self->_bz_store_real_schema; - } + if (!$index_exists) { + $self->bz_add_index_raw($table, $name, $definition); + $self->_bz_real_schema->set_index($table, $name, $definition); + $self->_bz_store_real_schema; + } } # bz_add_index_raw($table, $name, $silent) @@ -780,36 +793,36 @@ sub bz_add_index { # Returns: nothing # sub bz_add_index_raw { - my ($self, $table, $name, $definition, $silent) = @_; - my @statements = $self->_bz_schema->get_add_index_ddl( - $table, $name, $definition); - print "Adding new index '$name' to the $table table ...\n" unless $silent; - $self->do($_) foreach (@statements); + my ($self, $table, $name, $definition, $silent) = @_; + my @statements + = $self->_bz_schema->get_add_index_ddl($table, $name, $definition); + print "Adding new index '$name' to the $table table ...\n" unless $silent; + $self->do($_) foreach (@statements); } sub bz_add_table { - my ($self, $name, $options) = @_; - - my $table_exists = $self->bz_table_info($name); - - if (!$table_exists) { - $self->_bz_add_table_raw($name, $options); - my $table_def = dclone($self->_bz_schema->get_table_abstract($name)); - - my %fields = @{$table_def->{FIELDS}}; - foreach my $col (keys %fields) { - # Foreign Key references have to be added by Install::DB after - # initial table creation, because column names have changed - # over history and it's impossible to keep track of that info - # in ABSTRACT_SCHEMA. - next unless exists $fields{$col}->{REFERENCES}; - $fields{$col}->{REFERENCES}->{created} = - $self->_bz_real_schema->FK_ON_CREATE; - } - - $self->_bz_real_schema->add_table($name, $table_def); - $self->_bz_store_real_schema; + my ($self, $name, $options) = @_; + + my $table_exists = $self->bz_table_info($name); + + if (!$table_exists) { + $self->_bz_add_table_raw($name, $options); + my $table_def = dclone($self->_bz_schema->get_table_abstract($name)); + + my %fields = @{$table_def->{FIELDS}}; + foreach my $col (keys %fields) { + + # Foreign Key references have to be added by Install::DB after + # initial table creation, because column names have changed + # over history and it's impossible to keep track of that info + # in ABSTRACT_SCHEMA. + next unless exists $fields{$col}->{REFERENCES}; + $fields{$col}->{REFERENCES}->{created} = $self->_bz_real_schema->FK_ON_CREATE; } + + $self->_bz_real_schema->add_table($name, $table_def); + $self->_bz_store_real_schema; + } } # _bz_add_table_raw($name) - Private @@ -821,164 +834,174 @@ sub bz_add_table { # _bz_init_schema_storage. Used when you don't # yet have a Schema object but you need to # add a table, for some reason. -# Params: $name - The name of the table you're creating. -# The definition for the table is pulled from +# Params: $name - The name of the table you're creating. +# The definition for the table is pulled from # _bz_schema. # Returns: nothing # sub _bz_add_table_raw { - my ($self, $name, $options) = @_; - my @statements = $self->_bz_schema->get_table_ddl($name); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - and !$options->{silently}) - { - say install_string('db_table_new', { table => $name }); - } - $self->do($_) foreach (@statements); + my ($self, $name, $options) = @_; + my @statements = $self->_bz_schema->get_table_ddl($name); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) { + say install_string('db_table_new', {table => $name}); + } + $self->do($_) foreach (@statements); } sub _bz_add_field_table { - my ($self, $name, $schema_ref) = @_; - # We do nothing if the table already exists. - return if $self->bz_table_info($name); - - # Copy this so that we're not modifying the passed reference. - # (This avoids modifying a constant in Bugzilla::DB::Schema.) - my %table_schema = %$schema_ref; - my %indexes = @{ $table_schema{INDEXES} }; - my %fixed_indexes; - foreach my $key (keys %indexes) { - $fixed_indexes{$name . "_" . $key} = $indexes{$key}; - } - # INDEXES is supposed to be an arrayref, so we have to convert back. - my @indexes_array = %fixed_indexes; - $table_schema{INDEXES} = \@indexes_array; - # We add this to the abstract schema so that bz_add_table can find it. - $self->_bz_schema->add_table($name, \%table_schema); - $self->bz_add_table($name); + my ($self, $name, $schema_ref) = @_; + + # We do nothing if the table already exists. + return if $self->bz_table_info($name); + + # Copy this so that we're not modifying the passed reference. + # (This avoids modifying a constant in Bugzilla::DB::Schema.) + my %table_schema = %$schema_ref; + my %indexes = @{$table_schema{INDEXES}}; + my %fixed_indexes; + foreach my $key (keys %indexes) { + $fixed_indexes{$name . "_" . $key} = $indexes{$key}; + } + + # INDEXES is supposed to be an arrayref, so we have to convert back. + my @indexes_array = %fixed_indexes; + $table_schema{INDEXES} = \@indexes_array; + + # We add this to the abstract schema so that bz_add_table can find it. + $self->_bz_schema->add_table($name, \%table_schema); + $self->bz_add_table($name); } sub bz_add_field_tables { - my ($self, $field) = @_; - - $self->_bz_add_field_table($field->name, - $self->_bz_schema->FIELD_TABLE_SCHEMA, $field->type); - 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_add_fks($ms_table, - { bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', - DELETE => 'CASCADE'}, - - value => {TABLE => $field->name, COLUMN => 'value'} }); - } + my ($self, $field) = @_; + + $self->_bz_add_field_table($field->name, $self->_bz_schema->FIELD_TABLE_SCHEMA, + $field->type); + 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_add_fks( + $ms_table, + { + bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}, + + value => {TABLE => $field->name, COLUMN => 'value'} + } + ); + } } sub bz_drop_field_tables { - my ($self, $field) = @_; - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - $self->bz_drop_table('bug_' . $field->name); - } - $self->bz_drop_table($field->name); + my ($self, $field) = @_; + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + $self->bz_drop_table('bug_' . $field->name); + } + $self->bz_drop_table($field->name); } sub bz_drop_column { - my ($self, $table, $column) = @_; - - my $current_def = $self->bz_column_info($table, $column); - - if ($current_def) { - my @statements = $self->_bz_real_schema->get_drop_column_ddl( - $table, $column); - print get_text('install_column_drop', - { table => $table, column => $column }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - $self->_bz_real_schema->delete_column($table, $column); - $self->_bz_store_real_schema; + my ($self, $table, $column) = @_; + + my $current_def = $self->bz_column_info($table, $column); + + if ($current_def) { + my @statements = $self->_bz_real_schema->get_drop_column_ddl($table, $column); + print get_text('install_column_drop', {table => $table, column => $column}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + $self->_bz_real_schema->delete_column($table, $column); + $self->_bz_store_real_schema; + } } sub bz_drop_fk { - my ($self, $table, $column) = @_; - - my $fk_def = $self->bz_fk_info($table, $column); - if ($fk_def and $fk_def->{created}) { - print get_text('install_fk_drop', - { table => $table, column => $column, fk => $fk_def }) - . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - my @statements = - $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def); - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - # Under normal circumstances, we don't permanently drop the fk-- - # we want checksetup to re-create it again later. The only - # time that FKs get permanently dropped is if the column gets - # dropped. - $fk_def->{created} = 0; - $self->_bz_real_schema->set_fk($table, $column, $fk_def); - $self->_bz_store_real_schema; + my ($self, $table, $column) = @_; + + my $fk_def = $self->bz_fk_info($table, $column); + if ($fk_def and $fk_def->{created}) { + print get_text('install_fk_drop', + {table => $table, column => $column, fk => $fk_def}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + my @statements + = $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def); + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + # Under normal circumstances, we don't permanently drop the fk-- + # we want checksetup to re-create it again later. The only + # time that FKs get permanently dropped is if the column gets + # dropped. + $fk_def->{created} = 0; + $self->_bz_real_schema->set_fk($table, $column, $fk_def); + $self->_bz_store_real_schema; + } + } sub bz_get_related_fks { - my ($self, $table, $column) = @_; - my @tables = $self->_bz_real_schema->get_table_list(); - my @related; - foreach my $check_table (@tables) { - my @columns = $self->bz_table_columns($check_table); - foreach my $check_column (@columns) { - my $fk = $self->bz_fk_info($check_table, $check_column); - if ($fk - and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column) - or ($check_column eq $column and $check_table eq $table))) - { - push(@related, [$check_table, $check_column, $fk]); - } - } # foreach $column - } # foreach $table - - return \@related; + my ($self, $table, $column) = @_; + my @tables = $self->_bz_real_schema->get_table_list(); + my @related; + foreach my $check_table (@tables) { + my @columns = $self->bz_table_columns($check_table); + foreach my $check_column (@columns) { + my $fk = $self->bz_fk_info($check_table, $check_column); + if ( + $fk + and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column) + or ($check_column eq $column and $check_table eq $table)) + ) + { + push(@related, [$check_table, $check_column, $fk]); + } + } # foreach $column + } # foreach $table + + return \@related; } sub bz_drop_related_fks { - my $self = shift; - my $related = $self->bz_get_related_fks(@_); - foreach my $item (@$related) { - my ($table, $column) = @$item; - $self->bz_drop_fk($table, $column); - } - return $related; + my $self = shift; + my $related = $self->bz_get_related_fks(@_); + foreach my $item (@$related) { + my ($table, $column) = @$item; + $self->bz_drop_fk($table, $column); + } + return $related; } sub bz_drop_index { - my ($self, $table, $name) = @_; + my ($self, $table, $name) = @_; - my $index_exists = $self->bz_index_info($table, $name); + my $index_exists = $self->bz_index_info($table, $name); - if ($index_exists) { - if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) { - # We cannot delete an index used by a FK. - foreach my $column (@{$index_exists->{FIELDS}}) { - $self->bz_drop_related_fks($table, $column); - } - } - $self->bz_drop_index_raw($table, $name); - $self->_bz_real_schema->delete_index($table, $name); - $self->_bz_store_real_schema; + if ($index_exists) { + if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) { + + # We cannot delete an index used by a FK. + foreach my $column (@{$index_exists->{FIELDS}}) { + $self->bz_drop_related_fks($table, $column); + } } + $self->bz_drop_index_raw($table, $name); + $self->_bz_real_schema->delete_index($table, $name); + $self->_bz_store_real_schema; + } } # bz_drop_index_raw($table, $name, $silent) @@ -987,7 +1010,7 @@ sub bz_drop_index { # Drops an index from the database # without updating any Schema object. Generally # should only be called by bz_drop_index. -# Used when either: (1) You don't yet have a Schema +# Used when either: (1) You don't yet have a Schema # object but you need to drop an index, for some reason. # (2) You need to drop an index that somehow got into the # database but doesn't exist in Schema. @@ -998,108 +1021,111 @@ sub bz_drop_index { # Returns: nothing # sub bz_drop_index_raw { - my ($self, $table, $name, $silent) = @_; - my @statements = $self->_bz_schema->get_drop_index_ddl( - $table, $name); - print "Removing index '$name' from the $table table...\n" unless $silent; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@"; - } + my ($self, $table, $name, $silent) = @_; + my @statements = $self->_bz_schema->get_drop_index_ddl($table, $name); + print "Removing index '$name' from the $table table...\n" unless $silent; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@"; + } } sub bz_drop_table { - my ($self, $name) = @_; - - my $table_exists = $self->bz_table_info($name); - - if ($table_exists) { - my @statements = $self->_bz_schema->get_drop_table_ddl($name); - print get_text('install_table_drop', { name => $name }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - $self->_bz_real_schema->delete_table($name); - $self->_bz_store_real_schema; + my ($self, $name) = @_; + + my $table_exists = $self->bz_table_info($name); + + if ($table_exists) { + my @statements = $self->_bz_schema->get_drop_table_ddl($name); + print get_text('install_table_drop', {name => $name}) . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + $self->_bz_real_schema->delete_table($name); + $self->_bz_store_real_schema; + } } 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; + 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) = @_; + my ($self, $table, $old_name, $new_name) = @_; - my $old_col_exists = $self->bz_column_info($table, $old_name); + my $old_col_exists = $self->bz_column_info($table, $old_name); - if ($old_col_exists) { - my $already_renamed = $self->bz_column_info($table, $new_name); - ThrowCodeError('db_rename_conflict', - { old => "$table.$old_name", - new => "$table.$new_name" }) if $already_renamed; - my @statements = $self->_bz_real_schema->get_rename_column_ddl( - $table, $old_name, $new_name); + if ($old_col_exists) { + my $already_renamed = $self->bz_column_info($table, $new_name); + ThrowCodeError('db_rename_conflict', + {old => "$table.$old_name", new => "$table.$new_name"}) + if $already_renamed; + my @statements + = $self->_bz_real_schema->get_rename_column_ddl($table, $old_name, $new_name); - print get_text('install_column_rename', - { old => "$table.$old_name", new => "$table.$new_name" }) - . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + print get_text('install_column_rename', + {old => "$table.$old_name", new => "$table.$new_name"}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - $self->do($sql); - } - $self->_bz_real_schema->rename_column($table, $old_name, $new_name); - $self->_bz_store_real_schema; + foreach my $sql (@statements) { + $self->do($sql); } + $self->_bz_real_schema->rename_column($table, $old_name, $new_name); + $self->_bz_store_real_schema; + } } sub bz_rename_table { - my ($self, $old_name, $new_name) = @_; - my $old_table = $self->bz_table_info($old_name); - return if !$old_table; - - my $new = $self->bz_table_info($new_name); - ThrowCodeError('db_rename_conflict', { old => $old_name, - new => $new_name }) if $new; - - # FKs will all have the wrong names unless we drop and then let them - # be re-created later. Under normal circumstances, checksetup.pl will - # automatically re-create these dropped FKs at the end of its DB upgrade - # run, so we don't need to re-create them in this method. - my @columns = $self->bz_table_columns($old_name); - foreach my $column (@columns) { - # these just return silently if there's no FK to drop - $self->bz_drop_fk($old_name, $column); - $self->bz_drop_related_fks($old_name, $column); - } - - my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name); - print get_text('install_table_rename', - { old => $old_name, new => $new_name }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - $self->do($_) foreach @sql; - $self->_bz_real_schema->rename_table($old_name, $new_name); - $self->_bz_store_real_schema; + my ($self, $old_name, $new_name) = @_; + my $old_table = $self->bz_table_info($old_name); + return if !$old_table; + + my $new = $self->bz_table_info($new_name); + ThrowCodeError('db_rename_conflict', {old => $old_name, new => $new_name}) + if $new; + + # FKs will all have the wrong names unless we drop and then let them + # be re-created later. Under normal circumstances, checksetup.pl will + # automatically re-create these dropped FKs at the end of its DB upgrade + # run, so we don't need to re-create them in this method. + my @columns = $self->bz_table_columns($old_name); + foreach my $column (@columns) { + + # these just return silently if there's no FK to drop + $self->bz_drop_fk($old_name, $column); + $self->bz_drop_related_fks($old_name, $column); + } + + my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name); + print get_text('install_table_rename', {old => $old_name, new => $new_name}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + $self->do($_) foreach @sql; + $self->_bz_real_schema->rename_table($old_name, $new_name); + $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; + 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; } ##################################################################### @@ -1107,12 +1133,12 @@ sub bz_set_next_serial_value { ##################################################################### sub _bz_schema { - my ($self) = @_; - return $self->{private_bz_schema} if exists $self->{private_bz_schema}; - my @module_parts = split('::', ref $self); - my $module_name = pop @module_parts; - $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name); - return $self->{private_bz_schema}; + my ($self) = @_; + return $self->{private_bz_schema} if exists $self->{private_bz_schema}; + my @module_parts = split('::', ref $self); + my $module_name = pop @module_parts; + $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name); + return $self->{private_bz_schema}; } # _bz_get_initial_schema() @@ -1126,53 +1152,54 @@ sub _bz_schema { # Returns: A Schema object that can be serialized and written to disk # for _bz_init_schema_storage. sub _bz_get_initial_schema { - my ($self) = @_; - return $self->_bz_schema->get_empty_schema(); + my ($self) = @_; + return $self->_bz_schema->get_empty_schema(); } sub bz_column_info { - my ($self, $table, $column) = @_; - my $def = $self->_bz_real_schema->get_column_abstract($table, $column); - # We dclone it so callers can't modify the Schema. - $def = dclone($def) if defined $def; - return $def; + my ($self, $table, $column) = @_; + my $def = $self->_bz_real_schema->get_column_abstract($table, $column); + + # We dclone it so callers can't modify the Schema. + $def = dclone($def) if defined $def; + return $def; } sub bz_index_info { - my ($self, $table, $index) = @_; - my $index_def = - $self->_bz_real_schema->get_index_abstract($table, $index); - if (ref($index_def) eq 'ARRAY') { - $index_def = {FIELDS => $index_def, TYPE => ''}; - } - return $index_def; + my ($self, $table, $index) = @_; + my $index_def = $self->_bz_real_schema->get_index_abstract($table, $index); + if (ref($index_def) eq 'ARRAY') { + $index_def = {FIELDS => $index_def, TYPE => ''}; + } + return $index_def; } sub bz_table_info { - my ($self, $table) = @_; - return $self->_bz_real_schema->get_table_abstract($table); + my ($self, $table) = @_; + return $self->_bz_real_schema->get_table_abstract($table); } sub bz_table_columns { - my ($self, $table) = @_; - return $self->_bz_real_schema->get_table_columns($table); + my ($self, $table) = @_; + return $self->_bz_real_schema->get_table_columns($table); } sub bz_table_indexes { - my ($self, $table) = @_; - my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table); - my %return_indexes; - # We do this so that they're always hashes. - foreach my $name (keys %$indexes) { - $return_indexes{$name} = $self->bz_index_info($table, $name); - } - return \%return_indexes; + my ($self, $table) = @_; + my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table); + my %return_indexes; + + # We do this so that they're always hashes. + foreach my $name (keys %$indexes) { + $return_indexes{$name} = $self->bz_index_info($table, $name); + } + return \%return_indexes; } sub bz_table_list { - my ($self) = @_; - return $self->_bz_real_schema->get_table_list(); + my ($self) = @_; + return $self->_bz_real_schema->get_table_list(); } ##################################################################### @@ -1191,9 +1218,9 @@ sub bz_table_list { # Returns: An array of column names. # sub bz_table_columns_real { - my ($self, $table) = @_; - my $sth = $self->column_info(undef, undef, $table, '%'); - return @{ $self->selectcol_arrayref($sth, {Columns => [4]}) }; + my ($self, $table) = @_; + my $sth = $self->column_info(undef, undef, $table, '%'); + return @{$self->selectcol_arrayref($sth, {Columns => [4]})}; } # bz_table_list_real() @@ -1203,9 +1230,9 @@ sub bz_table_columns_real { # Params: none # Returns: An array containing table names. sub bz_table_list_real { - my ($self) = @_; - my $table_sth = $self->table_info(undef, undef, undef, "TABLE"); - return @{$self->selectcol_arrayref($table_sth, { Columns => [3] })}; + my ($self) = @_; + my $table_sth = $self->table_info(undef, undef, undef, "TABLE"); + return @{$self->selectcol_arrayref($table_sth, {Columns => [3]})}; } ##################################################################### @@ -1213,55 +1240,59 @@ sub bz_table_list_real { ##################################################################### sub bz_in_transaction { - return $_[0]->{private_bz_transaction_count} ? 1 : 0; + return $_[0]->{private_bz_transaction_count} ? 1 : 0; } sub bz_start_transaction { - my ($self) = @_; - - if ($self->bz_in_transaction) { - $self->{private_bz_transaction_count}++; - } else { - # Turn AutoCommit off and start a new transaction - $self->begin_work(); - # REPEATABLE READ means "We work on a snapshot of the DB that - # is created when we execute our first SQL statement." It's - # what we need in Bugzilla to be safe, for what we do. - # Different DBs have different defaults for their isolation - # level, so we just set it here manually. - if ($self->ISOLATION_LEVEL) { - $self->do('SET TRANSACTION ISOLATION LEVEL ' - . $self->ISOLATION_LEVEL); - } - $self->{private_bz_transaction_count} = 1; + my ($self) = @_; + + if ($self->bz_in_transaction) { + $self->{private_bz_transaction_count}++; + } + else { + # Turn AutoCommit off and start a new transaction + $self->begin_work(); + + # REPEATABLE READ means "We work on a snapshot of the DB that + # is created when we execute our first SQL statement." It's + # what we need in Bugzilla to be safe, for what we do. + # Different DBs have different defaults for their isolation + # level, so we just set it here manually. + if ($self->ISOLATION_LEVEL) { + $self->do('SET TRANSACTION ISOLATION LEVEL ' . $self->ISOLATION_LEVEL); } + $self->{private_bz_transaction_count} = 1; + } } sub bz_commit_transaction { - my ($self) = @_; - - if ($self->{private_bz_transaction_count} > 1) { - $self->{private_bz_transaction_count}--; - } elsif ($self->bz_in_transaction) { - $self->commit(); - $self->{private_bz_transaction_count} = 0; - Bugzilla::Mailer->send_staged_mail(); - } else { - ThrowCodeError('not_in_transaction'); - } + my ($self) = @_; + + if ($self->{private_bz_transaction_count} > 1) { + $self->{private_bz_transaction_count}--; + } + elsif ($self->bz_in_transaction) { + $self->commit(); + $self->{private_bz_transaction_count} = 0; + Bugzilla::Mailer->send_staged_mail(); + } + else { + ThrowCodeError('not_in_transaction'); + } } sub bz_rollback_transaction { - my ($self) = @_; - - # Unlike start and commit, if we rollback at any point it happens - # instantly, even if we're in a nested transaction. - if (!$self->bz_in_transaction) { - ThrowCodeError("not_in_transaction"); - } else { - $self->rollback(); - $self->{private_bz_transaction_count} = 0; - } + my ($self) = @_; + + # Unlike start and commit, if we rollback at any point it happens + # instantly, even if we're in a nested transaction. + if (!$self->bz_in_transaction) { + ThrowCodeError("not_in_transaction"); + } + else { + $self->rollback(); + $self->{private_bz_transaction_count} = 0; + } } ##################################################################### @@ -1269,42 +1300,43 @@ sub bz_rollback_transaction { ##################################################################### sub db_new { - my ($class, $params) = @_; - my ($dsn, $user, $pass, $override_attrs) = - @$params{qw(dsn user pass attrs)}; - - # set up default attributes used to connect to the database - # (may be overridden by DB driver implementations) - my $attributes = { RaiseError => 0, - AutoCommit => 1, - PrintError => 0, - ShowErrorStatement => 1, - HandleError => \&_handle_error, - TaintIn => 1, - # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933 - # for the reason to use NAME instead of NAME_lc (bug 253696). - FetchHashKeyName => 'NAME', - }; - - if ($override_attrs) { - foreach my $key (keys %$override_attrs) { - $attributes->{$key} = $override_attrs->{$key}; - } + my ($class, $params) = @_; + my ($dsn, $user, $pass, $override_attrs) = @$params{qw(dsn user pass attrs)}; + + # set up default attributes used to connect to the database + # (may be overridden by DB driver implementations) + my $attributes = { + RaiseError => 0, + AutoCommit => 1, + PrintError => 0, + ShowErrorStatement => 1, + HandleError => \&_handle_error, + TaintIn => 1, + + # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933 + # for the reason to use NAME instead of NAME_lc (bug 253696). + FetchHashKeyName => 'NAME', + }; + + if ($override_attrs) { + foreach my $key (keys %$override_attrs) { + $attributes->{$key} = $override_attrs->{$key}; } + } - # connect using our known info to the specified db - my $self = DBI->connect($dsn, $user, $pass, $attributes) - or die "\nCan't connect to the database.\nError: $DBI::errstr\n" - . " Is your database installed and up and running?\n Do you have" - . " the correct username and password selected in localconfig?\n\n"; + # connect using our known info to the specified db + my $self = DBI->connect($dsn, $user, $pass, $attributes) + or die "\nCan't connect to the database.\nError: $DBI::errstr\n" + . " Is your database installed and up and running?\n Do you have" + . " the correct username and password selected in localconfig?\n\n"; - # RaiseError was only set to 0 so that we could catch the - # above "die" condition. - $self->{RaiseError} = 1; + # RaiseError was only set to 0 so that we could catch the + # above "die" condition. + $self->{RaiseError} = 1; - bless ($self, $class); + bless($self, $class); - return $self; + return $self; } ##################################################################### @@ -1328,55 +1360,54 @@ These methods really are private. Do not override them in subclasses. =cut sub _bz_init_schema_storage { - my ($self) = @_; - - my $table_size; - eval { - $table_size = - $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - }; + my ($self) = @_; + + my $table_size; + eval { $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); }; + + if (!$table_size) { + my $init_schema = $self->_bz_get_initial_schema; + my $store_me = $init_schema->serialize_abstract(); + my $schema_version = $init_schema->SCHEMA_VERSION; + + # If table_size is not defined, then we hit an error reading the + # bz_schema table, which means it probably doesn't exist yet. So, + # we have to create it. If we failed above for some other reason, + # we'll see the failure here. + # However, we must create the table after we do get_initial_schema, + # because some versions of get_initial_schema read that the table + # exists and then add it to the Schema, where other versions don't. + if (!defined $table_size) { + $self->_bz_add_table_raw('bz_schema'); + } - if (!$table_size) { - my $init_schema = $self->_bz_get_initial_schema; - my $store_me = $init_schema->serialize_abstract(); - my $schema_version = $init_schema->SCHEMA_VERSION; - - # If table_size is not defined, then we hit an error reading the - # bz_schema table, which means it probably doesn't exist yet. So, - # we have to create it. If we failed above for some other reason, - # we'll see the failure here. - # However, we must create the table after we do get_initial_schema, - # because some versions of get_initial_schema read that the table - # exists and then add it to the Schema, where other versions don't. - if (!defined $table_size) { - $self->_bz_add_table_raw('bz_schema'); - } + say install_string('db_schema_init'); + my $sth = $self->prepare( + "INSERT INTO bz_schema " . " (schema_data, version) VALUES (?,?)"); + $sth->bind_param(1, $store_me, $self->BLOB_TYPE); + $sth->bind_param(2, $schema_version); + $sth->execute(); - say install_string('db_schema_init'); - my $sth = $self->prepare("INSERT INTO bz_schema " - ." (schema_data, version) VALUES (?,?)"); - $sth->bind_param(1, $store_me, $self->BLOB_TYPE); - $sth->bind_param(2, $schema_version); - $sth->execute(); - - # And now we have to update the on-disk schema to hold the bz_schema - # table, if the bz_schema table didn't exist when we were called. - if (!defined $table_size) { - $self->_bz_real_schema->add_table('bz_schema', - $self->_bz_schema->get_table_abstract('bz_schema')); - $self->_bz_store_real_schema; - } - } - # Sanity check - elsif ($table_size > 1) { - # We tell them to delete the newer one. Better to have checksetup - # run migration code too many times than to have it not run the - # correct migration code at all. - die "Attempted to initialize the schema but there are already " - . " $table_size copies of it stored.\nThis should never happen.\n" - . " Compare the rows of the bz_schema table and delete the " - . "newer one(s)."; + # And now we have to update the on-disk schema to hold the bz_schema + # table, if the bz_schema table didn't exist when we were called. + if (!defined $table_size) { + $self->_bz_real_schema->add_table('bz_schema', + $self->_bz_schema->get_table_abstract('bz_schema')); + $self->_bz_store_real_schema; } + } + + # Sanity check + elsif ($table_size > 1) { + + # We tell them to delete the newer one. Better to have checksetup + # run migration code too many times than to have it not run the + # correct migration code at all. + die "Attempted to initialize the schema but there are already " + . " $table_size copies of it stored.\nThis should never happen.\n" + . " Compare the rows of the bz_schema table and delete the " + . "newer one(s)."; + } } =item C<_bz_real_schema()> @@ -1390,24 +1421,23 @@ sub _bz_init_schema_storage { =cut sub _bz_real_schema { - my ($self) = @_; - return $self->{private_real_schema} if exists $self->{private_real_schema}; - - my $bz_schema; - unless ($bz_schema = Bugzilla->memcached->get({ key => 'bz_schema' })) { - $bz_schema = $self->selectrow_arrayref( - "SELECT schema_data, version FROM bz_schema" - ); - Bugzilla->memcached->set({ key => 'bz_schema', value => $bz_schema }); - } + my ($self) = @_; + return $self->{private_real_schema} if exists $self->{private_real_schema}; - (die "_bz_real_schema tried to read the bz_schema table but it's empty!") - if !$bz_schema; + my $bz_schema; + unless ($bz_schema = Bugzilla->memcached->get({key => 'bz_schema'})) { + $bz_schema + = $self->selectrow_arrayref("SELECT schema_data, version FROM bz_schema"); + Bugzilla->memcached->set({key => 'bz_schema', value => $bz_schema}); + } - $self->{private_real_schema} = - $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]); + (die "_bz_real_schema tried to read the bz_schema table but it's empty!") + if !$bz_schema; - return $self->{private_real_schema}; + $self->{private_real_schema} + = $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]); + + return $self->{private_real_schema}; } =item C<_bz_store_real_schema()> @@ -1427,106 +1457,135 @@ sub _bz_real_schema { =cut sub _bz_store_real_schema { - my ($self) = @_; - - # Make sure that there's a schema to update - my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - - die "Attempted to update the bz_schema table but there's nothing " - . "there to update. Run checksetup." unless $table_size; - - # We want to store the current object, not one - # that we read from the database. So we use the actual hash - # member instead of the subroutine call. If the hash - # member is not defined, we will (and should) fail. - my $update_schema = $self->{private_real_schema}; - my $store_me = $update_schema->serialize_abstract(); - my $schema_version = $update_schema->SCHEMA_VERSION; - my $sth = $self->prepare("UPDATE bz_schema - SET schema_data = ?, version = ?"); - $sth->bind_param(1, $store_me, $self->BLOB_TYPE); - $sth->bind_param(2, $schema_version); - $sth->execute(); + my ($self) = @_; + + # Make sure that there's a schema to update + my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - Bugzilla->memcached->clear({ key => 'bz_schema' }); + die "Attempted to update the bz_schema table but there's nothing " + . "there to update. Run checksetup." + unless $table_size; + + # We want to store the current object, not one + # that we read from the database. So we use the actual hash + # member instead of the subroutine call. If the hash + # member is not defined, we will (and should) fail. + my $update_schema = $self->{private_real_schema}; + my $store_me = $update_schema->serialize_abstract(); + my $schema_version = $update_schema->SCHEMA_VERSION; + my $sth = $self->prepare( + "UPDATE bz_schema + SET schema_data = ?, version = ?" + ); + $sth->bind_param(1, $store_me, $self->BLOB_TYPE); + $sth->bind_param(2, $schema_version); + $sth->execute(); + + Bugzilla->memcached->clear({key => 'bz_schema'}); } # For bz_populate_enum_tables sub _bz_populate_enum_table { - my ($self, $table, $valuelist) = @_; - - my $sql_table = $self->quote_identifier($table); - - # Check if there are any table entries - my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table"); - - # If the table is empty... - if (!$table_size) { - print " $table"; - my $insert = $self->prepare( - "INSERT INTO $sql_table (value,sortkey) VALUES (?,?)"); - my $sortorder = 0; - my $maxlen = max(map(length($_), @$valuelist)) + 2; - foreach my $value (@$valuelist) { - $sortorder += 100; - $insert->execute($value, $sortorder); - } + my ($self, $table, $valuelist) = @_; + + my $sql_table = $self->quote_identifier($table); + + # Check if there are any table entries + my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table"); + + # If the table is empty... + if (!$table_size) { + print " $table"; + my $insert + = $self->prepare("INSERT INTO $sql_table (value,sortkey) VALUES (?,?)"); + my $sortorder = 0; + my $maxlen = max(map(length($_), @$valuelist)) + 2; + foreach my $value (@$valuelist) { + $sortorder += 100; + $insert->execute($value, $sortorder); } + } } # This is used before adding a foreign key to a column, to make sure # that the database won't fail adding the key. sub _check_references { - my ($self, $table, $column, $fk) = @_; - my $foreign_table = $fk->{TABLE}; - my $foreign_column = $fk->{COLUMN}; - - # We use table aliases because sometimes we join a table to itself, - # and we can't use the same table name on both sides of the join. - # We also can't use the words "table" or "foreign" because those are - # reserved words. - my $bad_values = $self->selectcol_arrayref( - "SELECT DISTINCT tabl.$column + my ($self, $table, $column, $fk) = @_; + my $foreign_table = $fk->{TABLE}; + my $foreign_column = $fk->{COLUMN}; + + # We use table aliases because sometimes we join a table to itself, + # and we can't use the same table name on both sides of the join. + # We also can't use the words "table" or "foreign" because those are + # reserved words. + my $bad_values = $self->selectcol_arrayref( + "SELECT DISTINCT tabl.$column FROM $table AS tabl LEFT JOIN $foreign_table AS forn ON tabl.$column = forn.$foreign_column WHERE forn.$foreign_column IS NULL - AND tabl.$column IS NOT NULL"); - - if (@$bad_values) { - my $delete_action = $fk->{DELETE} || ''; - if ($delete_action eq 'CASCADE') { - $self->do("DELETE FROM $table WHERE $column IN (" - . join(',', ('?') x @$bad_values) . ")", - undef, @$bad_values); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_fk_invalid_fixed', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values, action => 'delete' }), "\n"; - } - } - elsif ($delete_action eq 'SET NULL') { - $self->do("UPDATE $table SET $column = NULL + AND tabl.$column IS NOT NULL" + ); + + if (@$bad_values) { + my $delete_action = $fk->{DELETE} || ''; + if ($delete_action eq 'CASCADE') { + $self->do( + "DELETE FROM $table WHERE $column IN (" . join(',', ('?') x @$bad_values) . ")", + undef, @$bad_values + ); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", + get_text( + 'install_fk_invalid_fixed', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values, + action => 'delete' + } + ), + "\n"; + } + } + elsif ($delete_action eq 'SET NULL') { + $self->do( + "UPDATE $table SET $column = NULL WHERE $column IN (" - . join(',', ('?') x @$bad_values) . ")", - undef, @$bad_values); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_fk_invalid_fixed', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values, action => 'null' }), "\n"; - } - } - else { - die "\n", get_text('install_fk_invalid', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values }), "\n"; + . join(',', ('?') x @$bad_values) . ")", undef, @$bad_values + ); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", + get_text( + 'install_fk_invalid_fixed', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values, + action => 'null' + } + ), + "\n"; + } + } + else { + die "\n", + get_text( + 'install_fk_invalid', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values } + ), + "\n"; } + } } 1; diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm index d0915f1e6..96a1fcb21 100644 --- a/Bugzilla/DB/Mysql.pm +++ b/Bugzilla/DB/Mysql.pm @@ -37,258 +37,265 @@ use List::Util qw(max); use Text::ParseWords; # This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug. -# In reality, you could have a LOT more comments than this, because +# In reality, you could have a LOT more comments than this, because # MAX_COMMENT_LENGTH is big. use constant MAX_COMMENTS => 50; use constant FULLTEXT_OR => '|'; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port, $sock) = - @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; - - # construct the DSN from the parameters we got - my $dsn = "dbi:mysql:host=$host;database=$dbname"; - $dsn .= ";port=$port" if $port; - $dsn .= ";mysql_socket=$sock" if $sock; - - my %attrs = ( - mysql_enable_utf8 => Bugzilla->params->{'utf8'}, - # Needs to be explicitly specified for command-line processes. - mysql_auto_reconnect => 1, - ); - - # MySQL SSL options - my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = - @$params{qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path - db_mysql_ssl_client_cert db_mysql_ssl_client_key)}; - if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) { - $attrs{'mysql_ssl'} = 1; - $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file; - $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path; - $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert; - $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port, $sock) + = @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; + + # construct the DSN from the parameters we got + my $dsn = "dbi:mysql:host=$host;database=$dbname"; + $dsn .= ";port=$port" if $port; + $dsn .= ";mysql_socket=$sock" if $sock; + + my %attrs = ( + mysql_enable_utf8 => Bugzilla->params->{'utf8'}, + + # Needs to be explicitly specified for command-line processes. + mysql_auto_reconnect => 1, + ); + + # MySQL SSL options + my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = @$params{ + qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path + db_mysql_ssl_client_cert db_mysql_ssl_client_key) + }; + if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) { + $attrs{'mysql_ssl'} = 1; + $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file; + $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path; + $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert; + $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key; + } + + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs}); + + # This makes sure that if the tables are encoded as UTF-8, we + # return their data correctly. + $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'}; + + # all class local variables stored in DBI derived class needs to have + # a prefix 'private_'. See DBI documentation. + $self->{private_bz_tables_locked} = ""; + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + bless($self, $class); + + # Check for MySQL modes. + my ($var, $sql_mode) + = $self->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'"); + + # Disable ANSI and strict modes, else Bugzilla will crash. + if ($sql_mode) { + + # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode, + # causing bug 321645. TRADITIONAL sets these modes (among others) as + # well, so it has to be stipped as well + my $new_sql_mode = join(",", + grep { $_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/ } + split(/,/, $sql_mode)); + + if ($sql_mode ne $new_sql_mode) { + $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); } + } - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => \%attrs }); - - # This makes sure that if the tables are encoded as UTF-8, we - # return their data correctly. - $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'}; - - # all class local variables stored in DBI derived class needs to have - # a prefix 'private_'. See DBI documentation. - $self->{private_bz_tables_locked} = ""; - - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; + # Allow large GROUP_CONCATs (largely for inserting comments + # into bugs_fulltext). + $self->do('SET SESSION group_concat_max_len = 128000000'); - bless ($self, $class); + # MySQL 5.5.2 and older have this variable set to true, which causes + # trouble, see bug 870369. + $self->do('SET SESSION sql_auto_is_null = 0'); - # Check for MySQL modes. - my ($var, $sql_mode) = $self->selectrow_array( - "SHOW VARIABLES LIKE 'sql\\_mode'"); - - # Disable ANSI and strict modes, else Bugzilla will crash. - if ($sql_mode) { - # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode, - # causing bug 321645. TRADITIONAL sets these modes (among others) as - # well, so it has to be stipped as well - my $new_sql_mode = - join(",", grep {$_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/} - split(/,/, $sql_mode)); - - if ($sql_mode ne $new_sql_mode) { - $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); - } - } - - # Allow large GROUP_CONCATs (largely for inserting comments - # into bugs_fulltext). - $self->do('SET SESSION group_concat_max_len = 128000000'); - - # MySQL 5.5.2 and older have this variable set to true, which causes - # trouble, see bug 870369. - $self->do('SET SESSION sql_auto_is_null = 0'); - - return $self; + return $self; } # when last_insert_id() is supported on MySQL by lowest DBI/DBD version # required by Bugzilla, this implementation can be removed. sub bz_last_key { - my ($self) = @_; + my ($self) = @_; - my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()'); + my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()'); - return $last_insert_id; + return $last_insert_id; } sub sql_group_concat { - my ($self, $column, $separator, $sort, $order_by) = @_; - $separator = $self->quote(', ') if !defined $separator; - $sort = 1 if !defined $sort; - if ($order_by) { - $column .= " ORDER BY $order_by"; - } - elsif ($sort) { - my $sort_order = $column; - $sort_order =~ s/^DISTINCT\s+//i; - $column = "$column ORDER BY $sort_order"; - } - return "GROUP_CONCAT($column SEPARATOR $separator)"; + my ($self, $column, $separator, $sort, $order_by) = @_; + $separator = $self->quote(', ') if !defined $separator; + $sort = 1 if !defined $sort; + if ($order_by) { + $column .= " ORDER BY $order_by"; + } + elsif ($sort) { + my $sort_order = $column; + $sort_order =~ s/^DISTINCT\s+//i; + $column = "$column ORDER BY $sort_order"; + } + return "GROUP_CONCAT($column SEPARATOR $separator)"; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr REGEXP $pattern"; + return "$expr REGEXP $pattern"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr NOT REGEXP $pattern"; + return "$expr NOT REGEXP $pattern"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $offset, $limit"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $offset, $limit"; + } + else { + return "LIMIT $limit"; + } } sub sql_string_concat { - my ($self, @params) = @_; - - return 'CONCAT(' . join(', ', @params) . ')'; + my ($self, @params) = @_; + + return 'CONCAT(' . join(', ', @params) . ')'; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - - # Add the boolean mode modifier if the search string contains - # boolean operators at the start or end of a word. - my $mode = ''; - if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) { - $mode = 'IN BOOLEAN MODE'; - - my @terms = split(quotemeta(FULLTEXT_OR), $text); - foreach my $term (@terms) { - # quote un-quoted compound words - my @words = quotewords('[\s()]+', 'delimiters', $term); - foreach my $word (@words) { - # match words that have non-word chars in the middle of them - if ($word =~ /\w\W+\w/ && $word !~ m/"/) { - $word = '"' . $word . '"'; - } - } - $term = join('', @words); + my ($self, $column, $text) = @_; + + # Add the boolean mode modifier if the search string contains + # boolean operators at the start or end of a word. + my $mode = ''; + if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) { + $mode = 'IN BOOLEAN MODE'; + + my @terms = split(quotemeta(FULLTEXT_OR), $text); + foreach my $term (@terms) { + + # quote un-quoted compound words + my @words = quotewords('[\s()]+', 'delimiters', $term); + foreach my $word (@words) { + + # match words that have non-word chars in the middle of them + if ($word =~ /\w\W+\w/ && $word !~ m/"/) { + $word = '"' . $word . '"'; } - $text = join(FULLTEXT_OR, @terms); + } + $term = join('', @words); } + $text = join(FULLTEXT_OR, @terms); + } - # quote the text for use in the MATCH AGAINST expression - $text = $self->quote($text); + # quote the text for use in the MATCH AGAINST expression + $text = $self->quote($text); - # untaint the text, since it's safe to use now that we've quoted it - trick_taint($text); + # untaint the text, since it's safe to use now that we've quoted it + trick_taint($text); - return "MATCH($column) AGAINST($text $mode)"; + return "MATCH($column) AGAINST($text $mode)"; } sub sql_istring { - my ($self, $string) = @_; - - return $string; + my ($self, $string) = @_; + + return $string; } sub sql_from_days { - my ($self, $days) = @_; + my ($self, $days) = @_; - return "FROM_DAYS($days)"; + return "FROM_DAYS($days)"; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return "TO_DAYS($date)"; + return "TO_DAYS($date)"; } sub sql_date_format { - my ($self, $date, $format) = @_; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; - $format = "%Y.%m.%d %H:%i:%s" if !$format; - - return "DATE_FORMAT($date, " . $self->quote($format) . ")"; + return "DATE_FORMAT($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - - return "$date $operator INTERVAL $interval $units"; + my ($self, $date, $operator, $interval, $units) = @_; + + return "$date $operator INTERVAL $interval $units"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - return "INSTR($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "INSTR($text, $fragment)"; } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))"; + return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))"; } sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; + my ($self, $needed_columns, $optional_columns) = @_; - # MySQL allows you to specify the minimal subset of columns to get - # a unique result. While it does allow specifying all columns as - # ANSI SQL requires, according to MySQL documentation, the fewer - # columns you specify, the faster the query runs. - return "GROUP BY $needed_columns"; + # MySQL allows you to specify the minimal subset of columns to get + # a unique result. While it does allow specifying all columns as + # ANSI SQL requires, according to MySQL documentation, the fewer + # columns you specify, the faster the query runs. + return "GROUP BY $needed_columns"; } sub bz_explain { - my ($self, $sql) = @_; - my $sth = $self->prepare("EXPLAIN $sql"); - $sth->execute(); - my $columns = $sth->{'NAME'}; - my $lengths = $sth->{'mysql_max_length'}; - my $format_string = '|'; - my $i = 0; - foreach my $column (@$columns) { - # Sometimes the column name is longer than the contents. - my $length = max($lengths->[$i], length($column)); - $format_string .= ' %-' . $length . 's |'; - $i++; - } - - my $first_row = sprintf($format_string, @$columns); - my @explain_rows = ($first_row, '-' x length($first_row)); - while (my $row = $sth->fetchrow_arrayref) { - my @fixed = map { defined $_ ? $_ : 'NULL' } @$row; - push(@explain_rows, sprintf($format_string, @fixed)); - } - - return join("\n", @explain_rows); + my ($self, $sql) = @_; + my $sth = $self->prepare("EXPLAIN $sql"); + $sth->execute(); + my $columns = $sth->{'NAME'}; + my $lengths = $sth->{'mysql_max_length'}; + my $format_string = '|'; + my $i = 0; + foreach my $column (@$columns) { + + # Sometimes the column name is longer than the contents. + my $length = max($lengths->[$i], length($column)); + $format_string .= ' %-' . $length . 's |'; + $i++; + } + + my $first_row = sprintf($format_string, @$columns); + my @explain_rows = ($first_row, '-' x length($first_row)); + while (my $row = $sth->fetchrow_arrayref) { + my @fixed = map { defined $_ ? $_ : 'NULL' } @$row; + push(@explain_rows, sprintf($format_string, @fixed)); + } + + return join("\n", @explain_rows); } sub _bz_get_initial_schema { - my ($self) = @_; - return $self->_bz_build_schema_from_disk(); + my ($self) = @_; + return $self->_bz_build_schema_from_disk(); } ##################################################################### @@ -296,493 +303,503 @@ sub _bz_get_initial_schema { ##################################################################### sub bz_check_server_version { - my $self = shift; + my $self = shift; - my $lc = Bugzilla->localconfig; - if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') { - die "It is not safe to run Bugzilla inside a database named 'mysql'.\n" - . " Please pick a different value for \$db_name in localconfig.\n"; - } + my $lc = Bugzilla->localconfig; + if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') { + die "It is not safe to run Bugzilla inside a database named 'mysql'.\n" + . " Please pick a different value for \$db_name in localconfig.\n"; + } - $self->SUPER::bz_check_server_version(@_); + $self->SUPER::bz_check_server_version(@_); } sub bz_setup_database { - my ($self) = @_; - - # The "comments" field of the bugs_fulltext table could easily exceed - # MySQL's default max_allowed_packet. Also, MySQL should never have - # a max_allowed_packet smaller than our max_attachment_size. So, we - # warn the user here if max_allowed_packet is too small. - my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH; - my (undef, $current_max_allowed) = $self->selectrow_array( - q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); - # This parameter is not yet defined when the DB is being built for - # the very first time. The code below still works properly, however, - # because the default maxattachmentsize is smaller than $min_max_allowed. - my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024; - my $needed_max_allowed = max($min_max_allowed, $max_attachment); - if ($current_max_allowed < $needed_max_allowed) { - warn install_string('max_allowed_packet', - { current => $current_max_allowed, - needed => $needed_max_allowed }) . "\n"; + my ($self) = @_; + + # The "comments" field of the bugs_fulltext table could easily exceed + # MySQL's default max_allowed_packet. Also, MySQL should never have + # a max_allowed_packet smaller than our max_attachment_size. So, we + # warn the user here if max_allowed_packet is too small. + my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH; + my (undef, $current_max_allowed) + = $self->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); + + # This parameter is not yet defined when the DB is being built for + # the very first time. The code below still works properly, however, + # because the default maxattachmentsize is smaller than $min_max_allowed. + my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024; + my $needed_max_allowed = max($min_max_allowed, $max_attachment); + if ($current_max_allowed < $needed_max_allowed) { + warn install_string('max_allowed_packet', + {current => $current_max_allowed, needed => $needed_max_allowed}) + . "\n"; + } + + # Make sure the installation has InnoDB turned on, or we're going to be + # doing silly things like making foreign keys on MyISAM tables, which is + # hard to fix later. We do this up here because none of the code below + # works if InnoDB is off. (Particularly if we've already converted the + # tables to InnoDB.) + my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1, 2]})}; + if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) { + die install_string('mysql_innodb_disabled'); + } + + + my ($sd_index_deleted, $longdescs_index_deleted); + my @tables = $self->bz_table_list_real(); + + # 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) and !grep($_ eq 'bugs_fulltext', @tables)) { + if ($self->bz_index_info_real('bugs', 'short_desc')) { + $self->bz_drop_index_raw('bugs', 'short_desc'); } - - # Make sure the installation has InnoDB turned on, or we're going to be - # doing silly things like making foreign keys on MyISAM tables, which is - # hard to fix later. We do this up here because none of the code below - # works if InnoDB is off. (Particularly if we've already converted the - # tables to InnoDB.) - my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1,2]})}; - if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) { - die install_string('mysql_innodb_disabled'); + if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) { + $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx'); + $sd_index_deleted = 1; # Used for later schema cleanup. } - - - my ($sd_index_deleted, $longdescs_index_deleted); - my @tables = $self->bz_table_list_real(); - # 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) - and !grep($_ eq 'bugs_fulltext', @tables)) - { - if ($self->bz_index_info_real('bugs', 'short_desc')) { - $self->bz_drop_index_raw('bugs', 'short_desc'); - } - if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) { - $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx'); - $sd_index_deleted = 1; # Used for later schema cleanup. - } + } + 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'); } - 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'); - } - if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) { - $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx'); - $longdescs_index_deleted = 1; # For later schema cleanup. - } + if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) { + $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx'); + $longdescs_index_deleted = 1; # For later schema cleanup. } - - # Upgrade tables from MyISAM to InnoDB - my $db_name = Bugzilla->localconfig->{db_name}; - my $myisam_tables = $self->selectcol_arrayref( - 'SELECT TABLE_NAME FROM information_schema.TABLES - WHERE TABLE_SCHEMA = ? AND ENGINE = ?', - undef, $db_name, 'MyISAM'); - foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) { - @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables; + } + + # Upgrade tables from MyISAM to InnoDB + my $db_name = Bugzilla->localconfig->{db_name}; + my $myisam_tables = $self->selectcol_arrayref( + 'SELECT TABLE_NAME FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND ENGINE = ?', undef, $db_name, 'MyISAM' + ); + foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) { + @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables; + } + + if (scalar @$myisam_tables) { + print "Bugzilla now uses the InnoDB storage engine in MySQL for", + " most tables.\nConverting tables to InnoDB:\n"; + foreach my $table (@$myisam_tables) { + print "Converting table $table... "; + $self->do("ALTER TABLE $table ENGINE = InnoDB"); + print "done.\n"; } - - if (scalar @$myisam_tables) { - print "Bugzilla now uses the InnoDB storage engine in MySQL for", - " most tables.\nConverting tables to InnoDB:\n"; - foreach my $table (@$myisam_tables) { - print "Converting table $table... "; - $self->do("ALTER TABLE $table ENGINE = InnoDB"); - print "done.\n"; - } + } + + # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did + # not provide explicit names for the table indexes. This means + # that our upgrades will not be reliable, because we look for the name + # of the index, not what fields it is on, when doing upgrades. + # (using the name is much better for cross-database compatibility + # and general reliability). It's also very important that our + # Schema object be consistent with what is on the disk. + # + # While we're at it, we also fix some inconsistent index naming + # from the original checkin of Bugzilla::DB::Schema. + + # We check for the existence of a particular "short name" index that + # has existed at least since Bugzilla 2.8, and probably earlier. + # For fixing the inconsistent naming of Schema indexes, + # we also check for one of those inconsistently-named indexes. + if ( + grep($_ eq 'bugs', @tables) + && ( $self->bz_index_info_real('bugs', 'assigned_to') + || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) + ) + { + + # This is a check unrelated to the indexes, to see if people are + # upgrading from 2.18 or below, but somehow have a bz_schema table + # already. This only happens if they have done a mysqldump into + # a database without doing a DROP DATABASE first. + # We just do the check here since this check is a reliable way + # of telling that we are upgrading from a version pre-2.20. + if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) { + die install_string('bz_schema_exists_before_220'); } - - # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did - # not provide explicit names for the table indexes. This means - # that our upgrades will not be reliable, because we look for the name - # of the index, not what fields it is on, when doing upgrades. - # (using the name is much better for cross-database compatibility - # and general reliability). It's also very important that our - # Schema object be consistent with what is on the disk. - # - # While we're at it, we also fix some inconsistent index naming - # from the original checkin of Bugzilla::DB::Schema. - - # We check for the existence of a particular "short name" index that - # has existed at least since Bugzilla 2.8, and probably earlier. - # For fixing the inconsistent naming of Schema indexes, - # we also check for one of those inconsistently-named indexes. - if (grep($_ eq 'bugs', @tables) - && ($self->bz_index_info_real('bugs', 'assigned_to') - || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) ) - { - # This is a check unrelated to the indexes, to see if people are - # upgrading from 2.18 or below, but somehow have a bz_schema table - # already. This only happens if they have done a mysqldump into - # a database without doing a DROP DATABASE first. - # We just do the check here since this check is a reliable way - # of telling that we are upgrading from a version pre-2.20. - if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) { - die install_string('bz_schema_exists_before_220'); - } + my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - # We estimate one minute for each 3000 bugs, plus 3 minutes just - # to handle basic MySQL stuff. - my $rename_time = int($bug_count / 3000) + 3; - # And 45 minutes for every 15,000 attachments, per some experiments. - my ($attachment_count) = - $self->selectrow_array("SELECT COUNT(*) FROM attachments"); - $rename_time += int(($attachment_count * 45) / 15000); - # If we're going to take longer than 5 minutes, we let the user know - # and allow them to abort. - if ($rename_time > 5) { - print "\n", install_string('mysql_index_renaming', - { minutes => $rename_time }); - # Wait 45 seconds for them to respond. - sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE}; - } - print "Renaming indexes...\n"; - - # We can't be interrupted, because of how the "if" - # works above. - local $SIG{INT} = 'IGNORE'; - local $SIG{TERM} = 'IGNORE'; - local $SIG{PIPE} = 'IGNORE'; - - # Certain indexes had names in Schema that did not easily conform - # to a standard. We store those names here, so that they - # can be properly renamed. - # Also, sometimes an old mysqldump would incorrectly rename - # unique indexes to "PRIMARY", so we address that here, also. - my $bad_names = { - # 'when' is a possible leftover from Bugzillas before 2.8 - bugs_activity => ['when', 'bugs_activity_bugid_idx', - 'bugs_activity_bugwhen_idx'], - cc => ['PRIMARY'], - longdescs => ['longdescs_bugid_idx', - 'longdescs_bugwhen_idx'], - flags => ['flags_bidattid_idx'], - flaginclusions => ['flaginclusions_tpcid_idx'], - flagexclusions => ['flagexclusions_tpc_id_idx'], - keywords => ['PRIMARY'], - milestones => ['PRIMARY'], - profiles_activity => ['profiles_activity_when_idx'], - group_control_map => ['group_control_map_gid_idx', 'PRIMARY'], - user_group_map => ['PRIMARY'], - group_group_map => ['PRIMARY'], - email_setting => ['PRIMARY'], - bug_group_map => ['PRIMARY'], - category_group_map => ['PRIMARY'], - watch => ['PRIMARY'], - namedqueries => ['PRIMARY'], - series_data => ['PRIMARY'], - # series_categories is dealt with below, not here. - }; - - # The series table is broken and needs to have one index - # dropped before we begin the renaming, because it had a - # useless index on it that would cause a naming conflict here. - if (grep($_ eq 'series', @tables)) { - my $dropname; - # This is what the bad index was called before Schema. - if ($self->bz_index_info_real('series', 'creator_2')) { - $dropname = 'creator_2'; - } - # This is what the bad index is called in Schema. - elsif ($self->bz_index_info_real('series', 'series_creator_idx')) { - $dropname = 'series_creator_idx'; - } - $self->bz_drop_index_raw('series', $dropname) if $dropname; - } + # We estimate one minute for each 3000 bugs, plus 3 minutes just + # to handle basic MySQL stuff. + my $rename_time = int($bug_count / 3000) + 3; - # The email_setting table also had the same problem. - if( grep($_ eq 'email_setting', @tables) - && $self->bz_index_info_real('email_setting', - 'email_settings_user_id_idx') ) - { - $self->bz_drop_index_raw('email_setting', - 'email_settings_user_id_idx'); - } - - # Go through all the tables. - foreach my $table (@tables) { - # Will contain the names of old indexes as keys, and the - # definition of the new indexes as a value. The values - # include an extra hash key, NAME, with the new name of - # the index. - my %rename_indexes; - # And go through all the columns on each table. - my @columns = $self->bz_table_columns_real($table); - - # We also want to fix the silly naming of unique indexes - # that happened when we first checked-in Bugzilla::DB::Schema. - if ($table eq 'series_categories') { - # The series_categories index had a nonstandard name. - push(@columns, 'series_cats_unique_idx'); - } - elsif ($table eq 'email_setting') { - # The email_setting table had a similar problem. - push(@columns, 'email_settings_unique_idx'); - } - else { - push(@columns, "${table}_unique_idx"); - } - # And this is how we fix the other inconsistent Schema naming. - push(@columns, @{$bad_names->{$table}}) - if (exists $bad_names->{$table}); - foreach my $column (@columns) { - # If we have an index named after this column, it's an - # old-style-name index. - if (my $index = $self->bz_index_info_real($table, $column)) { - # Fix the name to fit in with the new naming scheme. - $index->{NAME} = $table . "_" . - $index->{FIELDS}->[0] . "_idx"; - print "Renaming index $column to " - . $index->{NAME} . "...\n"; - $rename_indexes{$column} = $index; - } # if - } # foreach column - - my @rename_sql = $self->_bz_schema->get_rename_indexes_ddl( - $table, %rename_indexes); - $self->do($_) foreach (@rename_sql); - - } # foreach table - } # if old-name indexes - - # If there are no tables, but the DB isn't utf8 and it should be, - # then we should alter the database to be utf8. We know it should be - # if the utf8 parameter is true or there are no params at all. - # This kind of situation happens when people create the database - # themselves, and if we don't do this they will get the big - # scary WARNING statement about conversion to UTF8. - if ( !$self->bz_db_is_utf8 && !@tables - && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params}) ) - { - $self->_alter_db_charset_to_utf8(); - } + # And 45 minutes for every 15,000 attachments, per some experiments. + my ($attachment_count) + = $self->selectrow_array("SELECT COUNT(*) FROM attachments"); + $rename_time += int(($attachment_count * 45) / 15000); - # And now we create the tables and the Schema object. - $self->SUPER::bz_setup_database(); + # If we're going to take longer than 5 minutes, we let the user know + # and allow them to abort. + if ($rename_time > 5) { + print "\n", install_string('mysql_index_renaming', {minutes => $rename_time}); - if ($sd_index_deleted) { - $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx'); - $self->_bz_store_real_schema; + # Wait 45 seconds for them to respond. + sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE}; } - if ($longdescs_index_deleted) { - $self->_bz_real_schema->delete_index('longdescs', - 'longdescs_thetext_idx'); - $self->_bz_store_real_schema; + print "Renaming indexes...\n"; + + # We can't be interrupted, because of how the "if" + # works above. + local $SIG{INT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + # Certain indexes had names in Schema that did not easily conform + # to a standard. We store those names here, so that they + # can be properly renamed. + # Also, sometimes an old mysqldump would incorrectly rename + # unique indexes to "PRIMARY", so we address that here, also. + my $bad_names = { + + # 'when' is a possible leftover from Bugzillas before 2.8 + bugs_activity => + ['when', 'bugs_activity_bugid_idx', 'bugs_activity_bugwhen_idx'], + cc => ['PRIMARY'], + longdescs => ['longdescs_bugid_idx', 'longdescs_bugwhen_idx'], + flags => ['flags_bidattid_idx'], + flaginclusions => ['flaginclusions_tpcid_idx'], + flagexclusions => ['flagexclusions_tpc_id_idx'], + keywords => ['PRIMARY'], + milestones => ['PRIMARY'], + profiles_activity => ['profiles_activity_when_idx'], + group_control_map => ['group_control_map_gid_idx', 'PRIMARY'], + user_group_map => ['PRIMARY'], + group_group_map => ['PRIMARY'], + email_setting => ['PRIMARY'], + bug_group_map => ['PRIMARY'], + category_group_map => ['PRIMARY'], + watch => ['PRIMARY'], + namedqueries => ['PRIMARY'], + series_data => ['PRIMARY'], + + # series_categories is dealt with below, not here. + }; + + # The series table is broken and needs to have one index + # dropped before we begin the renaming, because it had a + # useless index on it that would cause a naming conflict here. + if (grep($_ eq 'series', @tables)) { + my $dropname; + + # This is what the bad index was called before Schema. + if ($self->bz_index_info_real('series', 'creator_2')) { + $dropname = 'creator_2'; + } + + # This is what the bad index is called in Schema. + elsif ($self->bz_index_info_real('series', 'series_creator_idx')) { + $dropname = 'series_creator_idx'; + } + $self->bz_drop_index_raw('series', $dropname) if $dropname; } - # The old timestamp fields need to be adjusted here instead of in - # checksetup. Otherwise the UPDATE statements inside of bz_add_column - # will cause accidental timestamp updates. - # The code that does this was moved here from checksetup. - - # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578 - # attachments creation time needs to be a datetime, not a timestamp - my $attach_creation = - $self->bz_column_info("attachments", "creation_ts"); - if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) { - print "Fixing creation time on attachments...\n"; + # The email_setting table also had the same problem. + if (grep($_ eq 'email_setting', @tables) + && $self->bz_index_info_real('email_setting', 'email_settings_user_id_idx')) + { + $self->bz_drop_index_raw('email_setting', 'email_settings_user_id_idx'); + } - my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments"); - $sth->execute(); - my ($attach_count) = $sth->fetchrow_array(); + # Go through all the tables. + foreach my $table (@tables) { - if ($attach_count > 1000) { - print "This may take a while...\n"; - } - my $i = 0; - - # This isn't just as simple as changing the field type, because - # the creation_ts was previously updated when an attachment was made - # obsolete from the attachment creation screen. So we have to go - # and recreate these times from the comments.. - $sth = $self->prepare("SELECT bug_id, attach_id, submitter_id " . - "FROM attachments"); - $sth->execute(); - - # Restrict this as much as possible in order to avoid false - # positives, and keep the db search time down - my $sth2 = $self->prepare("SELECT bug_when FROM longdescs - WHERE bug_id=? AND who=? - AND thetext LIKE ? - ORDER BY bug_when " . $self->sql_limit(1)); - while (my ($bug_id, $attach_id, $submitter_id) - = $sth->fetchrow_array()) - { - $sth2->execute($bug_id, $submitter_id, - "Created an attachment (id=$attach_id)%"); - my ($when) = $sth2->fetchrow_array(); - if ($when) { - $self->do("UPDATE attachments " . - "SET creation_ts='$when' " . - "WHERE attach_id=$attach_id"); - } else { - print "Warning - could not determine correct creation" - . " time for attachment $attach_id on bug $bug_id\n"; - } - ++$i; - print "Converted $i of $attach_count attachments\n" if !($i % 1000); - } - print "Done - converted $i attachments\n"; + # Will contain the names of old indexes as keys, and the + # definition of the new indexes as a value. The values + # include an extra hash key, NAME, with the new name of + # the index. + my %rename_indexes; + + # And go through all the columns on each table. + my @columns = $self->bz_table_columns_real($table); + + # We also want to fix the silly naming of unique indexes + # that happened when we first checked-in Bugzilla::DB::Schema. + if ($table eq 'series_categories') { + + # The series_categories index had a nonstandard name. + push(@columns, 'series_cats_unique_idx'); + } + elsif ($table eq 'email_setting') { + + # The email_setting table had a similar problem. + push(@columns, 'email_settings_unique_idx'); + } + else { + push(@columns, "${table}_unique_idx"); + } + + # And this is how we fix the other inconsistent Schema naming. + push(@columns, @{$bad_names->{$table}}) if (exists $bad_names->{$table}); + foreach my $column (@columns) { + + # If we have an index named after this column, it's an + # old-style-name index. + if (my $index = $self->bz_index_info_real($table, $column)) { + + # Fix the name to fit in with the new naming scheme. + $index->{NAME} = $table . "_" . $index->{FIELDS}->[0] . "_idx"; + print "Renaming index $column to " . $index->{NAME} . "...\n"; + $rename_indexes{$column} = $index; + } # if + } # foreach column + + my @rename_sql + = $self->_bz_schema->get_rename_indexes_ddl($table, %rename_indexes); + $self->do($_) foreach (@rename_sql); + + } # foreach table + } # if old-name indexes + + # If there are no tables, but the DB isn't utf8 and it should be, + # then we should alter the database to be utf8. We know it should be + # if the utf8 parameter is true or there are no params at all. + # This kind of situation happens when people create the database + # themselves, and if we don't do this they will get the big + # scary WARNING statement about conversion to UTF8. + if ( !$self->bz_db_is_utf8 + && !@tables + && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params})) + { + $self->_alter_db_charset_to_utf8(); + } + + # And now we create the tables and the Schema object. + $self->SUPER::bz_setup_database(); + + if ($sd_index_deleted) { + $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx'); + $self->_bz_store_real_schema; + } + if ($longdescs_index_deleted) { + $self->_bz_real_schema->delete_index('longdescs', 'longdescs_thetext_idx'); + $self->_bz_store_real_schema; + } + + # The old timestamp fields need to be adjusted here instead of in + # checksetup. Otherwise the UPDATE statements inside of bz_add_column + # will cause accidental timestamp updates. + # The code that does this was moved here from checksetup. + + # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578 + # attachments creation time needs to be a datetime, not a timestamp + my $attach_creation = $self->bz_column_info("attachments", "creation_ts"); + if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) { + print "Fixing creation time on attachments...\n"; + + my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments"); + $sth->execute(); + my ($attach_count) = $sth->fetchrow_array(); - $self->bz_alter_column("attachments", "creation_ts", - {TYPE => 'DATETIME', NOTNULL => 1}); + if ($attach_count > 1000) { + print "This may take a while...\n"; } + my $i = 0; - # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303 - # Change logincookies.lastused type from timestamp to datetime - my $login_lastused = $self->bz_column_info("logincookies", "lastused"); - if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) { - $self->bz_alter_column('logincookies', 'lastused', - { TYPE => 'DATETIME', NOTNULL => 1}); - } + # This isn't just as simple as changing the field type, because + # the creation_ts was previously updated when an attachment was made + # obsolete from the attachment creation screen. So we have to go + # and recreate these times from the comments.. + $sth = $self->prepare( + "SELECT bug_id, attach_id, submitter_id " . "FROM attachments"); + $sth->execute(); - # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315 - # Change bugs.delta_ts type from timestamp to datetime - my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts"); - if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) { - $self->bz_alter_column('bugs', 'delta_ts', - {TYPE => 'DATETIME', NOTNULL => 1}); + # Restrict this as much as possible in order to avoid false + # positives, and keep the db search time down + my $sth2 = $self->prepare( + "SELECT bug_when FROM longdescs + WHERE bug_id=? AND who=? + AND thetext LIKE ? + ORDER BY bug_when " . $self->sql_limit(1) + ); + while (my ($bug_id, $attach_id, $submitter_id) = $sth->fetchrow_array()) { + $sth2->execute($bug_id, $submitter_id, + "Created an attachment (id=$attach_id)%"); + my ($when) = $sth2->fetchrow_array(); + if ($when) { + $self->do("UPDATE attachments " + . "SET creation_ts='$when' " + . "WHERE attach_id=$attach_id"); + } + else { + print "Warning - could not determine correct creation" + . " time for attachment $attach_id on bug $bug_id\n"; + } + ++$i; + print "Converted $i of $attach_count attachments\n" if !($i % 1000); } - - # 2005-09-24 - bugreport@peshkin.net, bug 307602 - # Make sure that default 4G table limit is overridden - my $attach_data_create = $self->selectrow_array( - 'SELECT CREATE_OPTIONS FROM information_schema.TABLES - WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', - undef, $db_name, 'attach_data'); - if ($attach_data_create !~ /MAX_ROWS/i) { - print "Converting attach_data maximum size to 100G...\n"; - $self->do("ALTER TABLE attach_data + print "Done - converted $i attachments\n"; + + $self->bz_alter_column("attachments", "creation_ts", + {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303 + # Change logincookies.lastused type from timestamp to datetime + my $login_lastused = $self->bz_column_info("logincookies", "lastused"); + if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) { + $self->bz_alter_column('logincookies', 'lastused', + {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315 + # Change bugs.delta_ts type from timestamp to datetime + my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts"); + if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) { + $self->bz_alter_column('bugs', 'delta_ts', {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2005-09-24 - bugreport@peshkin.net, bug 307602 + # Make sure that default 4G table limit is overridden + my $attach_data_create = $self->selectrow_array( + 'SELECT CREATE_OPTIONS FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', undef, $db_name, 'attach_data' + ); + if ($attach_data_create !~ /MAX_ROWS/i) { + print "Converting attach_data maximum size to 100G...\n"; + $self->do( + "ALTER TABLE attach_data AVG_ROW_LENGTH=1000000, - MAX_ROWS=100000"); - } - - # Convert the database to UTF-8 if the utf8 parameter is on. - # We check if any table isn't utf8, because lots of crazy - # partial-conversion situations can happen, and this handles anything - # that could come up (including having the DB charset be utf8 but not - # the table charsets. - # - # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views. - my $non_utf8_tables = $self->selectrow_array( - "SELECT 1 FROM information_schema.TABLES + MAX_ROWS=100000" + ); + } + + # Convert the database to UTF-8 if the utf8 parameter is on. + # We check if any table isn't utf8, because lots of crazy + # partial-conversion situations can happen, and this handles anything + # that could come up (including having the DB charset be utf8 but not + # the table charsets. + # + # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views. + my $non_utf8_tables = $self->selectrow_array( + "SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_COLLATION IS NOT NULL AND TABLE_COLLATION NOT LIKE 'utf8%' - LIMIT 1", undef, $db_name); - - if (Bugzilla->params->{'utf8'} && $non_utf8_tables) { - print "\n", install_string('mysql_utf8_conversion'); - - if (!Bugzilla->installation_answers->{NO_PAUSE}) { - if (Bugzilla->installation_mode == - INSTALLATION_MODE_NON_INTERACTIVE) - { - die install_string('continue_without_answers'), "\n"; - } - else { - print "\n " . install_string('enter_or_ctrl_c'); - getc; - } - } - - print "Converting table storage format to UTF-8. This may take a", - " while.\n"; - 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. - next if $column->{Type} =~ /enum/i; - - # If this particular column isn't stored in utf-8 - if ($column->{Collation} - && $column->{Collation} ne 'NULL' - && $column->{Collation} !~ /utf8/) - { - my $name = $column->{Field}; - - print "$table.$name needs to be converted to UTF-8...\n"; - - # These will be automatically re-created at the end - # of checksetup. - $self->bz_drop_related_fks($table, $name); - - 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); - # We don't want MySQL to actually try to *convert* - # from our current charset to UTF-8, we just want to - # transfer the bytes directly. This is how we do that. - - # The CHARACTER SET part of the definition has to come - # right after the type, which will always come first. - my ($binary, $utf8) = ($sql_def, $sql_def); - 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/; - push(@binary_sql, "MODIFY COLUMN $name $binary"); - push(@utf8_sql, "MODIFY COLUMN $name $utf8"); - } - } # foreach column - - 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}; - } - } - - 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) + LIMIT 1", undef, $db_name + ); + + if (Bugzilla->params->{'utf8'} && $non_utf8_tables) { + print "\n", install_string('mysql_utf8_conversion'); + + if (!Bugzilla->installation_answers->{NO_PAUSE}) { + if (Bugzilla->installation_mode == INSTALLATION_MODE_NON_INTERACTIVE) { + die install_string('continue_without_answers'), "\n"; + } + else { + print "\n " . install_string('enter_or_ctrl_c'); + getc; + } } - # Sometimes you can have a situation where all the tables are utf8, - # but the database isn't. (This tends to happen when you've done - # a mysqldump.) So we have this change outside of the above block, - # so that it just happens silently if no actual *table* conversion - # needs to happen. - if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) { - $self->_alter_db_charset_to_utf8(); - } + print "Converting table storage format to UTF-8. This may take a", " while.\n"; + 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. + next if $column->{Type} =~ /enum/i; + + # If this particular column isn't stored in utf-8 + if ( $column->{Collation} + && $column->{Collation} ne 'NULL' + && $column->{Collation} !~ /utf8/) + { + my $name = $column->{Field}; - $self->_fix_defaults(); + print "$table.$name needs to be converted to UTF-8...\n"; - # Bug 451735 highlighted a bug in bz_drop_index() which didn't - # check for FKs before trying to delete an index. Consequently, - # the series_creator_idx index was considered to be deleted - # despite it was still present in the DB. That's why we have to - # force the deletion, bypassing the DB schema. - if (!$self->bz_index_info('series', 'series_category_idx')) { - if (!$self->bz_index_info('series', 'series_creator_idx') - && $self->bz_index_info_real('series', 'series_creator_idx')) - { - foreach my $column (qw(creator category subcategory name)) { - $self->bz_drop_related_fks('series', $column); - } - $self->bz_drop_index_raw('series', 'series_creator_idx'); + # These will be automatically re-created at the end + # of checksetup. + $self->bz_drop_related_fks($table, $name); + + 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); + + # We don't want MySQL to actually try to *convert* + # from our current charset to UTF-8, we just want to + # transfer the bytes directly. This is how we do that. + + # The CHARACTER SET part of the definition has to come + # right after the type, which will always come first. + my ($binary, $utf8) = ($sql_def, $sql_def); + 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/; + push(@binary_sql, "MODIFY COLUMN $name $binary"); + push(@utf8_sql, "MODIFY COLUMN $name $utf8"); } + } # foreach column + + 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}; + } + } + + 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) + } + + # Sometimes you can have a situation where all the tables are utf8, + # but the database isn't. (This tends to happen when you've done + # a mysqldump.) So we have this change outside of the above block, + # so that it just happens silently if no actual *table* conversion + # needs to happen. + if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) { + $self->_alter_db_charset_to_utf8(); + } + + $self->_fix_defaults(); + + # Bug 451735 highlighted a bug in bz_drop_index() which didn't + # check for FKs before trying to delete an index. Consequently, + # the series_creator_idx index was considered to be deleted + # despite it was still present in the DB. That's why we have to + # force the deletion, bypassing the DB schema. + if (!$self->bz_index_info('series', 'series_category_idx')) { + if (!$self->bz_index_info('series', 'series_creator_idx') + && $self->bz_index_info_real('series', 'series_creator_idx')) + { + foreach my $column (qw(creator category subcategory name)) { + $self->bz_drop_related_fks('series', $column); + } + $self->bz_drop_index_raw('series', 'series_creator_idx'); } + } } # When you import a MySQL 3/4 mysqldump into MySQL 5, columns that @@ -792,100 +809,109 @@ sub bz_setup_database { # looks like. So we remove defaults from columns that aren't supposed # to have them sub _fix_defaults { - my $self = shift; - my $maj_version = substr($self->bz_server_version, 0, 1); - return if $maj_version < 5; - - # The oldest column that could have this problem is bugs.assigned_to, - # so if it doesn't have the problem, we just skip doing this entirely. - my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to'); - my $assi_default = $assi_def->{COLUMN_DEF}; - # This "ne ''" thing is necessary because _raw_column_info seems to - # return COLUMN_DEF as an empty string for columns that don't have - # 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); - # 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) { - 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); - } + my $self = shift; + my $maj_version = substr($self->bz_server_version, 0, 1); + return if $maj_version < 5; + + # The oldest column that could have this problem is bugs.assigned_to, + # so if it doesn't have the problem, we just skip doing this entirely. + my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to'); + my $assi_default = $assi_def->{COLUMN_DEF}; + + # This "ne ''" thing is necessary because _raw_column_info seems to + # return COLUMN_DEF as an empty string for columns that don't have + # 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); + + # 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) { + 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); + } } sub _alter_db_charset_to_utf8 { - my $self = shift; - my $db_name = Bugzilla->localconfig->{db_name}; - $self->do("ALTER DATABASE $db_name CHARACTER SET utf8"); + my $self = shift; + my $db_name = Bugzilla->localconfig->{db_name}; + $self->do("ALTER DATABASE $db_name CHARACTER SET utf8"); } sub bz_db_is_utf8 { - my $self = shift; - my $db_collation = $self->selectrow_arrayref( - "SHOW VARIABLES LIKE 'character_set_database'"); - # First column holds the variable name, second column holds the value. - return $db_collation->[1] =~ /utf8/ ? 1 : 0; + my $self = shift; + my $db_collation + = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'"); + + # First column holds the variable name, second column holds the value. + return $db_collation->[1] =~ /utf8/ ? 1 : 0; } sub bz_enum_initial_values { - my ($self) = @_; - my %enum_values = %{$self->ENUM_DEFAULTS}; - # Get a complete description of the 'bugs' table; with DBD::MySQL - # there isn't a column-by-column way of doing this. Could use - # $dbh->column_info, but it would go slower and we would have to - # use the undocumented mysql_type_name accessor to get the type - # of each row. - my $sth = $self->prepare("DESCRIBE bugs"); - $sth->execute(); - # Look for the particular columns we are interested in. - while (my ($thiscol, $thistype) = $sth->fetchrow_array()) { - if (defined $enum_values{$thiscol}) { - # this is a column of interest. - my @value_list; - if ($thistype and ($thistype =~ /^enum\(/)) { - # it has an enum type; get the set of values. - while ($thistype =~ /'([^']*)'(.*)/) { - push(@value_list, $1); - $thistype = $2; - } - } - if (@value_list) { - # record the enum values found. - $enum_values{$thiscol} = \@value_list; - } + my ($self) = @_; + my %enum_values = %{$self->ENUM_DEFAULTS}; + + # Get a complete description of the 'bugs' table; with DBD::MySQL + # there isn't a column-by-column way of doing this. Could use + # $dbh->column_info, but it would go slower and we would have to + # use the undocumented mysql_type_name accessor to get the type + # of each row. + my $sth = $self->prepare("DESCRIBE bugs"); + $sth->execute(); + + # Look for the particular columns we are interested in. + while (my ($thiscol, $thistype) = $sth->fetchrow_array()) { + if (defined $enum_values{$thiscol}) { + + # this is a column of interest. + my @value_list; + if ($thistype and ($thistype =~ /^enum\(/)) { + + # it has an enum type; get the set of values. + while ($thistype =~ /'([^']*)'(.*)/) { + push(@value_list, $1); + $thistype = $2; } + } + if (@value_list) { + + # record the enum values found. + $enum_values{$thiscol} = \@value_list; + } } + } - return \%enum_values; + return \%enum_values; } ##################################################################### @@ -916,29 +942,29 @@ backwards-compatibility anyway, for versions of Bugzilla before 2.20. =cut sub bz_column_info_real { - my ($self, $table, $column) = @_; - my $col_data = $self->_bz_raw_column_info($table, $column); - return $self->_bz_schema->column_info_to_column($col_data); + my ($self, $table, $column) = @_; + my $col_data = $self->_bz_raw_column_info($table, $column); + return $self->_bz_schema->column_info_to_column($col_data); } sub _bz_raw_column_info { - my ($self, $table, $column) = @_; - - # DBD::mysql does not support selecting a specific column, - # so we have to get all the columns on the table and find - # the one we want. - my $info_sth = $self->column_info(undef, undef, $table, '%'); - - # Don't use fetchall_hashref as there's a Win32 DBI bug (292821) - my $col_data; - while ($col_data = $info_sth->fetchrow_hashref) { - last if $col_data->{'COLUMN_NAME'} eq $column; - } - - if (!defined $col_data) { - return undef; - } - return $col_data; + my ($self, $table, $column) = @_; + + # DBD::mysql does not support selecting a specific column, + # so we have to get all the columns on the table and find + # the one we want. + my $info_sth = $self->column_info(undef, undef, $table, '%'); + + # Don't use fetchall_hashref as there's a Win32 DBI bug (292821) + my $col_data; + while ($col_data = $info_sth->fetchrow_hashref) { + last if $col_data->{'COLUMN_NAME'} eq $column; + } + + if (!defined $col_data) { + return undef; + } + return $col_data; } =item C<bz_index_info_real($table, $index)> @@ -952,42 +978,43 @@ sub _bz_raw_column_info { =cut sub bz_index_info_real { - my ($self, $table, $index) = @_; - - my $sth = $self->prepare("SHOW INDEX FROM $table"); - $sth->execute; - - my @fields; - my $index_type; - # $raw_def will be an arrayref containing the following information: - # 0 = name of the table that the index is on - # 1 = 0 if unique, 1 if not unique - # 2 = name of the index - # 3 = seq_in_index (The order of the current field in the index). - # 4 = Name of ONE column that the index is on - # 5 = 'Collation' of the index. Usually 'A'. - # 6 = Cardinality. Either a number or undef. - # 7 = sub_part. Usually undef. Sometimes 1. - # 8 = "packed". Usually undef. - # 9 = Null. Sometimes undef, sometimes 'YES'. - # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT' - # 11 = 'Comment.' Usually undef. - while (my $raw_def = $sth->fetchrow_arrayref) { - if ($raw_def->[2] eq $index) { - push(@fields, $raw_def->[4]); - # No index can be both UNIQUE and FULLTEXT, that's why - # this is written this way. - $index_type = $raw_def->[1] ? '' : 'UNIQUE'; - $index_type = $raw_def->[10] eq 'FULLTEXT' - ? 'FULLTEXT' : $index_type; - } + my ($self, $table, $index) = @_; + + my $sth = $self->prepare("SHOW INDEX FROM $table"); + $sth->execute; + + my @fields; + my $index_type; + + # $raw_def will be an arrayref containing the following information: + # 0 = name of the table that the index is on + # 1 = 0 if unique, 1 if not unique + # 2 = name of the index + # 3 = seq_in_index (The order of the current field in the index). + # 4 = Name of ONE column that the index is on + # 5 = 'Collation' of the index. Usually 'A'. + # 6 = Cardinality. Either a number or undef. + # 7 = sub_part. Usually undef. Sometimes 1. + # 8 = "packed". Usually undef. + # 9 = Null. Sometimes undef, sometimes 'YES'. + # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT' + # 11 = 'Comment.' Usually undef. + while (my $raw_def = $sth->fetchrow_arrayref) { + if ($raw_def->[2] eq $index) { + push(@fields, $raw_def->[4]); + + # No index can be both UNIQUE and FULLTEXT, that's why + # this is written this way. + $index_type = $raw_def->[1] ? '' : 'UNIQUE'; + $index_type = $raw_def->[10] eq 'FULLTEXT' ? 'FULLTEXT' : $index_type; } + } - my $retval; - if (scalar(@fields)) { - $retval = {FIELDS => \@fields, TYPE => $index_type}; - } - return $retval; + my $retval; + if (scalar(@fields)) { + $retval = {FIELDS => \@fields, TYPE => $index_type}; + } + return $retval; } =item C<bz_index_list_real($table)> @@ -1000,10 +1027,11 @@ sub bz_index_info_real { =cut sub bz_index_list_real { - my ($self, $table) = @_; - my $sth = $self->prepare("SHOW INDEX FROM $table"); - # Column 3 of a SHOW INDEX statement contains the name of the index. - return @{ $self->selectcol_arrayref($sth, {Columns => [3]}) }; + my ($self, $table) = @_; + my $sth = $self->prepare("SHOW INDEX FROM $table"); + + # Column 3 of a SHOW INDEX statement contains the name of the index. + return @{$self->selectcol_arrayref($sth, {Columns => [3]})}; } ##################################################################### @@ -1027,34 +1055,33 @@ this code does. # bz_column_info_real function would be very difficult to create # properly for any other DB besides MySQL. sub _bz_build_schema_from_disk { - my ($self) = @_; - - my $schema = $self->_bz_schema->get_empty_schema(); - - my @tables = $self->bz_table_list_real(); - if (@tables) { - print "Building Schema object from database...\n"; + my ($self) = @_; + + my $schema = $self->_bz_schema->get_empty_schema(); + + my @tables = $self->bz_table_list_real(); + if (@tables) { + print "Building Schema object from database...\n"; + } + foreach my $table (@tables) { + $schema->add_table($table); + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + my $type_info = $self->bz_column_info_real($table, $column); + $schema->set_column($table, $column, $type_info); } - foreach my $table (@tables) { - $schema->add_table($table); - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - my $type_info = $self->bz_column_info_real($table, $column); - $schema->set_column($table, $column, $type_info); - } - my @indexes = $self->bz_index_list_real($table); - foreach my $index (@indexes) { - unless ($index eq 'PRIMARY') { - my $index_info = $self->bz_index_info_real($table, $index); - ($index_info = $index_info->{FIELDS}) - if (!$index_info->{TYPE}); - $schema->set_index($table, $index, $index_info); - } - } + my @indexes = $self->bz_index_list_real($table); + foreach my $index (@indexes) { + unless ($index eq 'PRIMARY') { + my $index_info = $self->bz_index_info_real($table, $index); + ($index_info = $index_info->{FIELDS}) if (!$index_info->{TYPE}); + $schema->set_index($table, $index, $index_info); + } } + } - return $schema; + return $schema; } 1; diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm index 7424019ac..337a0b5ba 100644 --- a/Bugzilla/DB/Oracle.pm +++ b/Bugzilla/DB/Oracle.pm @@ -38,461 +38,473 @@ use Bugzilla::Util; ##################################################################### # Constants ##################################################################### -use constant EMPTY_STRING => '__BZ_EMPTY_STR__'; +use constant EMPTY_STRING => '__BZ_EMPTY_STR__'; use constant ISOLATION_LEVEL => 'READ COMMITTED'; -use constant BLOB_TYPE => { ora_type => ORA_BLOB }; +use constant BLOB_TYPE => {ora_type => ORA_BLOB}; + # The max size allowed for LOB fields, in kilobytes. use constant MIN_LONG_READ_LEN => 32 * 1024; -use constant FULLTEXT_OR => ' OR '; +use constant FULLTEXT_OR => ' OR '; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port) = - @$params{qw(db_user db_pass db_host db_name db_port)}; - - # You can never connect to Oracle without a DB name, - # and there is no default DB. - $dbname ||= Bugzilla->localconfig->{db_name}; - - # Set the language enviroment - $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'}; - - # construct the DSN from the parameters we got - my $dsn = "dbi:Oracle:host=$host;sid=$dbname"; - $dsn .= ";port=$port" if $port; - my $attrs = { FetchHashKeyName => 'NAME_lc', - LongReadLen => max(Bugzilla->params->{'maxattachmentsize'} || 0, - MIN_LONG_READ_LEN) * 1024, - }; - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => $attrs }); - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; - - bless ($self, $class); - - # Set the session's default date format to match MySQL - $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") - if Bugzilla->params->{'utf8'}; - # To allow case insensitive query. - $self->do("ALTER SESSION SET NLS_COMP='ANSI'"); - $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); - return $self; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port) + = @$params{qw(db_user db_pass db_host db_name db_port)}; + + # You can never connect to Oracle without a DB name, + # and there is no default DB. + $dbname ||= Bugzilla->localconfig->{db_name}; + + # Set the language enviroment + $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'}; + + # construct the DSN from the parameters we got + my $dsn = "dbi:Oracle:host=$host;sid=$dbname"; + $dsn .= ";port=$port" if $port; + my $attrs = { + FetchHashKeyName => 'NAME_lc', + LongReadLen => + max(Bugzilla->params->{'maxattachmentsize'} || 0, MIN_LONG_READ_LEN) * 1024, + }; + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}); + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + bless($self, $class); + + # Set the session's default date format to match MySQL + $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") + if Bugzilla->params->{'utf8'}; + + # To allow case insensitive query. + $self->do("ALTER SESSION SET NLS_COMP='ANSI'"); + $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); + return $self; } sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq = $table . "_" . $column . "_SEQ"; - my ($last_insert_id) = $self->selectrow_array("SELECT $seq.CURRVAL " - . " FROM DUAL"); - return $last_insert_id; + my $seq = $table . "_" . $column . "_SEQ"; + my ($last_insert_id) + = $self->selectrow_array("SELECT $seq.CURRVAL " . " FROM DUAL"); + return $last_insert_id; } sub bz_check_regexp { - my ($self, $pattern) = @_; + my ($self, $pattern) = @_; - eval { $self->do("SELECT 1 FROM DUAL WHERE " - . $self->sql_regexp($self->quote("a"), $pattern, 1)) }; + eval { + $self->do("SELECT 1 FROM DUAL WHERE " + . $self->sql_regexp($self->quote("a"), $pattern, 1)); + }; - $@ && ThrowUserError('illegal_regexp', - { value => $pattern, dberror => $self->errstr }); + $@ + && ThrowUserError('illegal_regexp', + {value => $pattern, dberror => $self->errstr}); } -sub bz_explain { - my ($self, $sql) = @_; - my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); - $sth->execute(); - my $explain = $self->selectcol_arrayref( - "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)"); - return join("\n", @$explain); -} +sub bz_explain { + my ($self, $sql) = @_; + my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); + $sth->execute(); + my $explain = $self->selectcol_arrayref( + "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)"); + return join("\n", @$explain); +} sub sql_group_concat { - my ($self, $text, $separator) = @_; - $separator = $self->quote(', ') if !defined $separator; - my ($distinct, $rest) = $text =~/^(\s*DISTINCT\s|)(.+)$/i; - return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))"; + my ($self, $text, $separator) = @_; + $separator = $self->quote(', ') if !defined $separator; + my ($distinct, $rest) = $text =~ /^(\s*DISTINCT\s|)(.+)$/i; + return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))"; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "REGEXP_LIKE($expr, $pattern)"; + return "REGEXP_LIKE($expr, $pattern)"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "NOT REGEXP_LIKE($expr, $pattern)" + return "NOT REGEXP_LIKE($expr, $pattern)"; } sub sql_limit { - my ($self, $limit, $offset) = @_; + my ($self, $limit, $offset) = @_; - if(defined $offset) { - return "/* LIMIT $limit $offset */"; - } - return "/* LIMIT $limit */"; + if (defined $offset) { + return "/* LIMIT $limit $offset */"; + } + return "/* LIMIT $limit */"; } sub sql_string_concat { - my ($self, @params) = @_; + my ($self, @params) = @_; - return 'CONCAT(' . join(', ', @params) . ')'; + return 'CONCAT(' . join(', ', @params) . ')'; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return " TO_CHAR(TO_DATE($date),'J') "; + return " TO_CHAR(TO_DATE($date),'J') "; } -sub sql_from_days{ - my ($self, $date) = @_; - return " TO_DATE($date,'J') "; +sub sql_from_days { + my ($self, $date) = @_; + + return " TO_DATE($date,'J') "; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - state $label = 0; - $text = $self->quote($text); - trick_taint($text); - $label++; - return "CONTAINS($column,$text,$label) > 0", "SCORE($label)"; + my ($self, $column, $text) = @_; + state $label = 0; + $text = $self->quote($text); + trick_taint($text); + $label++; + return "CONTAINS($column,$text,$label) > 0", "SCORE($label)"; } sub sql_date_format { - my ($self, $date, $format) = @_; - - $format = "%Y.%m.%d %H:%i:%s" if !$format; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; - $format =~ s/\%Y/YYYY/g; - $format =~ s/\%y/YY/g; - $format =~ s/\%m/MM/g; - $format =~ s/\%d/DD/g; - $format =~ s/\%a/Dy/g; - $format =~ s/\%H/HH24/g; - $format =~ s/\%i/MI/g; - $format =~ s/\%s/SS/g; + $format =~ s/\%Y/YYYY/g; + $format =~ s/\%y/YY/g; + $format =~ s/\%m/MM/g; + $format =~ s/\%d/DD/g; + $format =~ s/\%a/Dy/g; + $format =~ s/\%H/HH24/g; + $format =~ s/\%i/MI/g; + $format =~ s/\%s/SS/g; - return "TO_CHAR($date, " . $self->quote($format) . ")"; + return "TO_CHAR($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - my $time_sql; - if ($units =~ /YEAR|MONTH/i) { - $time_sql = "NUMTOYMINTERVAL($interval,'$units')"; - } else{ - $time_sql = "NUMTODSINTERVAL($interval,'$units')"; - } - return "$date $operator $time_sql"; + my ($self, $date, $operator, $interval, $units) = @_; + my $time_sql; + if ($units =~ /YEAR|MONTH/i) { + $time_sql = "NUMTOYMINTERVAL($interval,'$units')"; + } + else { + $time_sql = "NUMTODSINTERVAL($interval,'$units')"; + } + return "$date $operator $time_sql"; } sub sql_position { - my ($self, $fragment, $text) = @_; - return "INSTR($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "INSTR($text, $fragment)"; } sub sql_in { - my ($self, $column_name, $in_list_ref, $negate) = @_; - my @in_list = @$in_list_ref; - return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) if $#in_list < 1000; - my @in_str; - while (@in_list) { - my $length = $#in_list + 1; - my $splice = $length > 1000 ? 1000 : $length; - my @sub_in_list = splice(@in_list, 0, $splice); - push(@in_str, - $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); - } - return "( " . join(" OR ", @in_str) . " )"; + my ($self, $column_name, $in_list_ref, $negate) = @_; + my @in_list = @$in_list_ref; + return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) + if $#in_list < 1000; + my @in_str; + while (@in_list) { + my $length = $#in_list + 1; + my $splice = $length > 1000 ? 1000 : $length; + my @sub_in_list = splice(@in_list, 0, $splice); + push(@in_str, $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); + } + return "( " . join(" OR ", @in_str) . " )"; } sub _bz_add_field_table { - my ($self, $name, $schema_ref, $type) = @_; - $self->SUPER::_bz_add_field_table($name, $schema_ref); - if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) { - my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value'); - $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)"); - } + my ($self, $name, $schema_ref, $type) = @_; + $self->SUPER::_bz_add_field_table($name, $schema_ref); + if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) { + my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value'); + $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)"); + } } sub bz_drop_table { - my ($self, $name) = @_; - my $table_exists = $self->bz_table_info($name); - if ($table_exists) { - $self->_bz_drop_fks($name); - $self->SUPER::bz_drop_table($name); - } + my ($self, $name) = @_; + my $table_exists = $self->bz_table_info($name); + if ($table_exists) { + $self->_bz_drop_fks($name); + $self->SUPER::bz_drop_table($name); + } } -# Dropping all FKs for a specified table. +# Dropping all FKs for a specified table. sub _bz_drop_fks { - my ($self, $table) = @_; - my @columns = $self->bz_table_columns($table); - foreach my $column (@columns) { - $self->bz_drop_fk($table, $column); - } + my ($self, $table) = @_; + my @columns = $self->bz_table_columns($table); + foreach my $column (@columns) { + $self->bz_drop_fk($table, $column); + } } sub _fix_empty { - my ($string) = @_; - $string = '' if $string eq EMPTY_STRING; - return $string; + my ($string) = @_; + $string = '' if $string eq EMPTY_STRING; + return $string; } sub _fix_arrayref { - my ($row) = @_; - return undef if !defined $row; - foreach my $field (@$row) { - $field = _fix_empty($field) if defined $field; - } - return $row; + my ($row) = @_; + return undef if !defined $row; + foreach my $field (@$row) { + $field = _fix_empty($field) if defined $field; + } + return $row; } sub _fix_hashref { - my ($row) = @_; - return undef if !defined $row; - foreach my $value (values %$row) { - $value = _fix_empty($value) if defined $value; - } - return $row; + my ($row) = @_; + return undef if !defined $row; + foreach my $value (values %$row) { + $value = _fix_empty($value) if defined $value; + } + return $row; } 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 - # escaping a single "'" in the literal. Therefore we must be trickier... - - # split the statement into parts by single-quotes. The negative value - # at the end to the split operator from dropping trailing empty strings - # (e.g., when $sql ends in "''") - my @parts = split /'/, $sql, -1; - - if( !(@parts % 2) ) { - # Either the string is empty or the quotes are mismatched - # Returning input unmodified. - return $sql; + 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 + # escaping a single "'" in the literal. Therefore we must be trickier... + + # split the statement into parts by single-quotes. The negative value + # at the end to the split operator from dropping trailing empty strings + # (e.g., when $sql ends in "''") + my @parts = split /'/, $sql, -1; + + if (!(@parts % 2)) { + + # Either the string is empty or the quotes are mismatched + # Returning input unmodified. + return $sql; + } + + # We already verified that we have an odd number of parts. If we take + # the first part off now, we know we're entering the loop with an even + # number of parts + my @result; + my $part = shift @parts; + + # Oracle requires a FROM clause in all SELECT statements, so append + # "FROM dual" to queries without one (e.g., "SELECT NOW()") + my $is_select = ($part =~ m/^\s*SELECT\b/io); + my $has_from = ($part =~ m/\bFROM\b/io) if $is_select; + + # Oracle includes the time in CURRENT_DATE. + $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + + # Oracle use SUBSTR instead of SUBSTRING + $part =~ s/\bSUBSTRING\b/SUBSTR/io; + + # Oracle need no 'AS' + $part =~ s/\bAS\b//ig; + + # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the + # query with "SELECT * FROM (...) WHERE rownum < $limit" + my ($limit, $offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o); + + push @result, $part; + while (@parts) { + my $string = shift @parts; + my $nonstring = shift @parts; + + # if the non-string part is zero-length and there are more parts left, + # then this is an escaped quote inside a string literal + while (!(length $nonstring) && @parts) { + + # we know it's safe to remove two parts at a time, since we + # entered the loop with an even number of parts + $string .= "''" . shift @parts; + $nonstring = shift @parts; } - # We already verified that we have an odd number of parts. If we take - # the first part off now, we know we're entering the loop with an even - # number of parts - my @result; - my $part = shift @parts; - - # Oracle requires a FROM clause in all SELECT statements, so append - # "FROM dual" to queries without one (e.g., "SELECT NOW()") - my $is_select = ($part =~ m/^\s*SELECT\b/io); - my $has_from = ($part =~ m/\bFROM\b/io) if $is_select; + # Look for a FROM if this is a SELECT and we haven't found one yet + $has_from = ($nonstring =~ m/\bFROM\b/io) if ($is_select and !$has_from); # Oracle includes the time in CURRENT_DATE. - $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; # Oracle use SUBSTR instead of SUBSTRING - $part =~ s/\bSUBSTRING\b/SUBSTR/io; - + $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; + # Oracle need no 'AS' - $part =~ s/\bAS\b//ig; - - # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the - # query with "SELECT * FROM (...) WHERE rownum < $limit" - my ($limit,$offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o); - - push @result, $part; - while( @parts ) { - my $string = shift @parts; - my $nonstring = shift @parts; - - # if the non-string part is zero-length and there are more parts left, - # then this is an escaped quote inside a string literal - while( !(length $nonstring) && @parts ) { - # we know it's safe to remove two parts at a time, since we - # entered the loop with an even number of parts - $string .= "''" . shift @parts; - $nonstring = shift @parts; - } + $nonstring =~ s/\bAS\b//ig; - # Look for a FROM if this is a SELECT and we haven't found one yet - $has_from = ($nonstring =~ m/\bFROM\b/io) - if ($is_select and !$has_from); + # Look for a LIMIT clause + ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); - # Oracle includes the time in CURRENT_DATE. - $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + if (!length($string)) { + push @result, EMPTY_STRING; + push @result, $nonstring; + } + else { + push @result, $string; + push @result, $nonstring; + } + } - # Oracle use SUBSTR instead of SUBSTRING - $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; + my $new_sql = join "'", @result; - # Oracle need no 'AS' - $nonstring =~ s/\bAS\b//ig; + # Append "FROM dual" if this is a SELECT without a FROM clause + $new_sql .= " FROM DUAL" if ($is_select and !$has_from); - # Look for a LIMIT clause - ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); + # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT - if(!length($string)){ - push @result, EMPTY_STRING; - push @result, $nonstring; - } else { - push @result, $string; - push @result, $nonstring; - } + if (defined($limit)) { + if ($new_sql !~ /\bWHERE\b/) { + $new_sql = $new_sql . " WHERE 1=1"; } - - my $new_sql = join "'", @result; - - # Append "FROM dual" if this is a SELECT without a FROM clause - $new_sql .= " FROM DUAL" if ($is_select and !$has_from); - - # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT - - if (defined($limit)) { - if ($new_sql !~ /\bWHERE\b/) { - $new_sql = $new_sql." WHERE 1=1"; - } - my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2); - if (defined($offset)) { - my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2); - $before_where = "$before_from FROM ($before_from," - . " ROW_NUMBER() OVER (ORDER BY 1) R " - . " FROM $after_from ) "; - $after_where = " R BETWEEN $offset+1 AND $limit+$offset"; - } else { - $after_where = " rownum <=$limit AND ".$after_where; - } - $new_sql = $before_where." WHERE ".$after_where; + my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2); + if (defined($offset)) { + my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2); + $before_where + = "$before_from FROM ($before_from," + . " ROW_NUMBER() OVER (ORDER BY 1) R " + . " FROM $after_from ) "; + $after_where = " R BETWEEN $offset+1 AND $limit+$offset"; + } + else { + $after_where = " rownum <=$limit AND " . $after_where; } - return $new_sql; + $new_sql = $before_where . " WHERE " . $after_where; + } + return $new_sql; } sub do { - my $self = shift; - my $sql = shift; - $sql = adjust_statement($sql); - unshift @_, $sql; - return $self->SUPER::do(@_); + my $self = shift; + my $sql = shift; + $sql = adjust_statement($sql); + unshift @_, $sql; + return $self->SUPER::do(@_); } sub selectrow_array { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - if ( wantarray ) { - my @row = $self->SUPER::selectrow_array(@_); - _fix_arrayref(\@row); - return @row; - } else { - my $row = $self->SUPER::selectrow_array(@_); - $row = _fix_empty($row) if defined $row; - return $row; - } + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + if (wantarray) { + my @row = $self->SUPER::selectrow_array(@_); + _fix_arrayref(\@row); + return @row; + } + else { + my $row = $self->SUPER::selectrow_array(@_); + $row = _fix_empty($row) if defined $row; + return $row; + } } sub selectrow_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectrow_arrayref(@_); - return undef if !defined $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectrow_arrayref(@_); + return undef if !defined $ref; - _fix_arrayref($ref); - return $ref; + _fix_arrayref($ref); + return $ref; } sub selectrow_hashref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectrow_hashref(@_); - return undef if !defined $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectrow_hashref(@_); + return undef if !defined $ref; - _fix_hashref($ref); - return $ref; + _fix_hashref($ref); + return $ref; } sub selectall_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectall_arrayref(@_); - return undef if !defined $ref; - - foreach my $row (@$ref) { - if (ref($row) eq 'ARRAY') { - _fix_arrayref($row); - } - elsif (ref($row) eq 'HASH') { - _fix_hashref($row); - } + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectall_arrayref(@_); + return undef if !defined $ref; + + foreach my $row (@$ref) { + if (ref($row) eq 'ARRAY') { + _fix_arrayref($row); } + elsif (ref($row) eq 'HASH') { + _fix_hashref($row); + } + } - return $ref; + return $ref; } sub selectall_hashref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $rows = $self->SUPER::selectall_hashref(@_); - return undef if !defined $rows; - foreach my $row (values %$rows) { - _fix_hashref($row); - } - return $rows; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $rows = $self->SUPER::selectall_hashref(@_); + return undef if !defined $rows; + foreach my $row (values %$rows) { + _fix_hashref($row); + } + return $rows; } sub selectcol_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectcol_arrayref(@_); - return undef if !defined $ref; - _fix_arrayref($ref); - return $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectcol_arrayref(@_); + return undef if !defined $ref; + _fix_arrayref($ref); + return $ref; } sub prepare { - my $self = shift; - my $sql = shift; - my $new_sql = adjust_statement($sql); - unshift @_, $new_sql; - return bless $self->SUPER::prepare(@_), - 'Bugzilla::DB::Oracle::st'; + my $self = shift; + my $sql = shift; + my $new_sql = adjust_statement($sql); + unshift @_, $new_sql; + return bless $self->SUPER::prepare(@_), 'Bugzilla::DB::Oracle::st'; } sub prepare_cached { - my $self = shift; - my $sql = shift; - my $new_sql = adjust_statement($sql); - unshift @_, $new_sql; - return bless $self->SUPER::prepare_cached(@_), - 'Bugzilla::DB::Oracle::st'; + my $self = shift; + my $sql = shift; + my $new_sql = adjust_statement($sql); + unshift @_, $new_sql; + return bless $self->SUPER::prepare_cached(@_), 'Bugzilla::DB::Oracle::st'; } sub quote_identifier { - my ($self,$id) = @_; - return $id; + my ($self, $id) = @_; + return $id; } ##################################################################### @@ -500,20 +512,22 @@ sub quote_identifier { ##################################################################### sub bz_table_columns_real { - my ($self, $table) = @_; - $table = uc($table); - my $cols = $self->selectcol_arrayref( - "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE - TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table); - return @$cols; + my ($self, $table) = @_; + $table = uc($table); + my $cols = $self->selectcol_arrayref( + "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE + TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table + ); + return @$cols; } sub bz_table_list_real { - my ($self) = @_; - my $tables = $self->selectcol_arrayref( - "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE - TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%'); - return @$tables; + my ($self) = @_; + my $tables = $self->selectcol_arrayref( + "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE + TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%' + ); + return @$tables; } ##################################################################### @@ -521,32 +535,37 @@ sub bz_table_list_real { ##################################################################### sub bz_setup_database { - my $self = shift; - - # Create a function that returns SYSDATE to emulate MySQL's "NOW()". - # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not - # have that function, So we have to create one ourself. - $self->do("CREATE OR REPLACE FUNCTION NOW " - . " 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 $type_exists = $self->selectrow_array("SELECT 1 FROM user_types - WHERE type_name = 'T_GROUP_CONCAT'"); - $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists; - $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT " - . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)" - . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2" - . ");"); - $self->do("CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS + my $self = shift; + + # Create a function that returns SYSDATE to emulate MySQL's "NOW()". + # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not + # have that function, So we have to create one ourself. + $self->do("CREATE OR REPLACE FUNCTION NOW " + . " 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 $type_exists = $self->selectrow_array( + "SELECT 1 FROM user_types + WHERE type_name = 'T_GROUP_CONCAT'" + ); + $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists; + $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT " + . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)" + . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2" + . ");"); + $self->do( + "CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2 is BEGIN RETURN p_CONTENT; END; - END;"); + END;" + ); - $self->do("CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT + $self->do( + "CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT ( CLOB_CONTENT CLOB, DELIMITER VARCHAR2(256), STATIC FUNCTION ODCIAGGREGATEINITIALIZE( @@ -564,9 +583,11 @@ sub bz_setup_database { MEMBER FUNCTION ODCIAGGREGATEMERGE( SELF IN OUT NOCOPY T_GROUP_CONCAT, CTX2 IN T_GROUP_CONCAT) - RETURN NUMBER);"); + RETURN NUMBER);" + ); - $self->do("CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS + $self->do( + "CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS STATIC FUNCTION ODCIAGGREGATEINITIALIZE( SCTX IN OUT NOCOPY T_GROUP_CONCAT) RETURN NUMBER IS @@ -610,110 +631,117 @@ sub bz_setup_database { DBMS_LOB.APPEND(SELF.CLOB_CONTENT, CTX2.CLOB_CONTENT); RETURN ODCICONST.SUCCESS; END; - END;"); + END;" + ); - # Create user-defined aggregate function group_concat - $self->do("CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) + # 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 - pre_owner = ?", - undef,'BZ_LEX',uc(Bugzilla->localconfig->{db_user})); - if(!@$lexer) { - $self->do("BEGIN CTX_DDL.CREATE_PREFERENCE - ('BZ_LEX', 'WORLD_LEXER'); END;"); - } - - $self->SUPER::bz_setup_database(@_); - - my $sth = $self->prepare("SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?"); - my @tables = $self->bz_table_list_real(); - - foreach my $table (@tables) { - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - my $def = $self->bz_column_info($table, $column); - # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys - # correctly (bug 731156). We have to add missing sequences and - # triggers ourselves. - if ($def->{TYPE} =~ /SERIAL/i) { - my $sequence = "${table}_${column}_SEQ"; - my $exists = $self->selectrow_array($sth, undef, $sequence); - if (!$exists) { - my @sql = $self->_get_create_seq_ddl($table, $column); - $self->do($_) foreach @sql; - } - } - - if ($def->{REFERENCES}) { - my $references = $def->{REFERENCES}; - my $update = $references->{UPDATE} || 'CASCADE'; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $fk_name = $self->_bz_schema->_get_fk_name($table, - $column, - $references); - # bz_rename_table didn't rename the trigger correctly. - if ($table eq 'bug_tag' && $to_table eq 'tags') { - $to_table = 'tag'; - } - if ( $update =~ /CASCADE/i ){ - my $trigger_name = uc($fk_name . "_UC"); - my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); - if(@$exist_trigger) { - $self->do("DROP TRIGGER $trigger_name"); - } - - my $tr_str = "CREATE OR REPLACE TRIGGER $trigger_name" - . " AFTER UPDATE OF $to_column ON $to_table " - . " REFERENCING " - . " NEW AS NEW " - . " OLD AS OLD " - . " FOR EACH ROW " - . " BEGIN " - . " UPDATE $table" - . " SET $column = :NEW.$to_column" - . " WHERE $column = :OLD.$to_column;" - . " END $trigger_name;"; - $self->do($tr_str); - } - } + 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 + pre_owner = ?", undef, 'BZ_LEX', uc(Bugzilla->localconfig->{db_user}) + ); + if (!@$lexer) { + $self->do( + "BEGIN CTX_DDL.CREATE_PREFERENCE + ('BZ_LEX', 'WORLD_LEXER'); END;" + ); + } + + $self->SUPER::bz_setup_database(@_); + + my $sth = $self->prepare( + "SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?"); + my @tables = $self->bz_table_list_real(); + + foreach my $table (@tables) { + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + my $def = $self->bz_column_info($table, $column); + + # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys + # correctly (bug 731156). We have to add missing sequences and + # triggers ourselves. + if ($def->{TYPE} =~ /SERIAL/i) { + my $sequence = "${table}_${column}_SEQ"; + my $exists = $self->selectrow_array($sth, undef, $sequence); + if (!$exists) { + my @sql = $self->_get_create_seq_ddl($table, $column); + $self->do($_) foreach @sql; } + } + + if ($def->{REFERENCES}) { + my $references = $def->{REFERENCES}; + my $update = $references->{UPDATE} || 'CASCADE'; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $fk_name = $self->_bz_schema->_get_fk_name($table, $column, $references); + + # bz_rename_table didn't rename the trigger correctly. + if ($table eq 'bug_tag' && $to_table eq 'tags') { + $to_table = 'tag'; + } + if ($update =~ /CASCADE/i) { + my $trigger_name = uc($fk_name . "_UC"); + my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); + if (@$exist_trigger) { + $self->do("DROP TRIGGER $trigger_name"); + } + + my $tr_str + = "CREATE OR REPLACE TRIGGER $trigger_name" + . " AFTER UPDATE OF $to_column ON $to_table " + . " REFERENCING " + . " NEW AS NEW " + . " OLD AS OLD " + . " FOR EACH ROW " + . " BEGIN " + . " UPDATE $table" + . " SET $column = :NEW.$to_column" + . " WHERE $column = :OLD.$to_column;" + . " END $trigger_name;"; + $self->do($tr_str); + } + } } + } - # Drop the trigger which causes bug 541553 - my $trigger_name = "PRODUCTS_MILESTONEURL"; - my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); - if(@$exist_trigger) { - $self->do("DROP TRIGGER $trigger_name"); - } + # Drop the trigger which causes bug 541553 + my $trigger_name = "PRODUCTS_MILESTONEURL"; + my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); + if (@$exist_trigger) { + $self->do("DROP TRIGGER $trigger_name"); + } } # These two methods have been copied from Bugzilla::DB::Schema::Oracle. sub _get_create_seq_ddl { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq_name = "${table}_${column}_SEQ"; - my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " . - "NOMAXVALUE NOCYCLE NOCACHE"; - my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name); - return ($seq_sql, $trigger_sql); + my $seq_name = "${table}_${column}_SEQ"; + my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " + . "NOMAXVALUE NOCYCLE NOCACHE"; + my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name); + return ($seq_sql, $trigger_sql); } sub _get_create_trigger_ddl { - my ($self, $table, $column, $seq_name) = @_; + my ($self, $table, $column, $seq_name) = @_; - my $trigger_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " - . " BEFORE INSERT ON $table " - . " FOR EACH ROW " - . " BEGIN " - . " SELECT ${seq_name}.NEXTVAL " - . " INTO :NEW.$column FROM DUAL; " - . " END;"; - return $trigger_sql; + my $trigger_sql + = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " + . " BEFORE INSERT ON $table " + . " FOR EACH ROW " + . " BEGIN " + . " SELECT ${seq_name}.NEXTVAL " + . " INTO :NEW.$column FROM DUAL; " . " END;"; + return $trigger_sql; } ############################################################################ @@ -725,68 +753,69 @@ use strict; use warnings; use parent -norequire, qw(DBI::st); - + sub fetchrow_arrayref { - my $self = shift; - my $ref = $self->SUPER::fetchrow_arrayref(@_); - return undef if !defined $ref; - Bugzilla::DB::Oracle::_fix_arrayref($ref); - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchrow_arrayref(@_); + return undef if !defined $ref; + Bugzilla::DB::Oracle::_fix_arrayref($ref); + return $ref; } sub fetchrow_array { - my $self = shift; - if ( wantarray ) { - my @row = $self->SUPER::fetchrow_array(@_); - Bugzilla::DB::Oracle::_fix_arrayref(\@row); - return @row; - } else { - my $row = $self->SUPER::fetchrow_array(@_); - $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row; - return $row; - } + my $self = shift; + if (wantarray) { + my @row = $self->SUPER::fetchrow_array(@_); + Bugzilla::DB::Oracle::_fix_arrayref(\@row); + return @row; + } + else { + my $row = $self->SUPER::fetchrow_array(@_); + $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row; + return $row; + } } sub fetchrow_hashref { - my $self = shift; - my $ref = $self->SUPER::fetchrow_hashref(@_); - return undef if !defined $ref; - Bugzilla::DB::Oracle::_fix_hashref($ref); - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchrow_hashref(@_); + return undef if !defined $ref; + Bugzilla::DB::Oracle::_fix_hashref($ref); + return $ref; } sub fetchall_arrayref { - my $self = shift; - my $ref = $self->SUPER::fetchall_arrayref(@_); - return undef if !defined $ref; - foreach my $row (@$ref) { - if (ref($row) eq 'ARRAY') { - Bugzilla::DB::Oracle::_fix_arrayref($row); - } - elsif (ref($row) eq 'HASH') { - Bugzilla::DB::Oracle::_fix_hashref($row); - } + my $self = shift; + my $ref = $self->SUPER::fetchall_arrayref(@_); + return undef if !defined $ref; + foreach my $row (@$ref) { + if (ref($row) eq 'ARRAY') { + Bugzilla::DB::Oracle::_fix_arrayref($row); } - return $ref; + elsif (ref($row) eq 'HASH') { + Bugzilla::DB::Oracle::_fix_hashref($row); + } + } + return $ref; } sub fetchall_hashref { - my $self = shift; - my $ref = $self->SUPER::fetchall_hashref(@_); - return undef if !defined $ref; - foreach my $row (values %$ref) { - Bugzilla::DB::Oracle::_fix_hashref($row); - } - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchall_hashref(@_); + return undef if !defined $ref; + foreach my $row (values %$ref) { + Bugzilla::DB::Oracle::_fix_hashref($row); + } + return $ref; } sub fetch { - my $self = shift; - my $row = $self->SUPER::fetch(@_); - if ($row) { - Bugzilla::DB::Oracle::_fix_arrayref($row); - } - return $row; + my $self = shift; + my $row = $self->SUPER::fetch(@_); + if ($row) { + Bugzilla::DB::Oracle::_fix_arrayref($row); + } + return $row; } 1; diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm index cbf8d7af1..15a268381 100644 --- a/Bugzilla/DB/Pg.pm +++ b/Bugzilla/DB/Pg.pm @@ -32,215 +32,227 @@ use DBD::Pg; # This module extends the DB interface via inheritance use parent qw(Bugzilla::DB); -use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA }; +use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA}; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port) = - @$params{qw(db_user db_pass db_host db_name db_port)}; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port) + = @$params{qw(db_user db_pass db_host db_name db_port)}; - # The default database name for PostgreSQL. We have - # to connect to SOME database, even if we have - # no $dbname parameter. - $dbname ||= 'template1'; + # The default database name for PostgreSQL. We have + # to connect to SOME database, even if we have + # no $dbname parameter. + $dbname ||= 'template1'; - # construct the DSN from the parameters we got - my $dsn = "dbi:Pg:dbname=$dbname"; - $dsn .= ";host=$host" if $host; - $dsn .= ";port=$port" if $port; + # construct the DSN from the parameters we got + my $dsn = "dbi:Pg:dbname=$dbname"; + $dsn .= ";host=$host" if $host; + $dsn .= ";port=$port" if $port; - # This stops Pg from printing out lots of "NOTICE" messages when - # creating tables. - $dsn .= ";options='-c client_min_messages=warning'"; + # This stops Pg from printing out lots of "NOTICE" messages when + # creating tables. + $dsn .= ";options='-c client_min_messages=warning'"; - my $attrs = { pg_enable_utf8 => Bugzilla->params->{'utf8'} }; + my $attrs = {pg_enable_utf8 => Bugzilla->params->{'utf8'}}; - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => $attrs }); + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}); - # all class local variables stored in DBI derived class needs to have - # a prefix 'private_'. See DBI documentation. - $self->{private_bz_tables_locked} = ""; - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; + # all class local variables stored in DBI derived class needs to have + # a prefix 'private_'. See DBI documentation. + $self->{private_bz_tables_locked} = ""; - bless ($self, $class); + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; - return $self; + bless($self, $class); + + return $self; } # if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version # supported by Bugzilla, this implementation can be removed. sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq = $table . "_" . $column . "_seq"; - my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')"); + my $seq = $table . "_" . $column . "_seq"; + my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')"); - return $last_insert_id; + return $last_insert_id; } sub sql_group_concat { - my ($self, $text, $separator, $sort, $order_by) = @_; - $sort = 1 if !defined $sort; - $separator = $self->quote(', ') if !defined $separator; - - # PostgreSQL 8.x doesn't support STRING_AGG - if (vers_cmp($self->bz_server_version, 9) < 0) { - my $sql = "ARRAY_ACCUM($text)"; - if ($sort) { - $sql = "ARRAY_SORT($sql)"; - } - return "ARRAY_TO_STRING($sql, $separator)"; - } - - if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) { - # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x - # ORDER BY y", we need to sort the list, and then get the unique - # values - return "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)"; - } - - # Determine the ORDER BY clause (if any) - if ($order_by) { - $order_by = " ORDER BY $order_by"; - } - elsif ($sort) { - # We don't include the DISTINCT keyword in an order by - $text =~ /^(?:DISTINCT\s*)?(.+)$/i; - $order_by = " ORDER BY $1"; + my ($self, $text, $separator, $sort, $order_by) = @_; + $sort = 1 if !defined $sort; + $separator = $self->quote(', ') if !defined $separator; + + # PostgreSQL 8.x doesn't support STRING_AGG + if (vers_cmp($self->bz_server_version, 9) < 0) { + my $sql = "ARRAY_ACCUM($text)"; + if ($sort) { + $sql = "ARRAY_SORT($sql)"; } - - return "STRING_AGG(${text}::text, $separator${order_by}::text)" + return "ARRAY_TO_STRING($sql, $separator)"; + } + + if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) { + + # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x + # ORDER BY y", we need to sort the list, and then get the unique + # values + return + "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)"; + } + + # Determine the ORDER BY clause (if any) + if ($order_by) { + $order_by = " ORDER BY $order_by"; + } + elsif ($sort) { + + # We don't include the DISTINCT keyword in an order by + $text =~ /^(?:DISTINCT\s*)?(.+)$/i; + $order_by = " ORDER BY $1"; + } + + return "STRING_AGG(${text}::text, $separator${order_by}::text)"; } sub sql_istring { - my ($self, $string) = @_; + my ($self, $string) = @_; - return "LOWER(${string}::text)"; + return "LOWER(${string}::text)"; } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "POSITION(${fragment}::text IN ${text}::text)"; + return "POSITION(${fragment}::text IN ${text}::text)"; } sub sql_like { - my ($self, $fragment, $column, $not) = @_; - $not //= ''; + my ($self, $fragment, $column, $not) = @_; + $not //= ''; - return "${column}::text $not LIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'"; + return + "${column}::text $not LIKE " + . $self->sql_like_escape($fragment) + . " ESCAPE '|'"; } sub sql_ilike { - my ($self, $fragment, $column, $not) = @_; - $not //= ''; + my ($self, $fragment, $column, $not) = @_; + $not //= ''; - return "${column}::text $not ILIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'"; + return + "${column}::text $not ILIKE " + . $self->sql_like_escape($fragment) + . " ESCAPE '|'"; } sub sql_not_ilike { - return shift->sql_ilike(@_, 'NOT'); + return shift->sql_ilike(@_, 'NOT'); } # Escapes any % or _ characters which are special in a LIKE match. # Also performs a $dbh->quote to escape any quote characters. sub sql_like_escape { - my ($self, $fragment) = @_; + my ($self, $fragment) = @_; - $fragment =~ s/\|/\|\|/g; # escape the escape character if it appears - $fragment =~ s/%/\|%/g; # percent and underscore are the special match - $fragment =~ s/_/\|_/g; # characters in SQL. + $fragment =~ s/\|/\|\|/g; # escape the escape character if it appears + $fragment =~ s/%/\|%/g; # percent and underscore are the special match + $fragment =~ s/_/\|_/g; # characters in SQL. - return $self->quote("%$fragment%"); + return $self->quote("%$fragment%"); } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "${expr}::text ~* $pattern"; + return "${expr}::text ~* $pattern"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "${expr}::text !~* $pattern" + return "${expr}::text !~* $pattern"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $limit OFFSET $offset"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $limit OFFSET $offset"; + } + else { + return "LIMIT $limit"; + } } sub sql_from_days { - my ($self, $days) = @_; + my ($self, $days) = @_; - return "TO_TIMESTAMP('$days', 'J')::date"; + return "TO_TIMESTAMP('$days', 'J')::date"; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return "TO_CHAR(${date}::date, 'J')::int"; + return "TO_CHAR(${date}::date, 'J')::int"; } sub sql_date_format { - my ($self, $date, $format) = @_; - - $format = "%Y.%m.%d %H:%i:%s" if !$format; - - $format =~ s/\%Y/YYYY/g; - $format =~ s/\%y/YY/g; - $format =~ s/\%m/MM/g; - $format =~ s/\%d/DD/g; - $format =~ s/\%a/Dy/g; - $format =~ s/\%H/HH24/g; - $format =~ s/\%i/MI/g; - $format =~ s/\%s/SS/g; - - return "TO_CHAR($date, " . $self->quote($format) . ")"; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; + + $format =~ s/\%Y/YYYY/g; + $format =~ s/\%y/YY/g; + $format =~ s/\%m/MM/g; + $format =~ s/\%d/DD/g; + $format =~ s/\%a/Dy/g; + $format =~ s/\%H/HH24/g; + $format =~ s/\%i/MI/g; + $format =~ s/\%s/SS/g; + + return "TO_CHAR($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - - return "$date $operator $interval * INTERVAL '1 $units'"; + my ($self, $date, $operator, $interval, $units) = @_; + + return "$date $operator $interval * INTERVAL '1 $units'"; } sub sql_string_concat { - my ($self, @params) = @_; - - # Postgres 7.3 does not support concatenating of different types, so we - # need to cast both parameters to text. Version 7.4 seems to handle this - # properly, so when we stop support 7.3, this can be removed. - return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))'; + my ($self, @params) = @_; + + # Postgres 7.3 does not support concatenating of different types, so we + # need to cast both parameters to text. Version 7.4 seems to handle this + # properly, so when we stop support 7.3, this can be removed. + return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))'; } # Tell us whether or not a particular sequence exists in the DB. sub bz_sequence_exists { - my ($self, $seq_name) = @_; - my $exists = $self->selectrow_array( - 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?', - undef, $seq_name); - return $exists || 0; + my ($self, $seq_name) = @_; + my $exists + = $self->selectrow_array( + 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?', + undef, $seq_name); + return $exists || 0; } sub bz_explain { - my ($self, $sql) = @_; - my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql"); - return join("\n", @$explain); + my ($self, $sql) = @_; + my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql"); + return join("\n", @$explain); } ##################################################################### @@ -248,42 +260,49 @@ sub bz_explain { ##################################################################### sub bz_check_server_version { - my $self = shift; - my ($db) = @_; - my $server_version = $self->SUPER::bz_check_server_version(@_); - my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/; - # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values. - # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists. - if ($major_version >= 9) { - local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2'; - local $db->{name} = $db->{name} . " ${major_version}.$minor_version"; - Bugzilla::DB::_bz_check_dbd(@_); - } + my $self = shift; + my ($db) = @_; + my $server_version = $self->SUPER::bz_check_server_version(@_); + my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/; + + # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values. + # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists. + if ($major_version >= 9) { + local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2'; + local $db->{name} = $db->{name} . " ${major_version}.$minor_version"; + Bugzilla::DB::_bz_check_dbd(@_); + } } sub bz_setup_database { - my $self = shift; - $self->SUPER::bz_setup_database(@_); - - my ($has_plpgsql) = $self->selectrow_array("SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'"); - $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql; - - if (vers_cmp($self->bz_server_version, 9) < 0) { - # Custom Functions for Postgres 8 - 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 ( + my $self = shift; + $self->SUPER::bz_setup_database(@_); + + my ($has_plpgsql) + = $self->selectrow_array( + "SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'"); + $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql; + + if (vers_cmp($self->bz_server_version, 9) < 0) { + + # Custom Functions for Postgres 8 + 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 = '{}' - )"); - } + )" + ); + } - $self->do(<<'END'); + $self->do(<<'END'); CREATE OR REPLACE FUNCTION array_sort(ANYARRAY) RETURNS ANYARRAY LANGUAGE SQL IMMUTABLE STRICT @@ -296,31 +315,32 @@ SELECT ARRAY( ); $$; END - } - else { - # Custom functions for Postgres 9.0+ - - # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC - # JDBurnZ: https://github.com/JDBurnZ - # Message In Action: https://www.messageinaction.com - # - #Permission is hereby granted, free of charge, to any person obtaining a copy of - #this software and associated documentation files (the "Software"), to deal in - #the Software without restriction, including without limitation the rights to - #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - #the Software, and to permit persons to whom the Software is furnished to do so, - #subject to the following conditions: - # - #The above copyright notice and this permission notice shall be included in all - #copies or substantial portions of the Software. - # - #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - $self->do(q| + } + else { + # Custom functions for Postgres 9.0+ + + # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC + # JDBurnZ: https://github.com/JDBurnZ + # Message In Action: https://www.messageinaction.com + # + #Permission is hereby granted, free of charge, to any person obtaining a copy of + #this software and associated documentation files (the "Software"), to deal in + #the Software without restriction, including without limitation the rights to + #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + #the Software, and to permit persons to whom the Software is furnished to do so, + #subject to the following conditions: + # + #The above copyright notice and this permission notice shall be included in all + #copies or substantial portions of the Software. + # + #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + $self->do( + q| DROP FUNCTION IF EXISTS anyarray_uniq(anyarray); CREATE OR REPLACE FUNCTION anyarray_uniq(with_array anyarray) RETURNS anyarray AS $BODY$ @@ -345,135 +365,152 @@ END RETURN return_array; END; $BODY$ LANGUAGE plpgsql; - |); + | + ); + } + + # 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. + $self->bz_drop_index('longdescs', 'longdescs_thetext_idx'); + + # Same for all the comments fields in the fulltext table. + $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx'); + $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_noprivate_idx'); + + # PostgreSQL also wants an index for calling LOWER on + # login_name, which we do with sql_istrcmp all over the place. + $self->bz_add_index( + 'profiles', + 'profiles_login_name_lower_idx', + {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'} + ); + + # Now that Bugzilla::Object uses sql_istrcmp, other tables + # also need a LOWER() index. + _fix_case_differences('fielddefs', 'name'); + $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + _fix_case_differences('keyworddefs', 'name'); + $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + _fix_case_differences('products', 'name'); + $self->bz_add_index('products', 'products_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + + # bz_rename_column and bz_rename_table didn't correctly rename + # the sequence. + $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', + 'fielddefs_id_seq'); + + # If the 'tags' table still exists, then bz_rename_table() + # will fix the sequence for us. + if (!$self->bz_table_info('tags')) { + my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq'); + + # If $res is true, then the sequence has been renamed, meaning that + # the primary key must be renamed too. + if ($res) { + $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey'); } - - # 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. - $self->bz_drop_index('longdescs', 'longdescs_thetext_idx'); - # Same for all the comments fields in the fulltext table. - $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx'); - $self->bz_drop_index('bugs_fulltext', - 'bugs_fulltext_comments_noprivate_idx'); - - # PostgreSQL also wants an index for calling LOWER on - # login_name, which we do with sql_istrcmp all over the place. - $self->bz_add_index('profiles', 'profiles_login_name_lower_idx', - {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'}); - - # Now that Bugzilla::Object uses sql_istrcmp, other tables - # also need a LOWER() index. - _fix_case_differences('fielddefs', 'name'); - $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - _fix_case_differences('keyworddefs', 'name'); - $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - _fix_case_differences('products', 'name'); - $self->bz_add_index('products', 'products_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - - # bz_rename_column and bz_rename_table didn't correctly rename - # the sequence. - $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', 'fielddefs_id_seq'); - # If the 'tags' table still exists, then bz_rename_table() - # will fix the sequence for us. - if (!$self->bz_table_info('tags')) { - my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq'); - # If $res is true, then the sequence has been renamed, meaning that - # the primary key must be renamed too. - if ($res) { - $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey'); - } - } - - # Certain sequences got upgraded before we required Pg 8.3, and - # so they were not properly associated with their columns. - my @tables = $self->bz_table_list_real; - foreach my $table (@tables) { - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - # All our SERIAL pks have "id" in their name at the end. - next unless $column =~ /id$/; - my $sequence = "${table}_${column}_seq"; - if ($self->bz_sequence_exists($sequence)) { - my $is_associated = $self->selectrow_array( - 'SELECT pg_get_serial_sequence(?,?)', - undef, $table, $column); - next if $is_associated; - print "Fixing $sequence to be associated" - . " with $table.$column...\n"; - $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column"); - # In order to produce an exactly identical schema to what - # a brand-new checksetup.pl run would produce, we also need - # to re-set the default on this column. - $self->do("ALTER TABLE $table + } + + # Certain sequences got upgraded before we required Pg 8.3, and + # so they were not properly associated with their columns. + my @tables = $self->bz_table_list_real; + foreach my $table (@tables) { + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + + # All our SERIAL pks have "id" in their name at the end. + next unless $column =~ /id$/; + my $sequence = "${table}_${column}_seq"; + if ($self->bz_sequence_exists($sequence)) { + my $is_associated = $self->selectrow_array('SELECT pg_get_serial_sequence(?,?)', + undef, $table, $column); + next if $is_associated; + print "Fixing $sequence to be associated" . " with $table.$column...\n"; + $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column"); + + # In order to produce an exactly identical schema to what + # a brand-new checksetup.pl run would produce, we also need + # to re-set the default on this column. + $self->do( + "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT nextval('$sequence')"); - } - } + SET DEFAULT nextval('$sequence')" + ); + } } + } } sub _fix_bad_sequence { - my ($self, $table, $column, $old_seq, $new_seq) = @_; - if ($self->bz_column_info($table, $column) - && $self->bz_sequence_exists($old_seq)) - { - print "Fixing $old_seq sequence...\n"; - $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq"); - $self->do("ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT NEXTVAL('$new_seq')"); - return 1; - } - return 0; + my ($self, $table, $column, $old_seq, $new_seq) = @_; + if ( $self->bz_column_info($table, $column) + && $self->bz_sequence_exists($old_seq)) + { + print "Fixing $old_seq sequence...\n"; + $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq"); + $self->do( + "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT NEXTVAL('$new_seq')" + ); + return 1; + } + return 0; } # Renames things that differ only in case. sub _fix_case_differences { - my ($table, $field) = @_; - my $dbh = Bugzilla->dbh; - - my $duplicates = $dbh->selectcol_arrayref( - "SELECT DISTINCT LOWER($field) FROM $table - GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1"); - - foreach my $name (@$duplicates) { - my $dups = $dbh->selectcol_arrayref( - "SELECT $field FROM $table WHERE LOWER($field) = ?", - undef, $name); - my $primary = shift @$dups; - foreach my $dup (@$dups) { - my $new_name = "${dup}_"; - # Make sure the new name isn't *also* a duplicate. - while (1) { - last if (!$dbh->selectrow_array( - "SELECT 1 FROM $table WHERE LOWER($field) = ?", - undef, lc($new_name))); - $new_name .= "_"; - } - print "$table '$primary' and '$dup' have names that differ", - " only in case.\nRenaming '$dup' to '$new_name'...\n"; - $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?", - undef, $new_name, $dup); - } + my ($table, $field) = @_; + my $dbh = Bugzilla->dbh; + + my $duplicates = $dbh->selectcol_arrayref( + "SELECT DISTINCT LOWER($field) FROM $table + GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1" + ); + + foreach my $name (@$duplicates) { + my $dups + = $dbh->selectcol_arrayref( + "SELECT $field FROM $table WHERE LOWER($field) = ?", + undef, $name); + my $primary = shift @$dups; + foreach my $dup (@$dups) { + my $new_name = "${dup}_"; + + # Make sure the new name isn't *also* a duplicate. + while (1) { + last + if (!$dbh->selectrow_array( + "SELECT 1 FROM $table WHERE LOWER($field) = ?", + undef, lc($new_name) + )); + $new_name .= "_"; + } + print "$table '$primary' and '$dup' have names that differ", + " only in case.\nRenaming '$dup' to '$new_name'...\n"; + $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?", + undef, $new_name, $dup); } + } } ##################################################################### # Custom Schema Information Functions ##################################################################### -# Pg includes the PostgreSQL system tables in table_list_real, so +# Pg includes the PostgreSQL system tables in table_list_real, so # we need to remove those. sub bz_table_list_real { - my $self = shift; + my $self = shift; + + my @full_table_list = $self->SUPER::bz_table_list_real(@_); - my @full_table_list = $self->SUPER::bz_table_list_real(@_); - # All PostgreSQL system tables start with "pg_" or "sql_" - my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); - return @table_list; + # All PostgreSQL system tables start with "pg_" or "sql_" + my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); + return @table_list; } 1; diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index d1c1dc7e9..4fc9ce8e2 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -29,6 +29,7 @@ use Digest::MD5 qw(md5_hex); use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys); use List::MoreUtils qw(firstidx natatime); use Safe; + # Historical, needed for SCHEMA_VERSION = '1.00' use Storable qw(dclone freeze thaw); @@ -197,1596 +198,1544 @@ update this column in this table." =cut -use constant SCHEMA_VERSION => 3; -use constant ADD_COLUMN => 'ADD COLUMN'; +use constant SCHEMA_VERSION => 3; +use constant ADD_COLUMN => 'ADD COLUMN'; + # Multiple FKs can be added using ALTER TABLE ADD CONSTRAINT in one # SQL statement. This isn't true for all databases. use constant MULTIPLE_FKS_IN_ALTER => 1; + # This is a reasonable default that's true for both PostgreSQL and MySQL. use constant MAX_IDENTIFIER_LEN => 63; use constant FIELD_TABLE_SCHEMA => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + visibility_value_id => {TYPE => 'INT2'}, + ], + + # Note that bz_add_field_table should prepend the table name + # to these index names. + INDEXES => [ + value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + sortkey_idx => ['sortkey', 'value'], + visibility_value_id_idx => ['visibility_value_id'], + ], +}; + +use constant ABSTRACT_SCHEMA => { + + # BUG-RELATED TABLES + # ------------------ + + # General Bug Information + # ----------------------- + bugs => { FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - visibility_value_id => {TYPE => 'INT2'}, + bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + assigned_to => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_file_loc => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1}, + bug_status => {TYPE => 'varchar(64)', NOTNULL => 1}, + creation_ts => {TYPE => 'DATETIME'}, + delta_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + 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, + REFERENCES => {TABLE => 'products', COLUMN => 'id'} + }, + rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1}, + reporter => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + version => {TYPE => 'varchar(64)', NOTNULL => 1}, + component_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id'} + }, + resolution => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + target_milestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, + qa_contact => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + lastdiffed => {TYPE => 'DATETIME'}, + everconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1}, + reporter_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + cclist_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + estimated_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + remaining_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + deadline => {TYPE => 'DATETIME'}, ], - # Note that bz_add_field_table should prepend the table name - # to these index names. INDEXES => [ - value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, - sortkey_idx => ['sortkey', 'value'], - visibility_value_id_idx => ['visibility_value_id'], + bugs_assigned_to_idx => ['assigned_to'], + bugs_creation_ts_idx => ['creation_ts'], + bugs_delta_ts_idx => ['delta_ts'], + bugs_bug_severity_idx => ['bug_severity'], + bugs_bug_status_idx => ['bug_status'], + bugs_op_sys_idx => ['op_sys'], + bugs_priority_idx => ['priority'], + bugs_product_id_idx => ['product_id'], + bugs_reporter_idx => ['reporter'], + bugs_version_idx => ['version'], + bugs_component_id_idx => ['component_id'], + bugs_resolution_idx => ['resolution'], + bugs_target_milestone_idx => ['target_milestone'], + bugs_qa_contact_idx => ['qa_contact'], ], -}; + }, -use constant ABSTRACT_SCHEMA => { + bugs_fulltext => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, + + # Comments are stored all together in one column for searching. + # This allows us to examine all comments together when deciding + # the relevance of a bug in fulltext search. + comments => {TYPE => 'LONGTEXT'}, + comments_noprivate => {TYPE => 'LONGTEXT'}, + ], + INDEXES => [ + bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], TYPE => 'FULLTEXT'}, + bugs_fulltext_comments_idx => {FIELDS => ['comments'], TYPE => 'FULLTEXT'}, + bugs_fulltext_comments_noprivate_idx => + {FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'}, + ], + }, - # BUG-RELATED TABLES - # ------------------ - - # General Bug Information - # ----------------------- - bugs => { - FIELDS => [ - bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - assigned_to => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_file_loc => {TYPE => 'MEDIUMTEXT', - NOTNULL => 1, DEFAULT => "''"}, - bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1}, - bug_status => {TYPE => 'varchar(64)', NOTNULL => 1}, - creation_ts => {TYPE => 'DATETIME'}, - delta_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - 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, - REFERENCES => {TABLE => 'products', - COLUMN => 'id'}}, - rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1}, - reporter => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - version => {TYPE => 'varchar(64)', NOTNULL => 1}, - component_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id'}}, - resolution => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "''"}, - target_milestone => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "'---'"}, - qa_contact => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, - lastdiffed => {TYPE => 'DATETIME'}, - everconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1}, - reporter_accessible => {TYPE => 'BOOLEAN', - NOTNULL => 1, DEFAULT => 'TRUE'}, - cclist_accessible => {TYPE => 'BOOLEAN', - NOTNULL => 1, DEFAULT => 'TRUE'}, - estimated_time => {TYPE => 'decimal(7,2)', - NOTNULL => 1, DEFAULT => '0'}, - remaining_time => {TYPE => 'decimal(7,2)', - NOTNULL => 1, DEFAULT => '0'}, - deadline => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - bugs_assigned_to_idx => ['assigned_to'], - bugs_creation_ts_idx => ['creation_ts'], - bugs_delta_ts_idx => ['delta_ts'], - bugs_bug_severity_idx => ['bug_severity'], - bugs_bug_status_idx => ['bug_status'], - bugs_op_sys_idx => ['op_sys'], - bugs_priority_idx => ['priority'], - bugs_product_id_idx => ['product_id'], - bugs_reporter_idx => ['reporter'], - bugs_version_idx => ['version'], - bugs_component_id_idx => ['component_id'], - bugs_resolution_idx => ['resolution'], - bugs_target_milestone_idx => ['target_milestone'], - bugs_qa_contact_idx => ['qa_contact'], - ], - }, - - bugs_fulltext => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, PRIMARYKEY => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, - # Comments are stored all together in one column for searching. - # This allows us to examine all comments together when deciding - # the relevance of a bug in fulltext search. - comments => {TYPE => 'LONGTEXT'}, - comments_noprivate => {TYPE => 'LONGTEXT'}, - ], - INDEXES => [ - bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], - TYPE => 'FULLTEXT'}, - bugs_fulltext_comments_idx => {FIELDS => ['comments'], - TYPE => 'FULLTEXT'}, - bugs_fulltext_comments_noprivate_idx => { - FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'}, - ], - }, - - bugs_activity => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - attach_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, - fieldid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - added => {TYPE => 'varchar(255)'}, - removed => {TYPE => 'varchar(255)'}, - comment_id => {TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bugs_activity_bug_id_idx => ['bug_id'], - bugs_activity_who_idx => ['who'], - bugs_activity_bug_when_idx => ['bug_when'], - bugs_activity_fieldid_idx => ['fieldid'], - bugs_activity_added_idx => ['added'], - bugs_activity_removed_idx => ['removed'], - ], - }, - - bugs_aliases => { - FIELDS => [ - alias => {TYPE => 'varchar(40)', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bugs_aliases_bug_id_idx => ['bug_id'], - bugs_aliases_alias_idx => {FIELDS => ['alias'], - TYPE => 'UNIQUE'}, - ], - }, - - cc => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - cc_bug_id_idx => {FIELDS => [qw(bug_id who)], - TYPE => 'UNIQUE'}, - cc_who_idx => ['who'], - ], - }, - - longdescs => { - FIELDS => [ - comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 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(7,2)', NOTNULL => 1, - DEFAULT => '0'}, - thetext => {TYPE => 'LONGTEXT', NOTNULL => 1}, - isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - type => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - extra_data => {TYPE => 'varchar(255)'} - ], - INDEXES => [ - longdescs_bug_id_idx => [qw(bug_id work_time)], - longdescs_who_idx => [qw(who bug_id)], - longdescs_bug_when_idx => ['bug_when'], - ], - }, - - longdescs_tags => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - comment_id => { TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE' }}, - tag => { TYPE => 'varchar(24)', NOTNULL => 1 }, - ], - INDEXES => [ - longdescs_tags_idx => { FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE' }, - ], - }, - - longdescs_tags_weights => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - tag => { TYPE => 'varchar(24)', NOTNULL => 1 }, - weight => { TYPE => 'INT3', NOTNULL => 1 }, - ], - INDEXES => [ - longdescs_tags_weights_tag_idx => { FIELDS => ['tag'], TYPE => 'UNIQUE' }, - ], - }, - - longdescs_tags_activity => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - bug_id => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' }}, - comment_id => { TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE' }}, - who => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'profiles', - COLUMN => 'userid' }}, - bug_when => { TYPE => 'DATETIME', NOTNULL => 1 }, - added => { TYPE => 'varchar(24)' }, - removed => { TYPE => 'varchar(24)' }, - ], - INDEXES => [ - longdescs_tags_activity_bug_id_idx => ['bug_id'], - ], - }, - - dependencies => { - FIELDS => [ - blocked => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - dependson => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - dependencies_blocked_idx => {FIELDS => [qw(blocked dependson)], - TYPE => 'UNIQUE'}, - dependencies_dependson_idx => ['dependson'], - ], - }, - - attachments => { - FIELDS => [ - attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - creation_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - modification_time => {TYPE => 'DATETIME', NOTNULL => 1}, - description => {TYPE => 'TINYTEXT', NOTNULL => 1}, - mimetype => {TYPE => 'TINYTEXT', NOTNULL => 1}, - ispatch => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - filename => {TYPE => 'varchar(255)', NOTNULL => 1}, - submitter_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - attachments_bug_id_idx => ['bug_id'], - attachments_creation_ts_idx => ['creation_ts'], - attachments_modification_time_idx => ['modification_time'], - attachments_submitter_id_idx => ['submitter_id', 'bug_id'], - ], - }, - attach_data => { - FIELDS => [ - id => {TYPE => 'INT3', NOTNULL => 1, - PRIMARYKEY => 1, - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, - ], - }, - - duplicates => { - FIELDS => [ - dupe_of => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - dupe => {TYPE => 'INT3', NOTNULL => 1, - PRIMARYKEY => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - }, - - bug_see_also => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(255)', NOTNULL => 1}, - class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, - ], - INDEXES => [ - bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - # Auditing - # -------- - - audit_log => { - FIELDS => [ - user_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - class => {TYPE => 'varchar(255)', NOTNULL => 1}, - object_id => {TYPE => 'INT4', NOTNULL => 1}, - field => {TYPE => 'varchar(64)', NOTNULL => 1}, - removed => {TYPE => 'MEDIUMTEXT'}, - added => {TYPE => 'MEDIUMTEXT'}, - at_time => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - audit_log_class_idx => ['class', 'at_time'], - ], - }, - - # Keywords - # -------- - - keyworddefs => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - ], - INDEXES => [ - keyworddefs_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - keywords => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - keywordid => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'keyworddefs', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - - ], - INDEXES => [ - keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)], - TYPE => 'UNIQUE'}, - keywords_keywordid_idx => ['keywordid'], - ], - }, - - # Flags - # ----- - - # "flags" stores one record for each flag on each bug/attachment. - flags => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - status => {TYPE => 'char(1)', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - attach_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - creation_date => {TYPE => 'DATETIME', NOTNULL => 1}, - modification_date => {TYPE => 'DATETIME'}, - setter_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - requestee_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - ], - INDEXES => [ - flags_bug_id_idx => [qw(bug_id attach_id)], - flags_setter_id_idx => ['setter_id'], - flags_requestee_id_idx => ['requestee_id'], - flags_type_id_idx => ['type_id'], - ], - }, - - # "flagtypes" defines the types of flags that can be set. - flagtypes => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(50)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - cc_list => {TYPE => 'varchar(200)'}, - target_type => {TYPE => 'char(1)', NOTNULL => 1, - DEFAULT => "'b'"}, - is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - is_requestable => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_requesteeble => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - grant_group_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL'}}, - request_group_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL'}}, - ], - }, - - # "flaginclusions" and "flagexclusions" specify the products/components - # a bug/attachment must belong to in order for flags of a given type - # to be set for them. - flaginclusions => { - FIELDS => [ - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - flaginclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }, - ], - }, - - flagexclusions => { - FIELDS => [ - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - flagexclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }, - ], - }, - - # General Field Information - # ------------------------- - - fielddefs => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - type => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => FIELD_TYPE_UNKNOWN}, - custom => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - description => {TYPE => 'TINYTEXT', NOTNULL => 1}, - long_desc => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, - mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1}, - obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - buglist => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - visibility_field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - value_field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - reverse_desc => {TYPE => 'TINYTEXT'}, - is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_numeric => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - fielddefs_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - fielddefs_sortkey_idx => ['sortkey'], - fielddefs_value_field_id_idx => ['value_field_id'], - fielddefs_is_mandatory_idx => ['is_mandatory'], - ], - }, - - # Field Visibility Information - # ------------------------- - - field_visibility => { - FIELDS => [ - field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - value_id => {TYPE => 'INT2', NOTNULL => 1} - ], - INDEXES => [ - field_visibility_field_id_idx => { - FIELDS => [qw(field_id value_id)], - TYPE => 'UNIQUE' - }, - ], - }, - - # Per-product Field Values - # ------------------------ - - versions => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - versions_product_id_idx => {FIELDS => [qw(product_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - milestones => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => 0}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - milestones_product_id_idx => {FIELDS => [qw(product_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - # Global Field Values - # ------------------- - - bug_status => { - FIELDS => [ - @{ dclone(FIELD_TABLE_SCHEMA->{FIELDS}) }, - is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, - - ], - INDEXES => [ - bug_status_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - bug_status_sortkey_idx => ['sortkey', 'value'], - bug_status_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - resolution => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - resolution_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - resolution_sortkey_idx => ['sortkey', 'value'], - resolution_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - bug_severity => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - bug_severity_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - bug_severity_sortkey_idx => ['sortkey', 'value'], - bug_severity_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - priority => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - priority_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - priority_sortkey_idx => ['sortkey', 'value'], - priority_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - rep_platform => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - rep_platform_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - rep_platform_sortkey_idx => ['sortkey', 'value'], - rep_platform_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - op_sys => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - op_sys_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - op_sys_sortkey_idx => ['sortkey', 'value'], - op_sys_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - status_workflow => { - FIELDS => [ - # On bug creation, there is no old value. - 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 => [ - status_workflow_idx => {FIELDS => ['old_status', 'new_status'], - TYPE => 'UNIQUE'}, - ], - }, - - # USER INFO - # --------- - - # General User Information - # ------------------------ - - profiles => { - FIELDS => [ - userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - login_name => {TYPE => 'varchar(255)', NOTNULL => 1}, - cryptpassword => {TYPE => 'varchar(128)'}, - realname => {TYPE => 'varchar(255)', NOTNULL => 1, - DEFAULT => "''"}, - disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, - disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - extern_id => {TYPE => 'varchar(64)'}, - is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - last_seen_date => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - profiles_login_name_idx => {FIELDS => ['login_name'], - TYPE => 'UNIQUE'}, - profiles_extern_id_idx => {FIELDS => ['extern_id'], - TYPE => 'UNIQUE'} - ], - }, - - profile_search => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_list => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - list_order => {TYPE => 'MEDIUMTEXT'}, - ], - INDEXES => [ - profile_search_user_id_idx => [qw(user_id)], - ], - }, - - profiles_activity => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, - fieldid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - oldvalue => {TYPE => 'TINYTEXT'}, - newvalue => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - profiles_activity_userid_idx => ['userid'], - profiles_activity_profiles_when_idx => ['profiles_when'], - profiles_activity_fieldid_idx => ['fieldid'], - ], - }, - - email_setting => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - relationship => {TYPE => 'INT1', NOTNULL => 1}, - event => {TYPE => 'INT1', NOTNULL => 1}, - ], - INDEXES => [ - email_setting_user_id_idx => - {FIELDS => [qw(user_id relationship event)], - TYPE => 'UNIQUE'}, - ], - }, - - email_bug_ignore => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - email_bug_ignore_user_id_idx => {FIELDS => [qw(user_id bug_id)], - TYPE => 'UNIQUE'}, - ], - }, - - watch => { - FIELDS => [ - watcher => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - watched => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - watch_watcher_idx => {FIELDS => [qw(watcher watched)], - TYPE => 'UNIQUE'}, - watch_watched_idx => ['watched'], - ], - }, - - namedqueries => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - query => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - namedqueries_userid_idx => {FIELDS => [qw(userid name)], - TYPE => 'UNIQUE'}, - ], - }, - - namedqueries_link_in_footer => { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - namedqueries_link_in_footer_id_idx => {FIELDS => [qw(namedquery_id user_id)], - TYPE => 'UNIQUE'}, - namedqueries_link_in_footer_userid_idx => ['user_id'], - ], - }, - - tag => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}, - ], - }, - - bug_tag => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - tag_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'tag', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'}, - ], - }, - - reports => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - query => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - reports_user_id_idx => {FIELDS => [qw(user_id name)], - TYPE => 'UNIQUE'}, - ], - }, - - component_cc => { - - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - component_cc_user_id_idx => {FIELDS => [qw(component_id user_id)], - TYPE => 'UNIQUE'}, - ], - }, - - # Authentication - # -------------- - - logincookies => { - FIELDS => [ - cookie => {TYPE => 'varchar(16)', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ipaddr => {TYPE => 'varchar(40)'}, - lastused => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - logincookies_lastused_idx => ['lastused'], - ], - }, - - 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. - tokens => { - FIELDS => [ - userid => {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - issuedate => {TYPE => 'DATETIME', NOTNULL => 1} , - token => {TYPE => 'varchar(16)', NOTNULL => 1, - PRIMARYKEY => 1}, - tokentype => {TYPE => 'varchar(16)', NOTNULL => 1} , - eventdata => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - tokens_userid_idx => ['userid'], - ], - }, - - # GROUPS - # ------ - - groups => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(255)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, - userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1, - DEFAULT => "''"}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - icon_url => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'}, - ], - }, - - group_control_map => { - FIELDS => [ - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - entry => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - membercontrol => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => CONTROLMAPNA}, - othercontrol => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => CONTROLMAPNA}, - canedit => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - canconfirm => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - group_control_map_product_id_idx => - {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'}, - group_control_map_group_id_idx => ['group_id'], - ], - }, - - # "user_group_map" determines the groups that a user belongs to - # directly or due to regexp and which groups can be blessed by a user. - # - # grant_type: - # if GRANT_DIRECT - record was explicitly granted - # if GRANT_DERIVED - record was derived from expanding a group hierarchy - # if GRANT_REGEXP - record was created by evaluating a regexp - user_group_map => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - isbless => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - grant_type => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => GRANT_DIRECT}, - ], - INDEXES => [ - user_group_map_user_id_idx => - {FIELDS => [qw(user_id group_id grant_type isbless)], - TYPE => 'UNIQUE'}, - ], - }, - - # This table determines which groups are made a member of another - # group, given the ability to bless another group, or given - # visibility to another groups existence and membership - # grant_type: - # if GROUP_MEMBERSHIP - member groups are made members of grantor - # if GROUP_BLESS - member groups may grant membership in grantor - # if GROUP_VISIBLE - member groups may see grantor group - group_group_map => { - FIELDS => [ - member_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - grantor_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - grant_type => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => GROUP_MEMBERSHIP}, - ], - INDEXES => [ - group_group_map_member_id_idx => - {FIELDS => [qw(member_id grantor_id grant_type)], - TYPE => 'UNIQUE'}, - ], - }, - - # This table determines which groups a user must be a member of - # in order to see a bug. - bug_group_map => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bug_group_map_bug_id_idx => - {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'}, - bug_group_map_group_id_idx => ['group_id'], - ], - }, - - # This table determines which groups a user must be a member of - # in order to see a named query somebody else shares. - namedquery_group_map => { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - namedquery_group_map_namedquery_id_idx => - {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'}, - namedquery_group_map_group_id_idx => ['group_id'], - ], - }, - - category_group_map => { - FIELDS => [ - category_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - category_group_map_category_id_idx => - {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'}, - ], - }, - - - # PRODUCTS - # -------- - - classifications => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, - ], - INDEXES => [ - classifications_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - products => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - classification_id => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '1', - REFERENCES => {TABLE => 'classifications', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 1}, - defaultmilestone => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "'---'"}, - allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - products_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - components => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - initialowner => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - initialqacontact => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - components_product_id_idx => {FIELDS => [qw(product_id name)], - TYPE => 'UNIQUE'}, - components_name_idx => ['name'], - ], - }, - - # CHARTS - # ------ - - series => { - FIELDS => [ - series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - creator => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - category => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - subcategory => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - frequency => {TYPE => 'INT2', NOTNULL => 1}, - query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - is_public => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - series_creator_idx => ['creator'], - series_category_idx => {FIELDS => [qw(category subcategory name)], - TYPE => 'UNIQUE'}, - ], - }, - - series_data => { - FIELDS => [ - series_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'series', - COLUMN => 'series_id', - DELETE => 'CASCADE'}}, - series_date => {TYPE => 'DATETIME', NOTNULL => 1}, - series_value => {TYPE => 'INT3', NOTNULL => 1}, - ], - INDEXES => [ - series_data_series_id_idx => - {FIELDS => [qw(series_id series_date)], - TYPE => 'UNIQUE'}, - ], - }, - - series_categories => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - ], - INDEXES => [ - series_categories_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - # WHINE SYSTEM - # ------------ - - whine_queries => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - eventid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'whine_events', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - query_name => {TYPE => 'varchar(64)', NOTNULL => 1, - DEFAULT => "''"}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - title => {TYPE => 'varchar(128)', NOTNULL => 1, - DEFAULT => "''"}, - ], - INDEXES => [ - whine_queries_eventid_idx => ['eventid'], - ], - }, - - whine_schedules => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - eventid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'whine_events', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - run_day => {TYPE => 'varchar(32)'}, - run_time => {TYPE => 'varchar(32)'}, - run_next => {TYPE => 'DATETIME'}, - mailto => {TYPE => 'INT3', NOTNULL => 1}, - mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, - ], - INDEXES => [ - whine_schedules_run_next_idx => ['run_next'], - whine_schedules_eventid_idx => ['eventid'], - ], - }, - - whine_events => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - owner_userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - subject => {TYPE => 'varchar(128)'}, - body => {TYPE => 'MEDIUMTEXT'}, - mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - }, - - # QUIPS - # ----- - - quips => { - FIELDS => [ - quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - quip => {TYPE => 'varchar(512)', NOTNULL => 1}, - approved => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - }, - - # SETTINGS - # -------- - # setting - each global setting will have exactly one entry - # in this table. - # setting_value - stores the list of acceptable values for each - # setting, and a sort index that controls the order - # in which the values are displayed. - # profile_setting - If a user has chosen to use a value other than the - # global default for a given setting, it will be - # stored in this table. Note: even if a setting is - # later changed so is_enabled = false, the stored - # value will remain in case it is ever enabled again. - # - setting => { - FIELDS => [ - name => {TYPE => 'varchar(32)', NOTNULL => 1, - PRIMARYKEY => 1}, - default_value => {TYPE => 'varchar(32)', NOTNULL => 1}, - is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - subclass => {TYPE => 'varchar(32)'}, - ], - }, - - setting_value => { - FIELDS => [ - name => {TYPE => 'varchar(32)', NOTNULL => 1, - REFERENCES => {TABLE => 'setting', - COLUMN => 'name', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(32)', NOTNULL => 1}, - sortindex => {TYPE => 'INT2', NOTNULL => 1}, - ], - INDEXES => [ - setting_value_nv_unique_idx => {FIELDS => [qw(name value)], - TYPE => 'UNIQUE'}, - setting_value_ns_unique_idx => {FIELDS => [qw(name sortindex)], - TYPE => 'UNIQUE'}, - ], - }, - - profile_setting => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - setting_name => {TYPE => 'varchar(32)', NOTNULL => 1, - REFERENCES => {TABLE => 'setting', - COLUMN => 'name', - DELETE => 'CASCADE'}}, - setting_value => {TYPE => 'varchar(32)', NOTNULL => 1}, - ], - INDEXES => [ - profile_setting_value_unique_idx => {FIELDS => [qw(user_id setting_name)], - TYPE => 'UNIQUE'}, - ], - }, - - # BUGMAIL - # ------- - - mail_staging => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, - message => {TYPE => 'LONGBLOB', NOTNULL => 1}, - ], - }, - - # THESCHWARTZ TABLES - # ------------------ - # Note: In the standard TheSchwartz schema, most integers are unsigned, - # but we didn't implement unsigned ints for Bugzilla schemas, so we - # just create signed ints, which should be fine. - - ts_funcmap => { - FIELDS => [ - funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, - funcname => {TYPE => 'varchar(255)', NOTNULL => 1}, - ], - INDEXES => [ - ts_funcmap_funcname_idx => {FIELDS => ['funcname'], - TYPE => 'UNIQUE'}, - ], - }, - - ts_job => { - FIELDS => [ - # In a standard TheSchwartz schema, this is a BIGINT, but we - # don't have those and I didn't want to add them just for this. - jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1}, - # In standard TheSchwartz, this is a MEDIUMBLOB. - arg => {TYPE => 'LONGBLOB'}, - uniqkey => {TYPE => 'varchar(255)'}, - insert_time => {TYPE => 'INT4'}, - run_after => {TYPE => 'INT4', NOTNULL => 1}, - grabbed_until => {TYPE => 'INT4', NOTNULL => 1}, - priority => {TYPE => 'INT2'}, - coalesce => {TYPE => 'varchar(255)'}, - ], - INDEXES => [ - ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], - TYPE => 'UNIQUE'}, - # In a standard TheSchewartz schema, these both go in the other - # direction, but there's no reason to have three indexes that - # all start with the same column, and our naming scheme doesn't - # allow it anyhow. - ts_job_run_after_idx => [qw(run_after funcid)], - ts_job_coalesce_idx => [qw(coalesce funcid)], - ], - }, - - ts_note => { - FIELDS => [ - # This is a BIGINT in standard TheSchwartz schemas. - jobid => {TYPE => 'INT4', NOTNULL => 1}, - notekey => {TYPE => 'varchar(255)'}, - value => {TYPE => 'LONGBLOB'}, - ], - INDEXES => [ - ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], - TYPE => 'UNIQUE'}, - ], - }, - - ts_error => { - FIELDS => [ - error_time => {TYPE => 'INT4', NOTNULL => 1}, - jobid => {TYPE => 'INT4', NOTNULL => 1}, - message => {TYPE => 'varchar(255)', NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, - ], - INDEXES => [ - ts_error_funcid_idx => [qw(funcid error_time)], - ts_error_error_time_idx => ['error_time'], - ts_error_jobid_idx => ['jobid'], - ], - }, - - ts_exitstatus => { - FIELDS => [ - jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, - status => {TYPE => 'INT2'}, - completion_time => {TYPE => 'INT4'}, - delete_after => {TYPE => 'INT4'}, - ], - INDEXES => [ - ts_exitstatus_funcid_idx => ['funcid'], - ts_exitstatus_delete_after_idx => ['delete_after'], - ], - }, - - # SCHEMA STORAGE - # -------------- - - bz_schema => { - FIELDS => [ - schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1}, - version => {TYPE => 'decimal(3,2)', NOTNULL => 1}, - ], - }, - - bug_user_last_visit => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], - TYPE => 'UNIQUE'}, - bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], - ], - }, - - user_api_keys => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1}, - description => {TYPE => 'VARCHAR(255)'}, - revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - last_used => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, - user_api_keys_user_id_idx => ['user_id'], - ], - }, -}; + bugs_activity => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + attach_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + fieldid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'} + }, + added => {TYPE => 'varchar(255)'}, + removed => {TYPE => 'varchar(255)'}, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bugs_activity_bug_id_idx => ['bug_id'], + bugs_activity_who_idx => ['who'], + bugs_activity_bug_when_idx => ['bug_when'], + bugs_activity_fieldid_idx => ['fieldid'], + bugs_activity_added_idx => ['added'], + bugs_activity_removed_idx => ['removed'], + ], + }, -# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables -use constant MULTI_SELECT_VALUE_TABLE => { + bugs_aliases => { + FIELDS => [ + alias => {TYPE => 'varchar(40)', NOTNULL => 1}, + bug_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bugs_aliases_bug_id_idx => ['bug_id'], + bugs_aliases_alias_idx => {FIELDS => ['alias'], TYPE => 'UNIQUE'}, + ], + }, + + cc => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + cc_bug_id_idx => {FIELDS => [qw(bug_id who)], TYPE => 'UNIQUE'}, + cc_who_idx => ['who'], + ], + }, + + longdescs => { + FIELDS => [ + comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 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(7,2)', NOTNULL => 1, DEFAULT => '0'}, + thetext => {TYPE => 'LONGTEXT', NOTNULL => 1}, + isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + extra_data => {TYPE => 'varchar(255)'} + ], + INDEXES => [ + longdescs_bug_id_idx => [qw(bug_id work_time)], + longdescs_who_idx => [qw(who bug_id)], + longdescs_bug_when_idx => ['bug_when'], + ], + }, + + longdescs_tags => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + tag => {TYPE => 'varchar(24)', NOTNULL => 1}, + ], + INDEXES => + [longdescs_tags_idx => {FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE'},], + }, + + longdescs_tags_weights => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + tag => {TYPE => 'varchar(24)', NOTNULL => 1}, + weight => {TYPE => 'INT3', NOTNULL => 1}, + ], + INDEXES => + [longdescs_tags_weights_tag_idx => {FIELDS => ['tag'], TYPE => 'UNIQUE'},], + }, + + longdescs_tags_activity => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + added => {TYPE => 'varchar(24)'}, + removed => {TYPE => 'varchar(24)'}, + ], + INDEXES => [longdescs_tags_activity_bug_id_idx => ['bug_id'],], + }, + + dependencies => { + FIELDS => [ + blocked => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + dependson => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + dependencies_blocked_idx => + {FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE'}, + dependencies_dependson_idx => ['dependson'], + ], + }, + + attachments => { + FIELDS => [ + attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + creation_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + modification_time => {TYPE => 'DATETIME', NOTNULL => 1}, + description => {TYPE => 'TINYTEXT', NOTNULL => 1}, + mimetype => {TYPE => 'TINYTEXT', NOTNULL => 1}, + ispatch => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + filename => {TYPE => 'varchar(255)', NOTNULL => 1}, + submitter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + attachments_bug_id_idx => ['bug_id'], + attachments_creation_ts_idx => ['creation_ts'], + attachments_modification_time_idx => ['modification_time'], + attachments_submitter_id_idx => ['submitter_id', 'bug_id'], + ], + }, + attach_data => { FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, + id => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, + ], + }, + + duplicates => { + FIELDS => [ + dupe_of => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + dupe => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + }, + + bug_see_also => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(255)', NOTNULL => 1}, + class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + ], + INDEXES => [ + bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], TYPE => 'UNIQUE'}, + ], + }, + + # Auditing + # -------- + + audit_log => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + class => {TYPE => 'varchar(255)', NOTNULL => 1}, + object_id => {TYPE => 'INT4', NOTNULL => 1}, + field => {TYPE => 'varchar(64)', NOTNULL => 1}, + removed => {TYPE => 'MEDIUMTEXT'}, + added => {TYPE => 'MEDIUMTEXT'}, + at_time => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [audit_log_class_idx => ['class', 'at_time'],], + }, + + # Keywords + # -------- + + keyworddefs => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + ], + INDEXES => [keyworddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + keywords => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + keywordid => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'keyworddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + + ], + INDEXES => [ + keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)], TYPE => 'UNIQUE'}, + keywords_keywordid_idx => ['keywordid'], + ], + }, + + # Flags + # ----- + + # "flags" stores one record for each flag on each bug/attachment. + flags => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + status => {TYPE => 'char(1)', NOTNULL => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + attach_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + creation_date => {TYPE => 'DATETIME', NOTNULL => 1}, + modification_date => {TYPE => 'DATETIME'}, + setter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + requestee_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + ], + INDEXES => [ + flags_bug_id_idx => [qw(bug_id attach_id)], + flags_setter_id_idx => ['setter_id'], + flags_requestee_id_idx => ['requestee_id'], + flags_type_id_idx => ['type_id'], + ], + }, + + # "flagtypes" defines the types of flags that can be set. + flagtypes => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(50)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + cc_list => {TYPE => 'varchar(200)'}, + target_type => {TYPE => 'char(1)', NOTNULL => 1, DEFAULT => "'b'"}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + is_requestable => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_requesteeble => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + grant_group_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'} + }, + request_group_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'} + }, + ], + }, + + # "flaginclusions" and "flagexclusions" specify the products/components + # a bug/attachment must belong to in order for flags of a given type + # to be set for them. + flaginclusions => { + FIELDS => [ + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, ], INDEXES => [ - bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'}, + flaginclusions_type_id_idx => + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}, + ], + }, + + flagexclusions => { + FIELDS => [ + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + flagexclusions_type_id_idx => + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}, + ], + }, + + # General Field Information + # ------------------------- + + fielddefs => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => FIELD_TYPE_UNKNOWN}, + custom => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + description => {TYPE => 'TINYTEXT', NOTNULL => 1}, + long_desc => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1}, + obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + buglist => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + visibility_field_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, + value_field_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, + reverse_desc => {TYPE => 'TINYTEXT'}, + is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_numeric => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + fielddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'}, + fielddefs_sortkey_idx => ['sortkey'], + fielddefs_value_field_id_idx => ['value_field_id'], + fielddefs_is_mandatory_idx => ['is_mandatory'], + ], + }, + + # Field Visibility Information + # ------------------------- + + field_visibility => { + FIELDS => [ + field_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + value_id => {TYPE => 'INT2', NOTNULL => 1} + ], + INDEXES => [ + field_visibility_field_id_idx => + {FIELDS => [qw(field_id value_id)], TYPE => 'UNIQUE'}, + ], + }, + + # Per-product Field Values + # ------------------------ + + versions => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + versions_product_id_idx => {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'}, + ], + }, + + milestones => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + milestones_product_id_idx => + {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'}, + ], + }, + + # Global Field Values + # ------------------- + + bug_status => { + FIELDS => [ + @{dclone(FIELD_TABLE_SCHEMA->{FIELDS})}, + is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + + ], + INDEXES => [ + bug_status_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + bug_status_sortkey_idx => ['sortkey', 'value'], + bug_status_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + resolution => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + resolution_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + resolution_sortkey_idx => ['sortkey', 'value'], + resolution_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + bug_severity => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + bug_severity_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + bug_severity_sortkey_idx => ['sortkey', 'value'], + bug_severity_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + priority => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + priority_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + priority_sortkey_idx => ['sortkey', 'value'], + priority_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + rep_platform => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + rep_platform_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + rep_platform_sortkey_idx => ['sortkey', 'value'], + rep_platform_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + op_sys => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + op_sys_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + op_sys_sortkey_idx => ['sortkey', 'value'], + op_sys_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + status_workflow => { + FIELDS => [ + + # On bug creation, there is no old value. + 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 => [ + status_workflow_idx => + {FIELDS => ['old_status', 'new_status'], TYPE => 'UNIQUE'}, + ], + }, + + # USER INFO + # --------- + + # General User Information + # ------------------------ + + profiles => { + FIELDS => [ + userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + login_name => {TYPE => 'varchar(255)', NOTNULL => 1}, + cryptpassword => {TYPE => 'varchar(128)'}, + realname => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + extern_id => {TYPE => 'varchar(64)'}, + is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + last_seen_date => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + profiles_login_name_idx => {FIELDS => ['login_name'], TYPE => 'UNIQUE'}, + profiles_extern_id_idx => {FIELDS => ['extern_id'], TYPE => 'UNIQUE'} + ], + }, + + profile_search => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_list => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + list_order => {TYPE => 'MEDIUMTEXT'}, + ], + INDEXES => [profile_search_user_id_idx => [qw(user_id)],], + }, + + profiles_activity => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, + fieldid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'} + }, + oldvalue => {TYPE => 'TINYTEXT'}, + newvalue => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [ + profiles_activity_userid_idx => ['userid'], + profiles_activity_profiles_when_idx => ['profiles_when'], + profiles_activity_fieldid_idx => ['fieldid'], + ], + }, + + email_setting => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + relationship => {TYPE => 'INT1', NOTNULL => 1}, + event => {TYPE => 'INT1', NOTNULL => 1}, + ], + INDEXES => [ + email_setting_user_id_idx => + {FIELDS => [qw(user_id relationship event)], TYPE => 'UNIQUE'}, + ], + }, + + email_bug_ignore => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + email_bug_ignore_user_id_idx => + {FIELDS => [qw(user_id bug_id)], TYPE => 'UNIQUE'}, + ], + }, + + watch => { + FIELDS => [ + watcher => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + watched => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + watch_watcher_idx => {FIELDS => [qw(watcher watched)], TYPE => 'UNIQUE'}, + watch_watched_idx => ['watched'], + ], + }, + + namedqueries => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + query => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => + [namedqueries_userid_idx => {FIELDS => [qw(userid name)], TYPE => 'UNIQUE'},], + }, + + namedqueries_link_in_footer => { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + namedqueries_link_in_footer_id_idx => + {FIELDS => [qw(namedquery_id user_id)], TYPE => 'UNIQUE'}, + namedqueries_link_in_footer_userid_idx => ['user_id'], + ], + }, + + tag => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => + [tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},], + }, + + bug_tag => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + tag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tag', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => + [bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'},], + }, + + reports => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + query => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => + [reports_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},], + }, + + component_cc => { + + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + component_cc_user_id_idx => + {FIELDS => [qw(component_id user_id)], TYPE => 'UNIQUE'}, + ], + }, + + # Authentication + # -------------- + + logincookies => { + FIELDS => [ + cookie => {TYPE => 'varchar(16)', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ipaddr => {TYPE => 'varchar(40)'}, + lastused => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [logincookies_lastused_idx => ['lastused'],], + }, + + 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. + tokens => { + FIELDS => [ + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + issuedate => {TYPE => 'DATETIME', NOTNULL => 1}, + token => {TYPE => 'varchar(16)', NOTNULL => 1, PRIMARYKEY => 1}, + tokentype => {TYPE => 'varchar(16)', NOTNULL => 1}, + eventdata => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [tokens_userid_idx => ['userid'],], + }, + + # GROUPS + # ------ + + groups => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(255)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, + userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + icon_url => {TYPE => 'TINYTEXT'}, ], + INDEXES => [groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + group_control_map => { + FIELDS => [ + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + entry => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + membercontrol => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}, + othercontrol => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}, + canedit => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + canconfirm => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + group_control_map_product_id_idx => + {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'}, + group_control_map_group_id_idx => ['group_id'], + ], + }, + + # "user_group_map" determines the groups that a user belongs to + # directly or due to regexp and which groups can be blessed by a user. + # + # grant_type: + # if GRANT_DIRECT - record was explicitly granted + # if GRANT_DERIVED - record was derived from expanding a group hierarchy + # if GRANT_REGEXP - record was created by evaluating a regexp + user_group_map => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + isbless => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GRANT_DIRECT}, + ], + INDEXES => [ + user_group_map_user_id_idx => + {FIELDS => [qw(user_id group_id grant_type isbless)], TYPE => 'UNIQUE'}, + ], + }, + + # This table determines which groups are made a member of another + # group, given the ability to bless another group, or given + # visibility to another groups existence and membership + # grant_type: + # if GROUP_MEMBERSHIP - member groups are made members of grantor + # if GROUP_BLESS - member groups may grant membership in grantor + # if GROUP_VISIBLE - member groups may see grantor group + group_group_map => { + FIELDS => [ + member_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + grantor_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GROUP_MEMBERSHIP}, + ], + INDEXES => [ + group_group_map_member_id_idx => + {FIELDS => [qw(member_id grantor_id grant_type)], TYPE => 'UNIQUE'}, + ], + }, + + # This table determines which groups a user must be a member of + # in order to see a bug. + bug_group_map => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bug_group_map_bug_id_idx => {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'}, + bug_group_map_group_id_idx => ['group_id'], + ], + }, + + # This table determines which groups a user must be a member of + # in order to see a named query somebody else shares. + namedquery_group_map => { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + namedquery_group_map_namedquery_id_idx => + {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'}, + namedquery_group_map_group_id_idx => ['group_id'], + ], + }, + + category_group_map => { + FIELDS => [ + category_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + category_group_map_category_id_idx => + {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'}, + ], + }, + + + # PRODUCTS + # -------- + + classifications => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + ], + INDEXES => + [classifications_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + products => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + classification_id => { + TYPE => 'INT2', + NOTNULL => 1, + DEFAULT => '1', + REFERENCES => {TABLE => 'classifications', COLUMN => 'id', DELETE => 'CASCADE'} + }, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 1}, + defaultmilestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, + allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [products_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + components => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + initialowner => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + initialqacontact => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + components_product_id_idx => + {FIELDS => [qw(product_id name)], TYPE => 'UNIQUE'}, + components_name_idx => ['name'], + ], + }, + + # CHARTS + # ------ + + series => { + FIELDS => [ + series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + creator => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + category => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + subcategory => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + frequency => {TYPE => 'INT2', NOTNULL => 1}, + query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + is_public => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + series_creator_idx => ['creator'], + series_category_idx => + {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}, + ], + }, + + series_data => { + FIELDS => [ + series_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'series', COLUMN => 'series_id', DELETE => 'CASCADE'} + }, + series_date => {TYPE => 'DATETIME', NOTNULL => 1}, + series_value => {TYPE => 'INT3', NOTNULL => 1}, + ], + INDEXES => [ + series_data_series_id_idx => + {FIELDS => [qw(series_id series_date)], TYPE => 'UNIQUE'}, + ], + }, + + series_categories => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => + [series_categories_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + # WHINE SYSTEM + # ------------ + + whine_queries => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + eventid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'} + }, + query_name => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + title => {TYPE => 'varchar(128)', NOTNULL => 1, DEFAULT => "''"}, + ], + INDEXES => [whine_queries_eventid_idx => ['eventid'],], + }, + + whine_schedules => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + eventid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'} + }, + run_day => {TYPE => 'varchar(32)'}, + run_time => {TYPE => 'varchar(32)'}, + run_next => {TYPE => 'DATETIME'}, + mailto => {TYPE => 'INT3', NOTNULL => 1}, + mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + ], + INDEXES => [ + whine_schedules_run_next_idx => ['run_next'], + whine_schedules_eventid_idx => ['eventid'], + ], + }, + + whine_events => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + owner_userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + subject => {TYPE => 'varchar(128)'}, + body => {TYPE => 'MEDIUMTEXT'}, + mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + }, + + # QUIPS + # ----- + + quips => { + FIELDS => [ + quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + quip => {TYPE => 'varchar(512)', NOTNULL => 1}, + approved => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + }, + + # SETTINGS + # -------- + # setting - each global setting will have exactly one entry + # in this table. + # setting_value - stores the list of acceptable values for each + # setting, and a sort index that controls the order + # in which the values are displayed. + # profile_setting - If a user has chosen to use a value other than the + # global default for a given setting, it will be + # stored in this table. Note: even if a setting is + # later changed so is_enabled = false, the stored + # value will remain in case it is ever enabled again. + # + setting => { + FIELDS => [ + name => {TYPE => 'varchar(32)', NOTNULL => 1, PRIMARYKEY => 1}, + default_value => {TYPE => 'varchar(32)', NOTNULL => 1}, + is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + subclass => {TYPE => 'varchar(32)'}, + ], + }, + + setting_value => { + FIELDS => [ + name => { + TYPE => 'varchar(32)', + NOTNULL => 1, + REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(32)', NOTNULL => 1}, + sortindex => {TYPE => 'INT2', NOTNULL => 1}, + ], + INDEXES => [ + setting_value_nv_unique_idx => {FIELDS => [qw(name value)], TYPE => 'UNIQUE'}, + setting_value_ns_unique_idx => + {FIELDS => [qw(name sortindex)], TYPE => 'UNIQUE'}, + ], + }, + + profile_setting => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + setting_name => { + TYPE => 'varchar(32)', + NOTNULL => 1, + REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'} + }, + setting_value => {TYPE => 'varchar(32)', NOTNULL => 1}, + ], + INDEXES => [ + profile_setting_value_unique_idx => + {FIELDS => [qw(user_id setting_name)], TYPE => 'UNIQUE'}, + ], + }, + + # BUGMAIL + # ------- + + mail_staging => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + message => {TYPE => 'LONGBLOB', NOTNULL => 1}, + ], + }, + + # THESCHWARTZ TABLES + # ------------------ + # Note: In the standard TheSchwartz schema, most integers are unsigned, + # but we didn't implement unsigned ints for Bugzilla schemas, so we + # just create signed ints, which should be fine. + + ts_funcmap => { + FIELDS => [ + funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcname => {TYPE => 'varchar(255)', NOTNULL => 1}, + ], + INDEXES => + [ts_funcmap_funcname_idx => {FIELDS => ['funcname'], TYPE => 'UNIQUE'},], + }, + + ts_job => { + FIELDS => [ + + # In a standard TheSchwartz schema, this is a BIGINT, but we + # don't have those and I didn't want to add them just for this. + jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1}, + + # In standard TheSchwartz, this is a MEDIUMBLOB. + arg => {TYPE => 'LONGBLOB'}, + uniqkey => {TYPE => 'varchar(255)'}, + insert_time => {TYPE => 'INT4'}, + run_after => {TYPE => 'INT4', NOTNULL => 1}, + grabbed_until => {TYPE => 'INT4', NOTNULL => 1}, + priority => {TYPE => 'INT2'}, + coalesce => {TYPE => 'varchar(255)'}, + ], + INDEXES => [ + ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], TYPE => 'UNIQUE'}, + + # In a standard TheSchewartz schema, these both go in the other + # direction, but there's no reason to have three indexes that + # all start with the same column, and our naming scheme doesn't + # allow it anyhow. + ts_job_run_after_idx => [qw(run_after funcid)], + ts_job_coalesce_idx => [qw(coalesce funcid)], + ], + }, + + ts_note => { + FIELDS => [ + + # This is a BIGINT in standard TheSchwartz schemas. + jobid => {TYPE => 'INT4', NOTNULL => 1}, + notekey => {TYPE => 'varchar(255)'}, + value => {TYPE => 'LONGBLOB'}, + ], + INDEXES => + [ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], TYPE => 'UNIQUE'},], + }, + + ts_error => { + FIELDS => [ + error_time => {TYPE => 'INT4', NOTNULL => 1}, + jobid => {TYPE => 'INT4', NOTNULL => 1}, + message => {TYPE => 'varchar(255)', NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + ], + INDEXES => [ + ts_error_funcid_idx => [qw(funcid error_time)], + ts_error_error_time_idx => ['error_time'], + ts_error_jobid_idx => ['jobid'], + ], + }, + + ts_exitstatus => { + FIELDS => [ + jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + status => {TYPE => 'INT2'}, + completion_time => {TYPE => 'INT4'}, + delete_after => {TYPE => 'INT4'}, + ], + INDEXES => [ + ts_exitstatus_funcid_idx => ['funcid'], + ts_exitstatus_delete_after_idx => ['delete_after'], + ], + }, + + # SCHEMA STORAGE + # -------------- + + bz_schema => { + FIELDS => [ + schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1}, + version => {TYPE => 'decimal(3,2)', NOTNULL => 1}, + ], + }, + + bug_user_last_visit => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [ + bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], TYPE => 'UNIQUE'}, + bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], + ], + }, + + user_api_keys => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1}, + description => {TYPE => 'VARCHAR(255)'}, + revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + last_used => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, + user_api_keys_user_id_idx => ['user_id'], + ], + }, +}; + +# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables +use constant MULTI_SELECT_VALUE_TABLE => { + FIELDS => [ + bug_id => {TYPE => 'INT3', NOTNULL => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => [bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},], }; #-------------------------------------------------------------------------- @@ -1821,27 +1770,28 @@ sub new { =cut - my $this = shift; - my $class = ref($this) || $this; - my $driver = shift; + my $this = shift; + my $class = ref($this) || $this; + my $driver = shift; - if ($driver) { - (my $subclass = $driver) =~ s/^(\S)/\U$1/; - $class .= '::' . $subclass; - eval "require $class;"; - die "The $class class could not be found ($subclass " . - "not supported?): $@" if ($@); - } - die "$class is an abstract base class. Instantiate a subclass instead." - if ($class eq __PACKAGE__); + if ($driver) { + (my $subclass = $driver) =~ s/^(\S)/\U$1/; + $class .= '::' . $subclass; + eval "require $class;"; + die "The $class class could not be found ($subclass " . "not supported?): $@" + if ($@); + } + die "$class is an abstract base class. Instantiate a subclass instead." + if ($class eq __PACKAGE__); + + my $self = {}; + bless $self, $class; + $self = $self->_initialize(@_); - my $self = {}; - bless $self, $class; - $self = $self->_initialize(@_); + return ($self); - return($self); +} #eosub--new -} #eosub--new #-------------------------------------------------------------------------- sub _initialize { @@ -1864,33 +1814,34 @@ sub _initialize { =cut - my $self = shift; - my $abstract_schema = shift; + my $self = shift; + my $abstract_schema = shift; - if (!$abstract_schema) { - # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be. - # So, we dclone it to prevent anything from mucking with the constant. - $abstract_schema = dclone(ABSTRACT_SCHEMA); + if (!$abstract_schema) { - # Let extensions add tables, but make sure they can't modify existing - # tables. If we don't lock/unlock keys, lock_value complains. - lock_keys(%$abstract_schema); - foreach my $table (keys %{ABSTRACT_SCHEMA()}) { - lock_value(%$abstract_schema, $table) - if exists $abstract_schema->{$table}; - } - unlock_keys(%$abstract_schema); - Bugzilla::Hook::process('db_schema_abstract_schema', - { schema => $abstract_schema }); - unlock_hash(%$abstract_schema); + # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be. + # So, we dclone it to prevent anything from mucking with the constant. + $abstract_schema = dclone(ABSTRACT_SCHEMA); + + # Let extensions add tables, but make sure they can't modify existing + # tables. If we don't lock/unlock keys, lock_value complains. + lock_keys(%$abstract_schema); + foreach my $table (keys %{ABSTRACT_SCHEMA()}) { + lock_value(%$abstract_schema, $table) if exists $abstract_schema->{$table}; } + unlock_keys(%$abstract_schema); + Bugzilla::Hook::process('db_schema_abstract_schema', + {schema => $abstract_schema}); + unlock_hash(%$abstract_schema); + } + + $self->{schema} = dclone($abstract_schema); + $self->{abstract_schema} = $abstract_schema; - $self->{schema} = dclone($abstract_schema); - $self->{abstract_schema} = $abstract_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------------- sub _adjust_schema { @@ -1906,36 +1857,41 @@ sub _adjust_schema { =cut - my $self = shift; - - # The _initialize method has already set up the db_specific hash with - # the information on how to implement the abstract data types for the - # instantiated DBMS-specific subclass. - my $db_specific = $self->{db_specific}; - - # Loop over each table in the abstract database schema. - foreach my $table (keys %{ $self->{schema} }) { - my %fields = (@{ $self->{schema}{$table}{FIELDS} }); - # Loop over the field definitions in each table. - foreach my $field_def (values %fields) { - # If the field type is an abstract data type defined in the - # $db_specific hash, replace it with the DBMS-specific data type - # that implements it. - if (exists($db_specific->{$field_def->{TYPE}})) { - $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}}; - } - # Replace abstract default values (such as 'TRUE' and 'FALSE') - # with their database-specific implementations. - if (exists($field_def->{DEFAULT}) - && exists($db_specific->{$field_def->{DEFAULT}})) { - $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}}; - } - } + my $self = shift; + + # The _initialize method has already set up the db_specific hash with + # the information on how to implement the abstract data types for the + # instantiated DBMS-specific subclass. + my $db_specific = $self->{db_specific}; + + # Loop over each table in the abstract database schema. + foreach my $table (keys %{$self->{schema}}) { + my %fields = (@{$self->{schema}{$table}{FIELDS}}); + + # Loop over the field definitions in each table. + foreach my $field_def (values %fields) { + + # If the field type is an abstract data type defined in the + # $db_specific hash, replace it with the DBMS-specific data type + # that implements it. + if (exists($db_specific->{$field_def->{TYPE}})) { + $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}}; + } + + # Replace abstract default values (such as 'TRUE' and 'FALSE') + # with their database-specific implementations. + if ( exists($field_def->{DEFAULT}) + && exists($db_specific->{$field_def->{DEFAULT}})) + { + $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}}; + } } + } + + return $self; - return $self; +} #eosub--_adjust_schema -} #eosub--_adjust_schema #-------------------------------------------------------------------------- sub get_type_ddl { @@ -1969,30 +1925,34 @@ C<ALTER TABLE> SQL statement =cut - my $self = shift; - my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : { @_ }; - my $type = $finfo->{TYPE}; - confess "A valid TYPE was not specified for this column (got " - . Dumper($finfo) . ")" unless ($type); - - my $default = $finfo->{DEFAULT}; - # Replace any abstract default value (such as 'TRUE' or 'FALSE') - # with its database-specific implementation. - if ( defined $default && exists($self->{db_specific}->{$default}) ) { - $default = $self->{db_specific}->{$default}; - } + my $self = shift; + my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : {@_}; + my $type = $finfo->{TYPE}; + confess "A valid TYPE was not specified for this column (got " + . Dumper($finfo) . ")" + unless ($type); + + my $default = $finfo->{DEFAULT}; + + # Replace any abstract default value (such as 'TRUE' or 'FALSE') + # with its database-specific implementation. + if (defined $default && exists($self->{db_specific}->{$default})) { + $default = $self->{db_specific}->{$default}; + } + + my $type_ddl = $self->convert_type($type); + + # DEFAULT attribute must appear before any column constraints + # (e.g., NOT NULL), for Oracle + $type_ddl .= " DEFAULT $default" if (defined($default)); - my $type_ddl = $self->convert_type($type); - # DEFAULT attribute must appear before any column constraints - # (e.g., NOT NULL), for Oracle - $type_ddl .= " DEFAULT $default" if (defined($default)); - # PRIMARY KEY must appear before NOT NULL for SQLite. - $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); - $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); + # PRIMARY KEY must appear before NOT NULL for SQLite. + $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); + $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); - return($type_ddl); + return ($type_ddl); -} #eosub--get_type_ddl +} #eosub--get_type_ddl sub get_fk_ddl { @@ -2026,78 +1986,80 @@ is undefined. =cut - my ($self, $table, $column, $references) = @_; - return "" if !$references; + my ($self, $table, $column, $references) = @_; + return "" if !$references; - my $update = $references->{UPDATE} || 'CASCADE'; - my $delete = $references->{DELETE} || 'RESTRICT'; - my $to_table = $references->{TABLE} || confess "No table in reference"; - my $to_column = $references->{COLUMN} || confess "No column in reference"; - my $fk_name = $self->_get_fk_name($table, $column, $references); + my $update = $references->{UPDATE} || 'CASCADE'; + my $delete = $references->{DELETE} || 'RESTRICT'; + my $to_table = $references->{TABLE} || confess "No table in reference"; + my $to_column = $references->{COLUMN} || confess "No column in reference"; + my $fk_name = $self->_get_fk_name($table, $column, $references); - return "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n" - . " REFERENCES $to_table($to_column)\n" - . " ON UPDATE $update ON DELETE $delete"; + return + "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n" + . " REFERENCES $to_table($to_column)\n" + . " ON UPDATE $update ON DELETE $delete"; } # Generates a name for a Foreign Key. It's separate from get_fk_ddl # so that certain databases can override it (for shorter identifiers or # other reasons). sub _get_fk_name { - my ($self, $table, $column, $references) = @_; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $name = "fk_${table}_${column}_${to_table}_${to_column}"; + my ($self, $table, $column, $references) = @_; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $name = "fk_${table}_${column}_${to_table}_${to_column}"; - if (length($name) > $self->MAX_IDENTIFIER_LEN) { - $name = 'fk_' . $self->_hash_identifier($name); - } + if (length($name) > $self->MAX_IDENTIFIER_LEN) { + $name = 'fk_' . $self->_hash_identifier($name); + } - return $name; + return $name; } sub _hash_identifier { - my ($invocant, $value) = @_; - # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something - # longer in the future. - return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7); + my ($invocant, $value) = @_; + + # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something + # longer in the future. + return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7); } sub get_add_fks_sql { - my ($self, $table, $column_fks) = @_; - - my @add = $self->_column_fks_to_ddl($table, $column_fks); - - my @sql; - if ($self->MULTIPLE_FKS_IN_ALTER) { - my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add); - push(@sql, $alter); + my ($self, $table, $column_fks) = @_; + + my @add = $self->_column_fks_to_ddl($table, $column_fks); + + my @sql; + if ($self->MULTIPLE_FKS_IN_ALTER) { + my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add); + push(@sql, $alter); + } + else { + foreach my $fk_string (@add) { + push(@sql, "ALTER TABLE $table ADD $fk_string"); } - else { - foreach my $fk_string (@add) { - push(@sql, "ALTER TABLE $table ADD $fk_string"); - } - } - return @sql; + } + return @sql; } sub _column_fks_to_ddl { - my ($self, $table, $column_fks) = @_; - my @ddl; - foreach my $column (keys %$column_fks) { - my $def = $column_fks->{$column}; - my $fk_string = $self->get_fk_ddl($table, $column, $def); - push(@ddl, $fk_string); - } - return @ddl; + my ($self, $table, $column_fks) = @_; + my @ddl; + foreach my $column (keys %$column_fks) { + my $def = $column_fks->{$column}; + my $fk_string = $self->get_fk_ddl($table, $column, $def); + push(@ddl, $fk_string); + } + return @ddl; } -sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name($table, $column, $references); +sub get_drop_fk_sql { + my ($self, $table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name($table, $column, $references); - return ("ALTER TABLE $table DROP CONSTRAINT $fk_name"); + return ("ALTER TABLE $table DROP CONSTRAINT $fk_name"); } sub convert_type { @@ -2108,8 +2070,8 @@ Converts a TYPE from the L</ABSTRACT_SCHEMA> format into the real SQL type. =cut - my ($self, $type) = @_; - return $self->{db_specific}->{$type} || $type; + my ($self, $type) = @_; + return $self->{db_specific}->{$type} || $type; } sub get_column { @@ -2126,16 +2088,16 @@ sub get_column { =cut - my($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - if (exists $self->{schema}->{$table}) { - my %fields = (@{ $self->{schema}{$table}{FIELDS} }); - return $fields{$column}; - } - return undef; -} #eosub--get_column + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + if (exists $self->{schema}->{$table}) { + my %fields = (@{$self->{schema}{$table}{FIELDS}}); + return $fields{$column}; + } + return undef; +} #eosub--get_column sub get_table_list { @@ -2150,8 +2112,8 @@ sub get_table_list { =cut - my $self = shift; - return sort keys %{$self->{schema}}; + my $self = shift; + return sort keys %{$self->{schema}}; } sub get_table_columns { @@ -2165,34 +2127,33 @@ sub get_table_columns { =cut - my($self, $table) = @_; - my @ddl = (); + my ($self, $table) = @_; + my @ddl = (); - my $thash = $self->{schema}{$table}; - die "Table $table does not exist in the database schema." - unless (ref($thash)); + my $thash = $self->{schema}{$table}; + die "Table $table does not exist in the database schema." unless (ref($thash)); - my @columns = (); - my @fields = @{ $thash->{FIELDS} }; - while (@fields) { - push(@columns, shift(@fields)); - shift(@fields); - } + my @columns = (); + my @fields = @{$thash->{FIELDS}}; + while (@fields) { + push(@columns, shift(@fields)); + shift(@fields); + } - return @columns; + return @columns; -} #eosub--get_table_columns +} #eosub--get_table_columns sub get_table_indexes_abstract { - my ($self, $table) = @_; - my $table_def = $self->get_table_abstract($table); - my %indexes = @{$table_def->{INDEXES} || []}; - return \%indexes; + my ($self, $table) = @_; + my $table_def = $self->get_table_abstract($table); + my %indexes = @{$table_def->{INDEXES} || []}; + return \%indexes; } sub get_create_database_sql { - my ($self, $name) = @_; - return ("CREATE DATABASE $name"); + my ($self, $name) = @_; + return ("CREATE DATABASE $name"); } sub get_table_ddl { @@ -2209,30 +2170,29 @@ sub get_table_ddl { =cut - my($self, $table) = @_; - my @ddl = (); + my ($self, $table) = @_; + my @ddl = (); - die "Table $table does not exist in the database schema." - unless (ref($self->{schema}{$table})); + die "Table $table does not exist in the database schema." + unless (ref($self->{schema}{$table})); - my $create_table = $self->_get_create_table_ddl($table); - push(@ddl, $create_table) if $create_table; + my $create_table = $self->_get_create_table_ddl($table); + push(@ddl, $create_table) if $create_table; - my @indexes = @{ $self->{schema}{$table}{INDEXES} || [] }; - while (@indexes) { - my $index_name = shift(@indexes); - my $index_info = shift(@indexes); - my $index_sql = $self->get_add_index_ddl($table, $index_name, - $index_info); - push(@ddl, $index_sql) if $index_sql; - } + my @indexes = @{$self->{schema}{$table}{INDEXES} || []}; + while (@indexes) { + my $index_name = shift(@indexes); + my $index_info = shift(@indexes); + my $index_sql = $self->get_add_index_ddl($table, $index_name, $index_info); + push(@ddl, $index_sql) if $index_sql; + } - push(@ddl, @{ $self->{schema}{$table}{DB_EXTRAS} }) - if (ref($self->{schema}{$table}{DB_EXTRAS})); + push(@ddl, @{$self->{schema}{$table}{DB_EXTRAS}}) + if (ref($self->{schema}{$table}{DB_EXTRAS})); - return @ddl; + return @ddl; -} #eosub--get_table_ddl +} #eosub--get_table_ddl sub _get_create_table_ddl { @@ -2245,30 +2205,29 @@ sub _get_create_table_ddl { =cut - my($self, $table) = @_; - - my $thash = $self->{schema}{$table}; - die "Table $table does not exist in the database schema." - unless ref $thash; - - my (@col_lines, @fk_lines); - my @fields = @{ $thash->{FIELDS} }; - while (@fields) { - my $field = shift(@fields); - my $finfo = shift(@fields); - push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo)); - if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) { - my $fk = $finfo->{REFERENCES}; - my $fk_ddl = $self->get_fk_ddl($table, $field, $fk); - push(@fk_lines, $fk_ddl); - } + my ($self, $table) = @_; + + my $thash = $self->{schema}{$table}; + die "Table $table does not exist in the database schema." unless ref $thash; + + my (@col_lines, @fk_lines); + my @fields = @{$thash->{FIELDS}}; + while (@fields) { + my $field = shift(@fields); + my $finfo = shift(@fields); + push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo)); + if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) { + my $fk = $finfo->{REFERENCES}; + my $fk_ddl = $self->get_fk_ddl($table, $field, $fk); + push(@fk_lines, $fk_ddl); } - - my $sql = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) - . "\n)"; - return $sql + } -} + my $sql + = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) . "\n)"; + return $sql; + +} sub _get_create_index_ddl { @@ -2284,16 +2243,17 @@ sub _get_create_index_ddl { =cut - my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + + my $sql = "CREATE "; + $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE'); + $sql + .= "INDEX $index_name ON $table_name \(" . join(", ", @$index_fields) . "\)"; - my $sql = "CREATE "; - $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE'); - $sql .= "INDEX $index_name ON $table_name \(" . - join(", ", @$index_fields) . "\)"; + return ($sql); - return($sql); +} #eosub--_get_create_index_ddl -} #eosub--_get_create_index_ddl #-------------------------------------------------------------------------- sub get_add_column_ddl { @@ -2312,22 +2272,25 @@ sub get_add_column_ddl { =cut - my ($self, $table, $column, $definition, $init_value) = @_; - my @statements; - push(@statements, "ALTER TABLE $table ". $self->ADD_COLUMN ." $column " . - $self->get_type_ddl($definition)); - - # XXX - Note that although this works for MySQL, most databases will fail - # before this point, if we haven't set a default. - (push(@statements, "UPDATE $table SET $column = $init_value")) - if defined $init_value; - - if (defined $definition->{REFERENCES}) { - push(@statements, $self->get_add_fks_sql($table, { $column => - $definition->{REFERENCES} })); - } - - return (@statements); + my ($self, $table, $column, $definition, $init_value) = @_; + my @statements; + push(@statements, + "ALTER TABLE $table " + . $self->ADD_COLUMN + . " $column " + . $self->get_type_ddl($definition)); + + # XXX - Note that although this works for MySQL, most databases will fail + # before this point, if we haven't set a default. + (push(@statements, "UPDATE $table SET $column = $init_value")) + if defined $init_value; + + if (defined $definition->{REFERENCES}) { + push(@statements, + $self->get_add_fks_sql($table, {$column => $definition->{REFERENCES}})); + } + + return (@statements); } sub get_add_index_ddl { @@ -2348,20 +2311,21 @@ sub get_add_index_ddl { =cut - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - my ($index_fields, $index_type); - # Index defs can be arrays or hashes - if (ref($definition) eq 'HASH') { - $index_fields = $definition->{FIELDS}; - $index_type = $definition->{TYPE}; - } else { - $index_fields = $definition; - $index_type = ''; - } - - return $self->_get_create_index_ddl($table, $name, $index_fields, - $index_type); + my ($index_fields, $index_type); + + # Index defs can be arrays or hashes + if (ref($definition) eq 'HASH') { + $index_fields = $definition->{FIELDS}; + $index_type = $definition->{TYPE}; + } + else { + $index_fields = $definition; + $index_type = ''; + } + + return $self->_get_create_index_ddl($table, $name, $index_fields, $index_type); } sub get_alter_column_ddl { @@ -2384,85 +2348,88 @@ sub get_alter_column_ddl { =cut - my $self = shift; - my ($table, $column, $new_def, $set_nulls_to) = @_; - - my @statements; - my $old_def = $self->get_column_abstract($table, $column); - my $specific = $self->{db_specific}; - - # If the types have changed, we have to deal with that. - if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { - push(@statements, $self->_get_alter_type_sql($table, $column, - $new_def, $old_def)); - } - - my $default = $new_def->{DEFAULT}; - my $default_old = $old_def->{DEFAULT}; - - if (defined $default) { - $default = $specific->{$default} if exists $specific->{$default}; - } - # This first condition prevents "uninitialized value" errors. - if (!defined $default && !defined $default_old) { - # Do Nothing - } - # If we went from having a default to not having one - elsif (!defined $default && defined $default_old) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " DROP DEFAULT"); - } - # If we went from no default to a default, or we changed the default. - elsif ( (defined $default && !defined $default_old) || - ($default ne $default_old) ) - { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column " - . " SET DEFAULT $default"); - } - - # If we went from NULL to NOT NULL. - if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { - push(@statements, $self->_set_nulls_sql(@_)); - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " SET NOT NULL"); - } - # If we went from NOT NULL to NULL - elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " DROP NOT NULL"); - } - - # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. - if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); - } - # If we went from being a PK to not being a PK - elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) { - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); - } - - return @statements; + my $self = shift; + my ($table, $column, $new_def, $set_nulls_to) = @_; + + my @statements; + my $old_def = $self->get_column_abstract($table, $column); + my $specific = $self->{db_specific}; + + # If the types have changed, we have to deal with that. + if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { + push(@statements, + $self->_get_alter_type_sql($table, $column, $new_def, $old_def)); + } + + my $default = $new_def->{DEFAULT}; + my $default_old = $old_def->{DEFAULT}; + + if (defined $default) { + $default = $specific->{$default} if exists $specific->{$default}; + } + + # This first condition prevents "uninitialized value" errors. + if (!defined $default && !defined $default_old) { + + # Do Nothing + } + + # If we went from having a default to not having one + elsif (!defined $default && defined $default_old) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP DEFAULT"); + } + + # If we went from no default to a default, or we changed the default. + elsif ((defined $default && !defined $default_old) + || ($default ne $default_old)) + { + push(@statements, + "ALTER TABLE $table ALTER COLUMN $column " . " SET DEFAULT $default"); + } + + # If we went from NULL to NOT NULL. + if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { + push(@statements, $self->_set_nulls_sql(@_)); + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " SET NOT NULL"); + } + + # If we went from NOT NULL to NULL + elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP NOT NULL"); + } + + # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. + if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + } + + # If we went from being a PK to not being a PK + elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } # Helps handle any fields that were NULL before, if we have a default, # when doing an ALTER COLUMN. sub _set_nulls_sql { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - my $default = $new_def->{DEFAULT}; - # If we have a set_nulls_to, that overrides the DEFAULT - # (although nobody would usually specify both a default and - # a set_nulls_to.) - $default = $set_nulls_to if defined $set_nulls_to; - if (defined $default) { - my $specific = $self->{db_specific}; - $default = $specific->{$default} if exists $specific->{$default}; - } - my @sql; - if (defined $default) { - push(@sql, "UPDATE $table SET $column = $default" - . " WHERE $column IS NULL"); - } - return @sql; + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + my $default = $new_def->{DEFAULT}; + + # If we have a set_nulls_to, that overrides the DEFAULT + # (although nobody would usually specify both a default and + # a set_nulls_to.) + $default = $set_nulls_to if defined $set_nulls_to; + if (defined $default) { + my $specific = $self->{db_specific}; + $default = $specific->{$default} if exists $specific->{$default}; + } + my @sql; + if (defined $default) { + push(@sql, "UPDATE $table SET $column = $default" . " WHERE $column IS NULL"); + } + return @sql; } sub get_drop_index_ddl { @@ -2476,11 +2443,11 @@ sub get_drop_index_ddl { =cut - my ($self, $table, $name) = @_; + my ($self, $table, $name) = @_; - # Although ANSI SQL-92 doesn't specify a method of dropping an index, - # many DBs support this syntax. - return ("DROP INDEX $name"); + # Although ANSI SQL-92 doesn't specify a method of dropping an index, + # many DBs support this syntax. + return ("DROP INDEX $name"); } sub get_drop_column_ddl { @@ -2494,8 +2461,8 @@ sub get_drop_column_ddl { =cut - my ($self, $table, $column) = @_; - return ("ALTER TABLE $table DROP COLUMN $column"); + my ($self, $table, $column) = @_; + return ("ALTER TABLE $table DROP COLUMN $column"); } =item C<get_drop_table_ddl($table)> @@ -2507,8 +2474,8 @@ sub get_drop_column_ddl { =cut sub get_drop_table_ddl { - my ($self, $table) = @_; - return ("DROP TABLE $table"); + my ($self, $table) = @_; + return ("DROP TABLE $table"); } sub get_rename_column_ddl { @@ -2526,8 +2493,8 @@ sub get_rename_column_ddl { =cut - die "ANSI SQL has no way to rename a column, and your database driver\n" - . " has not implemented a method."; + die "ANSI SQL has no way to rename a column, and your database driver\n" + . " has not implemented a method."; } @@ -2557,8 +2524,8 @@ Gets SQL to rename a table in the database. =cut - my ($self, $old_name, $new_name) = @_; - return ("ALTER TABLE $old_name RENAME TO $new_name"); + my ($self, $old_name, $new_name) = @_; + return ("ALTER TABLE $old_name RENAME TO $new_name"); } =item C<delete_table($name)> @@ -2571,13 +2538,13 @@ Gets SQL to rename a table in the database. =cut sub delete_table { - my ($self, $name) = @_; + my ($self, $name) = @_; - die "Attempted to delete nonexistent table '$name'." unless - $self->get_table_abstract($name); + die "Attempted to delete nonexistent table '$name'." + unless $self->get_table_abstract($name); - delete $self->{abstract_schema}->{$name}; - delete $self->{schema}->{$name}; + delete $self->{abstract_schema}->{$name}; + delete $self->{schema}->{$name}; } sub get_column_abstract { @@ -2594,15 +2561,15 @@ sub get_column_abstract { =cut - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - if ($self->get_table_abstract($table)) { - my %fields = (@{ $self->{abstract_schema}{$table}{FIELDS} }); - return $fields{$column}; - } - return undef; + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + if ($self->get_table_abstract($table)) { + my %fields = (@{$self->{abstract_schema}{$table}{FIELDS}}); + return $fields{$column}; + } + return undef; } =item C<get_indexes_on_column_abstract($table, $column)> @@ -2620,29 +2587,31 @@ sub get_column_abstract { =cut sub get_indexes_on_column_abstract { - my ($self, $table, $column) = @_; - my %ret_hash; - - my $table_def = $self->get_table_abstract($table); - if ($table_def && exists $table_def->{INDEXES}) { - my %indexes = (@{ $table_def->{INDEXES} }); - foreach my $index_name (keys %indexes) { - my $col_list; - # Get the column list, depending on whether the index - # is in hashref or arrayref format. - if (ref($indexes{$index_name}) eq 'HASH') { - $col_list = $indexes{$index_name}->{FIELDS}; - } else { - $col_list = $indexes{$index_name}; - } - - if(grep($_ eq $column, @$col_list)) { - $ret_hash{$index_name} = dclone($indexes{$index_name}); - } - } + my ($self, $table, $column) = @_; + my %ret_hash; + + my $table_def = $self->get_table_abstract($table); + if ($table_def && exists $table_def->{INDEXES}) { + my %indexes = (@{$table_def->{INDEXES}}); + foreach my $index_name (keys %indexes) { + my $col_list; + + # Get the column list, depending on whether the index + # is in hashref or arrayref format. + if (ref($indexes{$index_name}) eq 'HASH') { + $col_list = $indexes{$index_name}->{FIELDS}; + } + else { + $col_list = $indexes{$index_name}; + } + + if (grep($_ eq $column, @$col_list)) { + $ret_hash{$index_name} = dclone($indexes{$index_name}); + } } + } - return %ret_hash; + return %ret_hash; } sub get_index_abstract { @@ -2658,16 +2627,16 @@ sub get_index_abstract { =cut - my ($self, $table, $index) = @_; + my ($self, $table, $index) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - my $index_table = $self->get_table_abstract($table); - if ($index_table && exists $index_table->{INDEXES}) { - my %indexes = (@{ $index_table->{INDEXES} }); - return $indexes{$index}; - } - return undef; + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + my $index_table = $self->get_table_abstract($table); + if ($index_table && exists $index_table->{INDEXES}) { + my %indexes = (@{$index_table->{INDEXES}}); + return $indexes{$index}; + } + return undef; } =item C<get_table_abstract($table)> @@ -2681,8 +2650,8 @@ sub get_index_abstract { =cut sub get_table_abstract { - my ($self, $table) = @_; - return $self->{abstract_schema}->{$table}; + my ($self, $table) = @_; + return $self->{abstract_schema}->{$table}; } =item C<add_table($name, \%definition)> @@ -2698,22 +2667,20 @@ sub get_table_abstract { =cut sub add_table { - my ($self, $name, $definition) = @_; - (die "Table already exists: $name") - if exists $self->{abstract_schema}->{$name}; - if ($definition) { - $self->{abstract_schema}->{$name} = dclone($definition); - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); - } - else { - $self->{abstract_schema}->{$name} = {FIELDS => []}; - $self->{schema}->{$name} = {FIELDS => []}; - } + my ($self, $name, $definition) = @_; + (die "Table already exists: $name") if exists $self->{abstract_schema}->{$name}; + if ($definition) { + $self->{abstract_schema}->{$name} = dclone($definition); + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); + } + else { + $self->{abstract_schema}->{$name} = {FIELDS => []}; + $self->{schema}->{$name} = {FIELDS => []}; + } } - sub rename_table { =item C<rename_table> @@ -2723,10 +2690,10 @@ Renames a table from C<$old_name> to C<$new_name> in this Schema object. =cut - my ($self, $old_name, $new_name) = @_; - my $table = $self->get_table_abstract($old_name); - $self->delete_table($old_name); - $self->add_table($new_name, $table); + my ($self, $old_name, $new_name) = @_; + my $table = $self->get_table_abstract($old_name); + $self->delete_table($old_name); + $self->add_table($new_name, $table); } sub delete_column { @@ -2741,17 +2708,18 @@ sub delete_column { =cut - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS}; - my $name_position = firstidx { $_ eq $column } @$abstract_fields; - die "Attempted to delete nonexistent column ${table}.${column}" - if $name_position == -1; - # Delete the key/value pair from the array. - splice(@$abstract_fields, $name_position, 2); + my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS}; + my $name_position = firstidx { $_ eq $column } @$abstract_fields; + die "Attempted to delete nonexistent column ${table}.${column}" + if $name_position == -1; - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + # Delete the key/value pair from the array. + splice(@$abstract_fields, $name_position, 2); + + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } sub rename_column { @@ -2767,11 +2735,11 @@ sub rename_column { =cut - my ($self, $table, $old_name, $new_name) = @_; - my $def = $self->get_column_abstract($table, $old_name); - die "Renaming a column that doesn't exist" if !$def; - $self->delete_column($table, $old_name); - $self->set_column($table, $new_name, $def); + my ($self, $table, $old_name, $new_name) = @_; + my $def = $self->get_column_abstract($table, $old_name); + die "Renaming a column that doesn't exist" if !$def; + $self->delete_column($table, $old_name); + $self->set_column($table, $new_name, $def); } sub set_column { @@ -2792,10 +2760,10 @@ sub set_column { =cut - my ($self, $table, $column, $new_def) = @_; + my ($self, $table, $column, $new_def) = @_; - my $fields = $self->{abstract_schema}{$table}{FIELDS}; - $self->_set_object($table, $column, $new_def, $fields); + my $fields = $self->{abstract_schema}{$table}{FIELDS}; + $self->_set_object($table, $column, $new_def, $fields); } =item C<set_fk($table, $column \%fk_def)> @@ -2805,19 +2773,20 @@ Sets the C<REFERENCES> item on the specified column. =cut sub set_fk { - my ($self, $table, $column, $fk_def) = @_; - # Don't want to modify the source def before we explicitly set it below. - # This is just us being extra-cautious. - my $column_def = dclone($self->get_column_abstract($table, $column)); - die "Tried to set an fk on $table.$column, but that column doesn't exist" - if !$column_def; - if ($fk_def) { - $column_def->{REFERENCES} = $fk_def; - } - else { - delete $column_def->{REFERENCES}; - } - $self->set_column($table, $column, $column_def); + my ($self, $table, $column, $fk_def) = @_; + + # Don't want to modify the source def before we explicitly set it below. + # This is just us being extra-cautious. + my $column_def = dclone($self->get_column_abstract($table, $column)); + die "Tried to set an fk on $table.$column, but that column doesn't exist" + if !$column_def; + if ($fk_def) { + $column_def->{REFERENCES} = $fk_def; + } + else { + delete $column_def->{REFERENCES}; + } + $self->set_column($table, $column, $column_def); } sub set_index { @@ -2838,36 +2807,39 @@ sub set_index { =cut - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - if ( exists $self->{abstract_schema}{$table} - && !exists $self->{abstract_schema}{$table}{INDEXES} ) { - $self->{abstract_schema}{$table}{INDEXES} = []; - } + if (exists $self->{abstract_schema}{$table} + && !exists $self->{abstract_schema}{$table}{INDEXES}) + { + $self->{abstract_schema}{$table}{INDEXES} = []; + } - my $indexes = $self->{abstract_schema}{$table}{INDEXES}; - $self->_set_object($table, $name, $definition, $indexes); + my $indexes = $self->{abstract_schema}{$table}{INDEXES}; + $self->_set_object($table, $name, $definition, $indexes); } # A private helper for set_index and set_column. # This does the actual "work" of those two functions. # $array_to_change is an arrayref. sub _set_object { - my ($self, $table, $name, $definition, $array_to_change) = @_; + my ($self, $table, $name, $definition, $array_to_change) = @_; - my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1; - # If the object doesn't exist, then add it. - if (!$obj_position) { - push(@$array_to_change, $name); - push(@$array_to_change, $definition); - } - # We're modifying an existing object in the Schema. - else { - splice(@$array_to_change, $obj_position, 1, $definition); - } + my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1; - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + # If the object doesn't exist, then add it. + if (!$obj_position) { + push(@$array_to_change, $name); + push(@$array_to_change, $definition); + } + + # We're modifying an existing object in the Schema. + else { + splice(@$array_to_change, $obj_position, 1, $definition); + } + + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } =item C<delete_index($table, $name)> @@ -2885,16 +2857,17 @@ sub _set_object { =cut sub delete_index { - my ($self, $table, $name) = @_; - - my $indexes = $self->{abstract_schema}{$table}{INDEXES}; - my $name_position = firstidx { $_ eq $name } @$indexes; - die "Attempted to delete nonexistent index $name on the $table table" - if $name_position == -1; - # Delete the key/value pair from the array. - splice(@$indexes, $name_position, 2); - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + my ($self, $table, $name) = @_; + + my $indexes = $self->{abstract_schema}{$table}{INDEXES}; + my $name_position = firstidx { $_ eq $name } @$indexes; + die "Attempted to delete nonexistent index $name on the $table table" + if $name_position == -1; + + # Delete the key/value pair from the array. + splice(@$indexes, $name_position, 2); + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } sub columns_equal { @@ -2912,24 +2885,24 @@ sub columns_equal { =cut - my $self = shift; - my $col_one = dclone(shift); - my $col_two = dclone(shift); + my $self = shift; + my $col_one = dclone(shift); + my $col_two = dclone(shift); - $col_one->{TYPE} = uc($col_one->{TYPE}); - $col_two->{TYPE} = uc($col_two->{TYPE}); + $col_one->{TYPE} = uc($col_one->{TYPE}); + $col_two->{TYPE} = uc($col_two->{TYPE}); - # We don't care about foreign keys when comparing column definitions. - delete $col_one->{REFERENCES}; - delete $col_two->{REFERENCES}; + # We don't care about foreign keys when comparing column definitions. + delete $col_one->{REFERENCES}; + delete $col_two->{REFERENCES}; - my @col_one_array = %$col_one; - my @col_two_array = %$col_two; + my @col_one_array = %$col_one; + my @col_two_array = %$col_two; - my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array); + my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array); - # If there are no differences between the arrays, then they are equal. - return !scalar(@$removed) && !scalar(@$added) ? 1 : 0; + # If there are no differences between the arrays, then they are equal. + return !scalar(@$removed) && !scalar(@$added) ? 1 : 0; } @@ -2953,18 +2926,18 @@ sub columns_equal { =cut sub serialize_abstract { - my ($self) = @_; - - # Make it ok to eval - local $Data::Dumper::Purity = 1; - - # Avoid cross-refs - local $Data::Dumper::Deepcopy = 1; - - # Always sort keys to allow textual compare - local $Data::Dumper::Sortkeys = 1; - - return Dumper($self->{abstract_schema}); + my ($self) = @_; + + # Make it ok to eval + local $Data::Dumper::Purity = 1; + + # Avoid cross-refs + local $Data::Dumper::Deepcopy = 1; + + # Always sort keys to allow textual compare + local $Data::Dumper::Sortkeys = 1; + + return Dumper($self->{abstract_schema}); } =item C<deserialize_abstract($serialized, $version)> @@ -2983,36 +2956,34 @@ sub serialize_abstract { =cut sub deserialize_abstract { - my ($class, $serialized, $version) = @_; - - my $thawed_hash; - if ($version < 2) { - $thawed_hash = thaw($serialized); - } - else { - my $cpt = new Safe; - $cpt->reval($serialized) || - die "Unable to restore cached schema: " . $@; - $thawed_hash = ${$cpt->varglob('VAR1')}; - } - - # Version 2 didn't have the "created" key for REFERENCES items. - if ($version < 3) { - my $standard = $class->new()->{abstract_schema}; - foreach my $table_name (keys %$thawed_hash) { - my %standard_fields = - @{ $standard->{$table_name}->{FIELDS} || [] }; - my $table = $thawed_hash->{$table_name}; - my %fields = @{ $table->{FIELDS} || [] }; - while (my ($field, $def) = each %fields) { - if (exists $def->{REFERENCES}) { - $def->{REFERENCES}->{created} = 1; - } - } + my ($class, $serialized, $version) = @_; + + my $thawed_hash; + if ($version < 2) { + $thawed_hash = thaw($serialized); + } + else { + my $cpt = new Safe; + $cpt->reval($serialized) || die "Unable to restore cached schema: " . $@; + $thawed_hash = ${$cpt->varglob('VAR1')}; + } + + # Version 2 didn't have the "created" key for REFERENCES items. + if ($version < 3) { + my $standard = $class->new()->{abstract_schema}; + foreach my $table_name (keys %$thawed_hash) { + my %standard_fields = @{$standard->{$table_name}->{FIELDS} || []}; + my $table = $thawed_hash->{$table_name}; + my %fields = @{$table->{FIELDS} || []}; + while (my ($field, $def) = each %fields) { + if (exists $def->{REFERENCES}) { + $def->{REFERENCES}->{created} = 1; } + } } + } - return $class->new(undef, $thawed_hash); + return $class->new(undef, $thawed_hash); } ##################################################################### @@ -3040,8 +3011,8 @@ object. =cut sub get_empty_schema { - my ($class) = @_; - return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION); + my ($class) = @_; + return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION); } 1; diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm index 7ff8ade9f..fe2191486 100644 --- a/Bugzilla/DB/Schema/Mysql.pm +++ b/Bugzilla/DB/Schema/Mysql.pm @@ -21,7 +21,7 @@ use Bugzilla::Error; use parent qw(Bugzilla::DB::Schema); -# This is for column_info_to_column, to know when a tinyint is a +# This is for column_info_to_column, to know when a tinyint is a # boolean and when it's really a tinyint. This only has to be accurate # up to and through 2.19.3, because that's the only time we need # column_info_to_column. @@ -30,50 +30,59 @@ use parent qw(Bugzilla::DB::Schema); # that should be interpreted as a BOOLEAN instead of as an INT1 when # reading in the Schema from the disk. The values are discarded; I just # used "1" for simplicity. -# +# # THIS CONSTANT IS ONLY USED FOR UPGRADES FROM 2.18 OR EARLIER. DON'T # UPDATE IT TO MODERN COLUMN NAMES OR DEFINITIONS. use constant BOOLEAN_MAP => { - bugs => {everconfirmed => 1, reporter_accessible => 1, - cclist_accessible => 1, qacontact_accessible => 1, - assignee_accessible => 1}, - longdescs => {isprivate => 1, already_wrapped => 1}, - attachments => {ispatch => 1, isobsolete => 1, isprivate => 1}, - flags => {is_active => 1}, - flagtypes => {is_active => 1, is_requestable => 1, - is_requesteeble => 1, is_multiplicable => 1}, - fielddefs => {mailhead => 1, obsolete => 1}, - bug_status => {isactive => 1}, - resolution => {isactive => 1}, - bug_severity => {isactive => 1}, - priority => {isactive => 1}, - rep_platform => {isactive => 1}, - op_sys => {isactive => 1}, - profiles => {mybugslink => 1, newemailtech => 1}, - namedqueries => {linkinfooter => 1, watchfordiffs => 1}, - groups => {isbuggroup => 1, isactive => 1}, - group_control_map => {entry => 1, membercontrol => 1, othercontrol => 1, - canedit => 1}, - group_group_map => {isbless => 1}, - user_group_map => {isbless => 1, isderived => 1}, - products => {disallownew => 1}, - series => {public => 1}, - whine_queries => {onemailperbug => 1}, - quips => {approved => 1}, - setting => {is_enabled => 1} + bugs => { + everconfirmed => 1, + reporter_accessible => 1, + cclist_accessible => 1, + qacontact_accessible => 1, + assignee_accessible => 1 + }, + longdescs => {isprivate => 1, already_wrapped => 1}, + attachments => {ispatch => 1, isobsolete => 1, isprivate => 1}, + flags => {is_active => 1}, + flagtypes => { + is_active => 1, + is_requestable => 1, + is_requesteeble => 1, + is_multiplicable => 1 + }, + fielddefs => {mailhead => 1, obsolete => 1}, + bug_status => {isactive => 1}, + resolution => {isactive => 1}, + bug_severity => {isactive => 1}, + priority => {isactive => 1}, + rep_platform => {isactive => 1}, + op_sys => {isactive => 1}, + profiles => {mybugslink => 1, newemailtech => 1}, + namedqueries => {linkinfooter => 1, watchfordiffs => 1}, + groups => {isbuggroup => 1, isactive => 1}, + group_control_map => + {entry => 1, membercontrol => 1, othercontrol => 1, canedit => 1}, + group_group_map => {isbless => 1}, + user_group_map => {isbless => 1, isderived => 1}, + products => {disallownew => 1}, + series => {public => 1}, + whine_queries => {onemailperbug => 1}, + quips => {approved => 1}, + setting => {is_enabled => 1} }; # Maps the db_specific hash backwards, for use in column_info_to_column. use constant REVERSE_MAPPING => { - # Boolean and the SERIAL fields are handled in column_info_to_column, - # and so don't have an entry here. - TINYINT => 'INT1', - SMALLINT => 'INT2', - MEDIUMINT => 'INT3', - INTEGER => 'INT4', - - # All the other types have the same name in their abstract version - # as in their db-specific version, so no reverse mapping is needed. + + # Boolean and the SERIAL fields are handled in column_info_to_column, + # and so don't have an entry here. + TINYINT => 'INT1', + SMALLINT => 'INT2', + MEDIUMINT => 'INT3', + INTEGER => 'INT4', + + # All the other types have the same name in their abstract version + # as in their db-specific version, so no reverse mapping is needed. }; use constant MYISAM_TABLES => qw(bugs_fulltext); @@ -81,181 +90,196 @@ use constant MYISAM_TABLES => qw(bugs_fulltext); #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; + my $self = shift; + + $self = $self->SUPER::_initialize(@_); - $self = $self->SUPER::_initialize(@_); + $self->{db_specific} = { - $self->{db_specific} = { + BOOLEAN => 'tinyint', + FALSE => '0', + TRUE => '1', - BOOLEAN => 'tinyint', - FALSE => '0', - TRUE => '1', + INT1 => 'tinyint', + INT2 => 'smallint', + INT3 => 'mediumint', + INT4 => 'integer', - INT1 => 'tinyint', - INT2 => 'smallint', - INT3 => 'mediumint', - INT4 => 'integer', + SMALLSERIAL => 'smallint auto_increment', + MEDIUMSERIAL => 'mediumint auto_increment', + INTSERIAL => 'integer auto_increment', - SMALLSERIAL => 'smallint auto_increment', - MEDIUMSERIAL => 'mediumint auto_increment', - INTSERIAL => 'integer auto_increment', + TINYTEXT => 'tinytext', + MEDIUMTEXT => 'mediumtext', + LONGTEXT => 'mediumtext', - TINYTEXT => 'tinytext', - MEDIUMTEXT => 'mediumtext', - LONGTEXT => 'mediumtext', + LONGBLOB => 'longblob', - LONGBLOB => 'longblob', + DATETIME => 'datetime', + DATE => 'date', + }; - DATETIME => 'datetime', - DATE => 'date', - }; + $self->_adjust_schema; - $self->_adjust_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #------------------------------------------------------------------------------ sub _get_create_table_ddl { - # Extend superclass method to specify the MYISAM storage engine. - # Returns a "create table" SQL statement. - my($self, $table) = @_; + # Extend superclass method to specify the MYISAM storage engine. + # Returns a "create table" SQL statement. - my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : ''; - my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB'; - return($self->SUPER::_get_create_table_ddl($table) - . " ENGINE = $type $charset"); + my ($self, $table) = @_; + + my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : ''; + my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB'; + return ( + $self->SUPER::_get_create_table_ddl($table) . " ENGINE = $type $charset"); + +} #eosub--_get_create_table_ddl -} #eosub--_get_create_table_ddl #------------------------------------------------------------------------------ sub _get_create_index_ddl { - # Extend superclass method to create FULLTEXT indexes on text fields. - # Returns a "create index" SQL statement. - my($self, $table_name, $index_name, $index_fields, $index_type) = @_; + # Extend superclass method to create FULLTEXT indexes on text fields. + # Returns a "create index" SQL statement. + + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + + my $sql = "CREATE "; + $sql .= "$index_type " + if ($index_type eq 'UNIQUE' || $index_type eq 'FULLTEXT'); + $sql .= "INDEX \`$index_name\` ON $table_name \(" + . join(", ", @$index_fields) . "\)"; - my $sql = "CREATE "; - $sql .= "$index_type " if ($index_type eq 'UNIQUE' - || $index_type eq 'FULLTEXT'); - $sql .= "INDEX \`$index_name\` ON $table_name \(" . - join(", ", @$index_fields) . "\)"; + return ($sql); - return($sql); +} #eosub--_get_create_index_ddl -} #eosub--_get_create_index_ddl #-------------------------------------------------------------------- sub get_create_database_sql { - my ($self, $name) = @_; - # We only create as utf8 if we have no params (meaning we're doing - # a new installation) or if the utf8 param is on. - my $create_utf8 = Bugzilla->params->{'utf8'} - || !defined Bugzilla->params->{'utf8'}; - my $charset = $create_utf8 ? "CHARACTER SET utf8" : ''; - return ("CREATE DATABASE $name $charset"); + my ($self, $name) = @_; + + # We only create as utf8 if we have no params (meaning we're doing + # a new installation) or if the utf8 param is on. + my $create_utf8 + = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'}; + my $charset = $create_utf8 ? "CHARACTER SET utf8" : ''; + return ("CREATE DATABASE $name $charset"); } # MySQL has a simpler ALTER TABLE syntax than ANSI. sub get_alter_column_ddl { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - my $old_def = $self->get_column($table, $column); - my %new_def_copy = %$new_def; - if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - # If a column stays a primary key do NOT specify PRIMARY KEY in the - # ALTER TABLE statement. This avoids a MySQL error that two primary - # keys are not allowed. - delete $new_def_copy{PRIMARYKEY}; - } - - my @statements; - - push(@statements, "UPDATE $table SET $column = $set_nulls_to - WHERE $column IS NULL") if defined $set_nulls_to; - - # 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}); - } + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + my $old_def = $self->get_column($table, $column); + my %new_def_copy = %$new_def; + if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + + # If a column stays a primary key do NOT specify PRIMARY KEY in the + # ALTER TABLE statement. This avoids a MySQL error that two primary + # keys are not allowed. + delete $new_def_copy{PRIMARYKEY}; + } + + my @statements; + + push( + @statements, "UPDATE $table SET $column = $set_nulls_to + WHERE $column IS NULL" + ) if defined $set_nulls_to; + + # 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 { - 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"); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT " . $new_def->{DEFAULT} + ); } - - return @statements; + } + 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"); + } + + return @statements; } sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name($table, $column, $references); - my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name"); - my $dbh = Bugzilla->dbh; - - # MySQL requires, and will create, an index on any column with - # an FK. It will name it after the fk, which we never do. - # So if there's an index named after the fk, we also have to delete it. - if ($dbh->bz_index_info_real($table, $fk_name)) { - push(@sql, $self->get_drop_index_ddl($table, $fk_name)); - } - - return @sql; + my ($self, $table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name($table, $column, $references); + my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name"); + my $dbh = Bugzilla->dbh; + + # MySQL requires, and will create, an index on any column with + # an FK. It will name it after the fk, which we never do. + # So if there's an index named after the fk, we also have to delete it. + if ($dbh->bz_index_info_real($table, $fk_name)) { + push(@sql, $self->get_drop_index_ddl($table, $fk_name)); + } + + return @sql; } sub get_drop_index_ddl { - my ($self, $table, $name) = @_; - return ("DROP INDEX \`$name\` ON $table"); + my ($self, $table, $name) = @_; + return ("DROP INDEX \`$name\` ON $table"); } # A special function for MySQL, for renaming a lot of indexes. -# Index renames is a hash, where the key is a string - the +# Index renames is a hash, where the key is a string - the # old names of the index, and the value is a hash - the index # definition that we're renaming to, with an extra key of "NAME" # that contains the new index name. # The indexes in %indexes must be in hashref format. sub get_rename_indexes_ddl { - my ($self, $table, %indexes) = @_; - my @keys = keys %indexes or return (); - - my $sql = "ALTER TABLE $table "; - - foreach my $old_name (@keys) { - my $name = $indexes{$old_name}->{NAME}; - my $type = $indexes{$old_name}->{TYPE}; - $type ||= 'INDEX'; - my $fields = join(',', @{$indexes{$old_name}->{FIELDS}}); - # $old_name needs to be escaped, sometimes, because it was - # a reserved word. - $old_name = '`' . $old_name . '`'; - $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,"; - } - # Remove the last comma. - chop($sql); - return ($sql); + my ($self, $table, %indexes) = @_; + my @keys = keys %indexes or return (); + + my $sql = "ALTER TABLE $table "; + + foreach my $old_name (@keys) { + my $name = $indexes{$old_name}->{NAME}; + my $type = $indexes{$old_name}->{TYPE}; + $type ||= 'INDEX'; + my $fields = join(',', @{$indexes{$old_name}->{FIELDS}}); + + # $old_name needs to be escaped, sometimes, because it was + # a reserved word. + $old_name = '`' . $old_name . '`'; + $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,"; + } + + # Remove the last comma. + chop($sql); + return ($sql); } sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - return ("ALTER TABLE $table AUTO_INCREMENT = $value"); + my ($self, $table, $column, $value) = @_; + return ("ALTER TABLE $table AUTO_INCREMENT = $value"); } # Converts a DBI column_info output to an abstract column definition. @@ -263,124 +287,137 @@ sub get_set_serial_sql { # although there's a chance that it will also work properly if called # elsewhere. sub column_info_to_column { - my ($self, $column_info) = @_; - - # Unfortunately, we have to break Schema's normal "no database" - # barrier a few times in this function. - my $dbh = Bugzilla->dbh; - - my $table = $column_info->{TABLE_NAME}; - my $col_name = $column_info->{COLUMN_NAME}; - - my $column = {}; - - ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0; - - if ($column_info->{mysql_is_pri_key}) { - # In MySQL, if a table has no PK, but it has a UNIQUE index, - # that index will show up as the PK. So we have to eliminate - # that possibility. - # Unfortunately, the only way to definitely solve this is - # to break Schema's standard of not touching the live database - # and check if the index called PRIMARY is on that field. - my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY'); - if ( $pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}}) ) { - $column->{PRIMARYKEY} = 1; - } - } + my ($self, $column_info) = @_; - # MySQL frequently defines a default for a field even when we - # didn't explicitly set one. So we have to have some special - # hacks to determine whether or not we should actually put - # a default in the abstract schema for this field. - if (defined $column_info->{COLUMN_DEF}) { - # The defaults that MySQL inputs automatically are usually - # something that would be considered "false" by perl, either - # a 0 or an empty string. (Except for datetime and decimal - # fields, which have their own special auto-defaults.) - # - # Here's how we handle this: If it exists in the schema - # without a default, then we don't use the default. If it - # doesn't exist in the schema, then we're either going to - # be dropping it soon, or it's a custom end-user column, in which - # case having a bogus default won't harm anything. - my $schema_column = $self->get_column($table, $col_name); - unless ( (!$column_info->{COLUMN_DEF} - || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00' - || $column_info->{COLUMN_DEF} eq '0.00') - && $schema_column - && !exists $schema_column->{DEFAULT}) { - - my $default = $column_info->{COLUMN_DEF}; - # Schema uses '0' for the defaults for decimal fields. - $default = 0 if $default =~ /^0\.0+$/; - # If we're not a number, we're a string and need to be - # quoted. - $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); - $column->{DEFAULT} = $default; - } - } + # Unfortunately, we have to break Schema's normal "no database" + # barrier a few times in this function. + my $dbh = Bugzilla->dbh; - my $type = $column_info->{TYPE_NAME}; + my $table = $column_info->{TABLE_NAME}; + my $col_name = $column_info->{COLUMN_NAME}; - # Certain types of columns need the size/precision appended. - if ($type =~ /CHAR$/ || $type eq 'DECIMAL') { - # This is nicely lowercase and has the size/precision appended. - $type = $column_info->{mysql_type_name}; - } + my $column = {}; - # If we're a tinyint, we could be either a BOOLEAN or an INT1. - # Only the BOOLEAN_MAP knows the difference. - elsif ($type eq 'TINYINT' && exists BOOLEAN_MAP->{$table} - && exists BOOLEAN_MAP->{$table}->{$col_name}) { - $type = 'BOOLEAN'; - if (exists $column->{DEFAULT}) { - $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE'; - } - } + ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0; - # We also need to check if we're an auto_increment field. - elsif ($type =~ /INT/) { - # Unfortunately, the only way to do this in DBI is to query the - # database, so we have to break the rule here that Schema normally - # doesn't touch the live DB. - my $ref_sth = $dbh->prepare( - "SELECT $col_name FROM $table LIMIT 1"); - $ref_sth->execute; - if ($ref_sth->{mysql_is_auto_increment}->[0]) { - if ($type eq 'MEDIUMINT') { - $type = 'MEDIUMSERIAL'; - } - elsif ($type eq 'SMALLINT') { - $type = 'SMALLSERIAL'; - } - else { - $type = 'INTSERIAL'; - } - } - $ref_sth->finish; + if ($column_info->{mysql_is_pri_key}) { + # In MySQL, if a table has no PK, but it has a UNIQUE index, + # that index will show up as the PK. So we have to eliminate + # that possibility. + # Unfortunately, the only way to definitely solve this is + # to break Schema's standard of not touching the live database + # and check if the index called PRIMARY is on that field. + my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY'); + if ($pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}})) { + $column->{PRIMARYKEY} = 1; } + } + + # MySQL frequently defines a default for a field even when we + # didn't explicitly set one. So we have to have some special + # hacks to determine whether or not we should actually put + # a default in the abstract schema for this field. + if (defined $column_info->{COLUMN_DEF}) { + + # The defaults that MySQL inputs automatically are usually + # something that would be considered "false" by perl, either + # a 0 or an empty string. (Except for datetime and decimal + # fields, which have their own special auto-defaults.) + # + # Here's how we handle this: If it exists in the schema + # without a default, then we don't use the default. If it + # doesn't exist in the schema, then we're either going to + # be dropping it soon, or it's a custom end-user column, in which + # case having a bogus default won't harm anything. + my $schema_column = $self->get_column($table, $col_name); + unless ( + ( + !$column_info->{COLUMN_DEF} + || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00' + || $column_info->{COLUMN_DEF} eq '0.00' + ) + && $schema_column + && !exists $schema_column->{DEFAULT} + ) + { - # For all other db-specific types, check if they exist in - # REVERSE_MAPPING and use the type found there. - if (exists REVERSE_MAPPING->{$type}) { - $type = REVERSE_MAPPING->{$type}; + my $default = $column_info->{COLUMN_DEF}; + + # Schema uses '0' for the defaults for decimal fields. + $default = 0 if $default =~ /^0\.0+$/; + + # If we're not a number, we're a string and need to be + # quoted. + $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); + $column->{DEFAULT} = $default; + } + } + + my $type = $column_info->{TYPE_NAME}; + + # Certain types of columns need the size/precision appended. + if ($type =~ /CHAR$/ || $type eq 'DECIMAL') { + + # This is nicely lowercase and has the size/precision appended. + $type = $column_info->{mysql_type_name}; + } + + # If we're a tinyint, we could be either a BOOLEAN or an INT1. + # Only the BOOLEAN_MAP knows the difference. + elsif ($type eq 'TINYINT' + && exists BOOLEAN_MAP->{$table} + && exists BOOLEAN_MAP->{$table}->{$col_name}) + { + $type = 'BOOLEAN'; + if (exists $column->{DEFAULT}) { + $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE'; + } + } + + # We also need to check if we're an auto_increment field. + elsif ($type =~ /INT/) { + + # Unfortunately, the only way to do this in DBI is to query the + # database, so we have to break the rule here that Schema normally + # doesn't touch the live DB. + my $ref_sth = $dbh->prepare("SELECT $col_name FROM $table LIMIT 1"); + $ref_sth->execute; + if ($ref_sth->{mysql_is_auto_increment}->[0]) { + if ($type eq 'MEDIUMINT') { + $type = 'MEDIUMSERIAL'; + } + elsif ($type eq 'SMALLINT') { + $type = 'SMALLSERIAL'; + } + else { + $type = 'INTSERIAL'; + } } + $ref_sth->finish; - $column->{TYPE} = $type; + } - #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n"; + # For all other db-specific types, check if they exist in + # REVERSE_MAPPING and use the type found there. + if (exists REVERSE_MAPPING->{$type}) { + $type = REVERSE_MAPPING->{$type}; + } - return $column; + $column->{TYPE} = $type; + + #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n"; + + return $column; } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - my $def = $self->get_type_ddl($self->get_column($table, $old_name)); - # MySQL doesn't like having the PRIMARY KEY statement in a rename. - $def =~ s/PRIMARY KEY//i; - return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def"); + my ($self, $table, $old_name, $new_name) = @_; + my $def = $self->get_type_ddl($self->get_column($table, $old_name)); + + # MySQL doesn't like having the PRIMARY KEY statement in a rename. + $def =~ s/PRIMARY KEY//i; + return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def"); } 1; diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm index 8fb5479b1..0cf2b6c4b 100644 --- a/Bugzilla/DB/Schema/Oracle.pm +++ b/Bugzilla/DB/Schema/Oracle.pm @@ -21,8 +21,9 @@ use parent qw(Bugzilla::DB::Schema); use Carp qw(confess); use Bugzilla::Util; -use constant ADD_COLUMN => 'ADD'; +use constant ADD_COLUMN => 'ADD'; use constant MULTIPLE_FKS_IN_ALTER => 0; + # Whether this is true or not, this is what it needs to be in order for # hash_identifier to maintain backwards compatibility with versions before # 3.2rc2. @@ -31,123 +32,128 @@ use constant MAX_IDENTIFIER_LEN => 27; #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; + my $self = shift; + + $self = $self->SUPER::_initialize(@_); - $self = $self->SUPER::_initialize(@_); + $self->{db_specific} = { - $self->{db_specific} = { + BOOLEAN => 'integer', + FALSE => '0', + TRUE => '1', - BOOLEAN => 'integer', - FALSE => '0', - TRUE => '1', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + SMALLSERIAL => 'integer', + MEDIUMSERIAL => 'integer', + INTSERIAL => 'integer', - SMALLSERIAL => 'integer', - MEDIUMSERIAL => 'integer', - INTSERIAL => 'integer', + TINYTEXT => 'varchar(255)', + MEDIUMTEXT => 'varchar(4000)', + LONGTEXT => 'clob', - TINYTEXT => 'varchar(255)', - MEDIUMTEXT => 'varchar(4000)', - LONGTEXT => 'clob', + LONGBLOB => 'blob', - LONGBLOB => 'blob', + DATETIME => 'date', + DATE => 'date', + }; - DATETIME => 'date', - DATE => 'date', - }; + $self->_adjust_schema; - $self->_adjust_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------- sub get_table_ddl { - my $self = shift; - my $table = shift; - unshift @_, $table; - my @ddl = $self->SUPER::get_table_ddl(@_); - - my @fields = @{ $self->{abstract_schema}{$table}{FIELDS} || [] }; - while (@fields) { - my $field_name = shift @fields; - my $field_info = shift @fields; - # Create triggers to deal with empty string. - if ( $field_info->{TYPE} =~ /varchar|TEXT/i - && $field_info->{NOTNULL} ) { - push (@ddl, _get_notnull_trigger_ddl($table, $field_name)); - } - # Create sequences and triggers to emulate SERIAL datatypes. - if ( $field_info->{TYPE} =~ /SERIAL/i ) { - push (@ddl, $self->_get_create_seq_ddl($table, $field_name)); - } + my $self = shift; + my $table = shift; + unshift @_, $table; + my @ddl = $self->SUPER::get_table_ddl(@_); + + my @fields = @{$self->{abstract_schema}{$table}{FIELDS} || []}; + while (@fields) { + my $field_name = shift @fields; + my $field_info = shift @fields; + + # Create triggers to deal with empty string. + if ($field_info->{TYPE} =~ /varchar|TEXT/i && $field_info->{NOTNULL}) { + push(@ddl, _get_notnull_trigger_ddl($table, $field_name)); } - return @ddl; -} #eosub--get_table_ddl + # Create sequences and triggers to emulate SERIAL datatypes. + if ($field_info->{TYPE} =~ /SERIAL/i) { + push(@ddl, $self->_get_create_seq_ddl($table, $field_name)); + } + } + return @ddl; -# Extend superclass method to create Oracle Text indexes if index type +} #eosub--get_table_ddl + +# Extend superclass method to create Oracle Text indexes if index type # is FULLTEXT from schema. Returns a "create index" SQL statement. sub _get_create_index_ddl { - my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; - $index_name = "idx_" . $self->_hash_identifier($index_name); - if ($index_type eq 'FULLTEXT') { - my $sql = "CREATE INDEX $index_name ON $table_name (" - . join(',',@$index_fields) - . ") INDEXTYPE IS CTXSYS.CONTEXT " - . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')" ; - return $sql; - } - - return($self->SUPER::_get_create_index_ddl($table_name, $index_name, - $index_fields, $index_type)); + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + $index_name = "idx_" . $self->_hash_identifier($index_name); + if ($index_type eq 'FULLTEXT') { + my $sql + = "CREATE INDEX $index_name ON $table_name (" + . join(',', @$index_fields) + . ") INDEXTYPE IS CTXSYS.CONTEXT " + . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')"; + return $sql; + } + + return ($self->SUPER::_get_create_index_ddl( + $table_name, $index_name, $index_fields, $index_type + )); } sub get_drop_index_ddl { - my $self = shift; - my ($table, $name) = @_; + my $self = shift; + my ($table, $name) = @_; - $name = 'idx_' . $self->_hash_identifier($name); - return $self->SUPER::get_drop_index_ddl($table, $name); + $name = 'idx_' . $self->_hash_identifier($name); + return $self->SUPER::get_drop_index_ddl($table, $name); } -# Oracle supports the use of FOREIGN KEY integrity constraints +# Oracle supports the use of FOREIGN KEY integrity constraints # to define the referential integrity actions, including: # - Update and delete No Action (default) # - Delete CASCADE # - Delete SET NULL sub get_fk_ddl { - my $self = shift; - my $ddl = $self->SUPER::get_fk_ddl(@_); + my $self = shift; + my $ddl = $self->SUPER::get_fk_ddl(@_); - # iThe Bugzilla Oracle driver implements UPDATE via a trigger. - $ddl =~ s/ON UPDATE \S+//i; - # RESTRICT is the default for DELETE on Oracle and may not be specified. - $ddl =~ s/ON DELETE RESTRICT//i; + # iThe Bugzilla Oracle driver implements UPDATE via a trigger. + $ddl =~ s/ON UPDATE \S+//i; - return $ddl; + # RESTRICT is the default for DELETE on Oracle and may not be specified. + $ddl =~ s/ON DELETE RESTRICT//i; + + return $ddl; } sub get_add_fks_sql { - my $self = shift; - my ($table, $column_fks) = @_; - my @sql = $self->SUPER::get_add_fks_sql(@_); - - foreach my $column (keys %$column_fks) { - my $fk = $column_fks->{$column}; - next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE'; - my $fk_name = $self->_get_fk_name($table, $column, $fk); - my $to_column = $fk->{COLUMN}; - my $to_table = $fk->{TABLE}; - - my $trigger = <<END; + my $self = shift; + my ($table, $column_fks) = @_; + my @sql = $self->SUPER::get_add_fks_sql(@_); + + foreach my $column (keys %$column_fks) { + my $fk = $column_fks->{$column}; + next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE'; + my $fk_name = $self->_get_fk_name($table, $column, $fk); + my $to_column = $fk->{COLUMN}; + my $to_table = $fk->{TABLE}; + + my $trigger = <<END; CREATE OR REPLACE TRIGGER ${fk_name}_UC AFTER UPDATE OF $to_column ON $to_table REFERENCING NEW AS NEW OLD AS OLD @@ -158,351 +164,371 @@ CREATE OR REPLACE TRIGGER ${fk_name}_UC WHERE $column = :OLD.$to_column; END ${fk_name}_UC; END - push(@sql, $trigger); - } + push(@sql, $trigger); + } - return @sql; + return @sql; } sub get_drop_fk_sql { - my $self = shift; - my ($table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name(@_); - my @sql; - if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) { - push(@sql, "DROP TRIGGER ${fk_name}_uc"); - } - push(@sql, $self->SUPER::get_drop_fk_sql(@_)); - return @sql; + my $self = shift; + my ($table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name(@_); + my @sql; + if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) { + push(@sql, "DROP TRIGGER ${fk_name}_uc"); + } + push(@sql, $self->SUPER::get_drop_fk_sql(@_)); + return @sql; } sub _get_fk_name { - my ($self, $table, $column, $references) = @_; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $fk_name = "${table}_${column}_${to_table}_${to_column}"; - $fk_name = "fk_" . $self->_hash_identifier($fk_name); - - return $fk_name; + my ($self, $table, $column, $references) = @_; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $fk_name = "${table}_${column}_${to_table}_${to_column}"; + $fk_name = "fk_" . $self->_hash_identifier($fk_name); + + return $fk_name; } sub get_add_column_ddl { - my $self = shift; - my ($table, $column, $definition, $init_value) = @_; - my @sql; - - # Create sequences and triggers to emulate SERIAL datatypes. - if ($definition->{TYPE} =~ /SERIAL/i) { - # Clone the definition to not alter the original one. - my %def = %$definition; - # Oracle requires to define the column is several steps. - my $pk = delete $def{PRIMARYKEY}; - my $notnull = delete $def{NOTNULL}; - @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value); - push(@sql, $self->_get_create_seq_ddl($table, $column)); - push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL"); - push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull; - push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk; - } - else { - @sql = $self->SUPER::get_add_column_ddl(@_); - # Create triggers to deal with empty string. - if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) { - push(@sql, _get_notnull_trigger_ddl($table, $column)); - } + my $self = shift; + my ($table, $column, $definition, $init_value) = @_; + my @sql; + + # Create sequences and triggers to emulate SERIAL datatypes. + if ($definition->{TYPE} =~ /SERIAL/i) { + + # Clone the definition to not alter the original one. + my %def = %$definition; + + # Oracle requires to define the column is several steps. + my $pk = delete $def{PRIMARYKEY}; + my $notnull = delete $def{NOTNULL}; + @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value); + push(@sql, $self->_get_create_seq_ddl($table, $column)); + push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL"); + push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull; + push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk; + } + else { + @sql = $self->SUPER::get_add_column_ddl(@_); + + # Create triggers to deal with empty string. + if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($table, $column)); } + } - return @sql; + return @sql; } sub get_alter_column_ddl { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - - my @statements; - my $old_def = $self->get_column_abstract($table, $column); - my $specific = $self->{db_specific}; - - # If the types have changed, we have to deal with that. - if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { - push(@statements, $self->_get_alter_type_sql($table, $column, - $new_def, $old_def)); - } - - my $default = $new_def->{DEFAULT}; - my $default_old = $old_def->{DEFAULT}; - - if (defined $default) { - $default = $specific->{$default} if exists $specific->{$default}; - } - # This first condition prevents "uninitialized value" errors. - if (!defined $default && !defined $default_old) { - # Do Nothing - } - # If we went from having a default to not having one - elsif (!defined $default && defined $default_old) { - push(@statements, "ALTER TABLE $table MODIFY $column" - . " DEFAULT NULL"); - } - # If we went from no default to a default, or we changed the default. - elsif ( (defined $default && !defined $default_old) || - ($default ne $default_old) ) - { - push(@statements, "ALTER TABLE $table MODIFY $column " - . " DEFAULT $default"); - } - - # If we went from NULL to NOT NULL. - if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { - my $setdefault; - # Handle any fields that were NULL before, if we have a default, - $setdefault = $default if defined $default; - # But if we have a set_nulls_to, that overrides the DEFAULT - # (although nobody would usually specify both a default and - # a set_nulls_to.) - $setdefault = $set_nulls_to if defined $set_nulls_to; - if (defined $setdefault) { - push(@statements, "UPDATE $table SET $column = $setdefault" - . " WHERE $column IS NULL"); - } - push(@statements, "ALTER TABLE $table MODIFY $column" - . " NOT NULL"); - push (@statements, _get_notnull_trigger_ddl($table, $column)) - if $old_def->{TYPE} =~ /varchar|text/i - && $new_def->{TYPE} =~ /varchar|text/i; - } - # If we went from NOT NULL to NULL - elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { - push(@statements, "ALTER TABLE $table MODIFY $column" - . " NULL"); - push(@statements, "DROP TRIGGER ${table}_${column}") - if $new_def->{TYPE} =~ /varchar|text/i - && $old_def->{TYPE} =~ /varchar|text/i; - } - - # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. - if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + + my @statements; + my $old_def = $self->get_column_abstract($table, $column); + my $specific = $self->{db_specific}; + + # If the types have changed, we have to deal with that. + if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { + push(@statements, + $self->_get_alter_type_sql($table, $column, $new_def, $old_def)); + } + + my $default = $new_def->{DEFAULT}; + my $default_old = $old_def->{DEFAULT}; + + if (defined $default) { + $default = $specific->{$default} if exists $specific->{$default}; + } + + # This first condition prevents "uninitialized value" errors. + if (!defined $default && !defined $default_old) { + + # Do Nothing + } + + # If we went from having a default to not having one + elsif (!defined $default && defined $default_old) { + push(@statements, "ALTER TABLE $table MODIFY $column" . " DEFAULT NULL"); + } + + # If we went from no default to a default, or we changed the default. + elsif ((defined $default && !defined $default_old) + || ($default ne $default_old)) + { + push(@statements, "ALTER TABLE $table MODIFY $column " . " DEFAULT $default"); + } + + # If we went from NULL to NOT NULL. + if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { + my $setdefault; + + # Handle any fields that were NULL before, if we have a default, + $setdefault = $default if defined $default; + + # But if we have a set_nulls_to, that overrides the DEFAULT + # (although nobody would usually specify both a default and + # a set_nulls_to.) + $setdefault = $set_nulls_to if defined $set_nulls_to; + if (defined $setdefault) { + push(@statements, + "UPDATE $table SET $column = $setdefault" . " WHERE $column IS NULL"); } - # If we went from being a PK to not being a PK - elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) { - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); - } - - return @statements; + push(@statements, "ALTER TABLE $table MODIFY $column" . " NOT NULL"); + push(@statements, _get_notnull_trigger_ddl($table, $column)) + if $old_def->{TYPE} =~ /varchar|text/i && $new_def->{TYPE} =~ /varchar|text/i; + } + + # If we went from NOT NULL to NULL + elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { + push(@statements, "ALTER TABLE $table MODIFY $column" . " NULL"); + push(@statements, "DROP TRIGGER ${table}_${column}") + if $new_def->{TYPE} =~ /varchar|text/i && $old_def->{TYPE} =~ /varchar|text/i; + } + + # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. + if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + } + + # If we went from being a PK to not being a PK + elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } sub _get_alter_type_sql { - my ($self, $table, $column, $new_def, $old_def) = @_; - my @statements; - - my $type = $new_def->{TYPE}; - $type = $self->{db_specific}->{$type} - if exists $self->{db_specific}->{$type}; - - if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - die("You cannot specify a DEFAULT on a SERIAL-type column.") - if $new_def->{DEFAULT}; + my ($self, $table, $column, $new_def, $old_def) = @_; + my @statements; + + my $type = $new_def->{TYPE}; + $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type}; + + if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + die("You cannot specify a DEFAULT on a SERIAL-type column.") + if $new_def->{DEFAULT}; + } + + if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) + || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i)) + { + # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, + # just a way to work around. + # Determine whether column_temp is already exist. + my $dbh = Bugzilla->dbh; + my $column_exist = $dbh->selectcol_arrayref( + "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND + CNAME = UPPER(?)", undef, $table, $column . "_temp" + ); + if (!@$column_exist) { + push(@statements, "ALTER TABLE $table ADD ${column}_temp $type"); } - - if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) - || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i) - ) { - # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, - # just a way to work around. - # Determine whether column_temp is already exist. - my $dbh=Bugzilla->dbh; - my $column_exist = $dbh->selectcol_arrayref( - "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND - CNAME = UPPER(?)", undef,$table,$column . "_temp"); - if(!@$column_exist) { - push(@statements, - "ALTER TABLE $table ADD ${column}_temp $type"); - } - push(@statements, "UPDATE $table SET ${column}_temp = $column"); - push(@statements, "COMMIT"); - push(@statements, "ALTER TABLE $table DROP COLUMN $column"); - push(@statements, - "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column"); - } else { - push(@statements, "ALTER TABLE $table MODIFY $column $type"); - } - - if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - push(@statements, _get_create_seq_ddl($table, $column)); - } - - # If this column is no longer SERIAL, we need to drop the sequence - # that went along with it. - if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { - push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ"); - push(@statements, "DROP TRIGGER ${table}_${column}_TR"); - } - - # If this column is changed to type TEXT/VARCHAR, we need to deal with - # empty string. - if ( $old_def->{TYPE} !~ /varchar|text/i - && $new_def->{TYPE} =~ /varchar|text/i - && $new_def->{NOTNULL} ) - { - push (@statements, _get_notnull_trigger_ddl($table, $column)); - } - # If this column is no longer TEXT/VARCHAR, we need to drop the trigger - # that went along with it. - if ( $old_def->{TYPE} =~ /varchar|text/i - && $old_def->{NOTNULL} - && $new_def->{TYPE} !~ /varchar|text/i ) - { - push(@statements, "DROP TRIGGER ${table}_${column}"); - } - return @statements; + push(@statements, "UPDATE $table SET ${column}_temp = $column"); + push(@statements, "COMMIT"); + push(@statements, "ALTER TABLE $table DROP COLUMN $column"); + push(@statements, "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column"); + } + else { + push(@statements, "ALTER TABLE $table MODIFY $column $type"); + } + + if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + push(@statements, _get_create_seq_ddl($table, $column)); + } + + # If this column is no longer SERIAL, we need to drop the sequence + # that went along with it. + if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { + push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ"); + push(@statements, "DROP TRIGGER ${table}_${column}_TR"); + } + + # If this column is changed to type TEXT/VARCHAR, we need to deal with + # empty string. + if ( $old_def->{TYPE} !~ /varchar|text/i + && $new_def->{TYPE} =~ /varchar|text/i + && $new_def->{NOTNULL}) + { + push(@statements, _get_notnull_trigger_ddl($table, $column)); + } + + # If this column is no longer TEXT/VARCHAR, we need to drop the trigger + # that went along with it. + if ( $old_def->{TYPE} =~ /varchar|text/i + && $old_def->{NOTNULL} + && $new_def->{TYPE} !~ /varchar|text/i) + { + push(@statements, "DROP TRIGGER ${table}_${column}"); + } + return @statements; } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list. - return (); - } - my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); - my $def = $self->get_column_abstract($table, $old_name); - if ($def->{TYPE} =~ /SERIAL/i) { - # We have to rename the series also, and fix the default of the series. - my $old_seq = "${table}_${old_name}_SEQ"; - my $new_seq = "${table}_${new_name}_SEQ"; - push(@sql, "RENAME $old_seq TO $new_seq"); - push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq)); - push(@sql, "DROP TRIGGER ${table}_${old_name}_TR"); - } - if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL} ) { - push(@sql, _get_notnull_trigger_ddl($table,$new_name)); - push(@sql, "DROP TRIGGER ${table}_${old_name}"); - } - return @sql; + my ($self, $table, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list. + return (); + } + my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); + my $def = $self->get_column_abstract($table, $old_name); + if ($def->{TYPE} =~ /SERIAL/i) { + + # We have to rename the series also, and fix the default of the series. + my $old_seq = "${table}_${old_name}_SEQ"; + my $new_seq = "${table}_${new_name}_SEQ"; + push(@sql, "RENAME $old_seq TO $new_seq"); + push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq)); + push(@sql, "DROP TRIGGER ${table}_${old_name}_TR"); + } + if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($table, $new_name)); + push(@sql, "DROP TRIGGER ${table}_${old_name}"); + } + 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"); - } - # If this column is of type SERIAL, we need to drop the sequence - # and trigger that went along with it. - my $def = $self->get_column_abstract($table, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ"); - push(@sql, "DROP TRIGGER ${table}_${column}_TR"); - } - return @sql; + 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"); + } + + # If this column is of type SERIAL, we need to drop the sequence + # and trigger that went along with it. + my $def = $self->get_column_abstract($table, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ"); + push(@sql, "DROP TRIGGER ${table}_${column}_TR"); + } + return @sql; } sub get_rename_table_sql { - my ($self, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list. - return (); - } + my ($self, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list. + return (); + } - my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); - my @columns = $self->get_table_columns($old_name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($old_name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - # If there's a SERIAL column on this table, we also need - # to rename the sequence. - my $old_seq = "${old_name}_${column}_SEQ"; - my $new_seq = "${new_name}_${column}_SEQ"; - push(@sql, "RENAME $old_seq TO $new_seq"); - push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq)); - push(@sql, "DROP TRIGGER ${old_name}_${column}_TR"); - } - if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { - push(@sql, _get_notnull_trigger_ddl($new_name, $column)); - push(@sql, "DROP TRIGGER ${old_name}_${column}"); - } + my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); + my @columns = $self->get_table_columns($old_name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($old_name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + + # If there's a SERIAL column on this table, we also need + # to rename the sequence. + my $old_seq = "${old_name}_${column}_SEQ"; + my $new_seq = "${new_name}_${column}_SEQ"; + push(@sql, "RENAME $old_seq TO $new_seq"); + push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq)); + push(@sql, "DROP TRIGGER ${old_name}_${column}_TR"); + } + if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($new_name, $column)); + push(@sql, "DROP TRIGGER ${old_name}_${column}"); } + } - return @sql; + return @sql; } sub get_drop_table_ddl { - my ($self, $name) = @_; - my @sql; - - my @columns = $self->get_table_columns($name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - # If there's a SERIAL column on this table, we also need - # to remove the sequence. - push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ"); - } + my ($self, $name) = @_; + my @sql; + + my @columns = $self->get_table_columns($name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + + # If there's a SERIAL column on this table, we also need + # to remove the sequence. + push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ"); } - push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE"); + } + push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE"); - return @sql; + return @sql; } sub _get_notnull_trigger_ddl { - my ($table, $column) = @_; - - my $notnull_sql = "CREATE OR REPLACE TRIGGER " - . " ${table}_${column}" - . " BEFORE INSERT OR UPDATE ON ". $table - . " FOR EACH ROW" - . " BEGIN " - . " IF :NEW.". $column ." IS NULL THEN " - . " SELECT '" . Bugzilla::DB::Oracle->EMPTY_STRING - . "' INTO :NEW.". $column ." FROM DUAL; " - . " END IF; " - . " END ".$table.";"; - return $notnull_sql; + my ($table, $column) = @_; + + my $notnull_sql + = "CREATE OR REPLACE TRIGGER " + . " ${table}_${column}" + . " BEFORE INSERT OR UPDATE ON " + . $table + . " FOR EACH ROW" + . " BEGIN " + . " IF :NEW." + . $column + . " IS NULL THEN " + . " SELECT '" + . Bugzilla::DB::Oracle->EMPTY_STRING + . "' INTO :NEW." + . $column + . " FROM DUAL; " + . " END IF; " . " END " + . $table . ";"; + return $notnull_sql; } sub _get_create_seq_ddl { - my ($self, $table, $column, $start_with) = @_; - $start_with ||= 1; - my @ddl; - my $seq_name = "${table}_${column}_SEQ"; - my $seq_sql = "CREATE SEQUENCE $seq_name " - . " INCREMENT BY 1 " - . " START WITH $start_with " - . " NOMAXVALUE " - . " NOCYCLE " - . " NOCACHE"; - push (@ddl, $seq_sql); - push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name)); - - return @ddl; + my ($self, $table, $column, $start_with) = @_; + $start_with ||= 1; + my @ddl; + my $seq_name = "${table}_${column}_SEQ"; + my $seq_sql + = "CREATE SEQUENCE $seq_name " + . " INCREMENT BY 1 " + . " START WITH $start_with " + . " NOMAXVALUE " + . " NOCYCLE " + . " NOCACHE"; + push(@ddl, $seq_sql); + push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name)); + + return @ddl; } sub _get_create_trigger_ddl { - my ($self, $table, $column, $seq_name) = @_; - my $serial_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " - . " BEFORE INSERT ON $table " - . " FOR EACH ROW " - . " BEGIN " - . " SELECT ${seq_name}.NEXTVAL " - . " INTO :NEW.$column FROM DUAL; " - . " END;"; - return $serial_sql; + my ($self, $table, $column, $seq_name) = @_; + my $serial_sql + = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " + . " BEFORE INSERT ON $table " + . " FOR EACH ROW " + . " BEGIN " + . " SELECT ${seq_name}.NEXTVAL " + . " INTO :NEW.$column FROM DUAL; " . " END;"; + return $serial_sql; } -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_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; +} 1; diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm index 55a932272..cf28a02d9 100644 --- a/Bugzilla/DB/Schema/Pg.pm +++ b/Bugzilla/DB/Schema/Pg.pm @@ -23,169 +23,191 @@ use Storable qw(dclone); #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; - - $self = $self->SUPER::_initialize(@_); - - # Remove FULLTEXT index types from the schemas. - foreach my $table (keys %{ $self->{schema} }) { - if ($self->{schema}{$table}{INDEXES}) { - foreach my $index (@{ $self->{schema}{$table}{INDEXES} }) { - if (ref($index) eq 'HASH') { - delete($index->{TYPE}) if (exists $index->{TYPE} - && $index->{TYPE} eq 'FULLTEXT'); - } - } - foreach my $index (@{ $self->{abstract_schema}{$table}{INDEXES} }) { - if (ref($index) eq 'HASH') { - delete($index->{TYPE}) if (exists $index->{TYPE} - && $index->{TYPE} eq 'FULLTEXT'); - } - } + my $self = shift; + + $self = $self->SUPER::_initialize(@_); + + # Remove FULLTEXT index types from the schemas. + foreach my $table (keys %{$self->{schema}}) { + if ($self->{schema}{$table}{INDEXES}) { + foreach my $index (@{$self->{schema}{$table}{INDEXES}}) { + if (ref($index) eq 'HASH') { + delete($index->{TYPE}) + if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT'); + } + } + foreach my $index (@{$self->{abstract_schema}{$table}{INDEXES}}) { + if (ref($index) eq 'HASH') { + delete($index->{TYPE}) + if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT'); } + } } + } - $self->{db_specific} = { + $self->{db_specific} = { - BOOLEAN => 'smallint', - FALSE => '0', - TRUE => '1', + BOOLEAN => 'smallint', + FALSE => '0', + TRUE => '1', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - SMALLSERIAL => 'serial unique', - MEDIUMSERIAL => 'serial unique', - INTSERIAL => 'serial unique', + SMALLSERIAL => 'serial unique', + MEDIUMSERIAL => 'serial unique', + INTSERIAL => 'serial unique', - TINYTEXT => 'varchar(255)', - MEDIUMTEXT => 'text', - LONGTEXT => 'text', + TINYTEXT => 'varchar(255)', + MEDIUMTEXT => 'text', + LONGTEXT => 'text', - LONGBLOB => 'bytea', + LONGBLOB => 'bytea', - DATETIME => 'timestamp(0) without time zone', - DATE => 'date', - }; + DATETIME => 'timestamp(0) without time zone', + DATE => 'date', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; + +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------- sub get_create_database_sql { - my ($self, $name) = @_; - # We only create as utf8 if we have no params (meaning we're doing - # a new installation) or if the utf8 param is on. - my $create_utf8 = Bugzilla->params->{'utf8'} - || !defined Bugzilla->params->{'utf8'}; - my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : ''; - return ("CREATE DATABASE $name $charset"); + my ($self, $name) = @_; + + # We only create as utf8 if we have no params (meaning we're doing + # a new installation) or if the utf8 param is on. + my $create_utf8 + = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'}; + my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : ''; + return ("CREATE DATABASE $name $charset"); } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list, since Pg - # is case-insensitive and will return an error about a duplicate name - return (); - } - my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); - my $def = $self->get_column_abstract($table, $old_name); - if ($def->{TYPE} =~ /SERIAL/i) { - # We have to rename the series also. - push(@sql, "ALTER SEQUENCE ${table}_${old_name}_seq - RENAME TO ${table}_${new_name}_seq"); - } - return @sql; + my ($self, $table, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list, since Pg + # is case-insensitive and will return an error about a duplicate name + return (); + } + my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); + my $def = $self->get_column_abstract($table, $old_name); + if ($def->{TYPE} =~ /SERIAL/i) { + + # We have to rename the series also. + push( + @sql, "ALTER SEQUENCE ${table}_${old_name}_seq + RENAME TO ${table}_${new_name}_seq" + ); + } + return @sql; } sub get_rename_table_sql { - my ($self, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list, since Pg - # is case-insensitive and will return an error about a duplicate name - return (); + my ($self, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list, since Pg + # is case-insensitive and will return an error about a duplicate name + return (); + } + + my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); + + # If there's a SERIAL column on this table, we also need to rename the + # sequence. + # If there is a PRIMARY KEY, we need to rename it too. + my @columns = $self->get_table_columns($old_name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($old_name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + my $old_seq = "${old_name}_${column}_seq"; + my $new_seq = "${new_name}_${column}_seq"; + push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq"); + push( + @sql, "ALTER TABLE $new_name ALTER COLUMN $column + SET DEFAULT NEXTVAL('$new_seq')" + ); } - - my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); - - # If there's a SERIAL column on this table, we also need to rename the - # sequence. - # If there is a PRIMARY KEY, we need to rename it too. - my @columns = $self->get_table_columns($old_name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($old_name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - my $old_seq = "${old_name}_${column}_seq"; - my $new_seq = "${new_name}_${column}_seq"; - push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq"); - push(@sql, "ALTER TABLE $new_name ALTER COLUMN $column - SET DEFAULT NEXTVAL('$new_seq')"); - } - if ($def->{PRIMARYKEY}) { - my $old_pk = "${old_name}_pkey"; - my $new_pk = "${new_name}_pkey"; - push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk"); - } + if ($def->{PRIMARYKEY}) { + my $old_pk = "${old_name}_pkey"; + my $new_pk = "${new_name}_pkey"; + push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk"); } + } - return @sql; + return @sql; } sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - return ("SELECT setval('${table}_${column}_seq', $value, false) - FROM $table"); + 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; - - my $type = $new_def->{TYPE}; - $type = $self->{db_specific}->{$type} - if exists $self->{db_specific}->{$type}; - - if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - die("You cannot specify a DEFAULT on a SERIAL-type column.") - if $new_def->{DEFAULT}; - } - - $type =~ s/\bserial\b/integer/i; - - # On Pg, you don't need UNIQUE if you're a PK--it creates - # two identical indexes otherwise. - $type =~ s/unique//i if $new_def->{PRIMARYKEY}; - - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - TYPE $type"); - - if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - push(@statements, "CREATE SEQUENCE ${table}_${column}_seq - OWNED BY $table.$column"); - push(@statements, "SELECT setval('${table}_${column}_seq', + my ($self, $table, $column, $new_def, $old_def) = @_; + my @statements; + + my $type = $new_def->{TYPE}; + $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type}; + + if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + die("You cannot specify a DEFAULT on a SERIAL-type column.") + if $new_def->{DEFAULT}; + } + + $type =~ s/\bserial\b/integer/i; + + # On Pg, you don't need UNIQUE if you're a PK--it creates + # two identical indexes otherwise. + $type =~ s/unique//i if $new_def->{PRIMARYKEY}; + + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + TYPE $type" + ); + + if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + push( + @statements, "CREATE SEQUENCE ${table}_${column}_seq + OWNED BY $table.$column" + ); + push( + @statements, "SELECT setval('${table}_${column}_seq', MAX($table.$column)) - FROM $table"); - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT nextval('${table}_${column}_seq')"); - } - - # If this column is no longer SERIAL, we need to drop the sequence - # that went along with it. - if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - DROP DEFAULT"); - push(@statements, "ALTER SEQUENCE ${table}_${column}_seq - OWNED BY NONE"); - push(@statements, "DROP SEQUENCE ${table}_${column}_seq"); - } - - return @statements; + FROM $table" + ); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT nextval('${table}_${column}_seq')" + ); + } + + # If this column is no longer SERIAL, we need to drop the sequence + # that went along with it. + if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + DROP DEFAULT" + ); + push( + @statements, "ALTER SEQUENCE ${table}_${column}_seq + OWNED BY NONE" + ); + push(@statements, "DROP SEQUENCE ${table}_${column}_seq"); + } + + return @statements; } 1; diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm index ccdbfd8aa..73ee7cfc5 100644 --- a/Bugzilla/DB/Schema/Sqlite.pm +++ b/Bugzilla/DB/Schema/Sqlite.pm @@ -22,37 +22,37 @@ use constant FK_ON_CREATE => 1; sub _initialize { - my $self = shift; + my $self = shift; - $self = $self->SUPER::_initialize(@_); + $self = $self->SUPER::_initialize(@_); - $self->{db_specific} = { - BOOLEAN => 'integer', - FALSE => '0', - TRUE => '1', + $self->{db_specific} = { + BOOLEAN => 'integer', + FALSE => '0', + TRUE => '1', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - SMALLSERIAL => 'SERIAL', - MEDIUMSERIAL => 'SERIAL', - INTSERIAL => 'SERIAL', + SMALLSERIAL => 'SERIAL', + MEDIUMSERIAL => 'SERIAL', + INTSERIAL => 'SERIAL', - TINYTEXT => 'text', - MEDIUMTEXT => 'text', - LONGTEXT => 'text', + TINYTEXT => 'text', + MEDIUMTEXT => 'text', + LONGTEXT => 'text', - LONGBLOB => 'blob', + LONGBLOB => 'blob', - DATETIME => 'DATETIME', - DATE => 'DATETIME', - }; + DATETIME => 'DATETIME', + DATE => 'DATETIME', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; } @@ -61,83 +61,86 @@ sub _initialize { ################################# sub _sqlite_create_table { - my ($self, $table) = @_; - return scalar Bugzilla->dbh->selectrow_array( - "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'", - undef, $table); + my ($self, $table) = @_; + return + scalar Bugzilla->dbh->selectrow_array( + "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'", + undef, $table); } sub _sqlite_table_lines { - my $self = shift; - my $table_sql = $self->_sqlite_create_table(@_); - $table_sql =~ s/\n*\)$//s; - # The $ makes this work even if people some day add crazy stuff to their - # schema like multi-column foreign keys. - return split(/,\s*$/m, $table_sql); + my $self = shift; + my $table_sql = $self->_sqlite_create_table(@_); + $table_sql =~ s/\n*\)$//s; + + # The $ makes this work even if people some day add crazy stuff to their + # schema like multi-column foreign keys. + return split(/,\s*$/m, $table_sql); } # This does most of the "heavy lifting" of the schema-altering functions. sub _sqlite_alter_schema { - my ($self, $table, $create_table, $options) = @_; - - # $create_table is sometimes an array in the form that _sqlite_table_lines - # returns. - if (ref $create_table) { - $create_table = join(',', @$create_table) . "\n)"; - } - - my $dbh = Bugzilla->dbh; - - my $random = generate_random_password(5); - my $rename_to = "${table}_$random"; - - my @columns = $dbh->bz_table_columns_real($table); - push(@columns, $options->{extra_column}) if $options->{extra_column}; - if (my $exclude = $options->{exclude_column}) { - @columns = grep { $_ ne $exclude } @columns; - } - my @insert_cols = @columns; - my @select_cols = @columns; - if (my $rename = $options->{rename}) { - foreach my $from (keys %$rename) { - my $to = $rename->{$from}; - @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols; - } + my ($self, $table, $create_table, $options) = @_; + + # $create_table is sometimes an array in the form that _sqlite_table_lines + # returns. + if (ref $create_table) { + $create_table = join(',', @$create_table) . "\n)"; + } + + my $dbh = Bugzilla->dbh; + + my $random = generate_random_password(5); + my $rename_to = "${table}_$random"; + + my @columns = $dbh->bz_table_columns_real($table); + push(@columns, $options->{extra_column}) if $options->{extra_column}; + if (my $exclude = $options->{exclude_column}) { + @columns = grep { $_ ne $exclude } @columns; + } + my @insert_cols = @columns; + my @select_cols = @columns; + if (my $rename = $options->{rename}) { + foreach my $from (keys %$rename) { + my $to = $rename->{$from}; + @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols; } - - my $insert_str = join(',', @insert_cols); - my $select_str = join(',', @select_cols); - my $copy_sql = "INSERT INTO $table ($insert_str)" - . " SELECT $select_str FROM $rename_to"; - - # We have to turn FKs off before doing this. Otherwise, when we rename - # the table, all of the FKs in the other tables will be automatically - # updated to point to the renamed table. Note that PRAGMA foreign_keys - # can only be set outside of a transaction--otherwise it is a no-op. - if ($dbh->bz_in_transaction) { - die "can't alter the schema inside of a transaction"; - } - my @sql = ( - 'PRAGMA foreign_keys = OFF', - 'BEGIN EXCLUSIVE TRANSACTION', - @{ $options->{pre_sql} || [] }, - "ALTER TABLE $table RENAME TO $rename_to", - $create_table, - $copy_sql, - "DROP TABLE $rename_to", - 'COMMIT TRANSACTION', - 'PRAGMA foreign_keys = ON', - ); + } + + my $insert_str = join(',', @insert_cols); + my $select_str = join(',', @select_cols); + my $copy_sql + = "INSERT INTO $table ($insert_str)" . " SELECT $select_str FROM $rename_to"; + + # We have to turn FKs off before doing this. Otherwise, when we rename + # the table, all of the FKs in the other tables will be automatically + # updated to point to the renamed table. Note that PRAGMA foreign_keys + # can only be set outside of a transaction--otherwise it is a no-op. + if ($dbh->bz_in_transaction) { + die "can't alter the schema inside of a transaction"; + } + my @sql = ( + 'PRAGMA foreign_keys = OFF', + 'BEGIN EXCLUSIVE TRANSACTION', + @{$options->{pre_sql} || []}, + "ALTER TABLE $table RENAME TO $rename_to", + $create_table, + $copy_sql, + "DROP TABLE $rename_to", + 'COMMIT TRANSACTION', + 'PRAGMA foreign_keys = ON', + ); } # For finding a particular column's definition in a CREATE TABLE statement. sub _sqlite_column_regex { - my ($column) = @_; - # 1 = Comma at start - # 2 = Column name + Space - # 3 = Definition - # 4 = Ending comma - return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m; + my ($column) = @_; + + # 1 = Comma at start + # 2 = Column name + Space + # 3 = Definition + # 4 = Ending comma + return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m; } ############################# @@ -145,133 +148,137 @@ sub _sqlite_column_regex { ############################# sub get_create_database_sql { - # If we get here, it means there was some error creating the - # database file during bz_create_database in Bugzilla::DB, - # and we just want to display that error instead of doing - # anything else. - Bugzilla->dbh; - die "Reached an unreachable point"; + + # If we get here, it means there was some error creating the + # database file during bz_create_database in Bugzilla::DB, + # and we just want to display that error instead of doing + # anything else. + Bugzilla->dbh; + die "Reached an unreachable point"; } sub _get_create_table_ddl { - my $self = shift; - my ($table) = @_; - my $ddl = $self->SUPER::_get_create_table_ddl(@_); - - # TheSchwartz uses its own driver to access its tables, meaning - # that it doesn't understand "COLLATE bugzilla" and in fact - # SQLite throws an error when TheSchwartz tries to access its - # own tables, if COLLATE bugzilla is on them. We don't have - # to fix this elsewhere currently, because we only create - # TheSchwartz's tables, we never modify them. - if ($table =~ /^ts_/) { - $ddl =~ s/ COLLATE bugzilla//g; - } - return $ddl; + my $self = shift; + my ($table) = @_; + my $ddl = $self->SUPER::_get_create_table_ddl(@_); + + # TheSchwartz uses its own driver to access its tables, meaning + # that it doesn't understand "COLLATE bugzilla" and in fact + # SQLite throws an error when TheSchwartz tries to access its + # own tables, if COLLATE bugzilla is on them. We don't have + # to fix this elsewhere currently, because we only create + # TheSchwartz's tables, we never modify them. + if ($table =~ /^ts_/) { + $ddl =~ s/ COLLATE bugzilla//g; + } + return $ddl; } sub get_type_ddl { - my $self = shift; - my $def = dclone($_[0]); - - my $ddl = $self->SUPER::get_type_ddl(@_); - if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) { - $ddl =~ s/\bSERIAL\b/integer/; - $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/; - } - if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) { - $ddl .= " COLLATE bugzilla"; - } - # Don't collate DATETIME fields. - if ($def->{TYPE} eq 'DATETIME') { - $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/; - } - return $ddl; + my $self = shift; + my $def = dclone($_[0]); + + my $ddl = $self->SUPER::get_type_ddl(@_); + if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) { + $ddl =~ s/\bSERIAL\b/integer/; + $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/; + } + if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) { + $ddl .= " COLLATE bugzilla"; + } + + # Don't collate DATETIME fields. + if ($def->{TYPE} eq 'DATETIME') { + $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/; + } + return $ddl; } sub get_alter_column_ddl { - my $self = shift; - my ($table, $column, $new_def, $set_nulls_to) = @_; - my $dbh = Bugzilla->dbh; - - my $table_sql = $self->_sqlite_create_table($table); - my $new_ddl = $self->get_type_ddl($new_def); - # When we do ADD COLUMN, columns can show up all on one line separated - # by commas, so we have to account for that. - my $column_regex = _sqlite_column_regex($column); - $table_sql =~ s/$column_regex/$1$2$new_ddl$4/ - || die "couldn't find $column in $table:\n$table_sql"; - my @pre_sql = $self->_set_nulls_sql(@_); - return $self->_sqlite_alter_schema($table, $table_sql, - { pre_sql => \@pre_sql }); + my $self = shift; + my ($table, $column, $new_def, $set_nulls_to) = @_; + my $dbh = Bugzilla->dbh; + + my $table_sql = $self->_sqlite_create_table($table); + my $new_ddl = $self->get_type_ddl($new_def); + + # When we do ADD COLUMN, columns can show up all on one line separated + # by commas, so we have to account for that. + my $column_regex = _sqlite_column_regex($column); + $table_sql =~ s/$column_regex/$1$2$new_ddl$4/ + || die "couldn't find $column in $table:\n$table_sql"; + my @pre_sql = $self->_set_nulls_sql(@_); + return $self->_sqlite_alter_schema($table, $table_sql, {pre_sql => \@pre_sql}); } sub get_add_column_ddl { - my $self = shift; - my ($table, $column, $definition, $init_value) = @_; - # SQLite can use the normal ADD COLUMN when: - # * The column isn't a PK - if ($definition->{PRIMARYKEY}) { - if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) { - die "You can only add new SERIAL type PKs with SQLite"; - } - my $table_sql = $self->_sqlite_new_column_sql(@_); - # This works because _sqlite_alter_schema will exclude the new column - # in its INSERT ... SELECT statement, meaning that when the "new" - # table is populated, it will have AUTOINCREMENT values generated - # for it. - return $self->_sqlite_alter_schema($table, $table_sql); - } - # * The column has a default one way or another. Either it - # defaults to NULL (it lacks NOT NULL) or it has a DEFAULT - # clause. Since we also require this when doing bz_add_column (in - # the way of forcing an init_value for NOT NULL columns with no - # default), we first set the init_value as the default and then - # alter the column. - if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) { - my %with_default = %$definition; - $with_default{DEFAULT} = $init_value; - my @pre_sql = - $self->SUPER::get_add_column_ddl($table, $column, \%with_default); - my $table_sql = $self->_sqlite_new_column_sql(@_); - return $self->_sqlite_alter_schema($table, $table_sql, - { pre_sql => \@pre_sql, extra_column => $column }); + my $self = shift; + my ($table, $column, $definition, $init_value) = @_; + + # SQLite can use the normal ADD COLUMN when: + # * The column isn't a PK + if ($definition->{PRIMARYKEY}) { + if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) { + die "You can only add new SERIAL type PKs with SQLite"; } - - return $self->SUPER::get_add_column_ddl(@_); + my $table_sql = $self->_sqlite_new_column_sql(@_); + + # This works because _sqlite_alter_schema will exclude the new column + # in its INSERT ... SELECT statement, meaning that when the "new" + # table is populated, it will have AUTOINCREMENT values generated + # for it. + return $self->_sqlite_alter_schema($table, $table_sql); + } + + # * The column has a default one way or another. Either it + # defaults to NULL (it lacks NOT NULL) or it has a DEFAULT + # clause. Since we also require this when doing bz_add_column (in + # the way of forcing an init_value for NOT NULL columns with no + # default), we first set the init_value as the default and then + # alter the column. + if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) { + my %with_default = %$definition; + $with_default{DEFAULT} = $init_value; + my @pre_sql = $self->SUPER::get_add_column_ddl($table, $column, \%with_default); + my $table_sql = $self->_sqlite_new_column_sql(@_); + return $self->_sqlite_alter_schema($table, $table_sql, + {pre_sql => \@pre_sql, extra_column => $column}); + } + + return $self->SUPER::get_add_column_ddl(@_); } sub _sqlite_new_column_sql { - my ($self, $table, $column, $def) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $new_ddl = $self->get_type_ddl($def); - my $new_line = "\t$column\t$new_ddl"; - $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s - || die "Can't find start of CREATE TABLE:\n$table_sql"; - return $table_sql; + my ($self, $table, $column, $def) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $new_ddl = $self->get_type_ddl($def); + my $new_line = "\t$column\t$new_ddl"; + $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s + || die "Can't find start of CREATE TABLE:\n$table_sql"; + return $table_sql; } sub get_drop_column_ddl { - my ($self, $table, $column) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $column_regex = _sqlite_column_regex($column); - $table_sql =~ s/$column_regex/$1/ - || die "Can't find column $column: $table_sql"; - # Make sure we don't end up with a comma at the end of the definition. - $table_sql =~ s/,\s+\)$/\n)/s; - return $self->_sqlite_alter_schema($table, $table_sql, - { exclude_column => $column }); + my ($self, $table, $column) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $column_regex = _sqlite_column_regex($column); + $table_sql =~ s/$column_regex/$1/ + || die "Can't find column $column: $table_sql"; + + # Make sure we don't end up with a comma at the end of the definition. + $table_sql =~ s/,\s+\)$/\n)/s; + return $self->_sqlite_alter_schema($table, $table_sql, + {exclude_column => $column}); } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $column_regex = _sqlite_column_regex($old_name); - $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/ - || die "Can't find $old_name: $table_sql"; - my %rename = ($old_name => $new_name); - return $self->_sqlite_alter_schema($table, $table_sql, - { rename => \%rename }); + my ($self, $table, $old_name, $new_name) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $column_regex = _sqlite_column_regex($old_name); + $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/ + || die "Can't find $old_name: $table_sql"; + my %rename = ($old_name => $new_name); + return $self->_sqlite_alter_schema($table, $table_sql, {rename => \%rename}); } ################ @@ -279,24 +286,23 @@ sub get_rename_column_ddl { ################ sub get_add_fks_sql { - my ($self, $table, $column_fks) = @_; - my @clauses = $self->_sqlite_table_lines($table); - my @add = $self->_column_fks_to_ddl($table, $column_fks); - push(@clauses, @add); - return $self->_sqlite_alter_schema($table, \@clauses); + my ($self, $table, $column_fks) = @_; + my @clauses = $self->_sqlite_table_lines($table); + my @add = $self->_column_fks_to_ddl($table, $column_fks); + push(@clauses, @add); + return $self->_sqlite_alter_schema($table, \@clauses); } sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my @clauses = $self->_sqlite_table_lines($table); - my $fk_name = $self->_get_fk_name($table, $column, $references); - - my $line_re = qr/^\s+CONSTRAINT $fk_name /s; - grep { $line_re } @clauses - or die "Can't find $fk_name: " . join(',', @clauses); - @clauses = grep { $_ !~ $line_re } @clauses; - - return $self->_sqlite_alter_schema($table, \@clauses); + my ($self, $table, $column, $references) = @_; + my @clauses = $self->_sqlite_table_lines($table); + my $fk_name = $self->_get_fk_name($table, $column, $references); + + my $line_re = qr/^\s+CONSTRAINT $fk_name /s; + grep {$line_re} @clauses or die "Can't find $fk_name: " . join(',', @clauses); + @clauses = grep { $_ !~ $line_re } @clauses; + + return $self->_sqlite_alter_schema($table, \@clauses); } diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm index a56ed31ad..68180414c 100644 --- a/Bugzilla/DB/Sqlite.pm +++ b/Bugzilla/DB/Sqlite.pm @@ -46,23 +46,23 @@ sub _sqlite_collate_ci { lc($_[0]) cmp lc($_[1]) } sub _sqlite_mod { $_[0] % $_[1] } sub _sqlite_now { - my $now = DateTime->now(time_zone => Bugzilla->local_timezone); - return $now->ymd . ' ' . $now->hms; + my $now = DateTime->now(time_zone => Bugzilla->local_timezone); + return $now->ymd . ' ' . $now->hms; } # SQL's POSITION starts its values from 1 instead of 0 (so we add 1). sub _sqlite_position { - my ($text, $fragment) = @_; - if (!defined $text or !defined $fragment) { - return undef; - } - my $pos = index $text, $fragment; - return $pos + 1; + my ($text, $fragment) = @_; + if (!defined $text or !defined $fragment) { + return undef; + } + my $pos = index $text, $fragment; + return $pos + 1; } sub _sqlite_position_ci { - my ($text, $fragment) = @_; - return _sqlite_position(lc($text), lc($fragment)); + my ($text, $fragment) = @_; + return _sqlite_position(lc($text), lc($fragment)); } ############### @@ -70,76 +70,84 @@ sub _sqlite_position_ci { ############### sub new { - my ($class, $params) = @_; - my $db_name = $params->{db_name}; - - # Let people specify paths intead of data/ for the DB. - if ($db_name and $db_name !~ m{[\\/]}) { - # When the DB is first created, there's a chance that the - # data directory doesn't exist at all, because the Install::Filesystem - # code happens after DB creation. So we create the directory ourselves - # if it doesn't exist. - my $datadir = bz_locations()->{datadir}; - if (!-d $datadir) { - mkdir $datadir or warn "$datadir: $!"; - } - if (!-d "$datadir/db/") { - mkdir "$datadir/db/" or warn "$datadir/db: $!"; - } - $db_name = bz_locations()->{datadir} . "/db/$db_name"; + my ($class, $params) = @_; + my $db_name = $params->{db_name}; + + # Let people specify paths intead of data/ for the DB. + if ($db_name and $db_name !~ m{[\\/]}) { + + # When the DB is first created, there's a chance that the + # data directory doesn't exist at all, because the Install::Filesystem + # code happens after DB creation. So we create the directory ourselves + # if it doesn't exist. + my $datadir = bz_locations()->{datadir}; + if (!-d $datadir) { + mkdir $datadir or warn "$datadir: $!"; } - - # construct the DSN from the parameters we got - my $dsn = "dbi:SQLite:dbname=$db_name"; - - my $attrs = { - # XXX Should we just enforce this to be always on? - sqlite_unicode => Bugzilla->params->{'utf8'}, - }; - - my $self = $class->db_new({ dsn => $dsn, user => '', - pass => '', attrs => $attrs }); - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; - - my %pragmas = ( - # Make sure that the sqlite file doesn't grow without bound. - auto_vacuum => 1, - encoding => "'UTF-8'", - foreign_keys => 'ON', - # We want the latest file format. - legacy_file_format => 'OFF', - # This guarantees that we get column names like "foo" - # instead of "table.foo" in selectrow_hashref. - short_column_names => 'ON', - # The write-ahead log mode in SQLite 3.7 gets us better concurrency, - # but breaks backwards-compatibility with older versions of - # SQLite. (Which is important because people may also want to use - # command-line clients to access and back up their DB.) If you need - # better concurrency and don't need 3.6 compatibility, then you can - # uncomment this line. - #journal_mode => "'WAL'", - ); - - while (my ($name, $value) = each %pragmas) { - $self->do("PRAGMA $name = $value"); + if (!-d "$datadir/db/") { + mkdir "$datadir/db/" or warn "$datadir/db: $!"; } - - $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); - $self->sqlite_create_function('position', 2, \&_sqlite_position); - $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci); - # SQLite has a "substr" function, but other DBs call it "SUBSTRING" - # so that's what we use, and I don't know of any way in SQLite to - # alias the SQL "substr" function to be called "SUBSTRING". - $self->sqlite_create_function('substring', 3, \&CORE::substr); - $self->sqlite_create_function('char_length', 1, sub { length($_[0]) }); - $self->sqlite_create_function('mod', 2, \&_sqlite_mod); - $self->sqlite_create_function('now', 0, \&_sqlite_now); - $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); - $self->sqlite_create_function('floor', 1, \&POSIX::floor); - - bless ($self, $class); - return $self; + $db_name = bz_locations()->{datadir} . "/db/$db_name"; + } + + # construct the DSN from the parameters we got + my $dsn = "dbi:SQLite:dbname=$db_name"; + + my $attrs = { + + # XXX Should we just enforce this to be always on? + sqlite_unicode => Bugzilla->params->{'utf8'}, + }; + + my $self + = $class->db_new({dsn => $dsn, user => '', pass => '', attrs => $attrs}); + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + my %pragmas = ( + + # Make sure that the sqlite file doesn't grow without bound. + auto_vacuum => 1, + encoding => "'UTF-8'", + foreign_keys => 'ON', + + # We want the latest file format. + legacy_file_format => 'OFF', + + # This guarantees that we get column names like "foo" + # instead of "table.foo" in selectrow_hashref. + short_column_names => 'ON', + + # The write-ahead log mode in SQLite 3.7 gets us better concurrency, + # but breaks backwards-compatibility with older versions of + # SQLite. (Which is important because people may also want to use + # command-line clients to access and back up their DB.) If you need + # better concurrency and don't need 3.6 compatibility, then you can + # uncomment this line. + #journal_mode => "'WAL'", + ); + + while (my ($name, $value) = each %pragmas) { + $self->do("PRAGMA $name = $value"); + } + + $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); + $self->sqlite_create_function('position', 2, \&_sqlite_position); + $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci); + + # SQLite has a "substr" function, but other DBs call it "SUBSTRING" + # so that's what we use, and I don't know of any way in SQLite to + # alias the SQL "substr" function to be called "SUBSTRING". + $self->sqlite_create_function('substring', 3, \&CORE::substr); + $self->sqlite_create_function('char_length', 1, sub { length($_[0]) }); + $self->sqlite_create_function('mod', 2, \&_sqlite_mod); + $self->sqlite_create_function('now', 0, \&_sqlite_now); + $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); + $self->sqlite_create_function('floor', 1, \&POSIX::floor); + + bless($self, $class); + return $self; } ############### @@ -147,86 +155,89 @@ sub new { ############### sub sql_position { - my ($self, $fragment, $text) = @_; - return "POSITION($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "POSITION($text, $fragment)"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - return "IPOSITION($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "IPOSITION($text, $fragment)"; } # SQLite does not have to GROUP BY the optional columns. sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; - my $expression = "GROUP BY $needed_columns"; - return $expression; + my ($self, $needed_columns, $optional_columns) = @_; + my $expression = "GROUP BY $needed_columns"; + return $expression; } # XXX SQLite does not support sorting a GROUP_CONCAT, so $sort is unimplemented. sub sql_group_concat { - my ($self, $column, $separator, $sort) = @_; - $separator = $self->quote(', ') if !defined $separator; - # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't - # specify its separator, and has to accept the default of ",". - if ($column =~ /^DISTINCT/) { - return "GROUP_CONCAT($column)"; - } - return "GROUP_CONCAT($column, $separator)"; + my ($self, $column, $separator, $sort) = @_; + $separator = $self->quote(', ') if !defined $separator; + + # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't + # specify its separator, and has to accept the default of ",". + if ($column =~ /^DISTINCT/) { + return "GROUP_CONCAT($column)"; + } + return "GROUP_CONCAT($column, $separator)"; } sub sql_istring { - my ($self, $string) = @_; - return $string; + my ($self, $string) = @_; + return $string; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr REGEXP $pattern"; + return "$expr REGEXP $pattern"; } sub sql_not_regexp { - my $self = shift; - my $re_expression = $self->sql_regexp(@_); - return "NOT($re_expression)"; + my $self = shift; + my $re_expression = $self->sql_regexp(@_); + return "NOT($re_expression)"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $limit OFFSET $offset"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $limit OFFSET $offset"; + } + else { + return "LIMIT $limit"; + } } sub sql_from_days { - my ($self, $days) = @_; - return "DATETIME($days)"; + my ($self, $days) = @_; + return "DATETIME($days)"; } sub sql_to_days { - my ($self, $date) = @_; - return "JULIANDAY($date)"; + my ($self, $date) = @_; + return "JULIANDAY($date)"; } sub sql_date_format { - my ($self, $date, $format) = @_; - $format = "%Y.%m.%d %H:%M:%S" if !$format; - $format =~ s/\%i/\%M/g; - $format =~ s/\%s/\%S/g; - return "STRFTIME(" . $self->quote($format) . ", $date)"; + my ($self, $date, $format) = @_; + $format = "%Y.%m.%d %H:%M:%S" if !$format; + $format =~ s/\%i/\%M/g; + $format =~ s/\%s/\%S/g; + return "STRFTIME(" . $self->quote($format) . ", $date)"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - # We do the || thing (concatenation) so that placeholders work properly. - return "DATETIME($date, '$operator' || $interval || ' $units')"; + my ($self, $date, $operator, $interval, $units) = @_; + + # We do the || thing (concatenation) so that placeholders work properly. + return "DATETIME($date, '$operator' || $interval || ' $units')"; } ############### @@ -234,56 +245,57 @@ sub sql_date_math { ############### sub bz_setup_database { - my $self = shift; - $self->SUPER::bz_setup_database(@_); - - # If we created TheSchwartz tables with COLLATE bugzilla (during the - # 4.1.x development series) re-create them without it. - my @tables = $self->bz_table_list(); - my @ts_tables = grep { /^ts_/ } @tables; - my $drop_ok; - foreach my $table (@ts_tables) { - my $create_table = - $self->_bz_real_schema->_sqlite_create_table($table); - if ($create_table =~ /COLLATE bugzilla/) { - if (!$drop_ok) { - _sqlite_jobqueue_drop_message(); - $drop_ok = 1; - } - $self->bz_drop_table($table); - $self->bz_add_table($table); - } + my $self = shift; + $self->SUPER::bz_setup_database(@_); + + # If we created TheSchwartz tables with COLLATE bugzilla (during the + # 4.1.x development series) re-create them without it. + my @tables = $self->bz_table_list(); + my @ts_tables = grep {/^ts_/} @tables; + my $drop_ok; + foreach my $table (@ts_tables) { + my $create_table = $self->_bz_real_schema->_sqlite_create_table($table); + if ($create_table =~ /COLLATE bugzilla/) { + if (!$drop_ok) { + _sqlite_jobqueue_drop_message(); + $drop_ok = 1; + } + $self->bz_drop_table($table); + $self->bz_add_table($table); } + } } sub _sqlite_jobqueue_drop_message { - # This is not translated because this situation will only happen if - # you are updating from a 4.1.x development version of Bugzilla using - # SQLite, and we don't want to maintain this string in strings.txt.pl - # forever for just this one uncommon circumstance. - print <<END; + + # This is not translated because this situation will only happen if + # you are updating from a 4.1.x development version of Bugzilla using + # SQLite, and we don't want to maintain this string in strings.txt.pl + # forever for just this one uncommon circumstance. + print <<END; WARNING: We have to re-create all the database tables used by jobqueue.pl. If there are any pending jobs in the database (that is, emails that haven't been sent), they will be deleted. END - unless (Bugzilla->installation_answers->{NO_PAUSE}) { - print install_string('enter_or_ctrl_c'); - getc; - } + unless (Bugzilla->installation_answers->{NO_PAUSE}) { + print install_string('enter_or_ctrl_c'); + getc; + } } # XXX This needs to be implemented. sub bz_explain { } sub bz_table_list_real { - my $self = shift; - my @tables = $self->SUPER::bz_table_list_real(@_); - # SQLite includes a sqlite_sequence table in every database that isn't - # one of our real tables. We exclude any table that starts with sqlite_, - # just to be safe. - @tables = grep { $_ !~ /^sqlite_/ } @tables; - return @tables; + my $self = shift; + my @tables = $self->SUPER::bz_table_list_real(@_); + + # SQLite includes a sqlite_sequence table in every database that isn't + # one of our real tables. We exclude any table that starts with sqlite_, + # just to be safe. + @tables = grep { $_ !~ /^sqlite_/ } @tables; + return @tables; } 1; diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index ef6320d15..ac56b9b02 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -26,176 +26,185 @@ use Date::Format; # We cannot use $^S to detect if we are in an eval(), because mod_perl # already eval'uates everything, so $^S = 1 in all cases under mod_perl! sub _in_eval { - my $in_eval = 0; - for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - last if $sub =~ /^ModPerl/; - $in_eval = 1 if $sub =~ /^\(eval\)/; - } - return $in_eval; + my $in_eval = 0; + for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { + last if $sub =~ /^ModPerl/; + $in_eval = 1 if $sub =~ /^\(eval\)/; + } + return $in_eval; } sub _throw_error { - my ($name, $error, $vars) = @_; - my $dbh = Bugzilla->dbh; - $vars ||= {}; - - $vars->{error} = $error; - - # Make sure any transaction is rolled back (if supported). - # If we are within an eval(), do not roll back transactions as we are - # eval'uating some test on purpose. - $dbh->bz_rollback_transaction() if ($dbh->bz_in_transaction() && !_in_eval()); - - my $datadir = bz_locations()->{'datadir'}; - # If a writable $datadir/errorlog exists, log error details there. - if (-w "$datadir/errorlog") { - require Bugzilla::Util; - require Data::Dumper; - my $mesg = ""; - for (1..75) { $mesg .= "-"; }; - $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time()); - $mesg .= "$name $error "; - $mesg .= Bugzilla::Util::remote_ip(); - $mesg .= Bugzilla->user->login; - $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer; - $mesg .= "\n"; - my %params = Bugzilla->cgi->Vars; - $Data::Dumper::Useqq = 1; - for my $param (sort keys %params) { - my $val = $params{$param}; - # obscure passwords - $val = "*****" if $param =~ /password/i; - # limit line length - $val =~ s/^(.{512}).*$/$1\[CHOP\]/; - $mesg .= "[$$] " . Data::Dumper->Dump([$val],["param($param)"]); - } - for my $var (sort keys %ENV) { - my $val = $ENV{$var}; - $val = "*****" if $val =~ /password|http_pass/i; - $mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]); - } - open(ERRORLOGFID, ">>", "$datadir/errorlog"); - print ERRORLOGFID "$mesg\n"; - close ERRORLOGFID; - } - - my $template = Bugzilla->template; - my $message; - # There are some tests that throw and catch a lot of errors, - # and calling $template->process over and over for those errors - # is too slow. So instead, we just "die" with a dump of the arguments. - if (Bugzilla->error_mode != ERROR_MODE_TEST) { - $template->process($name, $vars, \$message) - || ThrowTemplateError($template->error()); + my ($name, $error, $vars) = @_; + my $dbh = Bugzilla->dbh; + $vars ||= {}; + + $vars->{error} = $error; + + # Make sure any transaction is rolled back (if supported). + # If we are within an eval(), do not roll back transactions as we are + # eval'uating some test on purpose. + $dbh->bz_rollback_transaction() if ($dbh->bz_in_transaction() && !_in_eval()); + + my $datadir = bz_locations()->{'datadir'}; + + # If a writable $datadir/errorlog exists, log error details there. + if (-w "$datadir/errorlog") { + require Bugzilla::Util; + require Data::Dumper; + my $mesg = ""; + for (1 .. 75) { $mesg .= "-"; } + $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time()); + $mesg .= "$name $error "; + $mesg .= Bugzilla::Util::remote_ip(); + $mesg .= Bugzilla->user->login; + $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer; + $mesg .= "\n"; + my %params = Bugzilla->cgi->Vars; + $Data::Dumper::Useqq = 1; + + for my $param (sort keys %params) { + my $val = $params{$param}; + + # obscure passwords + $val = "*****" if $param =~ /password/i; + + # limit line length + $val =~ s/^(.{512}).*$/$1\[CHOP\]/; + $mesg .= "[$$] " . Data::Dumper->Dump([$val], ["param($param)"]); } - - # Let's call the hook first, so that extensions can override - # or extend the default behavior, or add their own error codes. - Bugzilla::Hook::process('error_catch', { error => $error, vars => $vars, - message => \$message }); - - if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { - my $cgi = Bugzilla->cgi; - $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); - print $message; - print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; + for my $var (sort keys %ENV) { + my $val = $ENV{$var}; + $val = "*****" if $val =~ /password|http_pass/i; + $mesg .= "[$$] " . Data::Dumper->Dump([$val], ["env($var)"]); } - elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { - die Dumper($vars); + open(ERRORLOGFID, ">>", "$datadir/errorlog"); + print ERRORLOGFID "$mesg\n"; + close ERRORLOGFID; + } + + my $template = Bugzilla->template; + my $message; + + # There are some tests that throw and catch a lot of errors, + # and calling $template->process over and over for those errors + # is too slow. So instead, we just "die" with a dump of the arguments. + if (Bugzilla->error_mode != ERROR_MODE_TEST) { + $template->process($name, $vars, \$message) + || ThrowTemplateError($template->error()); + } + + # Let's call the hook first, so that extensions can override + # or extend the default behavior, or add their own error codes. + Bugzilla::Hook::process('error_catch', + {error => $error, vars => $vars, message => \$message}); + + if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { + my $cgi = Bugzilla->cgi; + $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); + print $message; + print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; + } + elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { + die Dumper($vars); + } + elsif (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("$message\n"); + } + elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT + || Bugzilla->error_mode == ERROR_MODE_JSON_RPC + || Bugzilla->error_mode == ERROR_MODE_REST) + { + # Clone the hash so we aren't modifying the constant. + my %error_map = %{WS_ERROR_CODE()}; + Bugzilla::Hook::process('webservice_error_codes', {error_map => \%error_map}); + my $code = $error_map{$error}; + if (!$code) { + $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i; + $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i; } - elsif (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("$message\n"); + + if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { + die SOAP::Fault->faultcode($code)->faultstring($message); } - elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT - || Bugzilla->error_mode == ERROR_MODE_JSON_RPC - || Bugzilla->error_mode == ERROR_MODE_REST) - { - # Clone the hash so we aren't modifying the constant. - my %error_map = %{ WS_ERROR_CODE() }; - Bugzilla::Hook::process('webservice_error_codes', - { error_map => \%error_map }); - my $code = $error_map{$error}; - if (!$code) { - $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i; - $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i; - } - - if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { - die SOAP::Fault->faultcode($code)->faultstring($message); - } - else { - my $server = Bugzilla->_json_server; - - my $status_code = 0; - if (Bugzilla->error_mode == ERROR_MODE_REST) { - my %status_code_map = %{ REST_STATUS_CODE_MAP() }; - $status_code = $status_code_map{$code} || $status_code_map{'_default'}; - } - # Technically JSON-RPC isn't allowed to have error numbers - # higher than 999, but we do this to avoid conflicts with - # the internal JSON::RPC error codes. - $server->raise_error(code => 100000 + $code, - status_code => $status_code, - message => $message, - id => $server->{_bz_request_id}, - version => $server->version); - # Most JSON-RPC Throw*Error calls happen within an eval inside - # of JSON::RPC. So, in that circumstance, instead of exiting, - # we die with no message. JSON::RPC checks raise_error before - # it checks $@, so it returns the proper error. - die if _in_eval(); - $server->response($server->error_response_header); - } + else { + my $server = Bugzilla->_json_server; + + my $status_code = 0; + if (Bugzilla->error_mode == ERROR_MODE_REST) { + my %status_code_map = %{REST_STATUS_CODE_MAP()}; + $status_code = $status_code_map{$code} || $status_code_map{'_default'}; + } + + # Technically JSON-RPC isn't allowed to have error numbers + # higher than 999, but we do this to avoid conflicts with + # the internal JSON::RPC error codes. + $server->raise_error( + code => 100000 + $code, + status_code => $status_code, + message => $message, + id => $server->{_bz_request_id}, + version => $server->version + ); + + # Most JSON-RPC Throw*Error calls happen within an eval inside + # of JSON::RPC. So, in that circumstance, instead of exiting, + # we die with no message. JSON::RPC checks raise_error before + # it checks $@, so it returns the proper error. + die if _in_eval(); + $server->response($server->error_response_header); } - exit; + } + exit; } sub ThrowUserError { - _throw_error("global/user-error.html.tmpl", @_); + _throw_error("global/user-error.html.tmpl", @_); } sub ThrowCodeError { - my (undef, $vars) = @_; + my (undef, $vars) = @_; + + # Don't show function arguments, in case they contain + # confidential data. + local $Carp::MaxArgNums = -1; - # Don't show function arguments, in case they contain - # confidential data. - local $Carp::MaxArgNums = -1; - # Don't show the error as coming from Bugzilla::Error, show it - # as coming from the caller. - local $Carp::CarpInternal{'Bugzilla::Error'} = 1; - $vars->{traceback} = Carp::longmess(); + # Don't show the error as coming from Bugzilla::Error, show it + # as coming from the caller. + local $Carp::CarpInternal{'Bugzilla::Error'} = 1; + $vars->{traceback} = Carp::longmess(); - _throw_error("global/code-error.html.tmpl", @_); + _throw_error("global/code-error.html.tmpl", @_); } sub ThrowTemplateError { - my ($template_err) = @_; - my $dbh = Bugzilla->dbh; + my ($template_err) = @_; + my $dbh = Bugzilla->dbh; - # Make sure the transaction is rolled back (if supported). - $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); + # Make sure the transaction is rolled back (if supported). + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); - my $vars = {}; - if (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("error: template error: $template_err"); - } + my $vars = {}; + if (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("error: template error: $template_err"); + } - $vars->{'template_error_msg'} = $template_err; - $vars->{'error'} = "template_error"; + $vars->{'template_error_msg'} = $template_err; + $vars->{'error'} = "template_error"; - my $template = Bugzilla->template; + my $template = Bugzilla->template; - # Try a template first; but if this one fails too, fall back - # on plain old print statements. - if (!$template->process("global/code-error.html.tmpl", $vars)) { - require Bugzilla::Util; - import Bugzilla::Util qw(html_quote); - my $maintainer = Bugzilla->params->{'maintainer'}; - my $error = html_quote($vars->{'template_error_msg'}); - my $error2 = html_quote($template->error()); - my $url = html_quote(Bugzilla->cgi->self_url); + # Try a template first; but if this one fails too, fall back + # on plain old print statements. + if (!$template->process("global/code-error.html.tmpl", $vars)) { + require Bugzilla::Util; + import Bugzilla::Util qw(html_quote); + my $maintainer = Bugzilla->params->{'maintainer'}; + my $error = html_quote($vars->{'template_error_msg'}); + my $error2 = html_quote($template->error()); + my $url = html_quote(Bugzilla->cgi->self_url); - print <<END; + print <<END; <p> Bugzilla has suffered an internal error. Please save this page and send it to $maintainer with details of what you were doing at the @@ -206,8 +215,8 @@ sub ThrowTemplateError { First error: $error<br> Second error: $error2</p> END - } - exit; + } + exit; } 1; diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm index e24ceb9eb..746fc4bfd 100644 --- a/Bugzilla/Extension.pm +++ b/Bugzilla/Extension.pm @@ -14,8 +14,8 @@ use warnings; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Install::Util qw( - extension_code_files extension_template_directory - extension_package_directory extension_web_directory); + extension_code_files extension_template_directory + extension_package_directory extension_web_directory); use File::Basename; use File::Spec; @@ -25,10 +25,10 @@ use File::Spec; #################### sub new { - my ($class, $params) = @_; - $params ||= {}; - bless $params, $class; - return $params; + my ($class, $params) = @_; + $params ||= {}; + bless $params, $class; + return $params; } ####################################### @@ -36,148 +36,151 @@ sub new { ####################################### sub load { - my ($class, $extension_file, $config_file) = @_; - my $package; - - # This is needed during checksetup.pl, because Extension packages can - # only be loaded once (they return "1" the second time they're loaded, - # instead of their name). During checksetup.pl, extensions are loaded - # once by Bugzilla::Install::Requirements, and then later again via - # Bugzilla->extensions (because of hooks). - my $map = Bugzilla->request_cache->{extension_requirement_package_map}; - - if ($config_file) { - if ($map and defined $map->{$config_file}) { - $package = $map->{$config_file}; - } - else { - my $name = require $config_file; - if ($name =~ /^\d+$/) { - ThrowCodeError('extension_must_return_name', - { extension => $config_file, - returned => $name }); - } - $package = "${class}::$name"; - } - - __do_call($package, 'modify_inc', $config_file); - } - - if ($map and defined $map->{$extension_file}) { - $package = $map->{$extension_file}; - $package->modify_inc($extension_file) if !$config_file; + my ($class, $extension_file, $config_file) = @_; + my $package; + + # This is needed during checksetup.pl, because Extension packages can + # only be loaded once (they return "1" the second time they're loaded, + # instead of their name). During checksetup.pl, extensions are loaded + # once by Bugzilla::Install::Requirements, and then later again via + # Bugzilla->extensions (because of hooks). + my $map = Bugzilla->request_cache->{extension_requirement_package_map}; + + if ($config_file) { + if ($map and defined $map->{$config_file}) { + $package = $map->{$config_file}; } else { - my $name = require $extension_file; - if ($name =~ /^\d+$/) { - ThrowCodeError('extension_must_return_name', - { extension => $extension_file, returned => $name }); - } - $package = "${class}::$name"; - $package->modify_inc($extension_file) if !$config_file; + my $name = require $config_file; + if ($name =~ /^\d+$/) { + ThrowCodeError('extension_must_return_name', + {extension => $config_file, returned => $name}); + } + $package = "${class}::$name"; } - $class->_validate_package($package, $extension_file); - return $package; -} - -sub _validate_package { - my ($class, $package, $extension_file) = @_; - - # For extensions from data/extensions/additional, we don't have a file - # name, so we fake it. - if (!$extension_file) { - $extension_file = $package; - $extension_file =~ s/::/\//g; - $extension_file .= '.pm'; + __do_call($package, 'modify_inc', $config_file); + } + + if ($map and defined $map->{$extension_file}) { + $package = $map->{$extension_file}; + $package->modify_inc($extension_file) if !$config_file; + } + else { + my $name = require $extension_file; + if ($name =~ /^\d+$/) { + ThrowCodeError('extension_must_return_name', + {extension => $extension_file, returned => $name}); } + $package = "${class}::$name"; + $package->modify_inc($extension_file) if !$config_file; + } - if (!eval { $package->NAME }) { - ThrowCodeError('extension_no_name', - { filename => $extension_file, package => $package }); - } + $class->_validate_package($package, $extension_file); + return $package; +} - if (!$package->isa($class)) { - ThrowCodeError('extension_must_be_subclass', - { filename => $extension_file, - package => $package, - class => $class }); - } +sub _validate_package { + my ($class, $package, $extension_file) = @_; + + # For extensions from data/extensions/additional, we don't have a file + # name, so we fake it. + if (!$extension_file) { + $extension_file = $package; + $extension_file =~ s/::/\//g; + $extension_file .= '.pm'; + } + + if (!eval { $package->NAME }) { + ThrowCodeError('extension_no_name', + {filename => $extension_file, package => $package}); + } + + if (!$package->isa($class)) { + ThrowCodeError('extension_must_be_subclass', + {filename => $extension_file, package => $package, class => $class}); + } } sub load_all { - my $class = shift; - my ($file_sets, $extra_packages) = extension_code_files(); - my @packages; - foreach my $file_set (@$file_sets) { - my $package = $class->load(@$file_set); - push(@packages, $package); - } - - # Extensions from data/extensions/additional - foreach my $package (@$extra_packages) { - # Don't load an "additional" extension if we already have an extension - # loaded with that name. - next if grep($_ eq $package, @packages); - # Untaint the package name - $package =~ /([\w:]+)/; - $package = $1; - eval("require $package") || die $@; - $package->_validate_package($package); - push(@packages, $package); - } - - return \@packages; + my $class = shift; + my ($file_sets, $extra_packages) = extension_code_files(); + my @packages; + foreach my $file_set (@$file_sets) { + my $package = $class->load(@$file_set); + push(@packages, $package); + } + + # Extensions from data/extensions/additional + foreach my $package (@$extra_packages) { + + # Don't load an "additional" extension if we already have an extension + # loaded with that name. + next if grep($_ eq $package, @packages); + + # Untaint the package name + $package =~ /([\w:]+)/; + $package = $1; + eval("require $package") || die $@; + $package->_validate_package($package); + push(@packages, $package); + } + + return \@packages; } # Modifies @INC so that extensions can use modules like # "use Bugzilla::Extension::Foo::Bar", when Bar.pm is in the lib/ # directory of the extension. sub modify_inc { - my ($class, $file) = @_; - - # Note that this package_dir call is necessary to set things up - # for my_inc, even if we didn't take its return value. - my $package_dir = __do_call($class, 'package_dir', $file); - # Don't modify @INC for extensions that are just files in the extensions/ - # directory. We don't want Bugzilla's base lib/CGI.pm being loaded as - # Bugzilla::Extension::Foo::CGI or any other confusing thing like that. - return if $package_dir eq bz_locations->{'extensionsdir'}; - unshift(@INC, sub { __do_call($class, 'my_inc', @_) }); + my ($class, $file) = @_; + + # Note that this package_dir call is necessary to set things up + # for my_inc, even if we didn't take its return value. + my $package_dir = __do_call($class, 'package_dir', $file); + + # Don't modify @INC for extensions that are just files in the extensions/ + # directory. We don't want Bugzilla's base lib/CGI.pm being loaded as + # Bugzilla::Extension::Foo::CGI or any other confusing thing like that. + return if $package_dir eq bz_locations->{'extensionsdir'}; + unshift(@INC, sub { __do_call($class, 'my_inc', @_) }); } # This is what gets put into @INC by modify_inc. sub my_inc { - my ($class, undef, $file) = @_; - - # This avoids infinite recursion in case anything inside of this function - # does a "require". (I know for sure that File::Spec->case_tolerant does - # a "require" on Windows, for example.) - return if $file !~ /^Bugzilla/; - - my $lib_dir = __do_call($class, 'lib_dir'); - my @class_parts = split('::', $class); - my ($vol, $dir, $file_name) = File::Spec->splitpath($file); - my @dir_parts = File::Spec->splitdir($dir); - # File::Spec::Win32 (any maybe other OSes) add an empty directory at the - # end of @dir_parts. - @dir_parts = grep { $_ ne '' } @dir_parts; - # Validate that this is a sub-package of Bugzilla::Extension::Foo ($class). - for (my $i = 0; $i < scalar(@class_parts); $i++) { - return if !@dir_parts; - if (File::Spec->case_tolerant) { - return if lc($class_parts[$i]) ne lc($dir_parts[0]); - } - else { - return if $class_parts[$i] ne $dir_parts[0]; - } - shift(@dir_parts); + my ($class, undef, $file) = @_; + + # This avoids infinite recursion in case anything inside of this function + # does a "require". (I know for sure that File::Spec->case_tolerant does + # a "require" on Windows, for example.) + return if $file !~ /^Bugzilla/; + + my $lib_dir = __do_call($class, 'lib_dir'); + my @class_parts = split('::', $class); + my ($vol, $dir, $file_name) = File::Spec->splitpath($file); + my @dir_parts = File::Spec->splitdir($dir); + + # File::Spec::Win32 (any maybe other OSes) add an empty directory at the + # end of @dir_parts. + @dir_parts = grep { $_ ne '' } @dir_parts; + + # Validate that this is a sub-package of Bugzilla::Extension::Foo ($class). + for (my $i = 0; $i < scalar(@class_parts); $i++) { + return if !@dir_parts; + if (File::Spec->case_tolerant) { + return if lc($class_parts[$i]) ne lc($dir_parts[0]); } - # For Bugzilla::Extension::Foo::Bar, this would look something like - # extensions/Example/lib/Bar.pm - my $resolved_path = File::Spec->catfile($lib_dir, @dir_parts, $file_name); - open(my $fh, '<', $resolved_path); - return $fh; + else { + return if $class_parts[$i] ne $dir_parts[0]; + } + shift(@dir_parts); + } + + # For Bugzilla::Extension::Foo::Bar, this would look something like + # extensions/Example/lib/Bar.pm + my $resolved_path = File::Spec->catfile($lib_dir, @dir_parts, $file_name); + open(my $fh, '<', $resolved_path); + return $fh; } #################### @@ -187,23 +190,24 @@ sub my_inc { use constant enabled => 1; sub lib_dir { - my $invocant = shift; - my $package_dir = __do_call($invocant, 'package_dir'); - # For extensions that are just files in the extensions/ directory, - # use the base lib/ dir as our "lib_dir". Note that Bugzilla never - # uses lib_dir in this case, though, because modify_inc is prevented - # from modifying @INC when we're just a file in the extensions/ directory. - # So this particular code block exists just to make lib_dir return - # something right in case an extension needs it for some odd reason. - if ($package_dir eq bz_locations()->{'extensionsdir'}) { - return bz_locations->{'ext_libpath'}; - } - return File::Spec->catdir($package_dir, 'lib'); + my $invocant = shift; + my $package_dir = __do_call($invocant, 'package_dir'); + + # For extensions that are just files in the extensions/ directory, + # use the base lib/ dir as our "lib_dir". Note that Bugzilla never + # uses lib_dir in this case, though, because modify_inc is prevented + # from modifying @INC when we're just a file in the extensions/ directory. + # So this particular code block exists just to make lib_dir return + # something right in case an extension needs it for some odd reason. + if ($package_dir eq bz_locations()->{'extensionsdir'}) { + return bz_locations->{'ext_libpath'}; + } + return File::Spec->catdir($package_dir, 'lib'); } sub template_dir { return extension_template_directory(@_); } -sub package_dir { return extension_package_directory(@_); } -sub web_dir { return extension_web_directory(@_); } +sub package_dir { return extension_package_directory(@_); } +sub web_dir { return extension_web_directory(@_); } ###################### # Helper Subroutines # @@ -217,13 +221,13 @@ sub web_dir { return extension_web_directory(@_); } # the method. This is necessary because Config.pm is not a subclass of # Bugzilla::Extension. sub __do_call { - my ($class, $method, @args) = @_; - if ($class->can($method)) { - return $class->$method(@args); - } - my $function_ref; - { no strict 'refs'; $function_ref = \&{$method}; } - return $function_ref->($class, @args); + my ($class, $method, @args) = @_; + if ($class->can($method)) { + return $class->$method(@args); + } + my $function_ref; + { no strict 'refs'; $function_ref = \&{$method}; } + return $function_ref->($class, @args); } 1; diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm index 761f7b94e..a7b543974 100644 --- a/Bugzilla/Field.pm +++ b/Bugzilla/Field.pm @@ -81,82 +81,80 @@ use constant DB_TABLE => 'fielddefs'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - long_desc - type - custom - mailhead - sortkey - obsolete - enter_bug - buglist - visibility_field_id - value_field_id - reverse_desc - is_mandatory - is_numeric + id + name + description + long_desc + type + custom + mailhead + sortkey + obsolete + enter_bug + buglist + visibility_field_id + value_field_id + reverse_desc + is_mandatory + is_numeric ); use constant VALIDATORS => { - custom => \&_check_custom, - description => \&_check_description, - long_desc => \&_check_long_desc, - enter_bug => \&_check_enter_bug, - buglist => \&Bugzilla::Object::check_boolean, - mailhead => \&_check_mailhead, - name => \&_check_name, - obsolete => \&_check_obsolete, - reverse_desc => \&_check_reverse_desc, - sortkey => \&_check_sortkey, - type => \&_check_type, - value_field_id => \&_check_value_field_id, - visibility_field_id => \&_check_visibility_field_id, - visibility_values => \&_check_visibility_values, - is_mandatory => \&Bugzilla::Object::check_boolean, - is_numeric => \&_check_is_numeric, + custom => \&_check_custom, + description => \&_check_description, + long_desc => \&_check_long_desc, + enter_bug => \&_check_enter_bug, + buglist => \&Bugzilla::Object::check_boolean, + mailhead => \&_check_mailhead, + name => \&_check_name, + obsolete => \&_check_obsolete, + reverse_desc => \&_check_reverse_desc, + sortkey => \&_check_sortkey, + type => \&_check_type, + value_field_id => \&_check_value_field_id, + visibility_field_id => \&_check_visibility_field_id, + visibility_values => \&_check_visibility_values, + is_mandatory => \&Bugzilla::Object::check_boolean, + is_numeric => \&_check_is_numeric, }; use constant VALIDATOR_DEPENDENCIES => { - is_numeric => ['type'], - name => ['custom'], - type => ['custom'], - reverse_desc => ['type'], - value_field_id => ['type'], - visibility_values => ['visibility_field_id'], + is_numeric => ['type'], + name => ['custom'], + type => ['custom'], + reverse_desc => ['type'], + value_field_id => ['type'], + visibility_values => ['visibility_field_id'], }; use constant UPDATE_COLUMNS => qw( - description - long_desc - mailhead - sortkey - obsolete - enter_bug - buglist - visibility_field_id - value_field_id - reverse_desc - is_mandatory - is_numeric - type + description + long_desc + mailhead + sortkey + obsolete + enter_bug + buglist + visibility_field_id + value_field_id + reverse_desc + is_mandatory + is_numeric + type ); # How various field types translate into SQL data definitions. use constant SQL_DEFINITIONS => { - # Using commas because these are constants and they shouldn't - # be auto-quoted by the "=>" operator. - FIELD_TYPE_FREETEXT, { TYPE => 'varchar(255)', - NOTNULL => 1, DEFAULT => "''"}, - FIELD_TYPE_SINGLE_SELECT, { TYPE => 'varchar(64)', NOTNULL => 1, - DEFAULT => "'---'" }, - FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT', - NOTNULL => 1, DEFAULT => "''"}, - FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' }, - FIELD_TYPE_DATE, { TYPE => 'DATE' }, - FIELD_TYPE_BUG_ID, { TYPE => 'INT3' }, - FIELD_TYPE_INTEGER, { TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0 }, + + # Using commas because these are constants and they shouldn't + # be auto-quoted by the "=>" operator. + FIELD_TYPE_FREETEXT, {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + FIELD_TYPE_SINGLE_SELECT, + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, FIELD_TYPE_TEXTAREA, + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, FIELD_TYPE_DATETIME, + {TYPE => 'DATETIME'}, FIELD_TYPE_DATE, {TYPE => 'DATE'}, FIELD_TYPE_BUG_ID, + {TYPE => 'INT3'}, FIELD_TYPE_INTEGER, + {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, }; # Field definitions for the fields that ship with Bugzilla. @@ -164,110 +162,232 @@ use constant SQL_DEFINITIONS => { # the fielddefs table. # 'days_elapsed' is set in populate_field_definitions() itself. use constant DEFAULT_FIELDS => ( - {name => 'bug_id', desc => 'Bug #', in_new_bugmail => 1, - buglist => 1, is_numeric => 1}, - {name => 'short_desc', desc => 'Summary', in_new_bugmail => 1, - is_mandatory => 1, buglist => 1}, - {name => 'classification', desc => 'Classification', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'product', desc => 'Product', in_new_bugmail => 1, - is_mandatory => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'version', desc => 'Version', in_new_bugmail => 1, - is_mandatory => 1, buglist => 1}, - {name => 'rep_platform', desc => 'Platform', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_file_loc', desc => 'URL', in_new_bugmail => 1, - buglist => 1}, - {name => 'op_sys', desc => 'OS/Version', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_status', desc => 'Status', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'status_whiteboard', desc => 'Status Whiteboard', - in_new_bugmail => 1, buglist => 1}, - {name => 'keywords', desc => 'Keywords', in_new_bugmail => 1, - type => FIELD_TYPE_KEYWORDS, buglist => 1}, - {name => 'resolution', desc => 'Resolution', - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_severity', desc => 'Severity', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'priority', desc => 'Priority', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'component', desc => 'Component', in_new_bugmail => 1, - is_mandatory => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'assigned_to', desc => 'AssignedTo', in_new_bugmail => 1, - buglist => 1}, - {name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1, - buglist => 1}, - {name => 'qa_contact', desc => 'QAContact', in_new_bugmail => 1, - buglist => 1}, - {name => 'assigned_to_realname', desc => 'AssignedToName', - in_new_bugmail => 0, buglist => 1}, - {name => 'reporter_realname', desc => 'ReportedByName', - in_new_bugmail => 0, buglist => 1}, - {name => 'qa_contact_realname', desc => 'QAContactName', - in_new_bugmail => 0, buglist => 1}, - {name => 'cc', desc => 'CC', in_new_bugmail => 1}, - {name => 'dependson', desc => 'Depends on', in_new_bugmail => 1, - is_numeric => 1, buglist => 1}, - {name => 'blocked', desc => 'Blocks', in_new_bugmail => 1, - is_numeric => 1, buglist => 1}, - - {name => 'attachments.description', desc => 'Attachment description'}, - {name => 'attachments.filename', desc => 'Attachment filename'}, - {name => 'attachments.mimetype', desc => 'Attachment mime type'}, - {name => 'attachments.ispatch', desc => 'Attachment is patch', - is_numeric => 1}, - {name => 'attachments.isobsolete', desc => 'Attachment is obsolete', - is_numeric => 1}, - {name => 'attachments.isprivate', desc => 'Attachment is private', - is_numeric => 1}, - {name => 'attachments.submitter', desc => 'Attachment creator'}, - - {name => 'target_milestone', desc => 'Target Milestone', - in_new_bugmail => 1, buglist => 1}, - {name => 'creation_ts', desc => 'Creation date', - buglist => 1}, - {name => 'delta_ts', desc => 'Last changed date', - buglist => 1}, - {name => 'longdesc', desc => 'Comment'}, - {name => 'longdescs.isprivate', desc => 'Comment is private', - is_numeric => 1}, - {name => 'longdescs.count', desc => 'Number of Comments', - buglist => 1, is_numeric => 1}, - {name => 'alias', desc => 'Alias', buglist => 1}, - {name => 'everconfirmed', desc => 'Ever Confirmed', - is_numeric => 1}, - {name => 'reporter_accessible', desc => 'Reporter Accessible', - is_numeric => 1}, - {name => 'cclist_accessible', desc => 'CC Accessible', - is_numeric => 1}, - {name => 'bug_group', desc => 'Group', in_new_bugmail => 1}, - {name => 'estimated_time', desc => 'Estimated Hours', - in_new_bugmail => 1, buglist => 1, is_numeric => 1}, - {name => 'remaining_time', desc => 'Remaining Hours', buglist => 1, - is_numeric => 1}, - {name => 'deadline', desc => 'Deadline', - type => FIELD_TYPE_DATETIME, in_new_bugmail => 1, buglist => 1}, - {name => 'commenter', desc => 'Commenter'}, - {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, - is_numeric => 1}, - {name => 'percentage_complete', desc => 'Percentage Complete', - buglist => 1, is_numeric => 1}, - {name => 'content', desc => 'Content'}, - {name => 'attach_data.thedata', desc => 'Attachment data'}, - {name => "owner_idle_time", desc => "Time Since Assignee Touched"}, - {name => 'see_also', desc => "See Also", - type => FIELD_TYPE_BUG_URLS}, - {name => 'tag', desc => 'Personal Tags', buglist => 1, - type => FIELD_TYPE_KEYWORDS}, - {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1, - type => FIELD_TYPE_DATETIME}, - {name => 'comment_tag', desc => 'Comment Tag'}, + { + name => 'bug_id', + desc => 'Bug #', + in_new_bugmail => 1, + buglist => 1, + is_numeric => 1 + }, + { + name => 'short_desc', + desc => 'Summary', + in_new_bugmail => 1, + is_mandatory => 1, + buglist => 1 + }, + { + name => 'classification', + desc => 'Classification', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'product', + desc => 'Product', + in_new_bugmail => 1, + is_mandatory => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'version', + desc => 'Version', + in_new_bugmail => 1, + is_mandatory => 1, + buglist => 1 + }, + { + name => 'rep_platform', + desc => 'Platform', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + {name => 'bug_file_loc', desc => 'URL', in_new_bugmail => 1, buglist => 1}, + { + name => 'op_sys', + desc => 'OS/Version', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'bug_status', + desc => 'Status', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'status_whiteboard', + desc => 'Status Whiteboard', + in_new_bugmail => 1, + buglist => 1 + }, + { + name => 'keywords', + desc => 'Keywords', + in_new_bugmail => 1, + type => FIELD_TYPE_KEYWORDS, + buglist => 1 + }, + { + name => 'resolution', + desc => 'Resolution', + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'bug_severity', + desc => 'Severity', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'priority', + desc => 'Priority', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'component', + desc => 'Component', + in_new_bugmail => 1, + is_mandatory => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'assigned_to', + desc => 'AssignedTo', + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1, buglist => 1}, + {name => 'qa_contact', desc => 'QAContact', in_new_bugmail => 1, buglist => 1}, + { + name => 'assigned_to_realname', + desc => 'AssignedToName', + in_new_bugmail => 0, + buglist => 1 + }, + { + name => 'reporter_realname', + desc => 'ReportedByName', + in_new_bugmail => 0, + buglist => 1 + }, + { + name => 'qa_contact_realname', + desc => 'QAContactName', + in_new_bugmail => 0, + buglist => 1 + }, + {name => 'cc', desc => 'CC', in_new_bugmail => 1}, + { + name => 'dependson', + desc => 'Depends on', + in_new_bugmail => 1, + is_numeric => 1, + buglist => 1 + }, + { + name => 'blocked', + desc => 'Blocks', + in_new_bugmail => 1, + is_numeric => 1, + buglist => 1 + }, + + {name => 'attachments.description', desc => 'Attachment description'}, + {name => 'attachments.filename', desc => 'Attachment filename'}, + {name => 'attachments.mimetype', desc => 'Attachment mime type'}, + {name => 'attachments.ispatch', desc => 'Attachment is patch', is_numeric => 1}, + { + name => 'attachments.isobsolete', + desc => 'Attachment is obsolete', + is_numeric => 1 + }, + { + name => 'attachments.isprivate', + desc => 'Attachment is private', + is_numeric => 1 + }, + {name => 'attachments.submitter', desc => 'Attachment creator'}, + + { + name => 'target_milestone', + desc => 'Target Milestone', + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'creation_ts', desc => 'Creation date', buglist => 1}, + {name => 'delta_ts', desc => 'Last changed date', buglist => 1}, + {name => 'longdesc', desc => 'Comment'}, + {name => 'longdescs.isprivate', desc => 'Comment is private', is_numeric => 1}, + { + name => 'longdescs.count', + desc => 'Number of Comments', + buglist => 1, + is_numeric => 1 + }, + {name => 'alias', desc => 'Alias', buglist => 1}, + {name => 'everconfirmed', desc => 'Ever Confirmed', is_numeric => 1}, + {name => 'reporter_accessible', desc => 'Reporter Accessible', is_numeric => 1}, + {name => 'cclist_accessible', desc => 'CC Accessible', is_numeric => 1}, + {name => 'bug_group', desc => 'Group', in_new_bugmail => 1}, + { + name => 'estimated_time', + desc => 'Estimated Hours', + in_new_bugmail => 1, + buglist => 1, + is_numeric => 1 + }, + { + name => 'remaining_time', + desc => 'Remaining Hours', + buglist => 1, + is_numeric => 1 + }, + { + name => 'deadline', + desc => 'Deadline', + type => FIELD_TYPE_DATETIME, + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'commenter', desc => 'Commenter'}, + {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, is_numeric => 1}, + { + name => 'percentage_complete', + desc => 'Percentage Complete', + buglist => 1, + is_numeric => 1 + }, + {name => 'content', desc => 'Content'}, + {name => 'attach_data.thedata', desc => 'Attachment data'}, + {name => "owner_idle_time", desc => "Time Since Assignee Touched"}, + {name => 'see_also', desc => "See Also", type => FIELD_TYPE_BUG_URLS}, + { + name => 'tag', + desc => 'Personal Tags', + buglist => 1, + type => FIELD_TYPE_KEYWORDS + }, + { + name => 'last_visit_ts', + desc => 'Last Visit', + buglist => 1, + type => FIELD_TYPE_DATETIME + }, + {name => 'comment_tag', desc => 'Comment Tag'}, ); ################ @@ -276,12 +396,12 @@ use constant DEFAULT_FIELDS => ( # Override match to add is_select. sub match { - my $self = shift; - my ($params) = @_; - if (delete $params->{is_select}) { - $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT]; - } - return $self->SUPER::match(@_); + my $self = shift; + my ($params) = @_; + if (delete $params->{is_select}) { + $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT]; + } + return $self->SUPER::match(@_); } ############## @@ -291,151 +411,153 @@ sub match { sub _check_custom { return $_[1] ? 1 : 0; } sub _check_description { - my ($invocant, $desc) = @_; - $desc = clean_text($desc); - $desc || ThrowUserError('field_missing_description'); - return $desc; + my ($invocant, $desc) = @_; + $desc = clean_text($desc); + $desc || ThrowUserError('field_missing_description'); + return $desc; } sub _check_long_desc { - my ($invocant, $long_desc) = @_; - $long_desc = clean_text($long_desc || ''); - if (length($long_desc) > MAX_FIELD_LONG_DESC_LENGTH) { - ThrowUserError('field_long_desc_too_long'); - } - return $long_desc; + my ($invocant, $long_desc) = @_; + $long_desc = clean_text($long_desc || ''); + if (length($long_desc) > MAX_FIELD_LONG_DESC_LENGTH) { + ThrowUserError('field_long_desc_too_long'); + } + return $long_desc; } sub _check_enter_bug { return $_[1] ? 1 : 0; } sub _check_is_numeric { - my ($invocant, $value, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - return 1 if $type == FIELD_TYPE_BUG_ID; - return $value ? 1 : 0; + my ($invocant, $value, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + return 1 if $type == FIELD_TYPE_BUG_ID; + return $value ? 1 : 0; } sub _check_mailhead { return $_[1] ? 1 : 0; } sub _check_name { - my ($class, $name, undef, $params) = @_; - $name = lc(clean_text($name)); - $name || ThrowUserError('field_missing_name'); - - # Don't want to allow a name that might mess up SQL. - my $name_regex = qr/^[\w\.]+$/; - # Custom fields have more restrictive name requirements than - # standard fields. - $name_regex = qr/^[a-zA-Z0-9_]+$/ if $params->{custom}; - # Custom fields can't be named just "cf_", and there is no normal - # field named just "cf_". - ($name =~ $name_regex && $name ne "cf_") - || ThrowUserError('field_invalid_name', { name => $name }); - - # If it's custom, prepend cf_ to the custom field name to distinguish - # it from standard fields. - if ($name !~ /^cf_/ && $params->{custom}) { - $name = 'cf_' . $name; - } + my ($class, $name, undef, $params) = @_; + $name = lc(clean_text($name)); + $name || ThrowUserError('field_missing_name'); + + # Don't want to allow a name that might mess up SQL. + my $name_regex = qr/^[\w\.]+$/; + + # Custom fields have more restrictive name requirements than + # standard fields. + $name_regex = qr/^[a-zA-Z0-9_]+$/ if $params->{custom}; + + # Custom fields can't be named just "cf_", and there is no normal + # field named just "cf_". + ($name =~ $name_regex && $name ne "cf_") + || ThrowUserError('field_invalid_name', {name => $name}); + + # If it's custom, prepend cf_ to the custom field name to distinguish + # it from standard fields. + if ($name !~ /^cf_/ && $params->{custom}) { + $name = 'cf_' . $name; + } - # Assure the name is unique. Names can't be changed, so we don't have - # to worry about what to do on updates. - my $field = new Bugzilla::Field({ name => $name }); - ThrowUserError('field_already_exists', {'field' => $field }) if $field; + # Assure the name is unique. Names can't be changed, so we don't have + # to worry about what to do on updates. + my $field = new Bugzilla::Field({name => $name}); + ThrowUserError('field_already_exists', {'field' => $field}) if $field; - return $name; + return $name; } sub _check_obsolete { return $_[1] ? 1 : 0; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - my $skey = $sortkey; - if (!defined $skey || $skey eq '') { - ($sortkey) = Bugzilla->dbh->selectrow_array( - 'SELECT MAX(sortkey) + 100 FROM fielddefs') || 100; - } - detaint_natural($sortkey) - || ThrowUserError('field_invalid_sortkey', { sortkey => $skey }); - return $sortkey; + my ($invocant, $sortkey) = @_; + my $skey = $sortkey; + if (!defined $skey || $skey eq '') { + ($sortkey) + = Bugzilla->dbh->selectrow_array('SELECT MAX(sortkey) + 100 FROM fielddefs') + || 100; + } + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', {sortkey => $skey}); + return $sortkey; } sub _check_type { - my ($invocant, $type, undef, $params) = @_; - my $saved_type = $type; - (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE) - || ThrowCodeError('invalid_customfield_type', { type => $saved_type }); - - my $custom = blessed($invocant) ? $invocant->custom : $params->{custom}; - if ($custom && !$type) { - ThrowCodeError('field_type_not_specified'); - } + my ($invocant, $type, undef, $params) = @_; + my $saved_type = $type; + (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE) + || ThrowCodeError('invalid_customfield_type', {type => $saved_type}); + + my $custom = blessed($invocant) ? $invocant->custom : $params->{custom}; + if ($custom && !$type) { + ThrowCodeError('field_type_not_specified'); + } - return $type; + return $type; } sub _check_value_field_id { - my ($invocant, $field_id, undef, $params) = @_; - my $is_select = $invocant->is_select($params); - if ($field_id && !$is_select) { - ThrowUserError('field_value_control_select_only'); - } - return $invocant->_check_visibility_field_id($field_id); + my ($invocant, $field_id, undef, $params) = @_; + my $is_select = $invocant->is_select($params); + if ($field_id && !$is_select) { + ThrowUserError('field_value_control_select_only'); + } + return $invocant->_check_visibility_field_id($field_id); } sub _check_visibility_field_id { - my ($invocant, $field_id) = @_; - $field_id = trim($field_id); - return undef if !$field_id; - my $field = Bugzilla::Field->check({ id => $field_id }); - if (blessed($invocant) && $field->id == $invocant->id) { - ThrowUserError('field_cant_control_self', { field => $field }); - } - if (!$field->is_select) { - ThrowUserError('field_control_must_be_select', - { field => $field }); - } - return $field->id; + my ($invocant, $field_id) = @_; + $field_id = trim($field_id); + return undef if !$field_id; + my $field = Bugzilla::Field->check({id => $field_id}); + if (blessed($invocant) && $field->id == $invocant->id) { + ThrowUserError('field_cant_control_self', {field => $field}); + } + if (!$field->is_select) { + ThrowUserError('field_control_must_be_select', {field => $field}); + } + return $field->id; } sub _check_visibility_values { - my ($invocant, $values, undef, $params) = @_; - my $field; - if (blessed $invocant) { - $field = $invocant->visibility_field; - } - elsif ($params->{visibility_field_id}) { - $field = $invocant->new($params->{visibility_field_id}); - } - # When no field is set, no values are set. - return [] if !$field; + my ($invocant, $values, undef, $params) = @_; + my $field; + if (blessed $invocant) { + $field = $invocant->visibility_field; + } + elsif ($params->{visibility_field_id}) { + $field = $invocant->new($params->{visibility_field_id}); + } - if (!scalar @$values) { - ThrowUserError('field_visibility_values_must_be_selected', - { field => $field }); - } + # When no field is set, no values are set. + return [] if !$field; + + if (!scalar @$values) { + ThrowUserError('field_visibility_values_must_be_selected', {field => $field}); + } - my @visibility_values; - my $choice = Bugzilla::Field::Choice->type($field); - foreach my $value (@$values) { - if (!blessed $value) { - $value = $choice->check({ id => $value }); - } - push(@visibility_values, $value); + my @visibility_values; + my $choice = Bugzilla::Field::Choice->type($field); + foreach my $value (@$values) { + if (!blessed $value) { + $value = $choice->check({id => $value}); } + push(@visibility_values, $value); + } - return \@visibility_values; + return \@visibility_values; } sub _check_reverse_desc { - my ($invocant, $reverse_desc, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - if ($type != FIELD_TYPE_BUG_ID) { - return undef; # store NULL for non-reversible field types - } - - $reverse_desc = clean_text($reverse_desc); - return $reverse_desc; + my ($invocant, $reverse_desc, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + if ($type != FIELD_TYPE_BUG_ID) { + return undef; # store NULL for non-reversible field types + } + + $reverse_desc = clean_text($reverse_desc); + return $reverse_desc; } sub _check_is_mandatory { return $_[1] ? 1 : 0; } @@ -583,11 +705,13 @@ objects. =cut sub is_select { - my ($invocant, $params) = @_; - # This allows this method to be called by create() validators. - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - return ($type == FIELD_TYPE_SINGLE_SELECT - || $type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0 + my ($invocant, $params) = @_; + + # This allows this method to be called by create() validators. + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + return ($type == FIELD_TYPE_SINGLE_SELECT || $type == FIELD_TYPE_MULTI_SELECT) + ? 1 + : 0; } =over @@ -608,19 +732,19 @@ This method returns C<1> if the field is "abnormal", C<0> otherwise. =cut sub is_abnormal { - my $self = shift; - return ABNORMAL_SELECTS->{$self->name} ? 1 : 0; + my $self = shift; + return ABNORMAL_SELECTS->{$self->name} ? 1 : 0; } sub legal_values { - my $self = shift; + my $self = shift; - if (!defined $self->{'legal_values'}) { - require Bugzilla::Field::Choice; - my @values = Bugzilla::Field::Choice->type($self)->get_all(); - $self->{'legal_values'} = \@values; - } - return $self->{'legal_values'}; + if (!defined $self->{'legal_values'}) { + require Bugzilla::Field::Choice; + my @values = Bugzilla::Field::Choice->type($self)->get_all(); + $self->{'legal_values'} = \@values; + } + return $self->{'legal_values'}; } =pod @@ -637,8 +761,8 @@ in the C<timetrackinggroup>. =cut sub is_timetracking { - my ($self) = @_; - return grep($_ eq $self->name, TIMETRACKING_FIELDS) ? 1 : 0; + my ($self) = @_; + return grep($_ eq $self->name, TIMETRACKING_FIELDS) ? 1 : 0; } =pod @@ -657,12 +781,11 @@ Returns undef if there is no field that controls this field's visibility. =cut sub visibility_field { - my $self = shift; - if ($self->{visibility_field_id}) { - $self->{visibility_field} ||= - $self->new($self->{visibility_field_id}); - } - return $self->{visibility_field}; + my $self = shift; + if ($self->{visibility_field_id}) { + $self->{visibility_field} ||= $self->new($self->{visibility_field_id}); + } + return $self->{visibility_field}; } =pod @@ -680,22 +803,23 @@ or undef if there is no C<visibility_field> set. =cut sub visibility_values { - my $self = shift; - my $dbh = Bugzilla->dbh; - - return [] if !$self->{visibility_field_id}; - - if (!defined $self->{visibility_values}) { - my $visibility_value_ids = - $dbh->selectcol_arrayref("SELECT value_id FROM field_visibility - WHERE field_id = ?", undef, $self->id); - - $self->{visibility_values} = - Bugzilla::Field::Choice->type($self->visibility_field) - ->new_from_list($visibility_value_ids); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + return [] if !$self->{visibility_field_id}; + + if (!defined $self->{visibility_values}) { + my $visibility_value_ids = $dbh->selectcol_arrayref( + "SELECT value_id FROM field_visibility + WHERE field_id = ?", undef, $self->id + ); - return $self->{visibility_values}; + $self->{visibility_values} + = Bugzilla::Field::Choice->type($self->visibility_field) + ->new_from_list($visibility_value_ids); + } + + return $self->{visibility_values}; } =pod @@ -712,10 +836,10 @@ field controls the visibility of. =cut sub controls_visibility_of { - my $self = shift; - $self->{controls_visibility_of} ||= - Bugzilla::Field->match({ visibility_field_id => $self->id }); - return $self->{controls_visibility_of}; + my $self = shift; + $self->{controls_visibility_of} + ||= Bugzilla::Field->match({visibility_field_id => $self->id}); + return $self->{controls_visibility_of}; } =pod @@ -733,11 +857,11 @@ Returns undef if there is no field that controls this field's visibility. =cut sub value_field { - my $self = shift; - if ($self->{value_field_id}) { - $self->{value_field} ||= $self->new($self->{value_field_id}); - } - return $self->{value_field}; + my $self = shift; + if ($self->{value_field_id}) { + $self->{value_field} ||= $self->new($self->{value_field_id}); + } + return $self->{value_field}; } =pod @@ -754,10 +878,10 @@ field controls the values of. =cut sub controls_values_of { - my $self = shift; - $self->{controls_values_of} ||= - Bugzilla::Field->match({ value_field_id => $self->id }); - return $self->{controls_values_of}; + my $self = shift; + $self->{controls_values_of} + ||= Bugzilla::Field->match({value_field_id => $self->id}); + return $self->{controls_values_of}; } =over @@ -771,15 +895,15 @@ See L<Bugzilla::Field::ChoiceInterface>. =cut sub is_visible_on_bug { - my ($self, $bug) = @_; + my ($self, $bug) = @_; - # Always return visible, if this field is not - # visibility controlled. - return 1 if !$self->{visibility_field_id}; + # Always return visible, if this field is not + # visibility controlled. + return 1 if !$self->{visibility_field_id}; - my $visibility_values = $self->visibility_values; + my $visibility_values = $self->visibility_values; - return (any { $_->is_set_on_bug($bug) } @$visibility_values) ? 1 : 0; + return (any { $_->is_set_on_bug($bug) } @$visibility_values) ? 1 : 0; } =over @@ -795,13 +919,13 @@ dependency tree display, and similar functionality. =cut -sub is_relationship { - my $self = shift; - my $desc = $self->reverse_desc; - if (defined $desc && $desc ne "") { - return 1; - } - return 0; +sub is_relationship { + my $self = shift; + my $desc = $self->reverse_desc; + if (defined $desc && $desc ne "") { + return 1; + } + return 0; } =over @@ -888,29 +1012,32 @@ They will throw an error if you try to set the values to something invalid. =cut -sub set_description { $_[0]->set('description', $_[1]); } -sub set_long_desc { $_[0]->set('long_desc', $_[1]); } -sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } -sub set_is_numeric { $_[0]->set('is_numeric', $_[1]); } -sub set_obsolete { $_[0]->set('obsolete', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_in_new_bugmail { $_[0]->set('mailhead', $_[1]); } -sub set_buglist { $_[0]->set('buglist', $_[1]); } -sub set_reverse_desc { $_[0]->set('reverse_desc', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_long_desc { $_[0]->set('long_desc', $_[1]); } +sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } +sub set_is_numeric { $_[0]->set('is_numeric', $_[1]); } +sub set_obsolete { $_[0]->set('obsolete', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_in_new_bugmail { $_[0]->set('mailhead', $_[1]); } +sub set_buglist { $_[0]->set('buglist', $_[1]); } +sub set_reverse_desc { $_[0]->set('reverse_desc', $_[1]); } + sub set_visibility_field { - my ($self, $value) = @_; - $self->set('visibility_field_id', $value); - delete $self->{visibility_field}; - delete $self->{visibility_values}; + my ($self, $value) = @_; + $self->set('visibility_field_id', $value); + delete $self->{visibility_field}; + delete $self->{visibility_values}; } + sub set_visibility_values { - my ($self, $value_ids) = @_; - $self->set('visibility_values', $value_ids); + my ($self, $value_ids) = @_; + $self->set('visibility_values', $value_ids); } + sub set_value_field { - my ($self, $value) = @_; - $self->set('value_field_id', $value); - delete $self->{value_field}; + my ($self, $value) = @_; + $self->set('value_field_id', $value); + delete $self->{value_field}; } sub set_is_mandatory { $_[0]->set('is_mandatory', $_[1]); } @@ -934,69 +1061,73 @@ there are no values specified (or EVER specified) for the field. =cut sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - my $name = $self->name; + my $name = $self->name; - if (!$self->custom) { - ThrowCodeError('field_not_custom', {'name' => $name }); - } + if (!$self->custom) { + ThrowCodeError('field_not_custom', {'name' => $name}); + } - if (!$self->obsolete) { - ThrowUserError('customfield_not_obsolete', {'name' => $self->name }); - } + if (!$self->obsolete) { + ThrowUserError('customfield_not_obsolete', {'name' => $self->name}); + } - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Check to see if bug activity table has records (should be fast with index) - my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity - WHERE fieldid = ?", undef, $self->id); - if ($has_activity) { - ThrowUserError('customfield_has_activity', {'name' => $name }); - } + # Check to see if bug activity table has records (should be fast with index) + my $has_activity = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bugs_activity + WHERE fieldid = ?", undef, $self->id + ); + if ($has_activity) { + ThrowUserError('customfield_has_activity', {'name' => $name}); + } - # Check to see if bugs table has records (slow) - my $bugs_query = ""; + # Check to see if bugs table has records (slow) + my $bugs_query = ""; - if ($self->type == FIELD_TYPE_MULTI_SELECT) { - $bugs_query = "SELECT COUNT(*) FROM bug_$name"; - } - else { - $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL"; - if ($self->type != FIELD_TYPE_BUG_ID - && $self->type != FIELD_TYPE_DATE - && $self->type != FIELD_TYPE_DATETIME) - { - $bugs_query .= " AND $name != ''"; - } - # Ignore the default single select value - if ($self->type == FIELD_TYPE_SINGLE_SELECT) { - $bugs_query .= " AND $name != '---'"; - } + if ($self->type == FIELD_TYPE_MULTI_SELECT) { + $bugs_query = "SELECT COUNT(*) FROM bug_$name"; + } + else { + $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL"; + if ( $self->type != FIELD_TYPE_BUG_ID + && $self->type != FIELD_TYPE_DATE + && $self->type != FIELD_TYPE_DATETIME) + { + $bugs_query .= " AND $name != ''"; } - my $has_bugs = $dbh->selectrow_array($bugs_query); - if ($has_bugs) { - ThrowUserError('customfield_has_contents', {'name' => $name }); + # Ignore the default single select value + if ($self->type == FIELD_TYPE_SINGLE_SELECT) { + $bugs_query .= " AND $name != '---'"; } + } - # Once we reach here, we should be OK to delete. - $self->SUPER::remove_from_db(); + my $has_bugs = $dbh->selectrow_array($bugs_query); + if ($has_bugs) { + ThrowUserError('customfield_has_contents', {'name' => $name}); + } - my $type = $self->type; + # Once we reach here, we should be OK to delete. + $self->SUPER::remove_from_db(); - # the values for multi-select are stored in a seperate table - if ($type != FIELD_TYPE_MULTI_SELECT) { - $dbh->bz_drop_column('bugs', $name); - } + my $type = $self->type; - if ($self->is_select) { - # Delete the table that holds the legal values for this field. - $dbh->bz_drop_field_tables($self); - } + # the values for multi-select are stored in a seperate table + if ($type != FIELD_TYPE_MULTI_SELECT) { + $dbh->bz_drop_column('bugs', $name); + } + + if ($self->is_select) { - $dbh->bz_commit_transaction() + # Delete the table that holds the legal values for this field. + $dbh->bz_drop_field_tables($self); + } + + $dbh->bz_commit_transaction(); } =pod @@ -1042,90 +1173,95 @@ C<is_mandatory> - boolean - Whether this field is mandatory. Defaults to 0. =cut sub create { - my $class = shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; - - # This makes sure the "sortkey" validator runs, even if - # the parameter isn't sent to create(). - $params->{sortkey} = undef if !exists $params->{sortkey}; - $params->{type} ||= 0; - # We mark the custom field as obsolete till it has been fully created, - # to avoid race conditions when viewing bugs at the same time. - my $is_obsolete = $params->{obsolete}; - $params->{obsolete} = 1 if $params->{custom}; - - $dbh->bz_start_transaction(); - $class->check_required_create_fields(@_); - my $field_values = $class->run_create_validators($params); - my $visibility_values = delete $field_values->{visibility_values}; - my $field = $class->insert_create_data($field_values); - - $field->set_visibility_values($visibility_values); - $field->_update_visibility_values(); - - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); + my $class = shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + # This makes sure the "sortkey" validator runs, even if + # the parameter isn't sent to create(). + $params->{sortkey} = undef if !exists $params->{sortkey}; + $params->{type} ||= 0; + + # We mark the custom field as obsolete till it has been fully created, + # to avoid race conditions when viewing bugs at the same time. + my $is_obsolete = $params->{obsolete}; + $params->{obsolete} = 1 if $params->{custom}; + + $dbh->bz_start_transaction(); + $class->check_required_create_fields(@_); + my $field_values = $class->run_create_validators($params); + my $visibility_values = delete $field_values->{visibility_values}; + my $field = $class->insert_create_data($field_values); + + $field->set_visibility_values($visibility_values); + $field->_update_visibility_values(); + + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + + if ($field->custom) { + my $name = $field->name; + my $type = $field->type; + if (SQL_DEFINITIONS->{$type}) { + + # Create the database column that stores the data for this field. + $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type}); + } + + if ($field->is_select) { - if ($field->custom) { - my $name = $field->name; - my $type = $field->type; - if (SQL_DEFINITIONS->{$type}) { - # Create the database column that stores the data for this field. - $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type}); - } - - if ($field->is_select) { - # Create the table that holds the legal values for this field. - $dbh->bz_add_field_tables($field); - } - - if ($type == FIELD_TYPE_SINGLE_SELECT) { - # Insert a default value of "---" into the legal values table. - $dbh->do("INSERT INTO $name (value) VALUES ('---')"); - } - - # Restore the original obsolete state of the custom field. - $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id) - unless $is_obsolete; - - Bugzilla->memcached->clear({ table => 'fielddefs', id => $field->id }); - Bugzilla->memcached->clear_config(); + # Create the table that holds the legal values for this field. + $dbh->bz_add_field_tables($field); } - return $field; -} + if ($type == FIELD_TYPE_SINGLE_SELECT) { -sub update { - my $self = shift; - my $changes = $self->SUPER::update(@_); - my $dbh = Bugzilla->dbh; - if ($changes->{value_field_id} && $self->is_select) { - $dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL"); + # Insert a default value of "---" into the legal values table. + $dbh->do("INSERT INTO $name (value) VALUES ('---')"); } - $self->_update_visibility_values(); + + # Restore the original obsolete state of the custom field. + $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id) + unless $is_obsolete; + + Bugzilla->memcached->clear({table => 'fielddefs', id => $field->id}); Bugzilla->memcached->clear_config(); - return $changes; + } + + return $field; +} + +sub update { + my $self = shift; + my $changes = $self->SUPER::update(@_); + my $dbh = Bugzilla->dbh; + if ($changes->{value_field_id} && $self->is_select) { + $dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL"); + } + $self->_update_visibility_values(); + Bugzilla->memcached->clear_config(); + return $changes; } sub _update_visibility_values { - my $self = shift; - my $dbh = Bugzilla->dbh; - - my @visibility_value_ids = map($_->id, @{$self->visibility_values}); - $self->_delete_visibility_values(); - for my $value_id (@visibility_value_ids) { - $dbh->do("INSERT INTO field_visibility (field_id, value_id) - VALUES (?, ?)", undef, $self->id, $value_id); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + my @visibility_value_ids = map($_->id, @{$self->visibility_values}); + $self->_delete_visibility_values(); + for my $value_id (@visibility_value_ids) { + $dbh->do( + "INSERT INTO field_visibility (field_id, value_id) + VALUES (?, ?)", undef, $self->id, $value_id + ); + } } sub _delete_visibility_values { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - $dbh->do("DELETE FROM field_visibility WHERE field_id = ?", - undef, $self->id); - delete $self->{visibility_values}; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + $dbh->do("DELETE FROM field_visibility WHERE field_id = ?", undef, $self->id); + delete $self->{visibility_values}; } =pod @@ -1148,13 +1284,14 @@ Returns: a reference to a list of valid values. =cut sub get_legal_field_values { - my ($field) = @_; - my $dbh = Bugzilla->dbh; - my $result_ref = $dbh->selectcol_arrayref( - "SELECT value FROM $field + my ($field) = @_; + my $dbh = Bugzilla->dbh; + my $result_ref = $dbh->selectcol_arrayref( + "SELECT value FROM $field WHERE isactive = ? - ORDER BY sortkey, value", undef, (1)); - return $result_ref; + ORDER BY sortkey, value", undef, (1) + ); + return $result_ref; } =over @@ -1173,107 +1310,114 @@ Returns: nothing =cut sub populate_field_definitions { - my $dbh = Bugzilla->dbh; - - # ADD and UPDATE field definitions - foreach my $def (DEFAULT_FIELDS) { - my $field = new Bugzilla::Field({ name => $def->{name} }); - if ($field) { - $field->set_description($def->{desc}); - $field->set_in_new_bugmail($def->{in_new_bugmail}); - $field->set_buglist($def->{buglist}); - $field->_set_type($def->{type}) if $def->{type}; - $field->set_is_mandatory($def->{is_mandatory}); - $field->set_is_numeric($def->{is_numeric}); - $field->update(); - } - else { - if (exists $def->{in_new_bugmail}) { - $def->{mailhead} = $def->{in_new_bugmail}; - delete $def->{in_new_bugmail}; - } - $def->{description} = delete $def->{desc}; - Bugzilla::Field->create($def); - } + my $dbh = Bugzilla->dbh; + + # ADD and UPDATE field definitions + foreach my $def (DEFAULT_FIELDS) { + my $field = new Bugzilla::Field({name => $def->{name}}); + if ($field) { + $field->set_description($def->{desc}); + $field->set_in_new_bugmail($def->{in_new_bugmail}); + $field->set_buglist($def->{buglist}); + $field->_set_type($def->{type}) if $def->{type}; + $field->set_is_mandatory($def->{is_mandatory}); + $field->set_is_numeric($def->{is_numeric}); + $field->update(); } + else { + if (exists $def->{in_new_bugmail}) { + $def->{mailhead} = $def->{in_new_bugmail}; + delete $def->{in_new_bugmail}; + } + $def->{description} = delete $def->{desc}; + Bugzilla::Field->create($def); + } + } - # DELETE fields which were added only accidentally, or which - # were never tracked in bugs_activity. Note that you can never - # delete fields which are used by bugs_activity. - - # Oops. Bug 163299 - $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'"); - # Oops. Bug 215319 - $dbh->do("DELETE FROM fielddefs WHERE name='requesters.login_name'"); - # This field was never tracked in bugs_activity, so it's safe to delete. - $dbh->do("DELETE FROM fielddefs WHERE name='attachments.thedata'"); - - # MODIFY old field definitions - - # 2005-11-13 LpSolit@gmail.com - Bug 302599 - # One of the field names was a fragment of SQL code, which is DB dependent. - # We have to rename it to a real name, which is DB independent. - my $new_field_name = 'days_elapsed'; - my $field_description = 'Days since bug changed'; - - my ($old_field_id, $old_field_name) = - $dbh->selectrow_array('SELECT id, name FROM fielddefs - WHERE description = ?', - undef, $field_description); - - if ($old_field_id && ($old_field_name ne $new_field_name)) { - say "SQL fragment found in the 'fielddefs' table..."; - say "Old field name: $old_field_name"; - # We have to fix saved searches first. Queries have been escaped - # before being saved. We have to do the same here to find them. - $old_field_name = url_quote($old_field_name); - my $broken_named_queries = - $dbh->selectall_arrayref('SELECT userid, name, query - FROM namedqueries WHERE ' . - $dbh->sql_istrcmp('query', '?', 'LIKE'), - undef, "%=$old_field_name%"); - - my $sth_UpdateQueries = $dbh->prepare('UPDATE namedqueries SET query = ? - WHERE userid = ? AND name = ?'); - - print "Fixing saved searches...\n" if scalar(@$broken_named_queries); - foreach my $named_query (@$broken_named_queries) { - my ($userid, $name, $query) = @$named_query; - $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; - $sth_UpdateQueries->execute($query, $userid, $name); - } - - # We now do the same with saved chart series. - my $broken_series = - $dbh->selectall_arrayref('SELECT series_id, query - FROM series WHERE ' . - $dbh->sql_istrcmp('query', '?', 'LIKE'), - undef, "%=$old_field_name%"); - - my $sth_UpdateSeries = $dbh->prepare('UPDATE series SET query = ? - WHERE series_id = ?'); - - print "Fixing saved chart series...\n" if scalar(@$broken_series); - foreach my $series (@$broken_series) { - my ($series_id, $query) = @$series; - $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; - $sth_UpdateSeries->execute($query, $series_id); - } - # Now that saved searches have been fixed, we can fix the field name. - say "Fixing the 'fielddefs' table..."; - say "New field name: $new_field_name"; - $dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?', - undef, ($new_field_name, $old_field_id)); + # DELETE fields which were added only accidentally, or which + # were never tracked in bugs_activity. Note that you can never + # delete fields which are used by bugs_activity. + + # Oops. Bug 163299 + $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'"); + + # Oops. Bug 215319 + $dbh->do("DELETE FROM fielddefs WHERE name='requesters.login_name'"); + + # This field was never tracked in bugs_activity, so it's safe to delete. + $dbh->do("DELETE FROM fielddefs WHERE name='attachments.thedata'"); + + # MODIFY old field definitions + + # 2005-11-13 LpSolit@gmail.com - Bug 302599 + # One of the field names was a fragment of SQL code, which is DB dependent. + # We have to rename it to a real name, which is DB independent. + my $new_field_name = 'days_elapsed'; + my $field_description = 'Days since bug changed'; + + my ($old_field_id, $old_field_name) = $dbh->selectrow_array( + 'SELECT id, name FROM fielddefs + WHERE description = ?', undef, $field_description + ); + + if ($old_field_id && ($old_field_name ne $new_field_name)) { + say "SQL fragment found in the 'fielddefs' table..."; + say "Old field name: $old_field_name"; + + # We have to fix saved searches first. Queries have been escaped + # before being saved. We have to do the same here to find them. + $old_field_name = url_quote($old_field_name); + my $broken_named_queries = $dbh->selectall_arrayref( + 'SELECT userid, name, query + FROM namedqueries WHERE ' + . $dbh->sql_istrcmp('query', '?', 'LIKE'), undef, "%=$old_field_name%" + ); + + my $sth_UpdateQueries = $dbh->prepare( + 'UPDATE namedqueries SET query = ? + WHERE userid = ? AND name = ?' + ); + + print "Fixing saved searches...\n" if scalar(@$broken_named_queries); + foreach my $named_query (@$broken_named_queries) { + my ($userid, $name, $query) = @$named_query; + $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; + $sth_UpdateQueries->execute($query, $userid, $name); } - # This field has to be created separately, or the above upgrade code - # might not run properly. - Bugzilla::Field->create({ name => $new_field_name, - description => $field_description }) - unless new Bugzilla::Field({ name => $new_field_name }); + # We now do the same with saved chart series. + my $broken_series = $dbh->selectall_arrayref( + 'SELECT series_id, query + FROM series WHERE ' + . $dbh->sql_istrcmp('query', '?', 'LIKE'), undef, "%=$old_field_name%" + ); + + my $sth_UpdateSeries = $dbh->prepare( + 'UPDATE series SET query = ? + WHERE series_id = ?' + ); + + print "Fixing saved chart series...\n" if scalar(@$broken_series); + foreach my $series (@$broken_series) { + my ($series_id, $query) = @$series; + $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; + $sth_UpdateSeries->execute($query, $series_id); + } -} + # Now that saved searches have been fixed, we can fix the field name. + say "Fixing the 'fielddefs' table..."; + say "New field name: $new_field_name"; + $dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?', + undef, ($new_field_name, $old_field_id)); + } + # This field has to be created separately, or the above upgrade code + # might not run properly. + Bugzilla::Field->create( + {name => $new_field_name, description => $field_description}) + unless new Bugzilla::Field({name => $new_field_name}); + +} =head2 Data Validation @@ -1305,32 +1449,32 @@ Returns: 1 on success; 0 on failure if $no_warn is true (else an =cut sub check_field { - my ($name, $value, $legalsRef, $no_warn) = @_; - 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 = Bugzilla::Field->new({name => $name})->legal_values; - my @values = map($_->name, @$legalsRef); - $legalsRef = \@values; + my ($name, $value, $legalsRef, $no_warn) = @_; + 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 = Bugzilla::Field->new({name => $name})->legal_values; + my @values = map($_->name, @$legalsRef); + $legalsRef = \@values; - } + } - if (!defined($value) - or trim($value) eq "" - or !grep { $_ eq $value } @$legalsRef) - { - return 0 if $no_warn; # We don't want an error to be thrown; return. - trick_taint($name); + if ( !defined($value) + or trim($value) eq "" + or !grep { $_ eq $value } @$legalsRef) + { + return 0 if $no_warn; # We don't want an error to be thrown; return. + trick_taint($name); - my $field = new Bugzilla::Field({ name => $name }); - my $field_desc = $field ? $field->description : $name; - ThrowCodeError('illegal_field', { field => $field_desc }); - } - return 1; + my $field = new Bugzilla::Field({name => $name}); + my $field_desc = $field ? $field->description : $name; + ThrowCodeError('illegal_field', {field => $field_desc}); + } + return 1; } =pod @@ -1352,10 +1496,10 @@ Returns: the corresponding field ID or an error if the field name =cut sub get_field_id { - my $field = Bugzilla->fields({ by_name => 1 })->{$_[0]} - or ThrowCodeError('invalid_field_name', {field => $_[0]}); + my $field = Bugzilla->fields({by_name => 1})->{$_[0]} + or ThrowCodeError('invalid_field_name', {field => $_[0]}); - return $field->id; + return $field->id; } 1; diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm index a66f69cee..360f851aa 100644 --- a/Bugzilla/Field/Choice.pm +++ b/Bugzilla/Field/Choice.pm @@ -28,42 +28,42 @@ use Scalar::Util qw(blessed); use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( - id - value - sortkey - isactive - visibility_value_id + id + value + sortkey + isactive + visibility_value_id ); use constant UPDATE_COLUMNS => qw( - value - sortkey - isactive - visibility_value_id + value + sortkey + isactive + visibility_value_id ); use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'sortkey, value'; use constant VALIDATORS => { - value => \&_check_value, - sortkey => \&_check_sortkey, - visibility_value_id => \&_check_visibility_value_id, - isactive => \&_check_isactive, + value => \&_check_value, + sortkey => \&_check_sortkey, + visibility_value_id => \&_check_visibility_value_id, + isactive => \&_check_isactive, }; use constant CLASS_MAP => { - bug_status => 'Bugzilla::Status', - classification => 'Bugzilla::Classification', - component => 'Bugzilla::Component', - product => 'Bugzilla::Product', + bug_status => 'Bugzilla::Status', + classification => 'Bugzilla::Classification', + component => 'Bugzilla::Component', + product => 'Bugzilla::Product', }; use constant DEFAULT_MAP => { - op_sys => 'defaultopsys', - rep_platform => 'defaultplatform', - priority => 'defaultpriority', - bug_severity => 'defaultseverity', + op_sys => 'defaultopsys', + rep_platform => 'defaultplatform', + priority => 'defaultpriority', + bug_severity => 'defaultseverity', }; ################# @@ -76,49 +76,50 @@ use constant DEFAULT_MAP => { # are Bugzilla::Status objects. sub type { - my ($class, $field) = @_; - my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field); - my $field_name = $field_obj->name; - - if (my $package = $class->CLASS_MAP->{$field_name}) { - # Callers expect the module to be already loaded. - eval "require $package"; - return $package; - } + my ($class, $field) = @_; + my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field); + my $field_name = $field_obj->name; + + if (my $package = $class->CLASS_MAP->{$field_name}) { - # For generic classes, we use a lowercase class name, so as - # not to interfere with any real subclasses we might make some day. - my $package = "Bugzilla::Field::Choice::$field_name"; - Bugzilla->request_cache->{"field_$package"} = $field_obj; - - # This package only needs to be created once. We check if the DB_TABLE - # glob for this package already exists, which tells us whether or not - # we need to create the package (this works even under mod_perl, where - # this package definition will persist across requests)). - if (!defined *{"${package}::DB_TABLE"}) { - eval <<EOC; + # Callers expect the module to be already loaded. + eval "require $package"; + return $package; + } + + # For generic classes, we use a lowercase class name, so as + # not to interfere with any real subclasses we might make some day. + my $package = "Bugzilla::Field::Choice::$field_name"; + Bugzilla->request_cache->{"field_$package"} = $field_obj; + + # This package only needs to be created once. We check if the DB_TABLE + # glob for this package already exists, which tells us whether or not + # we need to create the package (this works even under mod_perl, where + # this package definition will persist across requests)). + if (!defined *{"${package}::DB_TABLE"}) { + eval <<EOC; package $package; use parent qw(Bugzilla::Field::Choice); use constant DB_TABLE => '$field_name'; EOC - } + } - return $package; + return $package; } ################ # Constructors # ################ -# We just make new() enforce this, which should give developers +# We just make new() enforce this, which should give developers # the understanding that you can't use Bugzilla::Field::Choice # without calling type(). sub new { - my $class = shift; - if ($class eq 'Bugzilla::Field::Choice') { - ThrowCodeError('field_choice_must_use_type'); - } - $class->SUPER::new(@_); + my $class = shift; + if ($class eq 'Bugzilla::Field::Choice') { + ThrowCodeError('field_choice_must_use_type'); + } + $class->SUPER::new(@_); } ######################### @@ -130,64 +131,66 @@ sub new { # columns. (Normally Bugzilla::Object dies if you pass arguments # that aren't valid columns.) sub create { - my $class = shift; - my ($params) = @_; - foreach my $key (keys %$params) { - if (!grep {$_ eq $key} $class->_get_db_columns) { - delete $params->{$key}; - } + my $class = shift; + my ($params) = @_; + foreach my $key (keys %$params) { + if (!grep { $_ eq $key } $class->_get_db_columns) { + delete $params->{$key}; } - return $class->SUPER::create(@_); + } + return $class->SUPER::create(@_); } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $fname = $self->field->name; - - $dbh->bz_start_transaction(); - - my ($changes, $old_self) = $self->SUPER::update(@_); - if (exists $changes->{value}) { - my ($old, $new) = @{ $changes->{value} }; - if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { - $dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?", - undef, $new, $old); - } - else { - $dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?", - undef, $new, $old); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + my $fname = $self->field->name; - if ($old_self->is_default) { - my $param = $self->DEFAULT_MAP->{$self->field->name}; - SetParam($param, $self->name); - write_params(); - } + $dbh->bz_start_transaction(); + + my ($changes, $old_self) = $self->SUPER::update(@_); + if (exists $changes->{value}) { + my ($old, $new) = @{$changes->{value}}; + if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { + $dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?", undef, $new, $old); } + else { + $dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?", undef, $new, $old); + } + + if ($old_self->is_default) { + my $param = $self->DEFAULT_MAP->{$self->field->name}; + SetParam($param, $self->name); + write_params(); + } + } - $dbh->bz_commit_transaction(); - return wantarray ? ($changes, $old_self) : $changes; + $dbh->bz_commit_transaction(); + return wantarray ? ($changes, $old_self) : $changes; } sub remove_from_db { - my $self = shift; - if ($self->is_default) { - ThrowUserError('fieldvalue_is_default', - { field => $self->field, value => $self, - param_name => $self->DEFAULT_MAP->{$self->field->name}, - }); - } - if ($self->is_static) { - ThrowUserError('fieldvalue_not_deletable', - { field => $self->field, value => $self }); - } - if ($self->bug_count) { - ThrowUserError("fieldvalue_still_has_bugs", - { field => $self->field, value => $self }); - } - $self->_check_if_controller(); # From ChoiceInterface. - $self->SUPER::remove_from_db(); + my $self = shift; + if ($self->is_default) { + ThrowUserError( + 'fieldvalue_is_default', + { + field => $self->field, + value => $self, + param_name => $self->DEFAULT_MAP->{$self->field->name}, + } + ); + } + if ($self->is_static) { + ThrowUserError('fieldvalue_not_deletable', + {field => $self->field, value => $self}); + } + if ($self->bug_count) { + ThrowUserError("fieldvalue_still_has_bugs", + {field => $self->field, value => $self}); + } + $self->_check_if_controller(); # From ChoiceInterface. + $self->SUPER::remove_from_db(); } ############ @@ -195,12 +198,13 @@ sub remove_from_db { ############ sub set_is_active { $_[0]->set('isactive', $_[1]); } -sub set_name { $_[0]->set('value', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_name { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } + sub set_visibility_value { - my ($self, $value) = @_; - $self->set('visibility_value_id', $value); - delete $self->{visibility_value}; + my ($self, $value) = @_; + $self->set('visibility_value_id', $value); + delete $self->{visibility_value}; } ############## @@ -208,73 +212,74 @@ sub set_visibility_value { ############## sub _check_isactive { - my ($invocant, $value) = @_; - $value = Bugzilla::Object::check_boolean($invocant, $value); - if (!$value and ref $invocant) { - if ($invocant->is_default) { - my $field = $invocant->field; - ThrowUserError('fieldvalue_is_default', - { value => $invocant, field => $field, - param_name => $invocant->DEFAULT_MAP->{$field->name} - }); - } - if ($invocant->is_static) { - ThrowUserError('fieldvalue_not_deletable', - { value => $invocant, field => $invocant->field }); + my ($invocant, $value) = @_; + $value = Bugzilla::Object::check_boolean($invocant, $value); + if (!$value and ref $invocant) { + if ($invocant->is_default) { + my $field = $invocant->field; + ThrowUserError( + 'fieldvalue_is_default', + { + value => $invocant, + field => $field, + param_name => $invocant->DEFAULT_MAP->{$field->name} } + ); + } + if ($invocant->is_static) { + ThrowUserError('fieldvalue_not_deletable', + {value => $invocant, field => $invocant->field}); } - return $value; + } + return $value; } sub _check_value { - my ($invocant, $value) = @_; + my ($invocant, $value) = @_; - my $field = $invocant->field; + my $field = $invocant->field; - $value = trim($value); + $value = trim($value); - # Make sure people don't rename static values - if (blessed($invocant) && $value ne $invocant->name - && $invocant->is_static) - { - ThrowUserError('fieldvalue_not_editable', - { field => $field, old_value => $invocant }); - } + # Make sure people don't rename static values + if (blessed($invocant) && $value ne $invocant->name && $invocant->is_static) { + ThrowUserError('fieldvalue_not_editable', + {field => $field, old_value => $invocant}); + } - ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq ""; - ThrowUserError('fieldvalue_name_too_long', { value => $value }) - if length($value) > MAX_FIELD_VALUE_SIZE; + ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq ""; + ThrowUserError('fieldvalue_name_too_long', {value => $value}) + if length($value) > MAX_FIELD_VALUE_SIZE; - my $exists = $invocant->type($field)->new({ name => $value }); - if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) { - ThrowUserError('fieldvalue_already_exists', - { field => $field, value => $exists }); - } + my $exists = $invocant->type($field)->new({name => $value}); + if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) { + ThrowUserError('fieldvalue_already_exists', + {field => $field, value => $exists}); + } - return $value; + return $value; } sub _check_sortkey { - my ($invocant, $value) = @_; - $value = trim($value); - return 0 if !$value; - # Store for the error message in case detaint_natural clears it. - my $orig_value = $value; - (detaint_natural($value) && $value <= MAX_SMALLINT) - || ThrowUserError('fieldvalue_sortkey_invalid', - { sortkey => $orig_value, - field => $invocant->field }); - return $value; + my ($invocant, $value) = @_; + $value = trim($value); + return 0 if !$value; + + # Store for the error message in case detaint_natural clears it. + my $orig_value = $value; + (detaint_natural($value) && $value <= MAX_SMALLINT) + || ThrowUserError('fieldvalue_sortkey_invalid', + {sortkey => $orig_value, field => $invocant->field}); + return $value; } sub _check_visibility_value_id { - my ($invocant, $value_id) = @_; - $value_id = trim($value_id); - my $field = $invocant->field->value_field; - return undef if !$field || !$value_id; - my $value_obj = Bugzilla::Field::Choice->type($field) - ->check({ id => $value_id }); - return $value_obj->id; + my ($invocant, $value_id) = @_; + $value_id = trim($value_id); + my $field = $invocant->field->value_field; + return undef if !$field || !$value_id; + my $value_obj = Bugzilla::Field::Choice->type($field)->check({id => $value_id}); + return $value_obj->id; } 1; diff --git a/Bugzilla/Field/ChoiceInterface.pm b/Bugzilla/Field/ChoiceInterface.pm index 634d36ad1..eeedfca83 100644 --- a/Bugzilla/Field/ChoiceInterface.pm +++ b/Bugzilla/Field/ChoiceInterface.pm @@ -26,14 +26,19 @@ sub FIELD_NAME { return $_[0]->DB_TABLE; } #################### sub _check_if_controller { - my $self = shift; - my $vis_fields = $self->controls_visibility_of_fields; - my $values = $self->controlled_values_array; - if (@$vis_fields || @$values) { - ThrowUserError('fieldvalue_is_controller', - { value => $self, fields => [map($_->name, @$vis_fields)], - vals => $self->controlled_values }); - } + my $self = shift; + my $vis_fields = $self->controls_visibility_of_fields; + my $values = $self->controlled_values_array; + if (@$vis_fields || @$values) { + ThrowUserError( + 'fieldvalue_is_controller', + { + value => $self, + fields => [map($_->name, @$vis_fields)], + vals => $self->controlled_values + } + ); + } } @@ -42,145 +47,149 @@ sub _check_if_controller { ############# sub is_active { return $_[0]->{'isactive'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } sub bug_count { - my $self = shift; - return $self->{bug_count} if defined $self->{bug_count}; - my $dbh = Bugzilla->dbh; - my $fname = $self->field->name; - my $count; - if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { - $count = $dbh->selectrow_array("SELECT COUNT(*) FROM bug_$fname - WHERE value = ?", undef, $self->name); - } - else { - $count = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs - WHERE $fname = ?", - undef, $self->name); - } - $self->{bug_count} = $count; - return $count; + my $self = shift; + return $self->{bug_count} if defined $self->{bug_count}; + my $dbh = Bugzilla->dbh; + my $fname = $self->field->name; + my $count; + if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { + $count = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bug_$fname + WHERE value = ?", undef, $self->name + ); + } + else { + $count = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bugs + WHERE $fname = ?", undef, $self->name + ); + } + $self->{bug_count} = $count; + return $count; } sub field { - my $invocant = shift; - my $class = ref $invocant || $invocant; - my $cache = Bugzilla->request_cache; - # This is just to make life easier for subclasses. Our auto-generated - # subclasses from Bugzilla::Field::Choice->type() already have this set. - $cache->{"field_$class"} ||= - new Bugzilla::Field({ name => $class->FIELD_NAME }); - return $cache->{"field_$class"}; + my $invocant = shift; + my $class = ref $invocant || $invocant; + my $cache = Bugzilla->request_cache; + + # This is just to make life easier for subclasses. Our auto-generated + # subclasses from Bugzilla::Field::Choice->type() already have this set. + $cache->{"field_$class"} ||= new Bugzilla::Field({name => $class->FIELD_NAME}); + return $cache->{"field_$class"}; } sub is_default { - my $self = shift; - my $name = $self->DEFAULT_MAP->{$self->field->name}; - # If it doesn't exist in DEFAULT_MAP, then there is no parameter - # related to this field. - return 0 unless $name; - return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0; + my $self = shift; + my $name = $self->DEFAULT_MAP->{$self->field->name}; + + # If it doesn't exist in DEFAULT_MAP, then there is no parameter + # related to this field. + return 0 unless $name; + return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0; } sub is_static { - my $self = shift; - # If we need to special-case Resolution for *anything* else, it should - # get its own subclass. - if ($self->field->name eq 'resolution') { - return grep($_ eq $self->name, ('', 'FIXED', 'DUPLICATE')) - ? 1 : 0; - } - elsif ($self->field->custom) { - return $self->name eq '---' ? 1 : 0; - } - return 0; + my $self = shift; + + # If we need to special-case Resolution for *anything* else, it should + # get its own subclass. + if ($self->field->name eq 'resolution') { + return grep($_ eq $self->name, ('', 'FIXED', 'DUPLICATE')) ? 1 : 0; + } + elsif ($self->field->custom) { + return $self->name eq '---' ? 1 : 0; + } + return 0; } sub controls_visibility_of_fields { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!$self->{controls_visibility_of_fields}) { - my $ids = $dbh->selectcol_arrayref( - "SELECT id FROM fielddefs + if (!$self->{controls_visibility_of_fields}) { + my $ids = $dbh->selectcol_arrayref( + "SELECT id FROM fielddefs INNER JOIN field_visibility ON fielddefs.id = field_visibility.field_id - WHERE value_id = ? AND visibility_field_id = ?", undef, - $self->id, $self->field->id); + WHERE value_id = ? AND visibility_field_id = ?", undef, $self->id, + $self->field->id + ); - $self->{controls_visibility_of_fields} = - Bugzilla::Field->new_from_list($ids); - } + $self->{controls_visibility_of_fields} = Bugzilla::Field->new_from_list($ids); + } - return $self->{controls_visibility_of_fields}; + return $self->{controls_visibility_of_fields}; } sub visibility_value { - my $self = shift; - if ($self->{visibility_value_id}) { - require Bugzilla::Field::Choice; - $self->{visibility_value} ||= - Bugzilla::Field::Choice->type($self->field->value_field)->new( - $self->{visibility_value_id}); - } - return $self->{visibility_value}; + my $self = shift; + if ($self->{visibility_value_id}) { + require Bugzilla::Field::Choice; + $self->{visibility_value} + ||= Bugzilla::Field::Choice->type($self->field->value_field) + ->new($self->{visibility_value_id}); + } + return $self->{visibility_value}; } sub controlled_values { - my $self = shift; - return $self->{controlled_values} if defined $self->{controlled_values}; - my $fields = $self->field->controls_values_of; - my %controlled_values; - require Bugzilla::Field::Choice; - foreach my $field (@$fields) { - $controlled_values{$field->name} = - Bugzilla::Field::Choice->type($field) - ->match({ visibility_value_id => $self->id }); - } - $self->{controlled_values} = \%controlled_values; - return $self->{controlled_values}; + my $self = shift; + return $self->{controlled_values} if defined $self->{controlled_values}; + my $fields = $self->field->controls_values_of; + my %controlled_values; + require Bugzilla::Field::Choice; + foreach my $field (@$fields) { + $controlled_values{$field->name} = Bugzilla::Field::Choice->type($field) + ->match({visibility_value_id => $self->id}); + } + $self->{controlled_values} = \%controlled_values; + return $self->{controlled_values}; } sub controlled_values_array { - my ($self) = @_; - my $values = $self->controlled_values; - return [map { @{ $values->{$_} } } keys %$values]; + my ($self) = @_; + my $values = $self->controlled_values; + return [map { @{$values->{$_}} } keys %$values]; } sub is_visible_on_bug { - my ($self, $bug) = @_; + my ($self, $bug) = @_; - # Values currently set on the bug are always shown. - return 1 if $self->is_set_on_bug($bug); + # Values currently set on the bug are always shown. + return 1 if $self->is_set_on_bug($bug); - # Inactive values are, otherwise, never shown. - return 0 if !$self->is_active; + # Inactive values are, otherwise, never shown. + return 0 if !$self->is_active; - # Values without a visibility value are, otherwise, always shown. - my $visibility_value = $self->visibility_value; - return 1 if !$visibility_value; + # Values without a visibility value are, otherwise, always shown. + my $visibility_value = $self->visibility_value; + return 1 if !$visibility_value; - # Values with a visibility value are only shown if the visibility - # value is set on the bug. - return $visibility_value->is_set_on_bug($bug); + # Values with a visibility value are only shown if the visibility + # value is set on the bug. + return $visibility_value->is_set_on_bug($bug); } sub is_set_on_bug { - my ($self, $bug) = @_; - my $field_name = $self->FIELD_NAME; - # This allows bug/create/create.html.tmpl to pass in a hashref that - # looks like a bug object. - my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name}; - $value = $value->name if blessed($value); - return 0 if !defined $value; - - if ($self->field->type == FIELD_TYPE_BUG_URLS - or $self->field->type == FIELD_TYPE_MULTI_SELECT) - { - return grep($_ eq $self->name, @$value) ? 1 : 0; - } - return $value eq $self->name ? 1 : 0; + my ($self, $bug) = @_; + my $field_name = $self->FIELD_NAME; + + # This allows bug/create/create.html.tmpl to pass in a hashref that + # looks like a bug object. + my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name}; + $value = $value->name if blessed($value); + return 0 if !defined $value; + + if ( $self->field->type == FIELD_TYPE_BUG_URLS + or $self->field->type == FIELD_TYPE_MULTI_SELECT) + { + return grep($_ eq $self->name, @$value) ? 1 : 0; + } + return $value eq $self->name ? 1 : 0; } 1; diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm index 50474b885..5151e2dba 100644 --- a/Bugzilla/Flag.pm +++ b/Bugzilla/Flag.pm @@ -58,8 +58,9 @@ use parent qw(Bugzilla::Object Exporter); #### Initialization #### ############################### -use constant DB_TABLE => 'flags'; +use constant DB_TABLE => 'flags'; use constant LIST_ORDER => 'id'; + # Flags are tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; @@ -68,35 +69,32 @@ use constant AUDIT_REMOVES => 0; use constant SKIP_REQUESTEE_ON_ERROR => 1; sub DB_COLUMNS { - my $dbh = Bugzilla->dbh; - return qw( - id - type_id - bug_id - attach_id - requestee_id - setter_id - status), - $dbh->sql_date_format('creation_date', '%Y.%m.%d %H:%i:%s') . - ' AS creation_date', - $dbh->sql_date_format('modification_date', '%Y.%m.%d %H:%i:%s') . - ' AS modification_date'; + my $dbh = Bugzilla->dbh; + return qw( + id + type_id + bug_id + attach_id + requestee_id + setter_id + status), + $dbh->sql_date_format('creation_date', '%Y.%m.%d %H:%i:%s') + . ' AS creation_date', + $dbh->sql_date_format('modification_date', '%Y.%m.%d %H:%i:%s') + . ' AS modification_date'; } use constant UPDATE_COLUMNS => qw( - requestee_id - setter_id - status - type_id + requestee_id + setter_id + status + type_id ); -use constant VALIDATORS => { -}; +use constant VALIDATORS => {}; -use constant UPDATE_VALIDATORS => { - setter => \&_check_setter, - status => \&_check_status, -}; +use constant UPDATE_VALIDATORS => + {setter => \&_check_setter, status => \&_check_status,}; ############################### #### Accessors ###### @@ -138,15 +136,15 @@ Returns the timestamp when the flag was last modified. =cut -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'}; } -sub creation_date { return $_[0]->{'creation_date'}; } +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'}; } +sub creation_date { return $_[0]->{'creation_date'}; } sub modification_date { return $_[0]->{'modification_date'}; } ############################### @@ -180,40 +178,42 @@ is an attachment flag, else undefined. =cut sub type { - my $self = shift; + my $self = shift; - return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'}); + return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'}); } sub setter { - my $self = shift; + my $self = shift; - return $self->{'setter'} ||= new Bugzilla::User({ id => $self->{'setter_id'}, cache => 1 }); + return $self->{'setter'} + ||= new Bugzilla::User({id => $self->{'setter_id'}, cache => 1}); } sub requestee { - my $self = shift; + my $self = shift; - if (!defined $self->{'requestee'} && $self->{'requestee_id'}) { - $self->{'requestee'} = new Bugzilla::User({ id => $self->{'requestee_id'}, cache => 1 }); - } - return $self->{'requestee'}; + if (!defined $self->{'requestee'} && $self->{'requestee_id'}) { + $self->{'requestee'} + = new Bugzilla::User({id => $self->{'requestee_id'}, cache => 1}); + } + return $self->{'requestee'}; } sub attachment { - my $self = shift; - return undef unless $self->attach_id; + my $self = shift; + return undef unless $self->attach_id; - require Bugzilla::Attachment; - return $self->{'attachment'} - ||= new Bugzilla::Attachment({ id => $self->attach_id, cache => 1 }); + require Bugzilla::Attachment; + return $self->{'attachment'} + ||= new Bugzilla::Attachment({id => $self->attach_id, cache => 1}); } sub bug { - my $self = shift; + my $self = shift; - require Bugzilla::Bug; - return $self->{'bug'} ||= new Bugzilla::Bug({ id => $self->bug_id, cache => 1 }); + require Bugzilla::Bug; + return $self->{'bug'} ||= new Bugzilla::Bug({id => $self->bug_id, cache => 1}); } ################################ @@ -235,26 +235,27 @@ and returns an array of matching records. =cut sub match { - my $class = shift; - my ($criteria) = @_; - - # If the caller specified only bug or attachment flags, - # limit the query to those kinds of flags. - if (my $type = delete $criteria->{'target_type'}) { - if ($type eq 'bug') { - $criteria->{'attach_id'} = IS_NULL; - } - elsif (!defined $criteria->{'attach_id'}) { - $criteria->{'attach_id'} = NOT_NULL; - } + my $class = shift; + my ($criteria) = @_; + + # If the caller specified only bug or attachment flags, + # limit the query to those kinds of flags. + if (my $type = delete $criteria->{'target_type'}) { + if ($type eq 'bug') { + $criteria->{'attach_id'} = IS_NULL; } - # Flag->snapshot() calls Flag->match() with bug_id and attach_id - # as hash keys, even if attach_id is undefined. - if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) { - $criteria->{'attach_id'} = IS_NULL; + elsif (!defined $criteria->{'attach_id'}) { + $criteria->{'attach_id'} = NOT_NULL; } + } + + # Flag->snapshot() calls Flag->match() with bug_id and attach_id + # as hash keys, even if attach_id is undefined. + if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) { + $criteria->{'attach_id'} = IS_NULL; + } - return $class->SUPER::match(@_); + return $class->SUPER::match(@_); } =pod @@ -272,8 +273,8 @@ and returns an array of matching records. =cut sub count { - my $class = shift; - return scalar @{$class->match(@_)}; + my $class = shift; + return scalar @{$class->match(@_)}; } ###################################################################### @@ -281,144 +282,156 @@ sub count { ###################################################################### sub set_flag { - my ($class, $obj, $params) = @_; - - my ($bug, $attachment, $obj_flag, $requestee_changed); - if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { - $attachment = $obj; - $bug = $attachment->bug; + my ($class, $obj, $params) = @_; + + my ($bug, $attachment, $obj_flag, $requestee_changed); + 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}); + } + + # Make sure the user can change flags + my $privs; + $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs) + || ThrowUserError('illegal_change', + {field => 'flagtypes.name', privs => $privs}); + + # Update (or delete) an existing flag. + if ($params->{id}) { + my $flag = $class->check({id => $params->{id}}); + + # 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); } - elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { - $bug = $obj; - } - else { - ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj }); + ($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; + + ($obj_flag, $requestee_changed) + = $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}); + } } - # Make sure the user can change flags - my $privs; - $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs) - || ThrowUserError('illegal_change', - { field => 'flagtypes.name', privs => $privs }); - - # Update (or delete) an existing flag. - if ($params->{id}) { - my $flag = $class->check({ id => $params->{id} }); - - # 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); - } - ($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; - - ($obj_flag, $requestee_changed) = - $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 }); - } - } - - ($obj_flag, $requestee_changed) = - $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment); - } - else { - ThrowCodeError('param_required', { function => $class . '->set_flag', - param => 'id/type_id' }); - } - - if ($obj_flag - && $requestee_changed - && $obj_flag->requestee_id - && $obj_flag->requestee->setting('requestee_cc') eq 'on') - { - $bug->add_cc($obj_flag->requestee); - } + ($obj_flag, $requestee_changed) + = $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment); + } + else { + ThrowCodeError('param_required', + {function => $class . '->set_flag', param => 'id/type_id'}); + } + + if ( $obj_flag + && $requestee_changed + && $obj_flag->requestee_id + && $obj_flag->requestee->setting('requestee_cc') eq 'on') + { + $bug->add_cc($obj_flag->requestee); + } } 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; + my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_; - $obj_flag->_set_status($params->{status}); - $obj_flag->_set_requestee($params->{requestee}, $bug, $attachment, $params->{skip_roe}); - - # The requestee ID can be undefined. - my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0); - - # The setter field MUST NOT be updated if neither the status - # nor the requestee fields changed. - if (($obj_flag->status ne $old_status) || $requestee_changed) { - $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}}; - return; - } - # Add the newly created flag to the list. - elsif (!$obj_flag->id) { - push(@{$flag_type->{flags}}, $obj_flag); - } - return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag; + # 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}, $bug, $attachment, + $params->{skip_roe}); + + # The requestee ID can be undefined. + my $requestee_changed + = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0); + + # The setter field MUST NOT be updated if neither the status + # nor the requestee fields changed. + if (($obj_flag->status ne $old_status) || $requestee_changed) { + $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}}; + return; + } + + # Add the newly created flag to the list. + elsif (!$obj_flag->id) { + push(@{$flag_type->{flags}}, $obj_flag); + } + return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag; } =pod @@ -434,143 +447,151 @@ Creates a flag record in the database. =cut sub create { - my ($class, $flag, $timestamp) = @_; - $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + my ($class, $flag, $timestamp) = @_; + $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - my $params = {}; - my @columns = grep { $_ ne 'id' } $class->_get_db_columns; + my $params = {}; + my @columns = grep { $_ ne 'id' } $class->_get_db_columns; - # Some columns use date formatting so use alias instead - @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns; + # Some columns use date formatting so use alias instead + @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns; - $params->{$_} = $flag->{$_} foreach @columns; + $params->{$_} = $flag->{$_} foreach @columns; - $params->{creation_date} = $params->{modification_date} = $timestamp; + $params->{creation_date} = $params->{modification_date} = $timestamp; - $flag = $class->SUPER::create($params); - return $flag; + $flag = $class->SUPER::create($params); + return $flag; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - my $changes = $self->SUPER::update(@_); - - if (scalar(keys %$changes)) { - $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?', - undef, ($timestamp, $self->id)); - $self->{'modification_date'} = - format_time($timestamp, '%Y.%m.%d %T', Bugzilla->local_timezone); - Bugzilla->memcached->clear({ table => 'flags', id => $self->id }); - } - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my $changes = $self->SUPER::update(@_); + + if (scalar(keys %$changes)) { + $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?', + undef, ($timestamp, $self->id)); + $self->{'modification_date'} + = format_time($timestamp, '%Y.%m.%d %T', Bugzilla->local_timezone); + Bugzilla->memcached->clear({table => 'flags', id => $self->id}); + } + return $changes; } sub snapshot { - my ($class, $flags) = @_; - - my @summaries; - foreach my $flag (@$flags) { - my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status; - $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee; - push(@summaries, $summary); - } - return @summaries; + my ($class, $flags) = @_; + + my @summaries; + foreach my $flag (@$flags) { + my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status; + $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee; + push(@summaries, $summary); + } + return @summaries; } sub update_activity { - my ($class, $old_summaries, $new_summaries) = @_; + my ($class, $old_summaries, $new_summaries) = @_; - my ($removed, $added) = diff_arrays($old_summaries, $new_summaries); - if (scalar @$removed || scalar @$added) { - # Remove flag requester/setter information - foreach (@$removed, @$added) { s/^[^:]+:// } + my ($removed, $added) = diff_arrays($old_summaries, $new_summaries); + if (scalar @$removed || scalar @$added) { - $removed = join(", ", @$removed); - $added = join(", ", @$added); - return ($removed, $added); - } - return (); + # Remove flag requester/setter information + foreach (@$removed, @$added) {s/^[^:]+://} + + $removed = join(", ", @$removed); + $added = join(", ", @$added); + return ($removed, $added); + } + return (); } sub update_flags { - my ($class, $self, $old_self, $timestamp) = @_; + my ($class, $self, $old_self, $timestamp) = @_; - my @old_summaries = $class->snapshot($old_self->flags); - my %old_flags = map { $_->id => $_ } @{$old_self->flags}; + 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, $timestamp); - } - else { - my $changes = $new_flag->update($timestamp); - if (scalar(keys %$changes)) { - $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp); - } - delete $old_flags{$new_flag->id}; - } + 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, $timestamp); } - # These flags have been deleted. - foreach my $old_flag (values %old_flags) { - $class->notify(undef, $old_flag, $self, $timestamp); - $old_flag->remove_from_db(); + else { + my $changes = $new_flag->update($timestamp); + if (scalar(keys %$changes)) { + $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp); + } + delete $old_flags{$new_flag->id}; } - - # 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})) + } + + # These flags have been deleted. + foreach my $old_flag (values %old_flags) { + $class->notify(undef, $old_flag, $self, $timestamp); + $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', { - my @removed = $class->force_cleanup($self); - push(@old_summaries, @removed); + object => $self, + timestamp => $timestamp, + old_flags => \@old_summaries, + new_flags => \@new_summaries, } - - 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; + ); + 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; + 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 ($class, $bug) = @_; + my $dbh = Bugzilla->dbh; - my $flag_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT flags.id + my $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -578,48 +599,50 @@ sub force_cleanup { 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); + WHERE bugs.bug_id = ? AND i.type_id IS NULL', undef, $bug->id + ); - my @removed = $class->force_retarget($flag_ids, $bug); + my @removed = $class->force_retarget($flag_ids, $bug); - $flag_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT flags.id + $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); + undef, $bug->id + ); - push(@removed , $class->force_retarget($flag_ids, $bug)); - return @removed; + 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)); - Bugzilla->memcached->clear({ table => 'flags', 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(); - } + 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)); + Bugzilla->memcached->clear({table => 'flags', 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; + } + return @removed; } ############################### @@ -627,164 +650,178 @@ sub force_retarget { ############################### sub _set_requestee { - my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; + my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; - $self->{requestee} = - $self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error); + $self->{requestee} = $self->_check_requestee($requestee, $bug, $attachment, + $skip_requestee_on_error); - $self->{requestee_id} = - $self->{requestee} ? $self->{requestee}->id : undef; + $self->{requestee_id} = $self->{requestee} ? $self->{requestee}->id : undef; } sub _set_setter { - my ($self, $setter) = @_; + my ($self, $setter) = @_; - $self->set('setter', $setter); - $self->{setter_id} = $self->setter->id; + $self->set('setter', $setter); + $self->{setter_id} = $self->setter->id; } sub _set_status { - my ($self, $status) = @_; + my ($self, $status) = @_; - # Store the old flag status. It's needed by _check_setter(). - $self->{_old_status} = $self->status; - $self->set('status', $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, $bug, $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); + my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; + + # If the flag status is not "?", then no requestee can be defined. + return undef if ($self->status ne '?'); + + # 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 them or leave them alone. + ThrowUserError('flag_type_requestee_disabled', {type => $self->type}) + if !$self->type->is_requesteeble; + + # You can't ask a disabled account, as they don't have the ability to + # set the flag. + ThrowUserError('flag_requestee_disabled', {requestee => $requestee}) + if !$requestee->is_enabled; + + # Make sure the requestee can see the bug. + # Note that can_see_bug() will query the DB, so if the bug + # is being added/removed from some groups and these changes + # haven't been committed to the DB yet, they won't be taken + # into account here. In this case, old group restrictions matter. + # However, if the user has just been changed to the assignee, + # qa_contact, or added to the cc list of the bug and the bug + # is cclist_accessible, the requestee is allowed. + if ( + !$requestee->can_see_bug($self->bug_id) + && ( !$bug->cclist_accessible + || !grep($_->id == $requestee->id, @{$bug->cc_users}) + && $requestee->id != $bug->assigned_to->id + && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id)) + ) + { + if ($skip_requestee_on_error) { + undef $requestee; + } + else { + ThrowUserError( + 'flag_requestee_unauthorized', + { + flag_type => $self->type, + requestee => $requestee, + bug_id => $self->bug_id, + attach_id => $self->attach_id + } + ); + } } - else { + + # 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 + } + ); + } } - 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 them or leave them alone. - ThrowUserError('flag_type_requestee_disabled', { type => $self->type }) - if !$self->type->is_requesteeble; - - # You can't ask a disabled account, as they don't have the ability to - # set the flag. - ThrowUserError('flag_requestee_disabled', { requestee => $requestee }) - if !$requestee->is_enabled; - - # Make sure the requestee can see the bug. - # Note that can_see_bug() will query the DB, so if the bug - # is being added/removed from some groups and these changes - # haven't been committed to the DB yet, they won't be taken - # into account here. In this case, old group restrictions matter. - # However, if the user has just been changed to the assignee, - # qa_contact, or added to the cc list of the bug and the bug - # is cclist_accessible, the requestee is allowed. - if (!$requestee->can_see_bug($self->bug_id) - && (!$bug->cclist_accessible - || !grep($_->id == $requestee->id, @{ $bug->cc_users }) - && $requestee->id != $bug->assigned_to->id - && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id))) - { - if ($skip_requestee_on_error) { - undef $requestee; - } - else { - 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}); - } - } + # 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; + } + 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) - || ThrowUserError('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 request is being retargetted, we don't update - # the setter, so that the setter gets the notification. - if ($status eq '?' && $self->{_old_status} eq '?') { - return $self->setter; - } - return $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) + || ThrowUserError('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 request is being retargetted, we don't update + # the setter, so that the setter gets the notification. + if ($status eq '?' && $self->{_old_status} eq '?') { + 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)) - { - ThrowUserError('flag_status_invalid', { id => $self->id, - status => $status }); - } - return $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)) + { + ThrowUserError('flag_status_invalid', {id => $self->id, status => $status}); + } + return $status; } ###################################################################### @@ -805,128 +842,146 @@ array of hashes. This array is then passed to Flag::create(). =cut sub extract_flags_from_cgi { - my ($class, $bug, $attachment, $vars, $skip) = @_; - my $cgi = Bugzilla->cgi; - - my $match_status = Bugzilla::User::match_field({ - '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, - }, undef, $skip); - - $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'; + my ($class, $bug, $attachment, $vars, $skip) = @_; + my $cgi = Bugzilla->cgi; + + my $match_status + = Bugzilla::User::match_field( + {'^requestee(_type)?-(\d+)$' => {'type' => 'multi'},}, + undef, $skip); + + $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'; + } + + # 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); + + # Extract a list of existing flag IDs. + my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param()); + + return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids)); + + 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-$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 requestee. + my @requestees = $cgi->param("requestee-$flag_id"); + my $requestee_email; + if ($status eq "?" && scalar(@requestees) > 1 && $flag->type->is_multiplicable) + { + # The first person, for which we'll reuse the existing flag. + $requestee_email = shift(@requestees); + + # Create new flags like the existing one for each additional person. + foreach my $login (@requestees) { + push( + @new_flags, + { + type_id => $flag->type_id, + status => "?", + requestee => $login, + skip_roe => $skip + } + ); + } } + elsif ($status eq "?" && scalar(@requestees)) { - # 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); - - # Extract a list of existing flag IDs. - my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param()); - - return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids)); - - 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-$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 requestee. - my @requestees = $cgi->param("requestee-$flag_id"); - my $requestee_email; - if ($status eq "?" - && scalar(@requestees) > 1 - && $flag->type->is_multiplicable) - { - # The first person, for which we'll reuse the existing flag. - $requestee_email = shift(@requestees); - - # Create new flags like the existing one for each additional person. - foreach my $login (@requestees) { - push(@new_flags, { type_id => $flag->type_id, - status => "?", - requestee => $login, - skip_roe => $skip }); - } - } - 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") || ''); - } - - push(@flags, { id => $flag_id, - status => $status, - requestee => $requestee_email, - skip_roe => $skip }); + # 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") || ''); } - # 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 $flagtype_id (@flagtype_ids) { - # Checks if there are unexpected flags for the product/component. - if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { - $vars->{'message'} = 'unexpected_flag_types'; - last; - } + push( + @flags, + { + id => $flag_id, + status => $status, + requestee => $requestee_email, + skip_roe => $skip + } + ); + } + + # 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 $flagtype_id (@flagtype_ids) { + + # Checks if there are unexpected flags for the product/component. + if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { + $vars->{'message'} = 'unexpected_flag_types'; + last; } - - foreach my $flag_type (@$flag_types) { - my $type_id = $flag_type->id; - - # Bug flags are only valid for bugs, and attachment flags are - # only valid for attachments. So don't mix both. - 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 } @flagtype_ids); - - # Get the number of flags of this type already set for this target. - my $has_flags = $class->count( - { 'type_id' => $type_id, - 'target_type' => $attachment ? 'attachment' : 'bug', - 'bug_id' => $bug->bug_id, - 'attach_id' => $attachment ? $attachment->id : undef }); - - # Do not create a new flag of this type if this flag type is - # not multiplicable and already has a flag set. - next if (!$flag_type->is_multiplicable && $has_flags); - - my $status = $cgi->param("flag_type-$type_id"); - trick_taint($status); - - my @logins = $cgi->param("requestee_type-$type_id"); - if ($status eq "?" && scalar(@logins)) { - foreach my $login (@logins) { - push (@new_flags, { type_id => $type_id, - status => $status, - requestee => $login, - skip_roe => $skip }); - last unless $flag_type->is_multiplicable; - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status }); - } + } + + foreach my $flag_type (@$flag_types) { + my $type_id = $flag_type->id; + + # Bug flags are only valid for bugs, and attachment flags are + # only valid for attachments. So don't mix both. + 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 } @flagtype_ids); + + # Get the number of flags of this type already set for this target. + my $has_flags = $class->count({ + 'type_id' => $type_id, + 'target_type' => $attachment ? 'attachment' : 'bug', + 'bug_id' => $bug->bug_id, + 'attach_id' => $attachment ? $attachment->id : undef + }); + + # Do not create a new flag of this type if this flag type is + # not multiplicable and already has a flag set. + next if (!$flag_type->is_multiplicable && $has_flags); + + my $status = $cgi->param("flag_type-$type_id"); + trick_taint($status); + + my @logins = $cgi->param("requestee_type-$type_id"); + if ($status eq "?" && scalar(@logins)) { + foreach my $login (@logins) { + push( + @new_flags, + { + type_id => $type_id, + status => $status, + requestee => $login, + skip_roe => $skip + } + ); + last unless $flag_type->is_multiplicable; + } + } + else { + push(@new_flags, {type_id => $type_id, status => $status}); } + } - # Return the list of flags to update and/or to create. - return (\@flags, \@new_flags); + # Return the list of flags to update and/or to create. + return (\@flags, \@new_flags); } =pod @@ -944,100 +999,109 @@ from the previous sub-routine as it is called for changing multiple bugs =cut sub multi_extract_flags_from_cgi { - my ($class, $bug, $vars, $skip) = @_; - my $cgi = Bugzilla->cgi; - - my $match_status = Bugzilla::User::match_field({ - '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, - }, undef, $skip); - - $vars->{'match_field'} = 'requestee'; - if ($match_status == USER_MATCH_FAILED) { - $vars->{'message'} = 'user_match_failed'; + my ($class, $bug, $vars, $skip) = @_; + my $cgi = Bugzilla->cgi; + + my $match_status + = Bugzilla::User::match_field( + {'^requestee(_type)?-(\d+)$' => {'type' => 'multi'},}, + undef, $skip); + + $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'; + } + + # Extract a list of flag type IDs from field names. + my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); + + my (@new_flags, @flags); + + # 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 $flagtype_id (@flagtype_ids) { + + # Checks if there are unexpected flags for the product/component. + if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { + $vars->{'message'} = 'unexpected_flag_types'; + last; } - elsif ($match_status == USER_MATCH_MULTIPLE) { - $vars->{'message'} = 'user_match_multiple'; - } - - # Extract a list of flag type IDs from field names. - my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); - - my (@new_flags, @flags); - - # 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 $flagtype_id (@flagtype_ids) { - # Checks if there are unexpected flags for the product/component. - if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { - $vars->{'message'} = 'unexpected_flag_types'; - last; - } - } - - foreach my $flag_type (@$flag_types) { - my $type_id = $flag_type->id; - - # Bug flags are only valid for bugs - next unless ($flag_type->target_type eq 'bug'); - - # We are only interested in flags the user tries to create. - next unless scalar(grep { $_ == $type_id } @flagtype_ids); - - # Get the flags of this type already set for this bug. - my $current_flags = $class->match( - { 'type_id' => $type_id, - 'target_type' => 'bug', - 'bug_id' => $bug->bug_id }); - - # We will update existing flags (instead of creating new ones) - # if the flag exists and the user has not chosen the 'always add' - # option - my $update = scalar(@$current_flags) && ! $cgi->param("flags_add-$type_id"); - - my $status = $cgi->param("flag_type-$type_id"); - trick_taint($status); - - my @logins = $cgi->param("requestee_type-$type_id"); - if ($status eq "?" && scalar(@logins)) { - foreach my $login (@logins) { - if ($update) { - foreach my $current_flag (@$current_flags) { - push (@flags, { id => $current_flag->id, - status => $status, - requestee => $login, - skip_roe => $skip }); - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status, - requestee => $login, - skip_roe => $skip }); - } - - last unless $flag_type->is_multiplicable; - } + } + + foreach my $flag_type (@$flag_types) { + my $type_id = $flag_type->id; + + # Bug flags are only valid for bugs + next unless ($flag_type->target_type eq 'bug'); + + # We are only interested in flags the user tries to create. + next unless scalar(grep { $_ == $type_id } @flagtype_ids); + + # Get the flags of this type already set for this bug. + my $current_flags = $class->match( + {'type_id' => $type_id, 'target_type' => 'bug', 'bug_id' => $bug->bug_id}); + + # We will update existing flags (instead of creating new ones) + # if the flag exists and the user has not chosen the 'always add' + # option + my $update = scalar(@$current_flags) && !$cgi->param("flags_add-$type_id"); + + my $status = $cgi->param("flag_type-$type_id"); + trick_taint($status); + + my @logins = $cgi->param("requestee_type-$type_id"); + if ($status eq "?" && scalar(@logins)) { + foreach my $login (@logins) { + if ($update) { + foreach my $current_flag (@$current_flags) { + push( + @flags, + { + id => $current_flag->id, + status => $status, + requestee => $login, + skip_roe => $skip + } + ); + } } else { - if ($update) { - foreach my $current_flag (@$current_flags) { - push (@flags, { id => $current_flag->id, - status => $status }); - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status }); + push( + @new_flags, + { + type_id => $type_id, + status => $status, + requestee => $login, + skip_roe => $skip } + ); } + + last unless $flag_type->is_multiplicable; + } } + else { + if ($update) { + foreach my $current_flag (@$current_flags) { + push(@flags, {id => $current_flag->id, status => $status}); + } + } + else { + push(@new_flags, {type_id => $type_id, status => $status}); + } + } + } - # Return the list of flags to update and/or to create. - return (\@flags, \@new_flags); + # Return the list of flags to update and/or to create. + return (\@flags, \@new_flags); } =pod @@ -1054,113 +1118,121 @@ or deleted. =cut sub notify { - my ($class, $flag, $old_flag, $obj, $timestamp) = @_; - - 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 ($class, $flag, $old_flag, $obj, $timestamp) = @_; + + 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; } - - my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list; - # Is there someone to notify? - return unless ($addressee || $cc_list); - - # The email client will display the Date: header in the desired timezone, - # so we can always use UTC here. - $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); - - # 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 - # on the request type's CC: list, so we have to trawl the list for users - # not in those groups or email addresses that don't have an account. - my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups}; - my $attachment_is_private = $attachment ? $attachment->isprivate : undef; - - my %recipients; - 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); - # Prevent duplicated entries due to case sensitivity. - $cc = $ccuser ? $ccuser->email : $cc; - $recipients{$cc} = $ccuser; - } - - # Only notify if the addressee is allowed to receive the email. - 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) { - $default_lang = Bugzilla::User->new()->setting('lang'); - } - - # Get comments on the bug - my $all_comments = $bug->comments({ after => $bug->lastdiffed }); - @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments; - - # Get public only comments - my $public_comments = [ grep { !$_->is_private } @$all_comments ]; - - foreach my $to (keys %recipients) { - # Add threadingmarker to allow flag notification emails to be the - # threaded similar to normal bug change emails. - my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0; - - # We only want to show private comments to users in the is_insider group - my $comments = $recipients{$to} && $recipients{$to}->is_insider - ? $all_comments : $public_comments; - - my $vars = { - flag => $flag, - old_flag => $old_flag, - to => $to, - date => $timestamp, - bug => $bug, - attachment => $attachment, - threadingmarker => build_thread_marker($bug->id, $thread_user_id), - new_comments => $comments, - }; - - my $lang = $recipients{$to} ? - $recipients{$to}->setting('lang') : $default_lang; - - my $template = Bugzilla->template_inner($lang); - my $message; - $template->process("email/flagmail.txt.tmpl", $vars, \$message) - || ThrowTemplateError($template->error()); - - MessageToMTA($message); + } + 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); + + # The email client will display the Date: header in the desired timezone, + # so we can always use UTC here. + $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); + + # 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 + # on the request type's CC: list, so we have to trawl the list for users + # not in those groups or email addresses that don't have an account. + my @bug_in_groups = grep { $_->{'ison'} || $_->{'mandatory'} } @{$bug->groups}; + my $attachment_is_private = $attachment ? $attachment->isprivate : undef; + + my %recipients; + 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); + + # Prevent duplicated entries due to case sensitivity. + $cc = $ccuser ? $ccuser->email : $cc; + $recipients{$cc} = $ccuser; + } + + # Only notify if the addressee is allowed to receive the email. + 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) { + $default_lang = Bugzilla::User->new()->setting('lang'); + } + + # Get comments on the bug + my $all_comments = $bug->comments({after => $bug->lastdiffed}); + @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments; + + # Get public only comments + my $public_comments = [grep { !$_->is_private } @$all_comments]; + + foreach my $to (keys %recipients) { + + # Add threadingmarker to allow flag notification emails to be the + # threaded similar to normal bug change emails. + my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0; + + # We only want to show private comments to users in the is_insider group + my $comments = $recipients{$to} + && $recipients{$to}->is_insider ? $all_comments : $public_comments; + + my $vars = { + flag => $flag, + old_flag => $old_flag, + to => $to, + date => $timestamp, + bug => $bug, + attachment => $attachment, + threadingmarker => build_thread_marker($bug->id, $thread_user_id), + new_comments => $comments, + }; + + my $lang = $recipients{$to} ? $recipients{$to}->setting('lang') : $default_lang; + + my $template = Bugzilla->template_inner($lang); + my $message; + $template->process("email/flagmail.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + } } # This is an internal function used by $bug->flag_types @@ -1168,39 +1240,42 @@ sub notify { # flag types and existing flags set on them. You should never # call this function directly. sub _flag_types { - my ($class, $vars) = @_; - - my $target_type = $vars->{target_type}; - my $flags; - - # Retrieve all existing flags for this bug/attachment. - if ($target_type eq 'bug') { - my $bug_id = delete $vars->{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 = $class->match({attach_id => $attach_id}); - } - else { - ThrowCodeError('bad_arg', {argument => 'target_type', - function => $class . '->_flag_types'}); - } - - # Get all available flag types for the given product and component. - 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, 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; + my ($class, $vars) = @_; + + my $target_type = $vars->{target_type}; + my $flags; + + # Retrieve all existing flags for this bug/attachment. + if ($target_type eq 'bug') { + my $bug_id = delete $vars->{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 = $class->match({attach_id => $attach_id}); + } + else { + ThrowCodeError('bad_arg', + {argument => 'target_type', function => $class . '->_flag_types'}); + } + + # Get all available flag types for the given product and component. + 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, 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; } 1; diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm index 72b3f64c1..78123d548 100644 --- a/Bugzilla/FlagType.pm +++ b/Bugzilla/FlagType.pm @@ -49,113 +49,114 @@ use parent qw(Bugzilla::Object); #### Initialization #### ############################### -use constant DB_TABLE => 'flagtypes'; +use constant DB_TABLE => 'flagtypes'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - cc_list - target_type - sortkey - is_active - is_requestable - is_requesteeble - is_multiplicable - grant_group_id - request_group_id + id + name + description + cc_list + target_type + sortkey + is_active + is_requestable + is_requesteeble + is_multiplicable + grant_group_id + request_group_id ); use constant UPDATE_COLUMNS => qw( - name - description - cc_list - sortkey - is_active - is_requestable - is_requesteeble - is_multiplicable - grant_group_id - request_group_id + name + description + cc_list + sortkey + is_active + is_requestable + is_requesteeble + is_multiplicable + grant_group_id + request_group_id ); use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - cc_list => \&_check_cc_list, - target_type => \&_check_target_type, - sortkey => \&_check_sortkey, - is_active => \&Bugzilla::Object::check_boolean, - is_requestable => \&Bugzilla::Object::check_boolean, - is_requesteeble => \&Bugzilla::Object::check_boolean, - is_multiplicable => \&Bugzilla::Object::check_boolean, - grant_group => \&_check_group, - request_group => \&_check_group, + name => \&_check_name, + description => \&_check_description, + cc_list => \&_check_cc_list, + target_type => \&_check_target_type, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, + is_requestable => \&Bugzilla::Object::check_boolean, + is_requesteeble => \&Bugzilla::Object::check_boolean, + is_multiplicable => \&Bugzilla::Object::check_boolean, + grant_group => \&_check_group, + request_group => \&_check_group, }; -use constant UPDATE_VALIDATORS => { - grant_group_id => \&_check_group, - request_group_id => \&_check_group, -}; +use constant UPDATE_VALIDATORS => + {grant_group_id => \&_check_group, request_group_id => \&_check_group,}; ############################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; + my $class = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); - $dbh->bz_start_transaction(); + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - # In the DB, only the first character of the target type is stored. - $params->{target_type} = substr($params->{target_type}, 0, 1); + # In the DB, only the first character of the target type is stored. + $params->{target_type} = substr($params->{target_type}, 0, 1); - # Extract everything which is not a valid column name. - $params->{grant_group_id} = delete $params->{grant_group}; - $params->{request_group_id} = delete $params->{request_group}; - my $inclusions = delete $params->{inclusions}; - my $exclusions = delete $params->{exclusions}; + # Extract everything which is not a valid column name. + $params->{grant_group_id} = delete $params->{grant_group}; + $params->{request_group_id} = delete $params->{request_group}; + my $inclusions = delete $params->{inclusions}; + my $exclusions = delete $params->{exclusions}; - my $flagtype = $class->insert_create_data($params); + my $flagtype = $class->insert_create_data($params); - $flagtype->set_clusions({ inclusions => $inclusions, - exclusions => $exclusions }); - $flagtype->update(); + $flagtype->set_clusions({inclusions => $inclusions, exclusions => $exclusions}); + $flagtype->update(); - $dbh->bz_commit_transaction(); - return $flagtype; + $dbh->bz_commit_transaction(); + return $flagtype; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $flag_id = $self->id; + my $self = shift; + my $dbh = Bugzilla->dbh; + my $flag_id = $self->id; - $dbh->bz_start_transaction(); - my $changes = $self->SUPER::update(@_); + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); - # Update the flaginclusions and flagexclusions tables. - foreach my $category ('inclusions', 'exclusions') { - next unless delete $self->{"_update_$category"}; + # Update the flaginclusions and flagexclusions tables. + foreach my $category ('inclusions', 'exclusions') { + next unless delete $self->{"_update_$category"}; - $dbh->do("DELETE FROM flag$category WHERE type_id = ?", undef, $flag_id); + $dbh->do("DELETE FROM flag$category WHERE type_id = ?", undef, $flag_id); - my $sth = $dbh->prepare("INSERT INTO flag$category - (type_id, product_id, component_id) VALUES (?, ?, ?)"); + my $sth = $dbh->prepare( + "INSERT INTO flag$category + (type_id, product_id, component_id) VALUES (?, ?, ?)" + ); - foreach my $prod_comp (values %{$self->{$category}}) { - my ($prod_id, $comp_id) = split(':', $prod_comp); - $prod_id ||= undef; - $comp_id ||= undef; - $sth->execute($flag_id, $prod_id, $comp_id); - } - $changes->{$category} = [0, 1]; + foreach my $prod_comp (values %{$self->{$category}}) { + my ($prod_id, $comp_id) = split(':', $prod_comp); + $prod_id ||= undef; + $comp_id ||= undef; + $sth->execute($flag_id, $prod_id, $comp_id); } + $changes->{$category} = [0, 1]; + } - # Clear existing flags for bugs/attachments in categories no longer on - # the list of inclusions or that have been added to the list of exclusions. - my $flag_ids = $dbh->selectcol_arrayref('SELECT DISTINCT flags.id + # Clear existing flags for bugs/attachments in categories no longer on + # the list of inclusions or that have been added to the list of exclusions. + my $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -166,11 +167,13 @@ sub update { AND (bugs.component_id = i.component_id OR i.component_id IS NULL)) WHERE flags.type_id = ? - AND i.type_id IS NULL', - undef, $self->id); - Bugzilla::Flag->force_retarget($flag_ids); + AND i.type_id IS NULL', undef, + $self->id + ); + Bugzilla::Flag->force_retarget($flag_ids); - $flag_ids = $dbh->selectcol_arrayref('SELECT DISTINCT flags.id + $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -181,26 +184,29 @@ sub update { OR e.product_id IS NULL) AND (bugs.component_id = e.component_id OR e.component_id IS NULL)', - undef, $self->id); - Bugzilla::Flag->force_retarget($flag_ids); - - # Silently remove requestees from flags which are no longer - # specifically requestable. - if (!$self->is_requesteeble) { - my $ids = $dbh->selectcol_arrayref( - 'SELECT id FROM flags WHERE type_id = ? AND requestee_id IS NOT NULL', - undef, $self->id); - - if (@$ids) { - $dbh->do('UPDATE flags SET requestee_id = NULL WHERE ' . $dbh->sql_in('id', $ids)); - foreach my $id (@$ids) { - Bugzilla->memcached->clear({ table => 'flags', id => $id }); - } - } + undef, $self->id + ); + Bugzilla::Flag->force_retarget($flag_ids); + + # Silently remove requestees from flags which are no longer + # specifically requestable. + if (!$self->is_requesteeble) { + my $ids + = $dbh->selectcol_arrayref( + 'SELECT id FROM flags WHERE type_id = ? AND requestee_id IS NOT NULL', + undef, $self->id); + + if (@$ids) { + $dbh->do( + 'UPDATE flags SET requestee_id = NULL WHERE ' . $dbh->sql_in('id', $ids)); + foreach my $id (@$ids) { + Bugzilla->memcached->clear({table => 'flags', id => $id}); + } } + } - $dbh->bz_commit_transaction(); - return $changes; + $dbh->bz_commit_transaction(); + return $changes; } ############################### @@ -259,172 +265,174 @@ Returns the sortkey of the flagtype. =cut -sub id { return $_[0]->{'id'}; } -sub name { return $_[0]->{'name'}; } -sub description { return $_[0]->{'description'}; } -sub cc_list { return $_[0]->{'cc_list'}; } -sub target_type { return $_[0]->{'target_type'} eq 'b' ? 'bug' : 'attachment'; } -sub is_active { return $_[0]->{'is_active'}; } -sub is_requestable { return $_[0]->{'is_requestable'}; } -sub is_requesteeble { return $_[0]->{'is_requesteeble'}; } +sub id { return $_[0]->{'id'}; } +sub name { return $_[0]->{'name'}; } +sub description { return $_[0]->{'description'}; } +sub cc_list { return $_[0]->{'cc_list'}; } +sub target_type { return $_[0]->{'target_type'} eq 'b' ? 'bug' : 'attachment'; } +sub is_active { return $_[0]->{'is_active'}; } +sub is_requestable { return $_[0]->{'is_requestable'}; } +sub is_requesteeble { return $_[0]->{'is_requesteeble'}; } sub is_multiplicable { return $_[0]->{'is_multiplicable'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } sub request_group_id { return $_[0]->{'request_group_id'}; } -sub grant_group_id { return $_[0]->{'grant_group_id'}; } +sub grant_group_id { return $_[0]->{'grant_group_id'}; } ################################ # Validators ################################ sub _check_name { - my ($invocant, $name) = @_; + my ($invocant, $name) = @_; - $name = trim($name); - ($name && $name !~ /[\s,]/ && length($name) <= 50) - || ThrowUserError('flag_type_name_invalid', { name => $name }); - return $name; + $name = trim($name); + ($name && $name !~ /[\s,]/ && length($name) <= 50) + || ThrowUserError('flag_type_name_invalid', {name => $name}); + return $name; } sub _check_description { - my ($invocant, $desc) = @_; + my ($invocant, $desc) = @_; - $desc = trim($desc); - $desc || ThrowUserError('flag_type_description_invalid'); - return $desc; + $desc = trim($desc); + $desc || ThrowUserError('flag_type_description_invalid'); + return $desc; } sub _check_cc_list { - my ($invocant, $cc_list) = @_; - - length($cc_list) <= 200 - || ThrowUserError('flag_type_cc_list_invalid', { cc_list => $cc_list }); - - my @addresses = split(/[,\s]+/, $cc_list); - my $addr_spec = $Email::Address::addr_spec; - # We do not call check_email_syntax() because these addresses do not - # require to match 'emailregexp' and do not depend on 'emailsuffix'. - foreach my $address (@addresses) { - ($address !~ /\P{ASCII}/ && $address =~ /^$addr_spec$/) - || ThrowUserError('illegal_email_address', - {addr => $address, default => 1}); - } - return $cc_list; + my ($invocant, $cc_list) = @_; + + length($cc_list) <= 200 + || ThrowUserError('flag_type_cc_list_invalid', {cc_list => $cc_list}); + + my @addresses = split(/[,\s]+/, $cc_list); + my $addr_spec = $Email::Address::addr_spec; + + # We do not call check_email_syntax() because these addresses do not + # require to match 'emailregexp' and do not depend on 'emailsuffix'. + foreach my $address (@addresses) { + ($address !~ /\P{ASCII}/ && $address =~ /^$addr_spec$/) + || ThrowUserError('illegal_email_address', {addr => $address, default => 1}); + } + return $cc_list; } sub _check_target_type { - my ($invocant, $target_type) = @_; + my ($invocant, $target_type) = @_; - ($target_type eq 'bug' || $target_type eq 'attachment') - || ThrowCodeError('flag_type_target_type_invalid', { target_type => $target_type }); - return $target_type; + ($target_type eq 'bug' || $target_type eq 'attachment') + || ThrowCodeError('flag_type_target_type_invalid', + {target_type => $target_type}); + return $target_type; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; + my ($invocant, $sortkey) = @_; - (detaint_natural($sortkey) && $sortkey <= MAX_SMALLINT) - || ThrowUserError('flag_type_sortkey_invalid', { sortkey => $sortkey }); - return $sortkey; + (detaint_natural($sortkey) && $sortkey <= MAX_SMALLINT) + || ThrowUserError('flag_type_sortkey_invalid', {sortkey => $sortkey}); + return $sortkey; } sub _check_group { - my ($invocant, $group) = @_; - return unless $group; + my ($invocant, $group) = @_; + return unless $group; - trick_taint($group); - $group = Bugzilla::Group->check($group); - return $group->id; + trick_taint($group); + $group = Bugzilla::Group->check($group); + return $group->id; } ############################### #### Methods #### ############################### -sub set_name { $_[0]->set('name', $_[1]); } -sub set_description { $_[0]->set('description', $_[1]); } -sub set_cc_list { $_[0]->set('cc_list', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_is_active { $_[0]->set('is_active', $_[1]); } -sub set_is_requestable { $_[0]->set('is_requestable', $_[1]); } -sub set_is_specifically_requestable { $_[0]->set('is_requesteeble', $_[1]); } -sub set_is_multiplicable { $_[0]->set('is_multiplicable', $_[1]); } -sub set_grant_group { $_[0]->set('grant_group_id', $_[1]); } -sub set_request_group { $_[0]->set('request_group_id', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_cc_list { $_[0]->set('cc_list', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } +sub set_is_requestable { $_[0]->set('is_requestable', $_[1]); } +sub set_is_specifically_requestable { $_[0]->set('is_requesteeble', $_[1]); } +sub set_is_multiplicable { $_[0]->set('is_multiplicable', $_[1]); } +sub set_grant_group { $_[0]->set('grant_group_id', $_[1]); } +sub set_request_group { $_[0]->set('request_group_id', $_[1]); } sub set_clusions { - my ($self, $list) = @_; - my $user = Bugzilla->user; - my %products; - my $params = {}; - - # If the user has editcomponents privs, then we only need to make sure - # that the product exists. - if ($user->in_group('editcomponents')) { - $params->{allow_inaccessible} = 1; - } - - foreach my $category (keys %$list) { - my %clusions; - my %clusions_as_hash; - - foreach my $prod_comp (@{$list->{$category} || []}) { - my ($prod_id, $comp_id) = split(':', $prod_comp); - my $prod_name = '__Any__'; - my $comp_name = '__Any__'; - # Does the product exist? - if ($prod_id) { - detaint_natural($prod_id) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::FlagType::set_clusions' }); - - if (!$products{$prod_id}) { - $params->{id} = $prod_id; - $products{$prod_id} = Bugzilla::Product->check($params); - } - $prod_name = $products{$prod_id}->name; - - # Does the component belong to this product? - if ($comp_id) { - detaint_natural($comp_id) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::FlagType::set_clusions' }); - - my ($component) = grep { $_->id == $comp_id } @{$products{$prod_id}->components} - or ThrowUserError('product_unknown_component', - { product => $prod_name, comp_id => $comp_id }); - $comp_name = $component->name; - } - else { - $comp_id = 0; - } - } - else { - $prod_id = 0; - $comp_id = 0; - } - $clusions{"$prod_name:$comp_name"} = "$prod_id:$comp_id"; - $clusions_as_hash{$prod_id}->{$comp_id} = 1; + my ($self, $list) = @_; + my $user = Bugzilla->user; + my %products; + my $params = {}; + + # If the user has editcomponents privs, then we only need to make sure + # that the product exists. + if ($user->in_group('editcomponents')) { + $params->{allow_inaccessible} = 1; + } + + foreach my $category (keys %$list) { + my %clusions; + my %clusions_as_hash; + + foreach my $prod_comp (@{$list->{$category} || []}) { + my ($prod_id, $comp_id) = split(':', $prod_comp); + my $prod_name = '__Any__'; + my $comp_name = '__Any__'; + + # Does the product exist? + if ($prod_id) { + detaint_natural($prod_id) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::FlagType::set_clusions'}); + + if (!$products{$prod_id}) { + $params->{id} = $prod_id; + $products{$prod_id} = Bugzilla::Product->check($params); } - - # Check the user has the editcomponent permission on products that are changing - if (! $user->in_group('editcomponents')) { - my $current_clusions = $self->$category; - my ($removed, $added) - = diff_arrays([ values %$current_clusions ], [ values %clusions ]); - my @changed_product_ids - = uniq map { substr($_, 0, index($_, ':')) } @$removed, @$added; - foreach my $product_id (@changed_product_ids) { - $user->in_group('editcomponents', $product_id) - || ThrowUserError('product_access_denied', - { name => $products{$product_id}->name }); - } + $prod_name = $products{$prod_id}->name; + + # Does the component belong to this product? + if ($comp_id) { + detaint_natural($comp_id) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::FlagType::set_clusions'}); + + my ($component) = grep { $_->id == $comp_id } @{$products{$prod_id}->components} + or ThrowUserError('product_unknown_component', + {product => $prod_name, comp_id => $comp_id}); + $comp_name = $component->name; + } + else { + $comp_id = 0; } + } + else { + $prod_id = 0; + $comp_id = 0; + } + $clusions{"$prod_name:$comp_name"} = "$prod_id:$comp_id"; + $clusions_as_hash{$prod_id}->{$comp_id} = 1; + } - # Set the changes - $self->{$category} = \%clusions; - $self->{"${category}_as_hash"} = \%clusions_as_hash; - $self->{"_update_$category"} = 1; + # Check the user has the editcomponent permission on products that are changing + if (!$user->in_group('editcomponents')) { + my $current_clusions = $self->$category; + my ($removed, $added) + = diff_arrays([values %$current_clusions], [values %clusions]); + my @changed_product_ids = uniq map { substr($_, 0, index($_, ':')) } @$removed, + @$added; + foreach my $product_id (@changed_product_ids) { + $user->in_group('editcomponents', $product_id) + || ThrowUserError('product_access_denied', + {name => $products{$product_id}->name}); + } } + + # Set the changes + $self->{$category} = \%clusions; + $self->{"${category}_as_hash"} = \%clusions_as_hash; + $self->{"_update_$category"} = 1; + } } =pod @@ -465,76 +473,79 @@ explicitly excluded from the flagtype. =cut sub grant_list { - my $self = shift; - require Bugzilla::User; - my @custusers; - my @allusers = @{Bugzilla->user->get_userlist}; - foreach my $user (@allusers) { - my $user_obj = new Bugzilla::User({name => $user->{login}}); - push(@custusers, $user) if $user_obj->can_set_flag($self); - } - return \@custusers; + my $self = shift; + require Bugzilla::User; + my @custusers; + my @allusers = @{Bugzilla->user->get_userlist}; + foreach my $user (@allusers) { + my $user_obj = new Bugzilla::User({name => $user->{login}}); + push(@custusers, $user) if $user_obj->can_set_flag($self); + } + return \@custusers; } sub grant_group { - my $self = shift; + my $self = shift; - if (!defined $self->{'grant_group'} && $self->{'grant_group_id'}) { - $self->{'grant_group'} = new Bugzilla::Group($self->{'grant_group_id'}); - } - return $self->{'grant_group'}; + if (!defined $self->{'grant_group'} && $self->{'grant_group_id'}) { + $self->{'grant_group'} = new Bugzilla::Group($self->{'grant_group_id'}); + } + return $self->{'grant_group'}; } sub request_group { - my $self = shift; + my $self = shift; - if (!defined $self->{'request_group'} && $self->{'request_group_id'}) { - $self->{'request_group'} = new Bugzilla::Group($self->{'request_group_id'}); - } - return $self->{'request_group'}; + if (!defined $self->{'request_group'} && $self->{'request_group_id'}) { + $self->{'request_group'} = new Bugzilla::Group($self->{'request_group_id'}); + } + return $self->{'request_group'}; } sub flag_count { - my $self = shift; - - if (!defined $self->{'flag_count'}) { - $self->{'flag_count'} = - Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM flags - WHERE type_id = ?', undef, $self->{'id'}); - } - return $self->{'flag_count'}; + my $self = shift; + + if (!defined $self->{'flag_count'}) { + $self->{'flag_count'} = Bugzilla->dbh->selectrow_array( + 'SELECT COUNT(*) FROM flags + WHERE type_id = ?', undef, $self->{'id'} + ); + } + return $self->{'flag_count'}; } sub inclusions { - my $self = shift; + my $self = shift; - if (!defined $self->{inclusions}) { - ($self->{inclusions}, $self->{inclusions_as_hash}) = get_clusions($self->id, 'in'); - } - return $self->{inclusions}; + if (!defined $self->{inclusions}) { + ($self->{inclusions}, $self->{inclusions_as_hash}) + = get_clusions($self->id, 'in'); + } + return $self->{inclusions}; } sub inclusions_as_hash { - my $self = shift; + my $self = shift; - $self->inclusions unless defined $self->{inclusions_as_hash}; - return $self->{inclusions_as_hash}; + $self->inclusions unless defined $self->{inclusions_as_hash}; + return $self->{inclusions_as_hash}; } sub exclusions { - my $self = shift; + my $self = shift; - if (!defined $self->{exclusions}) { - ($self->{exclusions}, $self->{exclusions_as_hash}) = get_clusions($self->id, 'ex'); - } - return $self->{exclusions}; + if (!defined $self->{exclusions}) { + ($self->{exclusions}, $self->{exclusions_as_hash}) + = get_clusions($self->id, 'ex'); + } + return $self->{exclusions}; } sub exclusions_as_hash { - my $self = shift; + my $self = shift; - $self->exclusions unless defined $self->{exclusions_as_hash}; - return $self->{exclusions_as_hash}; + $self->exclusions unless defined $self->{exclusions_as_hash}; + return $self->{exclusions_as_hash}; } ###################################################################### @@ -558,11 +569,11 @@ $clusions{'product_name:component_name'} = "product_ID:component_ID" =cut sub get_clusions { - my ($id, $type) = @_; - my $dbh = Bugzilla->dbh; + my ($id, $type) = @_; + my $dbh = Bugzilla->dbh; - my $list = - $dbh->selectall_arrayref("SELECT products.id, products.name, + my $list = $dbh->selectall_arrayref( + "SELECT products.id, products.name, components.id, components.name FROM flagtypes INNER JOIN flag${type}clusions @@ -571,19 +582,19 @@ sub get_clusions { ON flag${type}clusions.product_id = products.id LEFT JOIN components ON flag${type}clusions.component_id = components.id - WHERE flagtypes.id = ?", - undef, $id); - my (%clusions, %clusions_as_hash); - foreach my $data (@$list) { - my ($product_id, $product_name, $component_id, $component_name) = @$data; - $product_id ||= 0; - $product_name ||= "__Any__"; - $component_id ||= 0; - $component_name ||= "__Any__"; - $clusions{"$product_name:$component_name"} = "$product_id:$component_id"; - $clusions_as_hash{$product_id}->{$component_id} = 1; - } - return (\%clusions, \%clusions_as_hash); + WHERE flagtypes.id = ?", undef, $id + ); + my (%clusions, %clusions_as_hash); + foreach my $data (@$list) { + my ($product_id, $product_name, $component_id, $component_name) = @$data; + $product_id ||= 0; + $product_name ||= "__Any__"; + $component_id ||= 0; + $component_name ||= "__Any__"; + $clusions{"$product_name:$component_name"} = "$product_id:$component_id"; + $clusions_as_hash{$product_id}->{$component_id} = 1; + } + return (\%clusions, \%clusions_as_hash); } =pod @@ -600,18 +611,19 @@ and returns a list of matching flagtype objects. =cut sub match { - my ($criteria) = @_; - my $dbh = Bugzilla->dbh; + my ($criteria) = @_; + my $dbh = Bugzilla->dbh; - # Depending on the criteria, we may have to append additional tables. - my $tables = [DB_TABLE]; - my @criteria = sqlify_criteria($criteria, $tables); - $tables = join(' ', @$tables); - $criteria = join(' AND ', @criteria); + # Depending on the criteria, we may have to append additional tables. + my $tables = [DB_TABLE]; + my @criteria = sqlify_criteria($criteria, $tables); + $tables = join(' ', @$tables); + $criteria = join(' AND ', @criteria); - my $flagtype_ids = $dbh->selectcol_arrayref("SELECT id FROM $tables WHERE $criteria"); + my $flagtype_ids + = $dbh->selectcol_arrayref("SELECT id FROM $tables WHERE $criteria"); - return Bugzilla::FlagType->new_from_list($flagtype_ids); + return Bugzilla::FlagType->new_from_list($flagtype_ids); } =pod @@ -627,18 +639,20 @@ Returns the total number of flag types matching the given criteria. =cut sub count { - my ($criteria) = @_; - my $dbh = Bugzilla->dbh; - - # Depending on the criteria, we may have to append additional tables. - my $tables = [DB_TABLE]; - my @criteria = sqlify_criteria($criteria, $tables); - $tables = join(' ', @$tables); - $criteria = join(' AND ', @criteria); - - my $count = $dbh->selectrow_array("SELECT COUNT(flagtypes.id) - FROM $tables WHERE $criteria"); - return $count; + my ($criteria) = @_; + my $dbh = Bugzilla->dbh; + + # Depending on the criteria, we may have to append additional tables. + my $tables = [DB_TABLE]; + my @criteria = sqlify_criteria($criteria, $tables); + $tables = join(' ', @$tables); + $criteria = join(' AND ', @criteria); + + my $count = $dbh->selectrow_array( + "SELECT COUNT(flagtypes.id) + FROM $tables WHERE $criteria" + ); + return $count; } ###################################################################### @@ -646,93 +660,98 @@ sub count { ###################################################################### # Converts a hash of criteria into a list of SQL criteria. -# $criteria is a reference to the criteria (field => value), -# $tables is a reference to an array of tables being accessed +# $criteria is a reference to the criteria (field => value), +# $tables is a reference to an array of tables being accessed # by the query. sub sqlify_criteria { - my ($criteria, $tables) = @_; - my $dbh = Bugzilla->dbh; - - # the generated list of SQL criteria; "1=1" is a clever way of making sure - # there's something in the list so calling code doesn't have to check list - # size before building a WHERE clause out of it - my @criteria = ("1=1"); - - if ($criteria->{name}) { - if (ref($criteria->{name}) eq 'ARRAY') { - my @names = map { $dbh->quote($_) } @{$criteria->{name}}; - # Detaint data as we have quoted it. - foreach my $name (@names) { - trick_taint($name); - } - push @criteria, $dbh->sql_in('flagtypes.name', \@names); - } - else { - my $name = $dbh->quote($criteria->{name}); - trick_taint($name); # Detaint data as we have quoted it. - push(@criteria, "flagtypes.name = $name"); - } + my ($criteria, $tables) = @_; + my $dbh = Bugzilla->dbh; + + # the generated list of SQL criteria; "1=1" is a clever way of making sure + # there's something in the list so calling code doesn't have to check list + # size before building a WHERE clause out of it + my @criteria = ("1=1"); + + if ($criteria->{name}) { + if (ref($criteria->{name}) eq 'ARRAY') { + my @names = map { $dbh->quote($_) } @{$criteria->{name}}; + + # Detaint data as we have quoted it. + foreach my $name (@names) { + trick_taint($name); + } + push @criteria, $dbh->sql_in('flagtypes.name', \@names); } - if ($criteria->{target_type}) { - # The target type is stored in the database as a one-character string - # ("a" for attachment and "b" for bug), but this function takes complete - # names ("attachment" and "bug") for clarity, so we must convert them. - my $target_type = $criteria->{target_type} eq 'bug'? 'b' : 'a'; - push(@criteria, "flagtypes.target_type = '$target_type'"); + else { + my $name = $dbh->quote($criteria->{name}); + trick_taint($name); # Detaint data as we have quoted it. + push(@criteria, "flagtypes.name = $name"); } - if (exists($criteria->{is_active})) { - my $is_active = $criteria->{is_active} ? "1" : "0"; - push(@criteria, "flagtypes.is_active = $is_active"); - } - if ($criteria->{product_id}) { - my $product_id = $criteria->{product_id}; - detaint_natural($product_id) - || ThrowCodeError('bad_arg', { argument => 'product_id', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - # Add inclusions to the query, which simply involves joining the table - # by flag type ID and target product/component. - push(@$tables, "INNER JOIN flaginclusions AS i ON flagtypes.id = i.type_id"); - push(@criteria, "(i.product_id = $product_id OR i.product_id IS NULL)"); - - # Add exclusions to the query, which is more complicated. First of all, - # we do a LEFT JOIN so we don't miss flag types with no exclusions. - # Then, as with inclusions, we join on flag type ID and target product/ - # component. However, since we want flag types that *aren't* on the - # exclusions list, we add a WHERE criteria to use only records with - # NULL exclusion type, i.e. without any exclusions. - my $join_clause = "flagtypes.id = e.type_id "; - - my $addl_join_clause = ""; - if ($criteria->{component_id}) { - my $component_id = $criteria->{component_id}; - detaint_natural($component_id) - || ThrowCodeError('bad_arg', { argument => 'component_id', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - push(@criteria, "(i.component_id = $component_id OR i.component_id IS NULL)"); - $join_clause .= "AND (e.component_id = $component_id OR e.component_id IS NULL) "; - } - else { - $addl_join_clause = "AND e.component_id IS NULL OR (i.component_id = e.component_id) "; - } - $join_clause .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_id IS NULL)"; - - push(@$tables, "LEFT JOIN flagexclusions AS e ON ($join_clause)"); - push(@criteria, "e.type_id IS NULL"); + } + if ($criteria->{target_type}) { + + # The target type is stored in the database as a one-character string + # ("a" for attachment and "b" for bug), but this function takes complete + # names ("attachment" and "bug") for clarity, so we must convert them. + my $target_type = $criteria->{target_type} eq 'bug' ? 'b' : 'a'; + push(@criteria, "flagtypes.target_type = '$target_type'"); + } + if (exists($criteria->{is_active})) { + my $is_active = $criteria->{is_active} ? "1" : "0"; + push(@criteria, "flagtypes.is_active = $is_active"); + } + if ($criteria->{product_id}) { + my $product_id = $criteria->{product_id}; + detaint_natural($product_id) + || ThrowCodeError('bad_arg', + {argument => 'product_id', function => 'Bugzilla::FlagType::sqlify_criteria'}); + + # Add inclusions to the query, which simply involves joining the table + # by flag type ID and target product/component. + push(@$tables, "INNER JOIN flaginclusions AS i ON flagtypes.id = i.type_id"); + push(@criteria, "(i.product_id = $product_id OR i.product_id IS NULL)"); + + # Add exclusions to the query, which is more complicated. First of all, + # we do a LEFT JOIN so we don't miss flag types with no exclusions. + # Then, as with inclusions, we join on flag type ID and target product/ + # component. However, since we want flag types that *aren't* on the + # exclusions list, we add a WHERE criteria to use only records with + # NULL exclusion type, i.e. without any exclusions. + my $join_clause = "flagtypes.id = e.type_id "; + + my $addl_join_clause = ""; + if ($criteria->{component_id}) { + my $component_id = $criteria->{component_id}; + detaint_natural($component_id) || ThrowCodeError('bad_arg', + {argument => 'component_id', function => 'Bugzilla::FlagType::sqlify_criteria'} + ); + + push(@criteria, "(i.component_id = $component_id OR i.component_id IS NULL)"); + $join_clause + .= "AND (e.component_id = $component_id OR e.component_id IS NULL) "; } - if ($criteria->{group}) { - my $gid = $criteria->{group}; - detaint_natural($gid) - || ThrowCodeError('bad_arg', { argument => 'group', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - push(@criteria, "(flagtypes.grant_group_id = $gid " . - " OR flagtypes.request_group_id = $gid)"); + else { + $addl_join_clause + = "AND e.component_id IS NULL OR (i.component_id = e.component_id) "; } - - return @criteria; + $join_clause + .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_id IS NULL)"; + + push(@$tables, "LEFT JOIN flagexclusions AS e ON ($join_clause)"); + push(@criteria, "e.type_id IS NULL"); + } + if ($criteria->{group}) { + my $gid = $criteria->{group}; + detaint_natural($gid) + || ThrowCodeError('bad_arg', + {argument => 'group', function => 'Bugzilla::FlagType::sqlify_criteria'}); + + push(@criteria, + "(flagtypes.grant_group_id = $gid " . " OR flagtypes.request_group_id = $gid)"); + } + + return @criteria; } 1; diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm index f7a50f7f1..157f8cd32 100644 --- a/Bugzilla/Group.pm +++ b/Bugzilla/Group.pm @@ -25,13 +25,13 @@ use Bugzilla::Config qw(:admin); use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( - groups.id - groups.name - groups.description - groups.isbuggroup - groups.userregexp - groups.isactive - groups.icon_url + groups.id + groups.name + groups.description + groups.isbuggroup + groups.userregexp + groups.isactive + groups.icon_url ); use constant DB_TABLE => 'groups'; @@ -39,136 +39,136 @@ use constant DB_TABLE => 'groups'; use constant LIST_ORDER => 'isbuggroup, name'; use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - userregexp => \&_check_user_regexp, - isactive => \&_check_is_active, - isbuggroup => \&_check_is_bug_group, - icon_url => \&_check_icon_url, + name => \&_check_name, + description => \&_check_description, + userregexp => \&_check_user_regexp, + isactive => \&_check_is_active, + isbuggroup => \&_check_is_bug_group, + icon_url => \&_check_icon_url, }; use constant UPDATE_COLUMNS => qw( - name - description - userregexp - isactive - icon_url + name + description + userregexp + isactive + icon_url ); # Parameters that are lists of groups. use constant GROUP_PARAMS => qw( - chartgroup comment_taggers_group debug_group insidergroup - querysharegroup timetrackinggroup + chartgroup comment_taggers_group debug_group insidergroup + querysharegroup timetrackinggroup ); ############################### #### Accessors ###### ############################### -sub description { return $_[0]->{'description'}; } -sub is_bug_group { return $_[0]->{'isbuggroup'}; } -sub user_regexp { return $_[0]->{'userregexp'}; } -sub is_active { return $_[0]->{'isactive'}; } -sub icon_url { return $_[0]->{'icon_url'}; } +sub description { return $_[0]->{'description'}; } +sub is_bug_group { return $_[0]->{'isbuggroup'}; } +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}; + 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) = @_; - $self->{members_direct} ||= $self->_get_members(GRANT_DIRECT); - return $self->{members_direct}; + my ($self) = @_; + $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}; + 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 = defined($grant_type) ? "AND grant_type = $grant_type" - : ""; - my $user_ids = $dbh->selectcol_arrayref( - "SELECT DISTINCT user_id + my ($self, $grant_type) = @_; + my $dbh = Bugzilla->dbh; + my $grant_clause = defined($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); + 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}; + 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} ||= {}; - return $self->{grant_direct}->{$type} - if defined $self->{grant_direct}->{$type}; - my $dbh = Bugzilla->dbh; - - my $ids = $dbh->selectcol_arrayref( - "SELECT member_id FROM group_group_map - WHERE grantor_id = ? AND grant_type = $type", - undef, $self->id) || []; - - $self->{grant_direct}->{$type} = $self->new_from_list($ids); - return $self->{grant_direct}->{$type}; + my ($self, $type) = @_; + $self->{grant_direct} ||= {}; + return $self->{grant_direct}->{$type} if defined $self->{grant_direct}->{$type}; + my $dbh = Bugzilla->dbh; + + my $ids = $dbh->selectcol_arrayref( + "SELECT member_id FROM group_group_map + WHERE grantor_id = ? AND grant_type = $type", undef, $self->id + ) || []; + + $self->{grant_direct}->{$type} = $self->new_from_list($ids); + return $self->{grant_direct}->{$type}; } sub granted_by_direct { - my ($self, $type) = @_; - $self->{granted_by_direct} ||= {}; - return $self->{granted_by_direct}->{$type} - if defined $self->{granted_by_direct}->{$type}; - my $dbh = Bugzilla->dbh; - - my $ids = $dbh->selectcol_arrayref( - "SELECT grantor_id FROM group_group_map - WHERE member_id = ? AND grant_type = $type", - undef, $self->id) || []; - - $self->{granted_by_direct}->{$type} = $self->new_from_list($ids); - return $self->{granted_by_direct}->{$type}; + my ($self, $type) = @_; + $self->{granted_by_direct} ||= {}; + return $self->{granted_by_direct}->{$type} + if defined $self->{granted_by_direct}->{$type}; + my $dbh = Bugzilla->dbh; + + my $ids = $dbh->selectcol_arrayref( + "SELECT grantor_id FROM group_group_map + WHERE member_id = ? AND grant_type = $type", undef, $self->id + ) || []; + + $self->{granted_by_direct}->{$type} = $self->new_from_list($ids); + 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, + 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}; + 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}; } ############################### @@ -176,126 +176,127 @@ sub products { ############################### sub check_members_are_visible { - my $self = shift; - my $user = Bugzilla->user; - return if !Bugzilla->params->{'usevisibilitygroups'}; - - my $group_id = $self->id; - my $is_visible = grep { $_ == $group_id } @{ $user->visible_groups_inherited }; - if (!$is_visible) { - ThrowUserError('group_not_visible', { group => $self }); - } + my $self = shift; + my $user = Bugzilla->user; + return if !Bugzilla->params->{'usevisibilitygroups'}; + + my $group_id = $self->id; + my $is_visible = grep { $_ == $group_id } @{$user->visible_groups_inherited}; + if (!$is_visible) { + ThrowUserError('group_not_visible', {group => $self}); + } } sub set_description { $_[0]->set('description', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } -sub set_name { $_[0]->set('name', $_[1]); } -sub set_user_regexp { $_[0]->set('userregexp', $_[1]); } -sub set_icon_url { $_[0]->set('icon_url', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_user_regexp { $_[0]->set('userregexp', $_[1]); } +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}) { - my ($old_name, $new_name) = @{$changes->{name}}; - my $update_params; - foreach my $group (GROUP_PARAMS) { - if ($old_name eq Bugzilla->params->{$group}) { - SetParam($group, $new_name); - $update_params = 1; - } - } - write_params() if $update_params; + my $self = shift; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); + + if (exists $changes->{name}) { + my ($old_name, $new_name) = @{$changes->{name}}; + my $update_params; + foreach my $group (GROUP_PARAMS) { + if ($old_name eq Bugzilla->params->{$group}) { + SetParam($group, $new_name); + $update_params = 1; + } } + write_params() if $update_params; + } - # If we've changed this group to be active, fix any Mandatory groups. - $self->_enforce_mandatory if (exists $changes->{isactive} - && $changes->{isactive}->[1]); + # If we've changed this group to be active, fix any Mandatory groups. + $self->_enforce_mandatory + if (exists $changes->{isactive} && $changes->{isactive}->[1]); - $self->_rederive_regexp() if exists $changes->{userregexp}; + $self->_rederive_regexp() if exists $changes->{userregexp}; - Bugzilla::Hook::process('group_end_of_update', - { group => $self, changes => $changes }); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - return $changes; + Bugzilla::Hook::process('group_end_of_update', + {group => $self, changes => $changes}); + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + 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 }); + 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}); + } - # 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'}; - return if $params->{'test_only'}; + my $cantdelete = 0; - my $cantdelete = 0; + my $users = $self->members_non_inherited; + if (scalar(@$users) && !$params->{'remove_from_users'}) { + $cantdelete = 1; + } - 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 $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 $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; - } + my $flag_types = $self->flag_types; + if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) { + $cantdelete = 1; + } - ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete; + 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(); + 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 { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $gid = $self->id; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + my $gid = $self->id; - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs INNER JOIN group_control_map ON group_control_map.product_id = bugs.product_id @@ -304,156 +305,171 @@ sub _enforce_mandatory { AND bug_group_map.group_id = group_control_map.group_id WHERE group_control_map.group_id = ? AND group_control_map.membercontrol = ? - AND bug_group_map.group_id IS NULL', - undef, ($gid, CONTROLMAPMANDATORY)); - - my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); - foreach my $bug_id (@$bug_ids) { - $sth->execute($bug_id, $gid); - } + AND bug_group_map.group_id IS NULL', undef, + ($gid, CONTROLMAPMANDATORY) + ); + + my $sth + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); + foreach my $bug_id (@$bug_ids) { + $sth->execute($bug_id, $gid); + } } sub is_active_bug_group { - my $self = shift; - return $self->is_active && $self->is_bug_group; + my $self = shift; + return $self->is_active && $self->is_bug_group; } sub _rederive_regexp { - my ($self) = @_; + my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("SELECT userid, login_name, group_id + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare( + "SELECT userid, login_name, group_id FROM profiles LEFT JOIN user_group_map ON user_group_map.user_id = profiles.userid AND group_id = ? AND grant_type = ? - AND isbless = 0"); - my $sthadd = $dbh->prepare("INSERT INTO user_group_map + AND isbless = 0" + ); + my $sthadd = $dbh->prepare( + "INSERT INTO user_group_map (user_id, group_id, grant_type, isbless) - VALUES (?, ?, ?, 0)"); - my $sthdel = $dbh->prepare("DELETE FROM user_group_map + VALUES (?, ?, ?, 0)" + ); + my $sthdel = $dbh->prepare( + "DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? - AND grant_type = ? and isbless = 0"); - $sth->execute($self->id, GRANT_REGEXP); - my $regexp = $self->user_regexp; - while (my ($uid, $login, $present) = $sth->fetchrow_array) { - if ($regexp ne '' and $login =~ /$regexp/i) { - $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present; - } else { - $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present; - } + AND grant_type = ? and isbless = 0" + ); + $sth->execute($self->id, GRANT_REGEXP); + my $regexp = $self->user_regexp; + + while (my ($uid, $login, $present) = $sth->fetchrow_array) { + if ($regexp ne '' and $login =~ /$regexp/i) { + $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present; + } + else { + $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present; } + } } sub flatten_group_membership { - my ($self, @groups) = @_; - - my $dbh = Bugzilla->dbh; - my $sth; - my @groupidstocheck = @groups; - my %groupidschecked = (); - $sth = $dbh->prepare("SELECT member_id FROM group_group_map + my ($self, @groups) = @_; + + my $dbh = Bugzilla->dbh; + my $sth; + my @groupidstocheck = @groups; + my %groupidschecked = (); + $sth = $dbh->prepare( + "SELECT member_id FROM group_group_map WHERE grantor_id = ? - AND grant_type = " . GROUP_MEMBERSHIP); - while (my $node = shift @groupidstocheck) { - $sth->execute($node); - my $member; - while (($member) = $sth->fetchrow_array) { - if (!$groupidschecked{$member}) { - $groupidschecked{$member} = 1; - push @groupidstocheck, $member; - push @groups, $member unless grep $_ == $member, @groups; - } - } + AND grant_type = " . GROUP_MEMBERSHIP + ); + while (my $node = shift @groupidstocheck) { + $sth->execute($node); + my $member; + while (($member) = $sth->fetchrow_array) { + if (!$groupidschecked{$member}) { + $groupidschecked{$member} = 1; + push @groupidstocheck, $member; + push @groups, $member unless grep $_ == $member, @groups; + } } - return \@groups; + } + return \@groups; } - - ################################ ##### Module Subroutines ### ################################ sub create { - my $class = shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; - - my $silently = delete $params->{silently}; - my $use_in_all_products = delete $params->{use_in_all_products}; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) { - print get_text('install_group_create', { name => $params->{name} }), - "\n"; - } + my $class = shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + my $silently = delete $params->{silently}; + my $use_in_all_products = delete $params->{use_in_all_products}; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) { + print get_text('install_group_create', {name => $params->{name}}), "\n"; + } - my $group = $class->SUPER::create(@_); + $dbh->bz_start_transaction(); - # Since we created a new group, give the "admin" group all privileges - # initially. - my $admin = new Bugzilla::Group({name => 'admin'}); - # This function is also used to create the "admin" group itself, - # so there's a chance it won't exist yet. - if ($admin) { - my $sth = $dbh->prepare('INSERT INTO group_group_map - (member_id, grantor_id, grant_type) - VALUES (?, ?, ?)'); - $sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP); - $sth->execute($admin->id, $group->id, GROUP_BLESS); - $sth->execute($admin->id, $group->id, GROUP_VISIBLE); - } + my $group = $class->SUPER::create(@_); - # Permit all existing products to use the new group if requested. - if ($use_in_all_products) { - $dbh->do('INSERT INTO group_control_map + # Since we created a new group, give the "admin" group all privileges + # initially. + my $admin = new Bugzilla::Group({name => 'admin'}); + + # This function is also used to create the "admin" group itself, + # so there's a chance it won't exist yet. + if ($admin) { + my $sth = $dbh->prepare( + 'INSERT INTO group_group_map + (member_id, grantor_id, grant_type) + VALUES (?, ?, ?)' + ); + $sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP); + $sth->execute($admin->id, $group->id, GROUP_BLESS); + $sth->execute($admin->id, $group->id, GROUP_VISIBLE); + } + + # Permit all existing products to use the new group if requested. + if ($use_in_all_products) { + $dbh->do( + 'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol) - SELECT ?, products.id, ?, ? FROM products', - undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA)); - } + SELECT ?, products.id, ?, ? FROM products', undef, + ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA) + ); + } - $group->_rederive_regexp() if $group->user_regexp; + $group->_rederive_regexp() if $group->user_regexp; - Bugzilla::Hook::process('group_end_of_create', { group => $group }); - $dbh->bz_commit_transaction(); - return $group; + Bugzilla::Hook::process('group_end_of_create', {group => $group}); + $dbh->bz_commit_transaction(); + return $group; } sub ValidateGroupName { - my ($name, @users) = (@_); - my $dbh = Bugzilla->dbh; - my $query = "SELECT id FROM groups " . - "WHERE name = ?"; - if (Bugzilla->params->{'usevisibilitygroups'}) { - my @visible = (-1); - foreach my $user (@users) { - $user && push @visible, @{$user->visible_groups_direct}; - } - my $visible = join(', ', @visible); - $query .= " AND id IN($visible)"; + my ($name, @users) = (@_); + my $dbh = Bugzilla->dbh; + my $query = "SELECT id FROM groups " . "WHERE name = ?"; + if (Bugzilla->params->{'usevisibilitygroups'}) { + my @visible = (-1); + foreach my $user (@users) { + $user && push @visible, @{$user->visible_groups_direct}; } - my $sth = $dbh->prepare($query); - $sth->execute($name); - my ($ret) = $sth->fetchrow_array(); - return $ret; + my $visible = join(', ', @visible); + $query .= " AND id IN($visible)"; + } + my $sth = $dbh->prepare($query); + $sth->execute($name); + my ($ret) = $sth->fetchrow_array(); + return $ret; } sub check_no_disclose { - my ($class, $params) = @_; - my $action = delete $params->{action}; + my ($class, $params) = @_; + my $action = delete $params->{action}; - $action =~ /^(?:add|remove)$/ - or ThrowCodeError('bad_arg', { argument => $action, - function => "${class}::check_no_disclose" }); + $action =~ /^(?:add|remove)$/ + or ThrowCodeError('bad_arg', + {argument => $action, function => "${class}::check_no_disclose"}); - $params->{_error} = ($action eq 'add') ? 'group_restriction_not_allowed' - : 'group_invalid_removal'; + $params->{_error} + = ($action eq 'add') + ? 'group_restriction_not_allowed' + : 'group_invalid_removal'; - my $group = $class->check($params); - return $group; + my $group = $class->check($params); + return $group; } ############################### @@ -461,34 +477,36 @@ sub check_no_disclose { ############################### sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError("empty_group_name"); - # If we're creating a Group or changing the name... - if (!ref($invocant) || lc($invocant->name) ne lc($name)) { - my $exists = new Bugzilla::Group({name => $name }); - ThrowUserError("group_exists", { name => $name }) if $exists; - } - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError("empty_group_name"); + + # If we're creating a Group or changing the name... + if (!ref($invocant) || lc($invocant->name) ne lc($name)) { + my $exists = new Bugzilla::Group({name => $name}); + ThrowUserError("group_exists", {name => $name}) if $exists; + } + return $name; } sub _check_description { - my ($invocant, $desc) = @_; - $desc = trim($desc); - $desc || ThrowUserError("empty_group_description"); - return $desc; + my ($invocant, $desc) = @_; + $desc = trim($desc); + $desc || ThrowUserError("empty_group_description"); + return $desc; } sub _check_user_regexp { - my ($invocant, $regex) = @_; - $regex = trim($regex) || ''; - ThrowUserError("invalid_regexp") unless (eval {qr/$regex/}); - return $regex; + my ($invocant, $regex) = @_; + $regex = trim($regex) || ''; + ThrowUserError("invalid_regexp") unless (eval {qr/$regex/}); + return $regex; } sub _check_is_active { return $_[1] ? 1 : 0; } + sub _check_is_bug_group { - return $_[1] ? 1 : 0; + return $_[1] ? 1 : 0; } sub _check_icon_url { return $_[1] ? clean_text($_[1]) : undef; } diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm index d8ae67463..3575d30c1 100644 --- a/Bugzilla/Hook.pm +++ b/Bugzilla/Hook.pm @@ -12,33 +12,33 @@ use strict; use warnings; sub process { - my ($name, $args) = @_; + my ($name, $args) = @_; - _entering($name); + _entering($name); - foreach my $extension (@{ Bugzilla->extensions }) { - if ($extension->can($name)) { - $extension->$name($args); - } + foreach my $extension (@{Bugzilla->extensions}) { + if ($extension->can($name)) { + $extension->$name($args); } + } - _leaving($name); + _leaving($name); } sub in { - my $hook_name = shift; - my $currently_in = Bugzilla->request_cache->{hook_stack}->[-1] || ''; - return $hook_name eq $currently_in ? 1 : 0; + my $hook_name = shift; + my $currently_in = Bugzilla->request_cache->{hook_stack}->[-1] || ''; + return $hook_name eq $currently_in ? 1 : 0; } sub _entering { - my ($hook_name) = @_; - my $hook_stack = Bugzilla->request_cache->{hook_stack} ||= []; - push(@$hook_stack, $hook_name); + my ($hook_name) = @_; + my $hook_stack = Bugzilla->request_cache->{hook_stack} ||= []; + push(@$hook_stack, $hook_name); } sub _leaving { - pop @{ Bugzilla->request_cache->{hook_stack} }; + pop @{Bugzilla->request_cache->{hook_stack}}; } 1; diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm index 07bc9d6c3..64c2a2cbb 100644 --- a/Bugzilla/Install.pm +++ b/Bugzilla/Install.pm @@ -7,7 +7,7 @@ package Bugzilla::Install; -# Functions in this this package can assume that the database +# Functions in this this package can assume that the database # has been set up, params are available, localconfig is # available, and any module can be used. # @@ -31,412 +31,421 @@ use Bugzilla::Util qw(get_text); use Bugzilla::Version; use constant STATUS_WORKFLOW => ( - [undef, 'UNCONFIRMED'], - [undef, 'CONFIRMED'], - [undef, 'IN_PROGRESS'], - ['UNCONFIRMED', 'CONFIRMED'], - ['UNCONFIRMED', 'IN_PROGRESS'], - ['UNCONFIRMED', 'RESOLVED'], - ['CONFIRMED', 'IN_PROGRESS'], - ['CONFIRMED', 'RESOLVED'], - ['IN_PROGRESS', 'CONFIRMED'], - ['IN_PROGRESS', 'RESOLVED'], - ['RESOLVED', 'UNCONFIRMED'], - ['RESOLVED', 'CONFIRMED'], - ['RESOLVED', 'VERIFIED'], - ['VERIFIED', 'UNCONFIRMED'], - ['VERIFIED', 'CONFIRMED'], + [undef, 'UNCONFIRMED'], + [undef, 'CONFIRMED'], + [undef, 'IN_PROGRESS'], + ['UNCONFIRMED', 'CONFIRMED'], + ['UNCONFIRMED', 'IN_PROGRESS'], + ['UNCONFIRMED', 'RESOLVED'], + ['CONFIRMED', 'IN_PROGRESS'], + ['CONFIRMED', 'RESOLVED'], + ['IN_PROGRESS', 'CONFIRMED'], + ['IN_PROGRESS', 'RESOLVED'], + ['RESOLVED', 'UNCONFIRMED'], + ['RESOLVED', 'CONFIRMED'], + ['RESOLVED', 'VERIFIED'], + ['VERIFIED', 'UNCONFIRMED'], + ['VERIFIED', 'CONFIRMED'], ); sub SETTINGS { - return { + return { # 2005-03-03 travis@sedsystems.ca -- Bug 41972 - display_quips => { options => ["on", "off"], default => "on" }, + display_quips => {options => ["on", "off"], default => "on"}, + # 2005-03-10 travis@sedsystems.ca -- Bug 199048 - comment_sort_order => { options => ["oldest_to_newest", "newest_to_oldest", - "newest_to_oldest_desc_first"], - default => "oldest_to_newest" }, + comment_sort_order => { + options => + ["oldest_to_newest", "newest_to_oldest", "newest_to_oldest_desc_first"], + default => "oldest_to_newest" + }, + # 2005-05-12 bugzilla@glob.com.au -- Bug 63536 - post_bug_submit_action => { options => ["next_bug", "same_bug", "nothing"], - default => "next_bug" }, + post_bug_submit_action => + {options => ["next_bug", "same_bug", "nothing"], default => "next_bug"}, + # 2005-06-29 wurblzap@gmail.com -- Bug 257767 - csv_colsepchar => { options => [',',';'], default => ',' }, + csv_colsepchar => {options => [',', ';'], default => ','}, + # 2005-10-26 wurblzap@gmail.com -- Bug 291459 - zoom_textareas => { options => ["on", "off"], default => "on" }, + zoom_textareas => {options => ["on", "off"], default => "on"}, + # 2006-05-01 olav@bkor.dhs.org -- Bug 7710 - state_addselfcc => { options => ['always', 'never', 'cc_unless_role'], - default => 'cc_unless_role' }, + state_addselfcc => { + options => ['always', 'never', 'cc_unless_role'], + default => 'cc_unless_role' + }, + # 2006-08-04 wurblzap@gmail.com -- Bug 322693 - skin => { subclass => 'Skin', default => 'Dusk' }, + skin => {subclass => 'Skin', default => 'Dusk'}, + # 2006-12-10 LpSolit@gmail.com -- Bug 297186 - lang => { subclass => 'Lang', - default => ${Bugzilla->languages}[0] }, + lang => {subclass => 'Lang', default => ${Bugzilla->languages}[0]}, + # 2007-07-02 altlist@gmail.com -- Bug 225731 - quote_replies => { options => ['quoted_reply', 'simple_reply', 'off'], - default => "quoted_reply" }, + quote_replies => { + options => ['quoted_reply', 'simple_reply', 'off'], + default => "quoted_reply" + }, + # 2009-02-01 mozilla@matt.mchenryfamily.org -- Bug 398473 - comment_box_position => { options => ['before_comments', 'after_comments'], - default => 'before_comments' }, + comment_box_position => { + options => ['before_comments', 'after_comments'], + default => 'before_comments' + }, + # 2008-08-27 LpSolit@gmail.com -- Bug 182238 - timezone => { subclass => 'Timezone', default => 'local' }, + timezone => {subclass => 'Timezone', default => 'local'}, + # 2011-02-07 dkl@mozilla.com -- Bug 580490 - quicksearch_fulltext => { options => ['on', 'off'], default => 'on' }, + quicksearch_fulltext => {options => ['on', 'off'], default => 'on'}, + # 2011-06-21 glob@mozilla.com -- Bug 589128 - email_format => { options => ['html', 'text_only'], - default => 'html' }, + email_format => {options => ['html', 'text_only'], default => 'html'}, + # 2011-10-11 glob@mozilla.com -- Bug 301656 - requestee_cc => { options => ['on', 'off'], default => 'on' }, + requestee_cc => {options => ['on', 'off'], default => 'on'}, + # 2012-04-30 glob@mozilla.com -- Bug 663747 - bugmail_new_prefix => { options => ['on', 'off'], default => 'on' }, + bugmail_new_prefix => {options => ['on', 'off'], default => 'on'}, + # 2013-07-26 joshi_sunil@in.com -- Bug 669535 - possible_duplicates => { options => ['on', 'off'], default => 'on' }, - } -}; + possible_duplicates => {options => ['on', 'off'], default => 'on'}, + }; +} use constant SYSTEM_GROUPS => ( - { - name => 'admin', - description => 'Administrators' - }, - { - name => 'tweakparams', - description => 'Can change Parameters' - }, - { - name => 'editusers', - description => 'Can edit or disable users' - }, - { - name => 'creategroups', - description => 'Can create and destroy groups' - }, - { - name => 'editclassifications', - description => 'Can create, destroy, and edit classifications' - }, - { - name => 'editcomponents', - description => 'Can create, destroy, and edit components' - }, - { - name => 'editkeywords', - description => 'Can create, destroy, and edit keywords' - }, - { - name => 'editbugs', - description => 'Can edit all bug fields', - userregexp => '.*' - }, - { - name => 'canconfirm', - description => 'Can confirm a bug or mark it a duplicate' - }, - { - name => 'bz_canusewhineatothers', - description => 'Can configure whine reports for other users', - }, - { - name => 'bz_canusewhines', - description => 'User can configure whine reports for self', - # inherited_by means that users in the groups listed below are - # automatically members of bz_canusewhines. - inherited_by => ['editbugs', 'bz_canusewhineatothers'], - }, - { - name => 'bz_sudoers', - description => 'Can perform actions as other users', - }, - { - name => 'bz_sudo_protect', - description => 'Can not be impersonated by other users', - inherited_by => ['bz_sudoers'], - }, - { - name => 'bz_quip_moderators', - description => 'Can moderate quips', - }, + {name => 'admin', description => 'Administrators'}, + {name => 'tweakparams', description => 'Can change Parameters'}, + {name => 'editusers', description => 'Can edit or disable users'}, + {name => 'creategroups', description => 'Can create and destroy groups'}, + { + name => 'editclassifications', + description => 'Can create, destroy, and edit classifications' + }, + { + name => 'editcomponents', + description => 'Can create, destroy, and edit components' + }, + { + name => 'editkeywords', + description => 'Can create, destroy, and edit keywords' + }, + { + name => 'editbugs', + description => 'Can edit all bug fields', + userregexp => '.*' + }, + { + name => 'canconfirm', + description => 'Can confirm a bug or mark it a duplicate' + }, + { + name => 'bz_canusewhineatothers', + description => 'Can configure whine reports for other users', + }, + { + name => 'bz_canusewhines', + description => 'User can configure whine reports for self', + + # inherited_by means that users in the groups listed below are + # automatically members of bz_canusewhines. + inherited_by => ['editbugs', 'bz_canusewhineatothers'], + }, + {name => 'bz_sudoers', description => 'Can perform actions as other users',}, + { + name => 'bz_sudo_protect', + description => 'Can not be impersonated by other users', + inherited_by => ['bz_sudoers'], + }, + {name => 'bz_quip_moderators', description => 'Can moderate quips',}, ); -use constant DEFAULT_CLASSIFICATION => { - name => 'Unclassified', - description => 'Not assigned to any classification' -}; +use constant DEFAULT_CLASSIFICATION => + {name => 'Unclassified', description => 'Not assigned to any classification'}; use constant DEFAULT_PRODUCT => { - name => 'TestProduct', - description => 'This is a test product.' - . ' This ought to be blown away and replaced with real stuff in a' - . ' finished installation of bugzilla.', - version => Bugzilla::Version::DEFAULT_VERSION, - classification => 'Unclassified', - defaultmilestone => DEFAULT_MILESTONE, + name => 'TestProduct', + description => 'This is a test product.' + . ' This ought to be blown away and replaced with real stuff in a' + . ' finished installation of bugzilla.', + version => Bugzilla::Version::DEFAULT_VERSION, + classification => 'Unclassified', + defaultmilestone => DEFAULT_MILESTONE, }; use constant DEFAULT_COMPONENT => { - name => 'TestComponent', - description => 'This is a test component in the test product database.' - . ' This ought to be blown away and replaced with real stuff in' - . ' a finished installation of Bugzilla.' + name => 'TestComponent', + description => 'This is a test component in the test product database.' + . ' This ought to be blown away and replaced with real stuff in' + . ' a finished installation of Bugzilla.' }; sub update_settings { - my $dbh = Bugzilla->dbh; - # If we're setting up settings for the first time, we want to be quieter. - my $any_settings = $dbh->selectrow_array( - 'SELECT 1 FROM setting ' . $dbh->sql_limit(1)); - if (!$any_settings) { - say get_text('install_setting_setup'); - } - - my %settings = %{SETTINGS()}; - foreach my $setting (keys %settings) { - add_setting($setting, - $settings{$setting}->{options}, - $settings{$setting}->{default}, - $settings{$setting}->{subclass}, undef, - !$any_settings); - } - - # Delete the obsolete 'per_bug_queries' user preference. Bug 616191. - $dbh->do('DELETE FROM setting WHERE name = ?', undef, 'per_bug_queries'); + my $dbh = Bugzilla->dbh; + + # If we're setting up settings for the first time, we want to be quieter. + my $any_settings + = $dbh->selectrow_array('SELECT 1 FROM setting ' . $dbh->sql_limit(1)); + if (!$any_settings) { + say get_text('install_setting_setup'); + } + + my %settings = %{SETTINGS()}; + foreach my $setting (keys %settings) { + add_setting( + $setting, + $settings{$setting}->{options}, + $settings{$setting}->{default}, + $settings{$setting}->{subclass}, + undef, !$any_settings + ); + } + + # Delete the obsolete 'per_bug_queries' user preference. Bug 616191. + $dbh->do('DELETE FROM setting WHERE name = ?', undef, 'per_bug_queries'); } sub update_system_groups { - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - # If there is no editbugs group, this is the first time we're - # adding groups. - my $editbugs_exists = new Bugzilla::Group({ name => 'editbugs' }); - if (!$editbugs_exists) { - say get_text('install_groups_setup'); - } - - # Create most of the system groups - foreach my $definition (SYSTEM_GROUPS) { - my $exists = new Bugzilla::Group({ name => $definition->{name} }); - if (!$exists) { - $definition->{isbuggroup} = 0; - $definition->{silently} = !$editbugs_exists; - my $inherited_by = delete $definition->{inherited_by}; - my $created = Bugzilla::Group->create($definition); - # Each group in inherited_by is automatically a member of this - # group. - if ($inherited_by) { - foreach my $name (@$inherited_by) { - my $member = Bugzilla::Group->check($name); - $dbh->do('INSERT INTO group_group_map (grantor_id, - member_id) VALUES (?,?)', - undef, $created->id, $member->id); - } - } + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + # If there is no editbugs group, this is the first time we're + # adding groups. + my $editbugs_exists = new Bugzilla::Group({name => 'editbugs'}); + if (!$editbugs_exists) { + say get_text('install_groups_setup'); + } + + # Create most of the system groups + foreach my $definition (SYSTEM_GROUPS) { + my $exists = new Bugzilla::Group({name => $definition->{name}}); + if (!$exists) { + $definition->{isbuggroup} = 0; + $definition->{silently} = !$editbugs_exists; + my $inherited_by = delete $definition->{inherited_by}; + my $created = Bugzilla::Group->create($definition); + + # Each group in inherited_by is automatically a member of this + # group. + if ($inherited_by) { + foreach my $name (@$inherited_by) { + my $member = Bugzilla::Group->check($name); + $dbh->do( + 'INSERT INTO group_group_map (grantor_id, + member_id) VALUES (?,?)', undef, $created->id, + $member->id + ); } + } } + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub create_default_classification { - my $dbh = Bugzilla->dbh; - - # Make the default Classification if it doesn't already exist. - if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) { - print get_text('install_default_classification', - { name => DEFAULT_CLASSIFICATION->{name} }) . "\n"; - Bugzilla::Classification->create(DEFAULT_CLASSIFICATION); - } + my $dbh = Bugzilla->dbh; + + # Make the default Classification if it doesn't already exist. + if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) { + print get_text('install_default_classification', + {name => DEFAULT_CLASSIFICATION->{name}}) + . "\n"; + Bugzilla::Classification->create(DEFAULT_CLASSIFICATION); + } } # This function should be called only after creating the admin user. sub create_default_product { - my $dbh = Bugzilla->dbh; - - # And same for the default product/component. - if (!$dbh->selectrow_array('SELECT 1 FROM products')) { - print get_text('install_default_product', - { name => DEFAULT_PRODUCT->{name} }) . "\n"; - - my $product = Bugzilla::Product->create(DEFAULT_PRODUCT); - - # Get the user who will be the owner of the Component. - # We pick the admin with the lowest id, which is probably the - # admin checksetup.pl just created. - my $admin_group = new Bugzilla::Group({name => 'admin'}); - my ($admin_id) = $dbh->selectrow_array( - 'SELECT user_id FROM user_group_map WHERE group_id = ? - ORDER BY user_id ' . $dbh->sql_limit(1), - undef, $admin_group->id); - my $admin = Bugzilla::User->new($admin_id); - - Bugzilla::Component->create({ - %{ DEFAULT_COMPONENT() }, product => $product, - initialowner => $admin->login }); - } + my $dbh = Bugzilla->dbh; -} + # And same for the default product/component. + if (!$dbh->selectrow_array('SELECT 1 FROM products')) { + print get_text('install_default_product', {name => DEFAULT_PRODUCT->{name}}) + . "\n"; -sub init_workflow { - my $dbh = Bugzilla->dbh; - my $has_workflow = $dbh->selectrow_array('SELECT 1 FROM status_workflow'); - return if $has_workflow; + my $product = Bugzilla::Product->create(DEFAULT_PRODUCT); - say get_text('install_workflow_init'); + # Get the user who will be the owner of the Component. + # We pick the admin with the lowest id, which is probably the + # admin checksetup.pl just created. + my $admin_group = new Bugzilla::Group({name => 'admin'}); + my ($admin_id) = $dbh->selectrow_array( + 'SELECT user_id FROM user_group_map WHERE group_id = ? + ORDER BY user_id ' . $dbh->sql_limit(1), undef, $admin_group->id + ); + my $admin = Bugzilla::User->new($admin_id); - my %status_ids = @{ $dbh->selectcol_arrayref( - 'SELECT value, id FROM bug_status', {Columns=>[1,2]}) }; + Bugzilla::Component->create( + {%{DEFAULT_COMPONENT()}, product => $product, initialowner => $admin->login}); + } - foreach my $pair (STATUS_WORKFLOW) { - my $old_id = $pair->[0] ? $status_ids{$pair->[0]} : undef; - my $new_id = $status_ids{$pair->[1]}; - $dbh->do('INSERT INTO status_workflow (old_status, new_status) - VALUES (?,?)', undef, $old_id, $new_id); - } } -sub create_admin { - my ($params) = @_; - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - - my $admin_group = new Bugzilla::Group({ name => 'admin' }); - my $admin_inheritors = - Bugzilla::Group->flatten_group_membership($admin_group->id); - my $admin_group_ids = join(',', @$admin_inheritors); - - my ($admin_count) = $dbh->selectrow_array( - "SELECT COUNT(*) FROM user_group_map - WHERE group_id IN ($admin_group_ids)"); - - return if $admin_count; - - my %answer = %{Bugzilla->installation_answers}; - my $login = $answer{'ADMIN_EMAIL'}; - my $password = $answer{'ADMIN_PASSWORD'}; - my $full_name = $answer{'ADMIN_REALNAME'}; - - if (!$login || !$password || !$full_name) { - say "\n" . get_text('install_admin_setup') . "\n"; - } +sub init_workflow { + my $dbh = Bugzilla->dbh; + my $has_workflow = $dbh->selectrow_array('SELECT 1 FROM status_workflow'); + return if $has_workflow; + + say get_text('install_workflow_init'); + + my %status_ids = @{ + $dbh->selectcol_arrayref('SELECT value, id FROM bug_status', + {Columns => [1, 2]}) + }; + + foreach my $pair (STATUS_WORKFLOW) { + my $old_id = $pair->[0] ? $status_ids{$pair->[0]} : undef; + my $new_id = $status_ids{$pair->[1]}; + $dbh->do( + 'INSERT INTO status_workflow (old_status, new_status) + VALUES (?,?)', undef, $old_id, $new_id + ); + } +} - while (!$login) { - print get_text('install_admin_get_email') . ' '; - $login = <STDIN>; - chomp $login; - eval { Bugzilla::User->check_login_name($login); }; - if ($@) { - say $@; - undef $login; - } +sub create_admin { + my ($params) = @_; + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + + my $admin_group = new Bugzilla::Group({name => 'admin'}); + my $admin_inheritors + = Bugzilla::Group->flatten_group_membership($admin_group->id); + my $admin_group_ids = join(',', @$admin_inheritors); + + my ($admin_count) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM user_group_map + WHERE group_id IN ($admin_group_ids)" + ); + + return if $admin_count; + + my %answer = %{Bugzilla->installation_answers}; + my $login = $answer{'ADMIN_EMAIL'}; + my $password = $answer{'ADMIN_PASSWORD'}; + my $full_name = $answer{'ADMIN_REALNAME'}; + + if (!$login || !$password || !$full_name) { + say "\n" . get_text('install_admin_setup') . "\n"; + } + + while (!$login) { + print get_text('install_admin_get_email') . ' '; + $login = <STDIN>; + chomp $login; + eval { Bugzilla::User->check_login_name($login); }; + if ($@) { + say $@; + undef $login; } + } - while (!defined $full_name) { - print get_text('install_admin_get_name') . ' '; - $full_name = <STDIN>; - chomp($full_name); - } + while (!defined $full_name) { + print get_text('install_admin_get_name') . ' '; + $full_name = <STDIN>; + chomp($full_name); + } - if (!$password) { - $password = _prompt_for_password( - get_text('install_admin_get_password')); - } + if (!$password) { + $password = _prompt_for_password(get_text('install_admin_get_password')); + } - my $admin = Bugzilla::User->create({ login_name => $login, - realname => $full_name, - cryptpassword => $password }); - make_admin($admin); + my $admin = Bugzilla::User->create( + {login_name => $login, realname => $full_name, cryptpassword => $password}); + make_admin($admin); } sub make_admin { - my ($user) = @_; - my $dbh = Bugzilla->dbh; - - $user = ref($user) ? $user - : new Bugzilla::User(login_to_id($user, THROW_ERROR)); - - my $group_insert = $dbh->prepare( - 'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, ?, ?)'); - - # Admins get explicit membership and bless capability for the admin group - my $admin_group = new Bugzilla::Group({ name => 'admin' }); - # These are run in an eval so that we can ignore the error of somebody - # already being granted these things. - eval { - $group_insert->execute($user->id, $admin_group->id, 0, GRANT_DIRECT); - }; - eval { - $group_insert->execute($user->id, $admin_group->id, 1, GRANT_DIRECT); - }; - - # Admins should also have editusers directly, even though they'll usually - # inherit it. People could have changed their inheritance structure. - my $editusers = new Bugzilla::Group({ name => 'editusers' }); - eval { - $group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT); - }; - - # If there is no maintainer set, make this user the maintainer. - if (!Bugzilla->params->{'maintainer'}) { - SetParam('maintainer', $user->email); - write_params(); - } - - # Make sure the new admin isn't disabled - if ($user->disabledtext) { - $user->set_disabledtext(''); - $user->update(); - } + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + $user + = ref($user) ? $user : new Bugzilla::User(login_to_id($user, THROW_ERROR)); + + my $group_insert = $dbh->prepare( + 'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)' + ); + + # Admins get explicit membership and bless capability for the admin group + my $admin_group = new Bugzilla::Group({name => 'admin'}); + + # These are run in an eval so that we can ignore the error of somebody + # already being granted these things. + eval { $group_insert->execute($user->id, $admin_group->id, 0, GRANT_DIRECT); }; + eval { $group_insert->execute($user->id, $admin_group->id, 1, GRANT_DIRECT); }; + + # Admins should also have editusers directly, even though they'll usually + # inherit it. People could have changed their inheritance structure. + my $editusers = new Bugzilla::Group({name => 'editusers'}); + eval { $group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT); }; + + # If there is no maintainer set, make this user the maintainer. + if (!Bugzilla->params->{'maintainer'}) { + SetParam('maintainer', $user->email); + write_params(); + } + + # Make sure the new admin isn't disabled + if ($user->disabledtext) { + $user->set_disabledtext(''); + $user->update(); + } - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - say "\n", get_text('install_admin_created', { user => $user }); - } + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + say "\n", get_text('install_admin_created', {user => $user}); + } } sub _prompt_for_password { - my $prompt = shift; - - my $password; - while (!$password) { - # trap a few interrupts so we can fix the echo if we get aborted. - local $SIG{HUP} = \&_password_prompt_exit; - local $SIG{INT} = \&_password_prompt_exit; - local $SIG{QUIT} = \&_password_prompt_exit; - local $SIG{TERM} = \&_password_prompt_exit; - - system("stty","-echo") unless ON_WINDOWS; # disable input echoing - - print $prompt, ' '; - $password = <STDIN>; - chomp $password; - print "\n", get_text('install_confirm_password'), ' '; - my $pass2 = <STDIN>; - chomp $pass2; - eval { validate_password($password, $pass2); }; - if ($@) { - say "\n$@"; - undef $password; - } - system("stty","echo") unless ON_WINDOWS; + my $prompt = shift; + + my $password; + while (!$password) { + + # trap a few interrupts so we can fix the echo if we get aborted. + local $SIG{HUP} = \&_password_prompt_exit; + local $SIG{INT} = \&_password_prompt_exit; + local $SIG{QUIT} = \&_password_prompt_exit; + local $SIG{TERM} = \&_password_prompt_exit; + + system("stty", "-echo") unless ON_WINDOWS; # disable input echoing + + print $prompt, ' '; + $password = <STDIN>; + chomp $password; + print "\n", get_text('install_confirm_password'), ' '; + my $pass2 = <STDIN>; + chomp $pass2; + eval { validate_password($password, $pass2); }; + if ($@) { + say "\n$@"; + undef $password; } - return $password; + system("stty", "echo") unless ON_WINDOWS; + } + return $password; } # This is just in case we get interrupted while getting a password. sub _password_prompt_exit { - # re-enable input echoing - system("stty","echo") unless ON_WINDOWS; - exit 1; + + # re-enable input echoing + system("stty", "echo") unless ON_WINDOWS; + exit 1; } sub reset_password { - my $login = shift; - my $user = Bugzilla::User->check($login); - my $prompt = "\n" . get_text('install_reset_password', { user => $user }); - my $password = _prompt_for_password($prompt); - $user->set_password($password); - $user->update(); - say "\n", get_text('install_reset_password_done'); + my $login = shift; + my $user = Bugzilla::User->check($login); + my $prompt = "\n" . get_text('install_reset_password', {user => $user}); + my $password = _prompt_for_password($prompt); + $user->set_password($password); + $user->update(); + say "\n", get_text('install_reset_password_done'); } 1; diff --git a/Bugzilla/Install/CPAN.pm b/Bugzilla/Install/CPAN.pm index 094784e1a..4321cac61 100644 --- a/Bugzilla/Install/CPAN.pm +++ b/Bugzilla/Install/CPAN.pm @@ -13,11 +13,11 @@ use warnings; use parent qw(Exporter); our @EXPORT = qw( - BZ_LIB + BZ_LIB - check_cpan_requirements - set_cpan_config - install_module + check_cpan_requirements + set_cpan_config + install_module ); use Bugzilla::Constants; @@ -32,32 +32,28 @@ use File::Path qw(rmtree); # These are required for install-module.pl to be able to install # all modules properly. use constant REQUIREMENTS => ( - { - module => 'CPAN', - package => 'CPAN', - version => '1.81', - }, - { - # When Module::Build isn't installed, the YAML module allows - # CPAN to read META.yml to determine that Module::Build first - # needs to be installed to compile a module. - module => 'YAML', - package => 'YAML', - version => 0, - }, - { - # Many modules on CPAN are now built with Dist::Zilla, which - # unfortunately means they require this version of EU::MM to install. - module => 'ExtUtils::MakeMaker', - package => 'ExtUtils-MakeMaker', - version => '6.31', - }, + {module => 'CPAN', package => 'CPAN', version => '1.81',}, + { + # When Module::Build isn't installed, the YAML module allows + # CPAN to read META.yml to determine that Module::Build first + # needs to be installed to compile a module. + module => 'YAML', + package => 'YAML', + version => 0, + }, + { + # Many modules on CPAN are now built with Dist::Zilla, which + # unfortunately means they require this version of EU::MM to install. + module => 'ExtUtils::MakeMaker', + package => 'ExtUtils-MakeMaker', + version => '6.31', + }, ); # We need the absolute path of ext_libpath, because CPAN chdirs around # and so we can't use a relative directory. # -# We need it often enough (and at compile time, in install-module.pl) so +# We need it often enough (and at compile time, in install-module.pl) so # we make it a constant. use constant BZ_LIB => abs_path(bz_locations()->{ext_libpath}); @@ -66,217 +62,229 @@ use constant BZ_LIB => abs_path(bz_locations()->{ext_libpath}); # defaults here for most of the required parameters we know about, in case # any of them aren't set. The rest are handled by set_cpan_defaults(). use constant CPAN_DEFAULTS => { - auto_commit => 0, - # We always force builds, so there's no reason to cache them. - build_cache => 0, - build_requires_install_policy => 'yes', - cache_metadata => 1, - colorize_output => 1, - colorize_print => 'bold', - index_expire => 1, - scan_cache => 'atstart', - - inhibit_startup_message => 1, - - bzip2 => bin_loc('bzip2'), - curl => bin_loc('curl'), - gzip => bin_loc('gzip'), - links => bin_loc('links'), - lynx => bin_loc('lynx'), - make => bin_loc('make'), - pager => bin_loc('less'), - tar => bin_loc('tar'), - unzip => bin_loc('unzip'), - wget => bin_loc('wget'), - - urllist => ['http://www.cpan.org/'], + auto_commit => 0, + + # We always force builds, so there's no reason to cache them. + build_cache => 0, + build_requires_install_policy => 'yes', + cache_metadata => 1, + colorize_output => 1, + colorize_print => 'bold', + index_expire => 1, + scan_cache => 'atstart', + + inhibit_startup_message => 1, + + bzip2 => bin_loc('bzip2'), + curl => bin_loc('curl'), + gzip => bin_loc('gzip'), + links => bin_loc('links'), + lynx => bin_loc('lynx'), + make => bin_loc('make'), + pager => bin_loc('less'), + tar => bin_loc('tar'), + unzip => bin_loc('unzip'), + wget => bin_loc('wget'), + + urllist => ['http://www.cpan.org/'], }; sub check_cpan_requirements { - my ($original_dir, $original_args) = @_; + my ($original_dir, $original_args) = @_; - _require_compiler(); + _require_compiler(); - my @install; - foreach my $module (REQUIREMENTS) { - my $installed = have_vers($module, 1); - push(@install, $module) if !$installed; - } + my @install; + foreach my $module (REQUIREMENTS) { + my $installed = have_vers($module, 1); + push(@install, $module) if !$installed; + } - return if !@install; + return if !@install; - my $restart_required; - foreach my $module (@install) { - $restart_required = 1 if $module->{module} eq 'CPAN'; - install_module($module->{module}, 1); - } + my $restart_required; + foreach my $module (@install) { + $restart_required = 1 if $module->{module} eq 'CPAN'; + install_module($module->{module}, 1); + } - if ($restart_required) { - chdir $original_dir; - exec($^X, $0, @$original_args); - } + if ($restart_required) { + chdir $original_dir; + exec($^X, $0, @$original_args); + } } sub _require_compiler { - my @errors; + my @errors; - my $cc_name = $Config{cc}; - my $cc_exists = bin_loc($cc_name); + my $cc_name = $Config{cc}; + my $cc_exists = bin_loc($cc_name); - if (!$cc_exists) { - push(@errors, install_string('install_no_compiler')); - } + if (!$cc_exists) { + push(@errors, install_string('install_no_compiler')); + } - my $make_name = $CPAN::Config->{make}; - my $make_exists = bin_loc($make_name); + my $make_name = $CPAN::Config->{make}; + my $make_exists = bin_loc($make_name); - if (!$make_exists) { - push(@errors, install_string('install_no_make')); - } + if (!$make_exists) { + push(@errors, install_string('install_no_make')); + } - die @errors if @errors; + die @errors if @errors; } sub install_module { - my ($name, $test) = @_; - my $bzlib = BZ_LIB; - - # Make Module::AutoInstall install all dependencies and never prompt. - local $ENV{PERL_AUTOINSTALL} = '--alldeps'; - # This makes Net::SSLeay not prompt the user, if it gets installed. - # It also makes any other MakeMaker prompts accept their defaults. - local $ENV{PERL_MM_USE_DEFAULT} = 1; - - # Certain modules require special stuff in order to not prompt us. - my $original_makepl = $CPAN::Config->{makepl_arg}; - # This one's a regex in case we're doing Template::Plugin::GD and it - # pulls in Template-Toolkit as a dependency. - if ($name =~ /^Template/) { - $CPAN::Config->{makepl_arg} .= " TT_ACCEPT=y TT_EXTRAS=n"; - } - elsif ($name eq 'XML::Twig') { - $CPAN::Config->{makepl_arg} = "-n $original_makepl"; - } - elsif ($name eq 'SOAP::Lite') { - $CPAN::Config->{makepl_arg} .= " --noprompt"; - } - - my $module = CPAN::Shell->expand('Module', $name); - if (!$module) { - die install_string('no_such_module', { module => $name }) . "\n"; - } - - print install_string('install_module', - { module => $name, version => $module->cpan_version }) . "\n"; - - if ($test) { - CPAN::Shell->force('install', $name); - } - else { - CPAN::Shell->notest('install', $name); - } - - # If it installed any binaries in the Bugzilla directory, delete them. - if (-d "$bzlib/bin") { - File::Path::rmtree("$bzlib/bin"); - } - - $CPAN::Config->{makepl_arg} = $original_makepl; + my ($name, $test) = @_; + my $bzlib = BZ_LIB; + + # Make Module::AutoInstall install all dependencies and never prompt. + local $ENV{PERL_AUTOINSTALL} = '--alldeps'; + + # This makes Net::SSLeay not prompt the user, if it gets installed. + # It also makes any other MakeMaker prompts accept their defaults. + local $ENV{PERL_MM_USE_DEFAULT} = 1; + + # Certain modules require special stuff in order to not prompt us. + my $original_makepl = $CPAN::Config->{makepl_arg}; + + # This one's a regex in case we're doing Template::Plugin::GD and it + # pulls in Template-Toolkit as a dependency. + if ($name =~ /^Template/) { + $CPAN::Config->{makepl_arg} .= " TT_ACCEPT=y TT_EXTRAS=n"; + } + elsif ($name eq 'XML::Twig') { + $CPAN::Config->{makepl_arg} = "-n $original_makepl"; + } + elsif ($name eq 'SOAP::Lite') { + $CPAN::Config->{makepl_arg} .= " --noprompt"; + } + + my $module = CPAN::Shell->expand('Module', $name); + if (!$module) { + die install_string('no_such_module', {module => $name}) . "\n"; + } + + print install_string('install_module', + {module => $name, version => $module->cpan_version}) + . "\n"; + + if ($test) { + CPAN::Shell->force('install', $name); + } + else { + CPAN::Shell->notest('install', $name); + } + + # If it installed any binaries in the Bugzilla directory, delete them. + if (-d "$bzlib/bin") { + File::Path::rmtree("$bzlib/bin"); + } + + $CPAN::Config->{makepl_arg} = $original_makepl; } sub set_cpan_config { - my $do_global = shift; - my $bzlib = BZ_LIB; - - # We set defaults before we do anything, otherwise CPAN will - # start asking us questions as soon as we load its configuration. - eval { require CPAN::Config; }; - _set_cpan_defaults(); - - # Calling a senseless autoload that does nothing makes us - # automatically load any existing configuration. - # We want to avoid the "invalid command" message. - open(my $saveout, ">&", "STDOUT"); - open(STDOUT, '>', '/dev/null'); - eval { CPAN->ignore_this_error_message_from_bugzilla; }; - undef $@; - close(STDOUT); - open(STDOUT, '>&', $saveout); - - my $dir = $CPAN::Config->{cpan_home}; - if (!defined $dir || !-w $dir) { - # If we can't use the standard CPAN build dir, we try to make one. - $dir = "$ENV{HOME}/.cpan"; - mkdir $dir; - - # If we can't make one, we finally try to use the Bugzilla directory. - if (!-w $dir) { - print STDERR install_string('cpan_bugzilla_home'), "\n"; - $dir = "$bzlib/.cpan"; - } - } - $CPAN::Config->{cpan_home} = $dir; - $CPAN::Config->{build_dir} = "$dir/build"; - # We always force builds, so there's no reason to cache them. - $CPAN::Config->{keep_source_where} = "$dir/source"; - # This is set both here and in defaults so that it's always true. - $CPAN::Config->{inhibit_startup_message} = 1; - # Automatically install dependencies. - $CPAN::Config->{prerequisites_policy} = 'follow'; - - # Unless specified, we install the modules into the Bugzilla directory. - if (!$do_global) { - require Config; - - $CPAN::Config->{makepl_arg} .= " LIB=\"$bzlib\"" - . " INSTALLMAN1DIR=\"$bzlib/man/man1\"" - . " INSTALLMAN3DIR=\"$bzlib/man/man3\"" - # The bindirs are here because otherwise we'll try to write to - # the system binary dirs, and that will cause CPAN to die. - . " INSTALLBIN=\"$bzlib/bin\"" - . " INSTALLSCRIPT=\"$bzlib/bin\"" - # INSTALLDIRS=perl is set because that makes sure that MakeMaker - # always uses the directories we've specified here. - . " INSTALLDIRS=perl"; - $CPAN::Config->{mbuild_arg} = " --install_base \"$bzlib\"" - . " --install_path lib=\"$bzlib\"" - . " --install_path arch=\"$bzlib/$Config::Config{archname}\""; - $CPAN::Config->{mbuild_install_arg} = $CPAN::Config->{mbuild_arg}; - - # When we're not root, sometimes newer versions of CPAN will - # try to read/modify things that belong to root, unless we set - # certain config variables. - $CPAN::Config->{histfile} = "$dir/histfile"; - $CPAN::Config->{use_sqlite} = 0; - $CPAN::Config->{prefs_dir} = "$dir/prefs"; - - # Unless we actually set PERL5LIB, some modules can't install - # themselves, like DBD::mysql, DBD::Pg, and XML::Twig. - my $current_lib = $ENV{PERL5LIB} ? $ENV{PERL5LIB} . ':' : ''; - $ENV{PERL5LIB} = $current_lib . $bzlib; + my $do_global = shift; + my $bzlib = BZ_LIB; + + # We set defaults before we do anything, otherwise CPAN will + # start asking us questions as soon as we load its configuration. + eval { require CPAN::Config; }; + _set_cpan_defaults(); + + # Calling a senseless autoload that does nothing makes us + # automatically load any existing configuration. + # We want to avoid the "invalid command" message. + open(my $saveout, ">&", "STDOUT"); + open(STDOUT, '>', '/dev/null'); + eval { CPAN->ignore_this_error_message_from_bugzilla; }; + undef $@; + close(STDOUT); + open(STDOUT, '>&', $saveout); + + my $dir = $CPAN::Config->{cpan_home}; + if (!defined $dir || !-w $dir) { + + # If we can't use the standard CPAN build dir, we try to make one. + $dir = "$ENV{HOME}/.cpan"; + mkdir $dir; + + # If we can't make one, we finally try to use the Bugzilla directory. + if (!-w $dir) { + print STDERR install_string('cpan_bugzilla_home'), "\n"; + $dir = "$bzlib/.cpan"; } + } + $CPAN::Config->{cpan_home} = $dir; + $CPAN::Config->{build_dir} = "$dir/build"; + + # We always force builds, so there's no reason to cache them. + $CPAN::Config->{keep_source_where} = "$dir/source"; + + # This is set both here and in defaults so that it's always true. + $CPAN::Config->{inhibit_startup_message} = 1; + + # Automatically install dependencies. + $CPAN::Config->{prerequisites_policy} = 'follow'; + + # Unless specified, we install the modules into the Bugzilla directory. + if (!$do_global) { + require Config; + + $CPAN::Config->{makepl_arg} + .= " LIB=\"$bzlib\"" + . " INSTALLMAN1DIR=\"$bzlib/man/man1\"" + . " INSTALLMAN3DIR=\"$bzlib/man/man3\"" + + # The bindirs are here because otherwise we'll try to write to + # the system binary dirs, and that will cause CPAN to die. + . " INSTALLBIN=\"$bzlib/bin\"" . " INSTALLSCRIPT=\"$bzlib/bin\"" + + # INSTALLDIRS=perl is set because that makes sure that MakeMaker + # always uses the directories we've specified here. + . " INSTALLDIRS=perl"; + $CPAN::Config->{mbuild_arg} + = " --install_base \"$bzlib\"" + . " --install_path lib=\"$bzlib\"" + . " --install_path arch=\"$bzlib/$Config::Config{archname}\""; + $CPAN::Config->{mbuild_install_arg} = $CPAN::Config->{mbuild_arg}; + + # When we're not root, sometimes newer versions of CPAN will + # try to read/modify things that belong to root, unless we set + # certain config variables. + $CPAN::Config->{histfile} = "$dir/histfile"; + $CPAN::Config->{use_sqlite} = 0; + $CPAN::Config->{prefs_dir} = "$dir/prefs"; + + # Unless we actually set PERL5LIB, some modules can't install + # themselves, like DBD::mysql, DBD::Pg, and XML::Twig. + my $current_lib = $ENV{PERL5LIB} ? $ENV{PERL5LIB} . ':' : ''; + $ENV{PERL5LIB} = $current_lib . $bzlib; + } } sub _set_cpan_defaults { - # If CPAN hasn't been configured, we try to use some reasonable defaults. - foreach my $key (keys %{CPAN_DEFAULTS()}) { - $CPAN::Config->{$key} = CPAN_DEFAULTS->{$key} - if !defined $CPAN::Config->{$key}; - } - my @missing; - # In newer CPANs, this is in HandleConfig. In older CPANs, it's in - # Config. - if (eval { require CPAN::HandleConfig }) { - @missing = CPAN::HandleConfig->missing_config_data; - } - else { - @missing = CPAN::Config->missing_config_data; - } - - foreach my $key (@missing) { - $CPAN::Config->{$key} = ''; - } + # If CPAN hasn't been configured, we try to use some reasonable defaults. + foreach my $key (keys %{CPAN_DEFAULTS()}) { + $CPAN::Config->{$key} = CPAN_DEFAULTS->{$key} if !defined $CPAN::Config->{$key}; + } + + my @missing; + + # In newer CPANs, this is in HandleConfig. In older CPANs, it's in + # Config. + if (eval { require CPAN::HandleConfig }) { + @missing = CPAN::HandleConfig->missing_config_data; + } + else { + @missing = CPAN::Config->missing_config_data; + } + + foreach my $key (@missing) { + $CPAN::Config->{$key} = ''; + } } 1; diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm index ed2539251..ffd179948 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -8,7 +8,7 @@ package Bugzilla::Install::DB; # NOTE: This package may "use" any modules that it likes, -# localconfig is available, and params are up to date. +# localconfig is available, and params are up to date. use 5.10.1; use strict; @@ -34,99 +34,104 @@ use URI::QueryParam; # NOTE: This is NOT the function for general table updates. See # update_table_definitions for that. This is only for the fielddefs table. sub update_fielddefs_definition { - my $dbh = Bugzilla->dbh; - - # 2005-02-21 - LpSolit@gmail.com - Bug 279910 - # qacontact_accessible and assignee_accessible field names no longer exist - # in the 'bugs' table. Their corresponding entries in the 'bugs_activity' - # table should therefore be marked as obsolete, meaning that they cannot - # be used anymore when querying the database - they are not deleted in - # order to keep track of these fields in the activity table. - if (!$dbh->bz_column_info('fielddefs', 'obsolete')) { - $dbh->bz_add_column('fielddefs', 'obsolete', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - print "Marking qacontact_accessible and assignee_accessible as", - " obsolete fields...\n"; - $dbh->do("UPDATE fielddefs SET obsolete = 1 + my $dbh = Bugzilla->dbh; + + # 2005-02-21 - LpSolit@gmail.com - Bug 279910 + # qacontact_accessible and assignee_accessible field names no longer exist + # in the 'bugs' table. Their corresponding entries in the 'bugs_activity' + # table should therefore be marked as obsolete, meaning that they cannot + # be used anymore when querying the database - they are not deleted in + # order to keep track of these fields in the activity table. + if (!$dbh->bz_column_info('fielddefs', 'obsolete')) { + $dbh->bz_add_column('fielddefs', 'obsolete', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + print "Marking qacontact_accessible and assignee_accessible as", + " obsolete fields...\n"; + $dbh->do( + "UPDATE fielddefs SET obsolete = 1 WHERE name = 'qacontact_accessible' - OR name = 'assignee_accessible'"); - } - - # 2005-08-10 Myk Melez <myk@mozilla.org> bug 287325 - # Record each field's type and whether or not it's a custom field, - # in fielddefs. - $dbh->bz_add_column('fielddefs', 'type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_column('fielddefs', 'custom', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - $dbh->bz_add_column('fielddefs', 'enter_bug', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - # Change the name of the fieldid column to id, so that fielddefs - # can use Bugzilla::Object easily. We have to do this up here, because - # otherwise adding these field definitions will fail. - $dbh->bz_rename_column('fielddefs', 'fieldid', 'id'); - - # If the largest fielddefs sortkey is less than 100, then - # we're using the old sorting system, and we should convert - # it to the new one before adding any new definitions. - if (!$dbh->selectrow_arrayref( - 'SELECT COUNT(id) FROM fielddefs WHERE sortkey >= 100')) - { - print "Updating the sortkeys for the fielddefs table...\n"; - my $field_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM fielddefs ORDER BY sortkey'); - my $sortkey = 100; - foreach my $field_id (@$field_ids) { - $dbh->do('UPDATE fielddefs SET sortkey = ? WHERE id = ?', - undef, $sortkey, $field_id); - $sortkey += 100; - } + OR name = 'assignee_accessible'" + ); + } + + # 2005-08-10 Myk Melez <myk@mozilla.org> bug 287325 + # Record each field's type and whether or not it's a custom field, + # in fielddefs. + $dbh->bz_add_column('fielddefs', 'type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('fielddefs', 'custom', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + $dbh->bz_add_column('fielddefs', 'enter_bug', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + # Change the name of the fieldid column to id, so that fielddefs + # can use Bugzilla::Object easily. We have to do this up here, because + # otherwise adding these field definitions will fail. + $dbh->bz_rename_column('fielddefs', 'fieldid', 'id'); + + # If the largest fielddefs sortkey is less than 100, then + # we're using the old sorting system, and we should convert + # it to the new one before adding any new definitions. + if (!$dbh->selectrow_arrayref( + 'SELECT COUNT(id) FROM fielddefs WHERE sortkey >= 100')) + { + print "Updating the sortkeys for the fielddefs table...\n"; + my $field_ids + = $dbh->selectcol_arrayref('SELECT id FROM fielddefs ORDER BY sortkey'); + my $sortkey = 100; + foreach my $field_id (@$field_ids) { + $dbh->do('UPDATE fielddefs SET sortkey = ? WHERE id = ?', + undef, $sortkey, $field_id); + $sortkey += 100; } + } - $dbh->bz_add_column('fielddefs', 'visibility_field_id', {TYPE => 'INT3'}); - $dbh->bz_add_column('fielddefs', 'value_field_id', {TYPE => 'INT3'}); - $dbh->bz_add_index('fielddefs', 'fielddefs_value_field_id_idx', - ['value_field_id']); + $dbh->bz_add_column('fielddefs', 'visibility_field_id', {TYPE => 'INT3'}); + $dbh->bz_add_column('fielddefs', 'value_field_id', {TYPE => 'INT3'}); + $dbh->bz_add_index('fielddefs', 'fielddefs_value_field_id_idx', + ['value_field_id']); - # Bug 344878 - if (!$dbh->bz_column_info('fielddefs', 'buglist')) { - $dbh->bz_add_column('fielddefs', 'buglist', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # Set non-multiselect custom fields as valid buglist fields - # Note that default fields will be handled in Field.pm - $dbh->do('UPDATE fielddefs SET buglist = 1 WHERE custom = 1 AND type != ' . FIELD_TYPE_MULTI_SELECT); - } + # Bug 344878 + if (!$dbh->bz_column_info('fielddefs', 'buglist')) { + $dbh->bz_add_column('fielddefs', 'buglist', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - #2008-08-26 elliotte_martin@yahoo.com - Bug 251556 - $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'}); + # Set non-multiselect custom fields as valid buglist fields + # Note that default fields will be handled in Field.pm + $dbh->do('UPDATE fielddefs SET buglist = 1 WHERE custom = 1 AND type != ' + . FIELD_TYPE_MULTI_SELECT); + } - $dbh->do('UPDATE fielddefs SET buglist = 1 - WHERE custom = 1 AND type = ' . FIELD_TYPE_MULTI_SELECT); + #2008-08-26 elliotte_martin@yahoo.com - Bug 251556 + $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'}); - $dbh->bz_add_column('fielddefs', 'is_mandatory', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_index('fielddefs', 'fielddefs_is_mandatory_idx', - ['is_mandatory']); + $dbh->do( + 'UPDATE fielddefs SET buglist = 1 + WHERE custom = 1 AND type = ' . FIELD_TYPE_MULTI_SELECT + ); - # 2010-04-05 dkl@redhat.com - Bug 479400 - _migrate_field_visibility_value(); + $dbh->bz_add_column('fielddefs', 'is_mandatory', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_index('fielddefs', 'fielddefs_is_mandatory_idx', ['is_mandatory']); - $dbh->bz_add_column('fielddefs', 'is_numeric', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->do('UPDATE fielddefs SET is_numeric = 1 WHERE type = ' - . FIELD_TYPE_BUG_ID); + # 2010-04-05 dkl@redhat.com - Bug 479400 + _migrate_field_visibility_value(); - # 2012-04-12 aliustek@gmail.com - Bug 728138 - $dbh->bz_add_column('fielddefs', 'long_desc', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); + $dbh->bz_add_column('fielddefs', 'is_numeric', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->do( + 'UPDATE fielddefs SET is_numeric = 1 WHERE type = ' . FIELD_TYPE_BUG_ID); - Bugzilla::Hook::process('install_update_db_fielddefs'); + # 2012-04-12 aliustek@gmail.com - Bug 728138 + $dbh->bz_add_column('fielddefs', 'long_desc', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); - # Remember, this is not the function for adding general table changes. - # That is below. Add new changes to the fielddefs table above this - # comment. + Bugzilla::Hook::process('install_update_db_fielddefs'); + + # Remember, this is not the function for adding general table changes. + # That is below. Add new changes to the fielddefs table above this + # comment. } # Small changes can be put directly into this function. @@ -136,14 +141,14 @@ sub update_fielddefs_definition { # # This function runs in historical order--from upgrades that older # installations need, to upgrades that newer installations need. -# The order of items inside this function should only be changed if +# The order of items inside this function should only be changed if # absolutely necessary. # # The subroutines should have long, descriptive names, so that you # can easily see what is being done, just by reading this function. # # This function is mostly self-documenting. If you're curious about -# what each of the added/removed columns does, you should see the schema +# what each of the added/removed columns does, you should see the schema # docs at: # http://www.ravenbrook.com/project/p4dti/tool/cgi/bugzilla-schema/ # @@ -152,1062 +157,1091 @@ sub update_fielddefs_definition { # the purpose of a column. # sub update_table_definitions { - my $old_params = shift; - my $dbh = Bugzilla->dbh; - _update_pre_checksetup_bugzillas(); - - $dbh->bz_add_column('attachments', 'submitter_id', - {TYPE => 'INT3', NOTNULL => 1}, 0); - - $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when'); - - _add_bug_vote_cache(); - _update_product_name_definition(); - - $dbh->bz_add_column('profiles', 'disabledtext', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - - _populate_longdescs(); - _update_bugs_activity_field_to_fieldid(); - - if (!$dbh->bz_column_info('bugs', 'lastdiffed')) { - $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE =>'DATETIME'}); - $dbh->do('UPDATE bugs SET lastdiffed = NOW()'); - } - - _add_unique_login_name_index_to_profiles(); - - $dbh->bz_add_column('profiles', 'mybugslink', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - _update_component_user_fields_to_ids(); - - $dbh->bz_add_column('bugs', 'everconfirmed', - {TYPE => 'BOOLEAN', NOTNULL => 1}, 1); + my $old_params = shift; + my $dbh = Bugzilla->dbh; + _update_pre_checksetup_bugzillas(); - _populate_milestones_table(); + $dbh->bz_add_column('attachments', 'submitter_id', + {TYPE => 'INT3', NOTNULL => 1}, 0); - _add_products_defaultmilestone(); + $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when'); - # 2000-03-24 Added unique indexes into the cc and keyword tables. This - # prevents certain database inconsistencies, and, moreover, is required for - # new generalized list code to work. - if (!$dbh->bz_index_info('cc', 'cc_bug_id_idx') - || !$dbh->bz_index_info('cc', 'cc_bug_id_idx')->{TYPE}) - { - $dbh->bz_drop_index('cc', 'cc_bug_id_idx'); - $dbh->bz_add_index('cc', 'cc_bug_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(bug_id who)]}); - } - if (!$dbh->bz_index_info('keywords', 'keywords_bug_id_idx') - || !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx')->{TYPE}) - { - $dbh->bz_drop_index('keywords', 'keywords_bug_id_idx'); - $dbh->bz_add_index('keywords', 'keywords_bug_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(bug_id keywordid)]}); - } + _add_bug_vote_cache(); + _update_product_name_definition(); - _copy_from_comments_to_longdescs(); - _populate_duplicates_table(); + $dbh->bz_add_column('profiles', 'disabledtext', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - if (!$dbh->bz_column_info('email_setting', 'user_id')) { - $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'}); - } - - $dbh->bz_add_column('groups', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + _populate_longdescs(); + _update_bugs_activity_field_to_fieldid(); - $dbh->bz_add_column('attachments', 'isobsolete', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + if (!$dbh->bz_column_info('bugs', 'lastdiffed')) { + $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + $dbh->do('UPDATE bugs SET lastdiffed = NOW()'); + } - $dbh->bz_drop_column("profiles", "emailnotification"); - $dbh->bz_drop_column("profiles", "newemailtech"); + _add_unique_login_name_index_to_profiles(); - # 2003-11-19; chicks@chicks.net; bug 225973: fix field size to accommodate - # wider algorithms such as Blowfish. Note that this needs to be run - # before recrypting passwords in the following block. - $dbh->bz_alter_column('profiles', 'cryptpassword', - {TYPE => 'varchar(128)'}); + $dbh->bz_add_column('profiles', 'mybugslink', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - _recrypt_plaintext_passwords(); + _update_component_user_fields_to_ids(); - # 2001-06-15 kiko@async.com.br - Change bug:version size to avoid - # truncates re http://bugzilla.mozilla.org/show_bug.cgi?id=9352 - $dbh->bz_alter_column('bugs', 'version', - {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_add_column('bugs', 'everconfirmed', {TYPE => 'BOOLEAN', NOTNULL => 1}, + 1); - _update_bugs_activity_to_only_record_changes(); + _populate_milestones_table(); - # bug 90933: Make disabledtext NOT NULL - if (!$dbh->bz_column_info('profiles', 'disabledtext')->{NOTNULL}) { - $dbh->bz_alter_column("profiles", "disabledtext", - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - } + _add_products_defaultmilestone(); - $dbh->bz_add_column("bugs", "reporter_accessible", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->bz_add_column("bugs", "cclist_accessible", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + # 2000-03-24 Added unique indexes into the cc and keyword tables. This + # prevents certain database inconsistencies, and, moreover, is required for + # new generalized list code to work. + if ( !$dbh->bz_index_info('cc', 'cc_bug_id_idx') + || !$dbh->bz_index_info('cc', 'cc_bug_id_idx')->{TYPE}) + { + $dbh->bz_drop_index('cc', 'cc_bug_id_idx'); + $dbh->bz_add_index('cc', 'cc_bug_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(bug_id who)]}); + } + if ( !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx') + || !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx')->{TYPE}) + { + $dbh->bz_drop_index('keywords', 'keywords_bug_id_idx'); + $dbh->bz_add_index('keywords', 'keywords_bug_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(bug_id keywordid)]}); + } - $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT3'}); + _copy_from_comments_to_longdescs(); + _populate_duplicates_table(); - _delete_logincookies_cryptpassword_and_handle_invalid_cookies(); + if (!$dbh->bz_column_info('email_setting', 'user_id')) { + $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'}); + } - # qacontact/assignee should always be able to see bugs: bug 97471 - $dbh->bz_drop_column("bugs", "qacontact_accessible"); - $dbh->bz_drop_column("bugs", "assignee_accessible"); + $dbh->bz_add_column('groups', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - # 2002-02-20 jeff.hedlund@matrixsi.com - bug 24789 time tracking - $dbh->bz_add_column("longdescs", "work_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "estimated_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "remaining_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "deadline", {TYPE => 'DATETIME'}); + $dbh->bz_add_column('attachments', 'isobsolete', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - _use_ip_instead_of_hostname_in_logincookies(); + $dbh->bz_drop_column("profiles", "emailnotification"); + $dbh->bz_drop_column("profiles", "newemailtech"); - $dbh->bz_add_column('longdescs', 'isprivate', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('attachments', 'isprivate', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # 2003-11-19; chicks@chicks.net; bug 225973: fix field size to accommodate + # wider algorithms such as Blowfish. Note that this needs to be run + # before recrypting passwords in the following block. + $dbh->bz_alter_column('profiles', 'cryptpassword', {TYPE => 'varchar(128)'}); + + _recrypt_plaintext_passwords(); + + # 2001-06-15 kiko@async.com.br - Change bug:version size to avoid + # truncates re http://bugzilla.mozilla.org/show_bug.cgi?id=9352 + $dbh->bz_alter_column('bugs', 'version', {TYPE => 'varchar(64)', NOTNULL => 1}); - _move_quips_into_db(); + _update_bugs_activity_to_only_record_changes(); - $dbh->bz_drop_column("namedqueries", "watchfordiffs"); + # bug 90933: Make disabledtext NOT NULL + if (!$dbh->bz_column_info('profiles', 'disabledtext')->{NOTNULL}) { + $dbh->bz_alter_column("profiles", "disabledtext", + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + } - _use_ids_for_products_and_components(); - _convert_groups_system_from_groupset(); - _convert_attachment_statuses_to_flags(); - _remove_spaces_and_commas_from_flagtypes(); - _setup_usebuggroups_backward_compatibility(); - _remove_user_series_map(); + $dbh->bz_add_column("bugs", "reporter_accessible", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + $dbh->bz_add_column("bugs", "cclist_accessible", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - # 2006-08-03 remi_zara@mac.com bug 346241, make series.creator nullable - # This must happen before calling _copy_old_charts_into_database(). - if ($dbh->bz_column_info('series', 'creator')->{NOTNULL}) { - $dbh->bz_alter_column('series', 'creator', {TYPE => 'INT3'}); - $dbh->do("UPDATE series SET creator = NULL WHERE creator = 0"); - } + $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT3'}); - _copy_old_charts_into_database(); + _delete_logincookies_cryptpassword_and_handle_invalid_cookies(); - _add_user_group_map_grant_type(); - _add_group_group_map_grant_type(); + # qacontact/assignee should always be able to see bugs: bug 97471 + $dbh->bz_drop_column("bugs", "qacontact_accessible"); + $dbh->bz_drop_column("bugs", "assignee_accessible"); - $dbh->bz_add_column("profiles", "extern_id", {TYPE => 'varchar(64)'}); + # 2002-02-20 jeff.hedlund@matrixsi.com - bug 24789 time tracking + $dbh->bz_add_column("longdescs", "work_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "estimated_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "remaining_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "deadline", {TYPE => 'DATETIME'}); - $dbh->bz_add_column('flagtypes', 'grant_group_id', {TYPE => 'INT3'}); - $dbh->bz_add_column('flagtypes', 'request_group_id', {TYPE => 'INT3'}); + _use_ip_instead_of_hostname_in_logincookies(); - # mailto is no longer just userids - $dbh->bz_rename_column('whine_schedules', 'mailto_userid', 'mailto'); - $dbh->bz_add_column('whine_schedules', 'mailto_type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column('longdescs', 'isprivate', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('attachments', 'isprivate', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + _move_quips_into_db(); + + $dbh->bz_drop_column("namedqueries", "watchfordiffs"); + + _use_ids_for_products_and_components(); + _convert_groups_system_from_groupset(); + _convert_attachment_statuses_to_flags(); + _remove_spaces_and_commas_from_flagtypes(); + _setup_usebuggroups_backward_compatibility(); + _remove_user_series_map(); + + # 2006-08-03 remi_zara@mac.com bug 346241, make series.creator nullable + # This must happen before calling _copy_old_charts_into_database(). + if ($dbh->bz_column_info('series', 'creator')->{NOTNULL}) { + $dbh->bz_alter_column('series', 'creator', {TYPE => 'INT3'}); + $dbh->do("UPDATE series SET creator = NULL WHERE creator = 0"); + } + + _copy_old_charts_into_database(); + + _add_user_group_map_grant_type(); + _add_group_group_map_grant_type(); + + $dbh->bz_add_column("profiles", "extern_id", {TYPE => 'varchar(64)'}); + + $dbh->bz_add_column('flagtypes', 'grant_group_id', {TYPE => 'INT3'}); + $dbh->bz_add_column('flagtypes', 'request_group_id', {TYPE => 'INT3'}); + + # mailto is no longer just userids + $dbh->bz_rename_column('whine_schedules', 'mailto_userid', 'mailto'); + $dbh->bz_add_column('whine_schedules', 'mailto_type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + + _add_longdescs_already_wrapped(); - _add_longdescs_already_wrapped(); + # Moved enum types to separate tables so we need change the old enum + # types to standard varchars in the bugs table. + $dbh->bz_alter_column('bugs', 'bug_status', + {TYPE => 'varchar(64)', NOTNULL => 1}); + + # 2005-03-23 Tomas.Kopal@altap.cz - add default value to resolution, + # bug 286695 + $dbh->bz_alter_column('bugs', 'resolution', + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}); + $dbh->bz_alter_column('bugs', 'priority', + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('bugs', 'bug_severity', + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('bugs', 'rep_platform', + {TYPE => 'varchar(64)', NOTNULL => 1}, ''); + $dbh->bz_alter_column('bugs', 'op_sys', {TYPE => 'varchar(64)', NOTNULL => 1}); + + # When migrating quips from the '$datadir/comments' file to the DB, + # the user ID should be NULL instead of 0 (which is an invalid user ID). + if ($dbh->bz_column_info('quips', 'userid')->{NOTNULL}) { + $dbh->bz_alter_column('quips', 'userid', {TYPE => 'INT3'}); + print "Changing owner to NULL for quips where the owner is", " unknown...\n"; + $dbh->do('UPDATE quips SET userid = NULL WHERE userid = 0'); + } + + _convert_attachments_filename_from_mediumtext(); + + $dbh->bz_add_column('quips', 'approved', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + # 2002-12-20 Bug 180870 - remove manual shadowdb replication code + $dbh->bz_drop_table("shadowlog"); + + _rename_votes_count_and_force_group_refresh(); - # Moved enum types to separate tables so we need change the old enum - # types to standard varchars in the bugs table. - $dbh->bz_alter_column('bugs', 'bug_status', - {TYPE => 'varchar(64)', NOTNULL => 1}); - # 2005-03-23 Tomas.Kopal@altap.cz - add default value to resolution, - # bug 286695 - $dbh->bz_alter_column('bugs', 'resolution', - {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}); - $dbh->bz_alter_column('bugs', 'priority', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('bugs', 'bug_severity', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('bugs', 'rep_platform', - {TYPE => 'varchar(64)', NOTNULL => 1}, ''); - $dbh->bz_alter_column('bugs', 'op_sys', - {TYPE => 'varchar(64)', NOTNULL => 1}); + # 2004/02/15 - Summaries shouldn't be null - see bug 220232 + if (!exists $dbh->bz_column_info('bugs', 'short_desc')->{NOTNULL}) { + $dbh->bz_alter_column('bugs', 'short_desc', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + } - # When migrating quips from the '$datadir/comments' file to the DB, - # the user ID should be NULL instead of 0 (which is an invalid user ID). - if ($dbh->bz_column_info('quips', 'userid')->{NOTNULL}) { - $dbh->bz_alter_column('quips', 'userid', {TYPE => 'INT3'}); - print "Changing owner to NULL for quips where the owner is", - " unknown...\n"; - $dbh->do('UPDATE quips SET userid = NULL WHERE userid = 0'); + $dbh->bz_add_column('products', 'classification_id', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + + _fix_group_with_empty_name(); + + $dbh->bz_add_index('bugs_activity', 'bugs_activity_who_idx', [qw(who)]); + + # Add defaults for some fields that should have them but didn't. + $dbh->bz_alter_column('bugs', 'status_whiteboard', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + if ($dbh->bz_column_info('bugs', 'votes')) { + $dbh->bz_alter_column('bugs', 'votes', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => '0'}); + } + + $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + + # 2005-03-09 qa_contact should be NULL instead of 0, bug 285534 + if ($dbh->bz_column_info('bugs', 'qa_contact')->{NOTNULL}) { + $dbh->bz_alter_column('bugs', 'qa_contact', {TYPE => 'INT3'}); + $dbh->do("UPDATE bugs SET qa_contact = NULL WHERE qa_contact = 0"); + } + + # 2005-03-27 initialqacontact should be NULL instead of 0, bug 287483 + if ($dbh->bz_column_info('components', 'initialqacontact')->{NOTNULL}) { + $dbh->bz_alter_column('components', 'initialqacontact', {TYPE => 'INT3'}); + } + $dbh->do("UPDATE components SET initialqacontact = NULL " + . "WHERE initialqacontact = 0"); + + _migrate_email_prefs_to_new_table(); + _initialize_new_email_prefs(); + _change_all_mysql_booleans_to_tinyint(); + + # make classification_id field type be consistent with DB:Schema + $dbh->bz_alter_column('products', 'classification_id', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + + # initialowner was accidentally NULL when we checked-in Schema, + # when it really should be NOT NULL. + $dbh->bz_alter_column('components', 'initialowner', + {TYPE => 'INT3', NOTNULL => 1}, 0); + + # 2005-03-28 - bug 238800 - index flags.type_id for editflagtypes.cgi + $dbh->bz_add_index('flags', 'flags_type_id_idx', [qw(type_id)]); + + # For a short time, the flags_type_id_idx was misnamed in upgraded installs. + $dbh->bz_drop_index('flags', 'type_id'); + + # 2005-04-28 - LpSolit@gmail.com - Bug 7233: add an index to versions + $dbh->bz_alter_column('versions', 'value', + {TYPE => 'varchar(64)', NOTNULL => 1}); + _add_versions_product_id_index(); + + if (!exists $dbh->bz_column_info('milestones', 'sortkey')->{DEFAULT}) { + $dbh->bz_alter_column('milestones', 'sortkey', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + } + + # 2005-06-14 - LpSolit@gmail.com - Bug 292544 + $dbh->bz_alter_column('bugs', 'creation_ts', {TYPE => 'DATETIME'}); + + _fix_whine_queries_title_and_op_sys_value(); + _fix_attachments_submitter_id_idx(); + _copy_attachments_thedata_to_attach_data(); + _fix_broken_all_closed_series(); + + # 2005-08-14 bugreport@peshkin.net -- Bug 304583 + # Get rid of leftover DERIVED group permissions + use constant GRANT_DERIVED => 1; + $dbh->do("DELETE FROM user_group_map WHERE grant_type = " . GRANT_DERIVED); + + _rederive_regex_groups(); + + # PUBLIC is a reserved word in Oracle. + $dbh->bz_rename_column('series', 'public', 'is_public'); + + # 2005-11-04 LpSolit@gmail.com - Bug 305927 + $dbh->bz_alter_column('groups', 'userregexp', + {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + + # 2005-09-26 - olav@bkor.dhs.org - Bug 119524 + $dbh->bz_alter_column('logincookies', 'cookie', + {TYPE => 'varchar(16)', PRIMARYKEY => 1, NOTNULL => 1}); + + _clean_control_characters_from_short_desc(); + + # 2005-12-07 altlst@sonic.net -- Bug 225221 + $dbh->bz_add_column('longdescs', 'comment_id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + _stop_storing_inactive_flags(); + _change_short_desc_from_mediumtext_to_varchar(); + + # 2006-07-01 wurblzap@gmail.com -- Bug 69000 + $dbh->bz_add_column('namedqueries', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + _move_namedqueries_linkinfooter_to_its_own_table(); + + _add_classifications_sortkey(); + _move_data_nomail_into_db(); + + # The products table lacked sensible defaults. + if ($dbh->bz_column_info('products', 'milestoneurl')) { + $dbh->bz_alter_column('products', 'milestoneurl', + {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + } + if ($dbh->bz_column_info('products', 'disallownew')) { + $dbh->bz_alter_column('products', 'disallownew', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); + + if ($dbh->bz_column_info('products', 'votesperuser')) { + $dbh->bz_alter_column('products', 'votesperuser', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_alter_column('products', 'votestoconfirm', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); } + } - _convert_attachments_filename_from_mediumtext(); + # 2006-08-04 LpSolit@gmail.com - Bug 305941 + $dbh->bz_drop_column('profiles', 'refreshed_when'); + $dbh->bz_drop_column('groups', 'last_changed'); - $dbh->bz_add_column('quips', 'approved', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + # 2006-08-06 LpSolit@gmail.com - Bug 347521 + $dbh->bz_alter_column('flagtypes', 'id', + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2002-12-20 Bug 180870 - remove manual shadowdb replication code - $dbh->bz_drop_table("shadowlog"); + $dbh->bz_alter_column('keyworddefs', 'id', + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _rename_votes_count_and_force_group_refresh(); + # 2006-08-19 LpSolit@gmail.com - Bug 87795 + $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'}); - # 2004/02/15 - Summaries shouldn't be null - see bug 220232 - if (!exists $dbh->bz_column_info('bugs', 'short_desc')->{NOTNULL}) { - $dbh->bz_alter_column('bugs', 'short_desc', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - } + $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx'); - $dbh->bz_add_column('products', 'classification_id', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + # The profiles table was missing some defaults. + $dbh->bz_alter_column('profiles', 'disabledtext', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + $dbh->bz_alter_column('profiles', 'realname', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); - _fix_group_with_empty_name(); + _update_longdescs_who_index(); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_who_idx', [qw(who)]); + $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'}); - # Add defaults for some fields that should have them but didn't. - $dbh->bz_alter_column('bugs', 'status_whiteboard', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - if ($dbh->bz_column_info('bugs', 'votes')) { - $dbh->bz_alter_column('bugs', 'votes', - {TYPE => 'INT3', NOTNULL => 1, DEFAULT => '0'}); - } + $dbh->bz_alter_column('longdescs', 'thetext', + {TYPE => 'LONGTEXT', NOTNULL => 1}, ''); - $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + # 2006-10-20 LpSolit@gmail.com - Bug 189627 + $dbh->bz_add_column('group_control_map', 'editcomponents', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('group_control_map', 'editbugs', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('group_control_map', 'canconfirm', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2005-03-09 qa_contact should be NULL instead of 0, bug 285534 - if ($dbh->bz_column_info('bugs', 'qa_contact')->{NOTNULL}) { - $dbh->bz_alter_column('bugs', 'qa_contact', {TYPE => 'INT3'}); - $dbh->do("UPDATE bugs SET qa_contact = NULL WHERE qa_contact = 0"); - } + # 2006-11-07 LpSolit@gmail.com - Bug 353656 + $dbh->bz_add_column('longdescs', 'type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column('longdescs', 'extra_data', {TYPE => 'varchar(255)'}); - # 2005-03-27 initialqacontact should be NULL instead of 0, bug 287483 - if ($dbh->bz_column_info('components', 'initialqacontact')->{NOTNULL}) { - $dbh->bz_alter_column('components', 'initialqacontact', - {TYPE => 'INT3'}); - } - $dbh->do("UPDATE components SET initialqacontact = NULL " . - "WHERE initialqacontact = 0"); + $dbh->bz_add_column('versions', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column('milestones', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _migrate_email_prefs_to_new_table(); - _initialize_new_email_prefs(); - _change_all_mysql_booleans_to_tinyint(); + _fix_uppercase_custom_field_names(); + _fix_uppercase_index_names(); - # make classification_id field type be consistent with DB:Schema - $dbh->bz_alter_column('products', 'classification_id', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + # 2007-05-17 LpSolit@gmail.com - Bug 344965 + _initialize_workflow_for_upgrade($old_params); - # initialowner was accidentally NULL when we checked-in Schema, - # when it really should be NOT NULL. - $dbh->bz_alter_column('components', 'initialowner', - {TYPE => 'INT3', NOTNULL => 1}, 0); + # 2007-08-08 LpSolit@gmail.com - Bug 332149 + $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'}); - # 2005-03-28 - bug 238800 - index flags.type_id for editflagtypes.cgi - $dbh->bz_add_index('flags', 'flags_type_id_idx', [qw(type_id)]); + # 2007-08-21 wurblzap@gmail.com - Bug 365378 + _make_lang_setting_dynamic(); - # For a short time, the flags_type_id_idx was misnamed in upgraded installs. - $dbh->bz_drop_index('flags', 'type_id'); + # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129 + _change_text_types(); - # 2005-04-28 - LpSolit@gmail.com - Bug 7233: add an index to versions - $dbh->bz_alter_column('versions', 'value', - {TYPE => 'varchar(64)', NOTNULL => 1}); - _add_versions_product_id_index(); + # 2007-09-09 LpSolit@gmail.com - Bug 99215 + _fix_attachment_modification_date(); - if (!exists $dbh->bz_column_info('milestones', 'sortkey')->{DEFAULT}) { - $dbh->bz_alter_column('milestones', 'sortkey', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - } - - # 2005-06-14 - LpSolit@gmail.com - Bug 292544 - $dbh->bz_alter_column('bugs', 'creation_ts', {TYPE => 'DATETIME'}); + $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx'); + _populate_bugs_fulltext(); - _fix_whine_queries_title_and_op_sys_value(); - _fix_attachments_submitter_id_idx(); - _copy_attachments_thedata_to_attach_data(); - _fix_broken_all_closed_series(); - # 2005-08-14 bugreport@peshkin.net -- Bug 304583 - # Get rid of leftover DERIVED group permissions - use constant GRANT_DERIVED => 1; - $dbh->do("DELETE FROM user_group_map WHERE grant_type = " . GRANT_DERIVED); + # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292 + $dbh->bz_alter_column('series', 'query', {TYPE => 'MEDIUMTEXT', NOTNULL => 1}); - _rederive_regex_groups(); + # Add FK to multi select field tables + _add_foreign_keys_to_multiselects(); - # PUBLIC is a reserved word in Oracle. - $dbh->bz_rename_column('series', 'public', 'is_public'); + # 2008-07-28 tfu@redhat.com - Bug 431669 + $dbh->bz_alter_column('group_control_map', 'product_id', + {TYPE => 'INT2', NOTNULL => 1}); - # 2005-11-04 LpSolit@gmail.com - Bug 305927 - $dbh->bz_alter_column('groups', 'userregexp', - {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + # 2008-09-07 LpSolit@gmail.com - Bug 452893 + _fix_illegal_flag_modification_dates(); - # 2005-09-26 - olav@bkor.dhs.org - Bug 119524 - $dbh->bz_alter_column('logincookies', 'cookie', - {TYPE => 'varchar(16)', PRIMARYKEY => 1, NOTNULL => 1}); + _add_visiblity_value_to_value_tables(); - _clean_control_characters_from_short_desc(); - - # 2005-12-07 altlst@sonic.net -- Bug 225221 - $dbh->bz_add_column('longdescs', 'comment_id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2009-03-02 arbingersys@gmail.com - Bug 423613 + _add_extern_id_index(); - _stop_storing_inactive_flags(); - _change_short_desc_from_mediumtext_to_varchar(); + # 2009-03-31 LpSolit@gmail.com - Bug 478972 + $dbh->bz_alter_column('group_control_map', 'entry', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_alter_column('group_control_map', 'canedit', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2006-07-01 wurblzap@gmail.com -- Bug 69000 - $dbh->bz_add_column('namedqueries', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _move_namedqueries_linkinfooter_to_its_own_table(); + # 2009-01-16 oreomike@gmail.com - Bug 302420 + $dbh->bz_add_column('whine_events', 'mailifnobugs', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - _add_classifications_sortkey(); - _move_data_nomail_into_db(); + _convert_disallownew_to_isactive(); - # The products table lacked sensible defaults. - if ($dbh->bz_column_info('products', 'milestoneurl')) { - $dbh->bz_alter_column('products', 'milestoneurl', - {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); - } - if ($dbh->bz_column_info('products', 'disallownew')){ - $dbh->bz_alter_column('products', 'disallownew', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); - - if ($dbh->bz_column_info('products', 'votesperuser')) { - $dbh->bz_alter_column('products', 'votesperuser', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_alter_column('products', 'votestoconfirm', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - } - } + $dbh->bz_alter_column('bugs_activity', 'added', {TYPE => 'varchar(255)'}); + $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']); - # 2006-08-04 LpSolit@gmail.com - Bug 305941 - $dbh->bz_drop_column('profiles', 'refreshed_when'); - $dbh->bz_drop_column('groups', 'last_changed'); + # 2009-09-28 LpSolit@gmail.com - Bug 519032 + $dbh->bz_drop_column('series', 'last_viewed'); - # 2006-08-06 LpSolit@gmail.com - Bug 347521 - $dbh->bz_alter_column('flagtypes', 'id', - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2009-09-28 LpSolit@gmail.com - Bug 399073 + _fix_logincookies_ipaddr(); - $dbh->bz_alter_column('keyworddefs', 'id', - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2009-11-01 LpSolit@gmail.com - Bug 525025 + _fix_invalid_custom_field_names(); - # 2006-08-19 LpSolit@gmail.com - Bug 87795 - $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'}); + _set_attachment_comment_types(); - $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx'); + $dbh->bz_drop_column('products', 'milestoneurl'); - # The profiles table was missing some defaults. - $dbh->bz_alter_column('profiles', 'disabledtext', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - $dbh->bz_alter_column('profiles', 'realname', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); + _add_allows_unconfirmed_to_product_table(); + _convert_flagtypes_fks_to_set_null(); + _fix_decimal_types(); + _fix_series_creator_fk(); - _update_longdescs_who_index(); + # 2009-11-14 dkl@redhat.com - Bug 310450 + $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); - $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'}); + # 2010-04-07 LpSolit@gmail.com - Bug 69621 + $dbh->bz_drop_column('bugs', 'keywords'); - $dbh->bz_alter_column('longdescs', 'thetext', - {TYPE => 'LONGTEXT', NOTNULL => 1}, ''); + # 2010-05-07 ewong@pw-wspx.org - Bug 463945 + $dbh->bz_alter_column('group_control_map', 'membercontrol', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); + $dbh->bz_alter_column('group_control_map', 'othercontrol', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); - # 2006-10-20 LpSolit@gmail.com - Bug 189627 - $dbh->bz_add_column('group_control_map', 'editcomponents', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('group_control_map', 'editbugs', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('group_control_map', 'canconfirm', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # Add NOT NULL to some columns that need it, and DEFAULT to + # attachments.ispatch. + $dbh->bz_alter_column('attachments', 'ispatch', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_alter_column('keyworddefs', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + $dbh->bz_alter_column('products', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - # 2006-11-07 LpSolit@gmail.com - Bug 353656 - $dbh->bz_add_column('longdescs', 'type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column('longdescs', 'extra_data', {TYPE => 'varchar(255)'}); + # Change the default of allows_unconfirmed to TRUE as part + # of the new workflow. + $dbh->bz_alter_column('products', 'allows_unconfirmed', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->bz_add_column('versions', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column('milestones', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2010-07-18 LpSolit@gmail.com - Bug 119703 + _remove_attachment_isurl(); - _fix_uppercase_custom_field_names(); - _fix_uppercase_index_names(); + # 2009-05-07 ghendricks@novell.com - Bug 77193 + _add_isactive_to_product_fields(); - # 2007-05-17 LpSolit@gmail.com - Bug 344965 - _initialize_workflow_for_upgrade($old_params); + # 2010-10-09 LpSolit@gmail.com - Bug 505165 + $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1}); - # 2007-08-08 LpSolit@gmail.com - Bug 332149 - $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'}); + # 2010-10-09 LpSolit@gmail.com - Bug 451735 + _fix_series_indexes(); - # 2007-08-21 wurblzap@gmail.com - Bug 365378 - _make_lang_setting_dynamic(); - - # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129 - _change_text_types(); + $dbh->bz_add_column('bug_see_also', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2007-09-09 LpSolit@gmail.com - Bug 99215 - _fix_attachment_modification_date(); + _rename_tags_to_tag(); - $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx'); - _populate_bugs_fulltext(); + # 2011-01-29 LpSolit@gmail.com - Bug 616185 + _migrate_user_tags(); - # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292 - $dbh->bz_alter_column('series', 'query', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }); + _populate_bug_see_also_class(); - # Add FK to multi select field tables - _add_foreign_keys_to_multiselects(); + # 2011-06-15 dkl@mozilla.com - Bug 658929 + _migrate_disabledtext_boolean(); - # 2008-07-28 tfu@redhat.com - Bug 431669 - $dbh->bz_alter_column('group_control_map', 'product_id', - { TYPE => 'INT2', NOTNULL => 1 }); + # 2011-11-01 glob@mozilla.com - Bug 240437 + $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'}); - # 2008-09-07 LpSolit@gmail.com - Bug 452893 - _fix_illegal_flag_modification_dates(); + # 2011-10-11 miketosh - Bug 690173 + _on_delete_set_null_for_audit_log_userid(); - _add_visiblity_value_to_value_tables(); + # 2011-11-23 gerv@gerv.net - Bug 705058 - make filenames longer + $dbh->bz_alter_column('attachments', 'filename', + {TYPE => 'varchar(255)', NOTNULL => 1}); - # 2009-03-02 arbingersys@gmail.com - Bug 423613 - _add_extern_id_index(); + # 2011-11-28 dkl@mozilla.com - Bug 685611 + _fix_notnull_defaults(); - # 2009-03-31 LpSolit@gmail.com - Bug 478972 - $dbh->bz_alter_column('group_control_map', 'entry', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_alter_column('group_control_map', 'canedit', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # 2012-02-15 LpSolit@gmail.com - Bug 722113 + if ($dbh->bz_index_info('profile_search', 'profile_search_user_id')) { + $dbh->bz_drop_index('profile_search', 'profile_search_user_id'); + $dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', + [qw(user_id)]); + } - # 2009-01-16 oreomike@gmail.com - Bug 302420 - $dbh->bz_add_column('whine_events', 'mailifnobugs', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - _convert_disallownew_to_isactive(); + # 2012-03-23 LpSolit@gmail.com - Bug 448551 + $dbh->bz_alter_column('bugs', 'target_milestone', + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}); - $dbh->bz_alter_column('bugs_activity', 'added', - { TYPE => 'varchar(255)' }); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']); + $dbh->bz_alter_column('milestones', 'value', + {TYPE => 'varchar(64)', NOTNULL => 1}); - # 2009-09-28 LpSolit@gmail.com - Bug 519032 - $dbh->bz_drop_column('series', 'last_viewed'); + $dbh->bz_alter_column('products', 'defaultmilestone', + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}); - # 2009-09-28 LpSolit@gmail.com - Bug 399073 - _fix_logincookies_ipaddr(); + # 2012-04-15 Frank@Frank-Becker.de - Bug 740536 + $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']); - # 2009-11-01 LpSolit@gmail.com - Bug 525025 - _fix_invalid_custom_field_names(); + # 2012-06-06 dkl@mozilla.com - Bug 762288 + $dbh->bz_alter_column('bugs_activity', 'removed', {TYPE => 'varchar(255)'}); + $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']); - _set_attachment_comment_types(); + # 2012-06-13 dkl@mozilla.com - Bug 764457 + $dbh->bz_add_column('bugs_activity', 'id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_drop_column('products', 'milestoneurl'); + # 2012-06-13 dkl@mozilla.com - Bug 764466 + $dbh->bz_add_column('profiles_activity', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _add_allows_unconfirmed_to_product_table(); - _convert_flagtypes_fks_to_set_null(); - _fix_decimal_types(); - _fix_series_creator_fk(); + # 2012-07-24 dkl@mozilla.com - Bug 776972 + $dbh->bz_alter_column('bugs_activity', 'id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2009-11-14 dkl@redhat.com - Bug 310450 - $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); - # 2010-04-07 LpSolit@gmail.com - Bug 69621 - $dbh->bz_drop_column('bugs', 'keywords'); - - # 2010-05-07 ewong@pw-wspx.org - Bug 463945 - $dbh->bz_alter_column('group_control_map', 'membercontrol', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); - $dbh->bz_alter_column('group_control_map', 'othercontrol', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); + # 2012-07-24 dkl@mozilla.com - Bug 776982 + _fix_longdescs_primary_key(); - # Add NOT NULL to some columns that need it, and DEFAULT to - # attachments.ispatch. - $dbh->bz_alter_column('attachments', 'ispatch', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_alter_column('keyworddefs', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); - $dbh->bz_alter_column('products', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); + # 2012-08-02 dkl@mozilla.com - Bug 756953 + _fix_dependencies_dupes(); - # Change the default of allows_unconfirmed to TRUE as part - # of the new workflow. - $dbh->bz_alter_column('products', 'allows_unconfirmed', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE' }); + # 2012-08-01 koosha.khajeh@gmail.com - Bug 187753 + _shorten_long_quips(); - # 2010-07-18 LpSolit@gmail.com - Bug 119703 - _remove_attachment_isurl(); + # 2012-12-29 reed@reedloden.com - Bug 785283 + _add_password_salt_separator(); - # 2009-05-07 ghendricks@novell.com - Bug 77193 - _add_isactive_to_product_fields(); + # 2013-01-02 LpSolit@gmail.com - Bug 824361 + _fix_longdescs_indexes(); - # 2010-10-09 LpSolit@gmail.com - Bug 505165 - $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1}); + # 2013-02-04 dkl@mozilla.com - Bug 824346 + _fix_flagclusions_indexes(); - # 2010-10-09 LpSolit@gmail.com - Bug 451735 - _fix_series_indexes(); + # 2013-08-26 sgreen@redhat.com - Bug 903895 + _fix_components_primary_key(); - $dbh->bz_add_column('bug_see_also', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2014-06-09 dylan@mozilla.com - Bug 1022923 + $dbh->bz_add_index('bug_user_last_visit', + 'bug_user_last_visit_last_visit_ts_idx', + ['last_visit_ts']); - _rename_tags_to_tag(); + # 2014-07-14 sgreen@redhat.com - Bug 726696 + $dbh->bz_alter_column('tokens', 'tokentype', + {TYPE => 'varchar(16)', NOTNULL => 1}); - # 2011-01-29 LpSolit@gmail.com - Bug 616185 - _migrate_user_tags(); + # 2014-07-27 LpSolit@gmail.com - Bug 1044561 + _fix_user_api_keys_indexes(); - _populate_bug_see_also_class(); + # 2014-08-11 sgreen@redhat.com - Bug 1012506 + _update_alias(); - # 2011-06-15 dkl@mozilla.com - Bug 658929 - _migrate_disabledtext_boolean(); + # 2014-11-10 dkl@mozilla.com - Bug 1093928 + $dbh->bz_drop_column('longdescs', 'is_markdown'); - # 2011-11-01 glob@mozilla.com - Bug 240437 - $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'}); + # 2015-12-16 LpSolit@gmail.com - Bug 1232578 + _sanitize_audit_log_table(); - # 2011-10-11 miketosh - Bug 690173 - _on_delete_set_null_for_audit_log_userid(); - - # 2011-11-23 gerv@gerv.net - Bug 705058 - make filenames longer - $dbh->bz_alter_column('attachments', 'filename', - { TYPE => 'varchar(255)', NOTNULL => 1 }); + ################################################################ + # New --TABLE-- changes should go *** A B O V E *** this point # + ################################################################ - # 2011-11-28 dkl@mozilla.com - Bug 685611 - _fix_notnull_defaults(); + Bugzilla::Hook::process('install_update_db'); - # 2012-02-15 LpSolit@gmail.com - Bug 722113 - if ($dbh->bz_index_info('profile_search', 'profile_search_user_id')) { - $dbh->bz_drop_index('profile_search', 'profile_search_user_id'); - $dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', [qw(user_id)]); - } + # We do this here because otherwise the foreign key from + # products.classification_id to classifications.id will fail + # (because products.classification_id defaults to "1", so on upgraded + # installations it's already been set before the first Classification + # exists). + Bugzilla::Install::create_default_classification(); - # 2012-03-23 LpSolit@gmail.com - Bug 448551 - $dbh->bz_alter_column('bugs', 'target_milestone', - {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}); - - $dbh->bz_alter_column('milestones', 'value', {TYPE => 'varchar(64)', NOTNULL => 1}); - - $dbh->bz_alter_column('products', 'defaultmilestone', - {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}); - - # 2012-04-15 Frank@Frank-Becker.de - Bug 740536 - $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']); - - # 2012-06-06 dkl@mozilla.com - Bug 762288 - $dbh->bz_alter_column('bugs_activity', 'removed', - { TYPE => 'varchar(255)' }); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']); - - # 2012-06-13 dkl@mozilla.com - Bug 764457 - $dbh->bz_add_column('bugs_activity', 'id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - # 2012-06-13 dkl@mozilla.com - Bug 764466 - $dbh->bz_add_column('profiles_activity', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - # 2012-07-24 dkl@mozilla.com - Bug 776972 - $dbh->bz_alter_column('bugs_activity', 'id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - - # 2012-07-24 dkl@mozilla.com - Bug 776982 - _fix_longdescs_primary_key(); - - # 2012-08-02 dkl@mozilla.com - Bug 756953 - _fix_dependencies_dupes(); - - # 2012-08-01 koosha.khajeh@gmail.com - Bug 187753 - _shorten_long_quips(); - - # 2012-12-29 reed@reedloden.com - Bug 785283 - _add_password_salt_separator(); - - # 2013-01-02 LpSolit@gmail.com - Bug 824361 - _fix_longdescs_indexes(); - - # 2013-02-04 dkl@mozilla.com - Bug 824346 - _fix_flagclusions_indexes(); - - # 2013-08-26 sgreen@redhat.com - Bug 903895 - _fix_components_primary_key(); - - # 2014-06-09 dylan@mozilla.com - Bug 1022923 - $dbh->bz_add_index('bug_user_last_visit', - 'bug_user_last_visit_last_visit_ts_idx', - ['last_visit_ts']); - - # 2014-07-14 sgreen@redhat.com - Bug 726696 - $dbh->bz_alter_column('tokens', 'tokentype', - {TYPE => 'varchar(16)', NOTNULL => 1}); - - # 2014-07-27 LpSolit@gmail.com - Bug 1044561 - _fix_user_api_keys_indexes(); - - # 2014-08-11 sgreen@redhat.com - Bug 1012506 - _update_alias(); - - # 2014-11-10 dkl@mozilla.com - Bug 1093928 - $dbh->bz_drop_column('longdescs', 'is_markdown'); - - # 2015-12-16 LpSolit@gmail.com - Bug 1232578 - _sanitize_audit_log_table(); - - ################################################################ - # New --TABLE-- changes should go *** A B O V E *** this point # - ################################################################ - - Bugzilla::Hook::process('install_update_db'); - - # We do this here because otherwise the foreign key from - # products.classification_id to classifications.id will fail - # (because products.classification_id defaults to "1", so on upgraded - # installations it's already been set before the first Classification - # exists). - Bugzilla::Install::create_default_classification(); - - $dbh->bz_setup_foreign_keys(); + $dbh->bz_setup_foreign_keys(); } # Subroutines should be ordered in the order that they are called. # Thus, newer subroutines should be at the bottom. sub _update_pre_checksetup_bugzillas { - my $dbh = Bugzilla->dbh; - # really old fields that were added before checksetup.pl existed - # but aren't in very old bugzilla's (like 2.1) - # Steve Stock (sstock@iconnect-inc.com) - - $dbh->bz_add_column('bugs', 'target_milestone', - {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); - $dbh->bz_add_column('bugs', 'qa_contact', {TYPE => 'INT3'}); - $dbh->bz_add_column('bugs', 'status_whiteboard', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - if (!$dbh->bz_column_info('products', 'isactive')){ - $dbh->bz_add_column('products', 'disallownew', - {TYPE => 'BOOLEAN', NOTNULL => 1}, 0); - } - - $dbh->bz_add_column('components', 'initialqacontact', - {TYPE => 'TINYTEXT'}); - $dbh->bz_add_column('components', 'description', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + my $dbh = Bugzilla->dbh; + + # really old fields that were added before checksetup.pl existed + # but aren't in very old bugzilla's (like 2.1) + # Steve Stock (sstock@iconnect-inc.com) + + $dbh->bz_add_column('bugs', 'target_milestone', + {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); + $dbh->bz_add_column('bugs', 'qa_contact', {TYPE => 'INT3'}); + $dbh->bz_add_column('bugs', 'status_whiteboard', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + if (!$dbh->bz_column_info('products', 'isactive')) { + $dbh->bz_add_column('products', 'disallownew', + {TYPE => 'BOOLEAN', NOTNULL => 1}, 0); + } + + $dbh->bz_add_column('components', 'initialqacontact', {TYPE => 'TINYTEXT'}); + $dbh->bz_add_column('components', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); } sub _add_bug_vote_cache { - my $dbh = Bugzilla->dbh; - # 1999-10-11 Restructured voting database to add a cached value in each - # bug recording how many total votes that bug has. While I'm at it, - # I removed the unused "area" field from the bugs database. It is - # distressing to realize that the bugs table has reached the maximum - # number of indices allowed by MySQL (16), which may make future - # enhancements awkward. - # (P.S. All is not lost; it appears that the latest betas of MySQL - # support a new table format which will allow 32 indices.) - - if ($dbh->bz_column_info('bugs', 'area')) { - $dbh->bz_drop_column('bugs', 'area'); - $dbh->bz_add_column('bugs', 'votes', {TYPE => 'INT3', NOTNULL => 1, - DEFAULT => 0}); - $dbh->bz_add_index('bugs', 'bugs_votes_idx', [qw(votes)]); - $dbh->bz_add_column('products', 'votesperuser', - {TYPE => 'INT2', NOTNULL => 1}, 0); - } + my $dbh = Bugzilla->dbh; + + # 1999-10-11 Restructured voting database to add a cached value in each + # bug recording how many total votes that bug has. While I'm at it, + # I removed the unused "area" field from the bugs database. It is + # distressing to realize that the bugs table has reached the maximum + # number of indices allowed by MySQL (16), which may make future + # enhancements awkward. + # (P.S. All is not lost; it appears that the latest betas of MySQL + # support a new table format which will allow 32 indices.) + + if ($dbh->bz_column_info('bugs', 'area')) { + $dbh->bz_drop_column('bugs', 'area'); + $dbh->bz_add_column('bugs', 'votes', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_index('bugs', 'bugs_votes_idx', [qw(votes)]); + $dbh->bz_add_column('products', 'votesperuser', {TYPE => 'INT2', NOTNULL => 1}, + 0); + } } sub _update_product_name_definition { - my $dbh = Bugzilla->dbh; - # The product name used to be very different in various tables. - # - # It was varchar(16) in bugs - # tinytext in components - # tinytext in products - # tinytext in versions - # - # tinytext is equivalent to varchar(255), which is quite huge, so I change - # them all to varchar(64). - - # Only do this if these fields still exist - they're removed in - # a later change - if ($dbh->bz_column_info('products', 'product')) { - $dbh->bz_alter_column('bugs', 'product', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('components', 'program', {TYPE => 'varchar(64)'}); - $dbh->bz_alter_column('products', 'product', {TYPE => 'varchar(64)'}); - $dbh->bz_alter_column('versions', 'program', - {TYPE => 'varchar(64)', NOTNULL => 1}); - } + my $dbh = Bugzilla->dbh; + + # The product name used to be very different in various tables. + # + # It was varchar(16) in bugs + # tinytext in components + # tinytext in products + # tinytext in versions + # + # tinytext is equivalent to varchar(255), which is quite huge, so I change + # them all to varchar(64). + + # Only do this if these fields still exist - they're removed in + # a later change + if ($dbh->bz_column_info('products', 'product')) { + $dbh->bz_alter_column('bugs', 'product', {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('components', 'program', {TYPE => 'varchar(64)'}); + $dbh->bz_alter_column('products', 'product', {TYPE => 'varchar(64)'}); + $dbh->bz_alter_column('versions', 'program', + {TYPE => 'varchar(64)', NOTNULL => 1}); + } } # A helper for the function below. sub _write_one_longdesc { - my ($id, $who, $when, $buffer) = (@_); - my $dbh = Bugzilla->dbh; - $buffer = trim($buffer); - return if !$buffer; - $dbh->do("INSERT INTO longdescs (bug_id, who, bug_when, thetext) - VALUES (?,?,?,?)", undef, $id, $who, - time2str("%Y/%m/%d %H:%M:%S", $when), $buffer); + my ($id, $who, $when, $buffer) = (@_); + my $dbh = Bugzilla->dbh; + $buffer = trim($buffer); + return if !$buffer; + $dbh->do( + "INSERT INTO longdescs (bug_id, who, bug_when, thetext) + VALUES (?,?,?,?)", undef, $id, $who, + time2str("%Y/%m/%d %H:%M:%S", $when), $buffer + ); } sub _populate_longdescs { - my $dbh = Bugzilla->dbh; - # 2000-01-20 Added a new "longdescs" table, which is supposed to have - # all the long descriptions in it, replacing the old long_desc field - # in the bugs table. The below hideous code populates this new table - # with things from the old field, with ugly parsing and heuristics. - - if ($dbh->bz_column_info('bugs', 'long_desc')) { - my ($total) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs"); - - print "Populating new long_desc table. This is slow. There are", - " $total\nbugs to process; a line of dots will be printed", - " for each 50.\n\n"; - local $| = 1; - - # On MySQL, longdescs doesn't benefit from transactions, but this - # doesn't hurt. - $dbh->bz_start_transaction(); - - $dbh->do('DELETE FROM longdescs'); - - my $sth = $dbh->prepare("SELECT bug_id, creation_ts, reporter, - long_desc FROM bugs ORDER BY bug_id"); - $sth->execute(); - my $count = 0; - while (my ($id, $createtime, $reporterid, $desc) = - $sth->fetchrow_array()) + my $dbh = Bugzilla->dbh; + + # 2000-01-20 Added a new "longdescs" table, which is supposed to have + # all the long descriptions in it, replacing the old long_desc field + # in the bugs table. The below hideous code populates this new table + # with things from the old field, with ugly parsing and heuristics. + + if ($dbh->bz_column_info('bugs', 'long_desc')) { + my ($total) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs"); + + print "Populating new long_desc table. This is slow. There are", + " $total\nbugs to process; a line of dots will be printed", + " for each 50.\n\n"; + local $| = 1; + + # On MySQL, longdescs doesn't benefit from transactions, but this + # doesn't hurt. + $dbh->bz_start_transaction(); + + $dbh->do('DELETE FROM longdescs'); + + my $sth = $dbh->prepare( + "SELECT bug_id, creation_ts, reporter, + long_desc FROM bugs ORDER BY bug_id" + ); + $sth->execute(); + my $count = 0; + while (my ($id, $createtime, $reporterid, $desc) = $sth->fetchrow_array()) { + $count++; + indicate_progress({total => $total, current => $count}); + $desc =~ s/\r//g; + my $who = $reporterid; + my $when = str2time($createtime); + my $buffer = ""; + foreach my $line (split(/\n/, $desc)) { + $line =~ s/\s+$//g; # Trim trailing whitespace. + if ($line =~ /^------- Additional Comments From ([^\s]+)\s+(\d.+\d)\s+-------$/) { - $count++; - indicate_progress({ total => $total, current => $count }); - $desc =~ s/\r//g; - my $who = $reporterid; - my $when = str2time($createtime); - my $buffer = ""; - foreach my $line (split(/\n/, $desc)) { - $line =~ s/\s+$//g; # Trim trailing whitespace. - if ($line =~ /^------- Additional Comments From ([^\s]+)\s+(\d.+\d)\s+-------$/) - { - my $name = $1; - my $date = str2time($2); - # Oy, what a hack. The creation time is accurate to the - # second. But the long text only contains things accurate - # to the And so, if someone makes a comment within a - # minute of the original bug creation, then the comment can - # come *before* the bug creation. So, we add 59 seconds to - # the time of all comments, so that they are always - # considered to have happened at the *end* of the given - # minute, not the beginning. - $date += 59; - if ($date >= $when) { - _write_one_longdesc($id, $who, $when, $buffer); - $buffer = ""; - $when = $date; - my $s2 = $dbh->prepare("SELECT userid FROM profiles " . - "WHERE login_name = ?"); - $s2->execute($name); - ($who) = ($s2->fetchrow_array()); - - if (!$who) { - # This username doesn't exist. Maybe someone - # renamed them or something. Invent a new profile - # entry disabled, just to represent them. - $dbh->do("INSERT INTO profiles (login_name, + my $name = $1; + my $date = str2time($2); + + # Oy, what a hack. The creation time is accurate to the + # second. But the long text only contains things accurate + # to the And so, if someone makes a comment within a + # minute of the original bug creation, then the comment can + # come *before* the bug creation. So, we add 59 seconds to + # the time of all comments, so that they are always + # considered to have happened at the *end* of the given + # minute, not the beginning. + $date += 59; + if ($date >= $when) { + _write_one_longdesc($id, $who, $when, $buffer); + $buffer = ""; + $when = $date; + my $s2 = $dbh->prepare("SELECT userid FROM profiles " . "WHERE login_name = ?"); + $s2->execute($name); + ($who) = ($s2->fetchrow_array()); + + if (!$who) { + + # This username doesn't exist. Maybe someone + # renamed them or something. Invent a new profile + # entry disabled, just to represent them. + $dbh->do( + "INSERT INTO profiles (login_name, cryptpassword, disabledtext) VALUES (?,?,?)", undef, $name, '*', - "Account created only to maintain" - . " database integrity"); - $who = $dbh->bz_last_key('profiles', 'userid'); - } - next; - } - } - $buffer .= $line . "\n"; + "Account created only to maintain" . " database integrity" + ); + $who = $dbh->bz_last_key('profiles', 'userid'); } - _write_one_longdesc($id, $who, $when, $buffer); - } # while loop + next; + } + } + $buffer .= $line . "\n"; + } + _write_one_longdesc($id, $who, $when, $buffer); + } # while loop - print "\n\n"; - $dbh->bz_drop_column('bugs', 'long_desc'); - $dbh->bz_commit_transaction(); - } # main if + print "\n\n"; + $dbh->bz_drop_column('bugs', 'long_desc'); + $dbh->bz_commit_transaction(); + } # main if } sub _update_bugs_activity_field_to_fieldid { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - # 2000-01-18 Added a new table fielddefs that records information about the - # different fields we keep an activity log on. The bugs_activity table - # now has a pointer into that table instead of recording the name directly. - if ($dbh->bz_column_info('bugs_activity', 'field')) { - $dbh->bz_add_column('bugs_activity', 'fieldid', - {TYPE => 'INT3', NOTNULL => 1}, 0); + # 2000-01-18 Added a new table fielddefs that records information about the + # different fields we keep an activity log on. The bugs_activity table + # now has a pointer into that table instead of recording the name directly. + if ($dbh->bz_column_info('bugs_activity', 'field')) { + $dbh->bz_add_column('bugs_activity', 'fieldid', {TYPE => 'INT3', NOTNULL => 1}, + 0); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_fieldid_idx', - [qw(fieldid)]); - print "Populating new bugs_activity.fieldid field...\n"; + $dbh->bz_add_index('bugs_activity', 'bugs_activity_fieldid_idx', [qw(fieldid)]); + print "Populating new bugs_activity.fieldid field...\n"; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my $ids = $dbh->selectall_arrayref( - 'SELECT DISTINCT fielddefs.id, bugs_activity.field + my $ids = $dbh->selectall_arrayref( + 'SELECT DISTINCT fielddefs.id, bugs_activity.field FROM bugs_activity LEFT JOIN fielddefs - ON bugs_activity.field = fielddefs.name', {Slice=>{}}); - - foreach my $item (@$ids) { - my $id = $item->{id}; - my $field = $item->{field}; - # If the id is NULL - if (!$id) { - $dbh->do("INSERT INTO fielddefs (name, description) VALUES " . - "(?, ?)", undef, $field, $field); - $id = $dbh->bz_last_key('fielddefs', 'id'); - } - $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE field = ?", - undef, $id, $field); - } - $dbh->bz_commit_transaction(); + ON bugs_activity.field = fielddefs.name', {Slice => {}} + ); - $dbh->bz_drop_column('bugs_activity', 'field'); + foreach my $item (@$ids) { + my $id = $item->{id}; + my $field = $item->{field}; + + # If the id is NULL + if (!$id) { + $dbh->do("INSERT INTO fielddefs (name, description) VALUES " . "(?, ?)", + undef, $field, $field); + $id = $dbh->bz_last_key('fielddefs', 'id'); + } + $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE field = ?", + undef, $id, $field); } + $dbh->bz_commit_transaction(); + + $dbh->bz_drop_column('bugs_activity', 'field'); + } } sub _add_unique_login_name_index_to_profiles { - my $dbh = Bugzilla->dbh; - - # 2000-01-22 The "login_name" field in the "profiles" table was not - # declared to be unique. Sure enough, somehow, I got 22 duplicated entries - # in my database. This code detects that, cleans up the duplicates, and - # then tweaks the table to declare the field to be unique. What a pain. - if (!$dbh->bz_index_info('profiles', 'profiles_login_name_idx') - || !$dbh->bz_index_info('profiles', 'profiles_login_name_idx')->{TYPE}) - { - print "Searching for duplicate entries in the profiles table...\n"; - while (1) { - # This code is weird in that it loops around and keeps doing this - # select again. That's because I'm paranoid about deleting entries - # out from under us in the profiles table. Things get weird if - # there are *three* or more entries for the same user... - my $sth = $dbh->prepare("SELECT p1.userid, p2.userid, p1.login_name + my $dbh = Bugzilla->dbh; + + # 2000-01-22 The "login_name" field in the "profiles" table was not + # declared to be unique. Sure enough, somehow, I got 22 duplicated entries + # in my database. This code detects that, cleans up the duplicates, and + # then tweaks the table to declare the field to be unique. What a pain. + if ( !$dbh->bz_index_info('profiles', 'profiles_login_name_idx') + || !$dbh->bz_index_info('profiles', 'profiles_login_name_idx')->{TYPE}) + { + print "Searching for duplicate entries in the profiles table...\n"; + while (1) { + + # This code is weird in that it loops around and keeps doing this + # select again. That's because I'm paranoid about deleting entries + # out from under us in the profiles table. Things get weird if + # there are *three* or more entries for the same user... + my $sth = $dbh->prepare( + "SELECT p1.userid, p2.userid, p1.login_name FROM profiles AS p1, profiles AS p2 WHERE p1.userid < p2.userid AND p1.login_name = p2.login_name - ORDER BY p1.login_name"); - $sth->execute(); - my ($u1, $u2, $n) = ($sth->fetchrow_array); - last if !$u1; - - print "Both $u1 & $u2 are ids for $n! Merging $u2 into $u1...\n"; - foreach my $i (["bugs", "reporter"], - ["bugs", "assigned_to"], - ["bugs", "qa_contact"], - ["attachments", "submitter_id"], - ["bugs_activity", "who"], - ["cc", "who"], - ["votes", "who"], - ["longdescs", "who"]) { - my ($table, $field) = (@$i); - if ($dbh->bz_table_info($table)) { - print " Updating $table.$field...\n"; - $dbh->do("UPDATE $table SET $field = $u1 " . - "WHERE $field = $u2"); - } - } - $dbh->do("DELETE FROM profiles WHERE userid = $u2"); + ORDER BY p1.login_name" + ); + $sth->execute(); + my ($u1, $u2, $n) = ($sth->fetchrow_array); + last if !$u1; + + print "Both $u1 & $u2 are ids for $n! Merging $u2 into $u1...\n"; + foreach my $i ( + ["bugs", "reporter"], + ["bugs", "assigned_to"], + ["bugs", "qa_contact"], + ["attachments", "submitter_id"], + ["bugs_activity", "who"], + ["cc", "who"], + ["votes", "who"], + ["longdescs", "who"] + ) + { + my ($table, $field) = (@$i); + if ($dbh->bz_table_info($table)) { + print " Updating $table.$field...\n"; + $dbh->do("UPDATE $table SET $field = $u1 " . "WHERE $field = $u2"); } - print "OK, changing index type to prevent duplicates in the", - " future...\n"; - - $dbh->bz_drop_index('profiles', 'profiles_login_name_idx'); - $dbh->bz_add_index('profiles', 'profiles_login_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(login_name)]}); + } + $dbh->do("DELETE FROM profiles WHERE userid = $u2"); } + print "OK, changing index type to prevent duplicates in the", " future...\n"; + + $dbh->bz_drop_index('profiles', 'profiles_login_name_idx'); + $dbh->bz_add_index('profiles', 'profiles_login_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(login_name)]}); + } } sub _update_component_user_fields_to_ids { - my $dbh = Bugzilla->dbh; - - # components.initialowner - my $comp_init_owner = $dbh->bz_column_info('components', 'initialowner'); - if ($comp_init_owner && $comp_init_owner->{TYPE} eq 'TINYTEXT') { - my $sth = $dbh->prepare("SELECT program, value, initialowner - FROM components"); - $sth->execute(); - while (my ($program, $value, $initialowner) = $sth->fetchrow_array()) { - my ($id) = $dbh->selectrow_array( - "SELECT userid FROM profiles WHERE login_name = ?", - undef, $initialowner); - - unless (defined $id) { - print "Warning: You have an invalid default assignee", - " '$initialowner'\n in component '$value' of program", - " '$program'!\n"; - $id = 0; - } + my $dbh = Bugzilla->dbh; - $dbh->do("UPDATE components SET initialowner = ? - WHERE program = ? AND value = ?", undef, - $id, $program, $value); - } - $dbh->bz_alter_column('components','initialowner',{TYPE => 'INT3'}); + # components.initialowner + my $comp_init_owner = $dbh->bz_column_info('components', 'initialowner'); + if ($comp_init_owner && $comp_init_owner->{TYPE} eq 'TINYTEXT') { + my $sth = $dbh->prepare( + "SELECT program, value, initialowner + FROM components" + ); + $sth->execute(); + while (my ($program, $value, $initialowner) = $sth->fetchrow_array()) { + my ($id) + = $dbh->selectrow_array("SELECT userid FROM profiles WHERE login_name = ?", + undef, $initialowner); + + unless (defined $id) { + print "Warning: You have an invalid default assignee", + " '$initialowner'\n in component '$value' of program", " '$program'!\n"; + $id = 0; + } + + $dbh->do( + "UPDATE components SET initialowner = ? + WHERE program = ? AND value = ?", undef, $id, $program, $value + ); } + $dbh->bz_alter_column('components', 'initialowner', {TYPE => 'INT3'}); + } - # components.initialqacontact - my $comp_init_qa = $dbh->bz_column_info('components', 'initialqacontact'); - if ($comp_init_qa && $comp_init_qa->{TYPE} eq 'TINYTEXT') { - my $sth = $dbh->prepare("SELECT program, value, initialqacontact - FROM components"); - $sth->execute(); - while (my ($program, $value, $initialqacontact) = - $sth->fetchrow_array()) - { - my ($id) = $dbh->selectrow_array( - "SELECT userid FROM profiles WHERE login_name = ?", - undef, $initialqacontact); - - unless (defined $id) { - if ($initialqacontact) { - print "Warning: You have an invalid default QA contact", - " $initialqacontact' in program '$program',", - " component '$value'!\n"; - } - $id = 0; - } - - $dbh->do("UPDATE components SET initialqacontact = ? - WHERE program = ? AND value = ?", undef, - $id, $program, $value); + # components.initialqacontact + my $comp_init_qa = $dbh->bz_column_info('components', 'initialqacontact'); + if ($comp_init_qa && $comp_init_qa->{TYPE} eq 'TINYTEXT') { + my $sth = $dbh->prepare( + "SELECT program, value, initialqacontact + FROM components" + ); + $sth->execute(); + while (my ($program, $value, $initialqacontact) = $sth->fetchrow_array()) { + my ($id) + = $dbh->selectrow_array("SELECT userid FROM profiles WHERE login_name = ?", + undef, $initialqacontact); + + unless (defined $id) { + if ($initialqacontact) { + print "Warning: You have an invalid default QA contact", + " $initialqacontact' in program '$program',", " component '$value'!\n"; } + $id = 0; + } - $dbh->bz_alter_column('components','initialqacontact',{TYPE => 'INT3'}); + $dbh->do( + "UPDATE components SET initialqacontact = ? + WHERE program = ? AND value = ?", undef, $id, $program, $value + ); } + + $dbh->bz_alter_column('components', 'initialqacontact', {TYPE => 'INT3'}); + } } sub _populate_milestones_table { - my $dbh = Bugzilla->dbh; - # 2000-03-21 Adding a table for target milestones to - # database - matthew@zeroknowledge.com - # If the milestones table is empty, and we're still back in a Bugzilla - # that has a bugs.product field, that means that we just created - # the milestones table and it needs to be populated. - my $milestones_exist = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM milestones"); - if (!$milestones_exist && $dbh->bz_column_info('bugs', 'product')) { - print "Replacing blank milestones...\n"; - - $dbh->do("UPDATE bugs + my $dbh = Bugzilla->dbh; + + # 2000-03-21 Adding a table for target milestones to + # database - matthew@zeroknowledge.com + # If the milestones table is empty, and we're still back in a Bugzilla + # that has a bugs.product field, that means that we just created + # the milestones table and it needs to be populated. + my $milestones_exist + = $dbh->selectrow_array("SELECT DISTINCT 1 FROM milestones"); + if (!$milestones_exist && $dbh->bz_column_info('bugs', 'product')) { + print "Replacing blank milestones...\n"; + + $dbh->do( + "UPDATE bugs SET target_milestone = '---' - WHERE target_milestone = ' '"); - - # If we are upgrading from 2.8 or earlier, we will have *created* - # the milestones table with a product_id field, but Bugzilla expects - # it to have a "product" field. So we change the field backward so - # other code can run. The change will be reversed later in checksetup. - if ($dbh->bz_column_info('milestones', 'product_id')) { - # Dropping the column leaves us with a milestones_product_id_idx - # index that is only on the "value" column. We need to drop the - # whole index so that it can be correctly re-created later. - $dbh->bz_drop_index('milestones', 'milestones_product_id_idx'); - $dbh->bz_drop_column('milestones', 'product_id'); - $dbh->bz_add_column('milestones', 'product', - {TYPE => 'varchar(64)', NOTNULL => 1}, ''); - } + WHERE target_milestone = ' '" + ); - # Populate the milestone table with all existing values in the database - my $sth = $dbh->prepare("SELECT DISTINCT target_milestone, product - FROM bugs"); - $sth->execute(); + # If we are upgrading from 2.8 or earlier, we will have *created* + # the milestones table with a product_id field, but Bugzilla expects + # it to have a "product" field. So we change the field backward so + # other code can run. The change will be reversed later in checksetup. + if ($dbh->bz_column_info('milestones', 'product_id')) { + + # Dropping the column leaves us with a milestones_product_id_idx + # index that is only on the "value" column. We need to drop the + # whole index so that it can be correctly re-created later. + $dbh->bz_drop_index('milestones', 'milestones_product_id_idx'); + $dbh->bz_drop_column('milestones', 'product_id'); + $dbh->bz_add_column('milestones', 'product', + {TYPE => 'varchar(64)', NOTNULL => 1}, ''); + } - print "Populating milestones table...\n"; + # Populate the milestone table with all existing values in the database + my $sth = $dbh->prepare( + "SELECT DISTINCT target_milestone, product + FROM bugs" + ); + $sth->execute(); - while (my ($value, $product) = $sth->fetchrow_array()) { - # check if the value already exists - my $sortkey = substr($value, 1); - if ($sortkey !~ /^\d+$/) { - $sortkey = 0; - } else { - $sortkey *= 10; - } - my $ms_exists = $dbh->selectrow_array( - "SELECT value FROM milestones - WHERE value = ? AND product = ?", undef, $value, $product); + print "Populating milestones table...\n"; - if (!$ms_exists) { - $dbh->do("INSERT INTO milestones(value, product, sortkey) - VALUES (?,?,?)", undef, $value, $product, $sortkey); - } - } + while (my ($value, $product) = $sth->fetchrow_array()) { + + # check if the value already exists + my $sortkey = substr($value, 1); + if ($sortkey !~ /^\d+$/) { + $sortkey = 0; + } + else { + $sortkey *= 10; + } + my $ms_exists = $dbh->selectrow_array( + "SELECT value FROM milestones + WHERE value = ? AND product = ?", undef, $value, $product + ); + + if (!$ms_exists) { + $dbh->do( + "INSERT INTO milestones(value, product, sortkey) + VALUES (?,?,?)", undef, $value, $product, $sortkey + ); + } } + } } sub _add_products_defaultmilestone { - my $dbh = Bugzilla->dbh; - - # 2000-03-23 Added a defaultmilestone field to the products table, so that - # we know which milestone to initially assign bugs to. - if (!$dbh->bz_column_info('products', 'defaultmilestone')) { - $dbh->bz_add_column('products', 'defaultmilestone', - {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); - my $sth = $dbh->prepare( - "SELECT product, defaultmilestone FROM products"); - $sth->execute(); - while (my ($product, $default_ms) = $sth->fetchrow_array()) { - my $exists = $dbh->selectrow_array( - "SELECT value FROM milestones - WHERE value = ? AND product = ?", - undef, $default_ms, $product); - if (!$exists) { - $dbh->do("INSERT INTO milestones(value, product) " . - "VALUES (?, ?)", undef, $default_ms, $product); - } - } + my $dbh = Bugzilla->dbh; + + # 2000-03-23 Added a defaultmilestone field to the products table, so that + # we know which milestone to initially assign bugs to. + if (!$dbh->bz_column_info('products', 'defaultmilestone')) { + $dbh->bz_add_column('products', 'defaultmilestone', + {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); + my $sth = $dbh->prepare("SELECT product, defaultmilestone FROM products"); + $sth->execute(); + while (my ($product, $default_ms) = $sth->fetchrow_array()) { + my $exists = $dbh->selectrow_array( + "SELECT value FROM milestones + WHERE value = ? AND product = ?", undef, $default_ms, $product + ); + if (!$exists) { + $dbh->do("INSERT INTO milestones(value, product) " . "VALUES (?, ?)", + undef, $default_ms, $product); + } } + } } sub _copy_from_comments_to_longdescs { - my $dbh = Bugzilla->dbh; - # 2000-11-27 For Bugzilla 2.5 and later. Copy data from 'comments' to - # 'longdescs' - the new name of the comments table. - if ($dbh->bz_table_info('comments')) { - print "Copying data from 'comments' to 'longdescs'...\n"; - my $quoted_when = $dbh->quote_identifier('when'); - $dbh->do("INSERT INTO longdescs (bug_when, bug_id, who, thetext) + my $dbh = Bugzilla->dbh; + + # 2000-11-27 For Bugzilla 2.5 and later. Copy data from 'comments' to + # 'longdescs' - the new name of the comments table. + if ($dbh->bz_table_info('comments')) { + print "Copying data from 'comments' to 'longdescs'...\n"; + my $quoted_when = $dbh->quote_identifier('when'); + $dbh->do( + "INSERT INTO longdescs (bug_when, bug_id, who, thetext) SELECT $quoted_when, bug_id, who, comment - FROM comments"); - $dbh->bz_drop_table("comments"); - } + FROM comments" + ); + $dbh->bz_drop_table("comments"); + } } sub _populate_duplicates_table { - my $dbh = Bugzilla->dbh; - # 2000-07-15 Added duplicates table so Bugzilla tracks duplicates in a - # better way than it used to. This code searches the comments to populate - # the table initially. It's executed if the table is empty; if it's - # empty because there are no dupes (as opposed to having just created - # the table) it won't have any effect anyway, so it doesn't matter. - my ($dups_exist) = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM duplicates"); - # We also check against a schema change that happened later. - if (!$dups_exist && !$dbh->bz_column_info('groups', 'isactive')) { - # populate table - print "Populating duplicates table from comments...\n"; - - my $sth = $dbh->prepare( - "SELECT longdescs.bug_id, thetext + my $dbh = Bugzilla->dbh; + + # 2000-07-15 Added duplicates table so Bugzilla tracks duplicates in a + # better way than it used to. This code searches the comments to populate + # the table initially. It's executed if the table is empty; if it's + # empty because there are no dupes (as opposed to having just created + # the table) it won't have any effect anyway, so it doesn't matter. + my ($dups_exist) = $dbh->selectrow_array("SELECT DISTINCT 1 FROM duplicates"); + + # We also check against a schema change that happened later. + if (!$dups_exist && !$dbh->bz_column_info('groups', 'isactive')) { + + # populate table + print "Populating duplicates table from comments...\n"; + + my $sth = $dbh->prepare( + "SELECT longdescs.bug_id, thetext FROM longdescs LEFT JOIN bugs ON longdescs.bug_id = bugs.bug_id - WHERE (" . $dbh->sql_regexp("thetext", - "'[.*.]{3} This bug has been marked as a duplicate" - . " of [[:digit:]]+ [.*.]{3}'") - . ") + WHERE (" + . $dbh->sql_regexp("thetext", + "'[.*.]{3} This bug has been marked as a duplicate" + . " of [[:digit:]]+ [.*.]{3}'") + . ") AND resolution = 'DUPLICATE' - ORDER BY longdescs.bug_when"); - $sth->execute(); - - my (%dupes, $key); - # Because of the way hashes work, this loop removes all but the - # last dupe resolution found for a given bug. - while (my ($dupe, $dupe_of) = $sth->fetchrow_array()) { - $dupes{$dupe} = $dupe_of; - } + ORDER BY longdescs.bug_when" + ); + $sth->execute(); - foreach $key (keys(%dupes)){ - $dupes{$key} =~ /^.*\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*$/ms; - $dupes{$key} = $1; - $dbh->do("INSERT INTO duplicates VALUES(?, ?)", undef, - $dupes{$key}, $key); - # BugItsADupeOf Dupe - } + my (%dupes, $key); + + # Because of the way hashes work, this loop removes all but the + # last dupe resolution found for a given bug. + while (my ($dupe, $dupe_of) = $sth->fetchrow_array()) { + $dupes{$dupe} = $dupe_of; } + + foreach $key (keys(%dupes)) { + $dupes{$key} + =~ /^.*\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*$/ms; + $dupes{$key} = $1; + $dbh->do("INSERT INTO duplicates VALUES(?, ?)", undef, $dupes{$key}, $key); + + # BugItsADupeOf Dupe + } + } } sub _recrypt_plaintext_passwords { - my $dbh = Bugzilla->dbh; - # 2001-06-12; myk@mozilla.org; bugs 74032, 77473: - # Recrypt passwords using Perl &crypt instead of the mysql equivalent - # and delete plaintext passwords from the database. - if ($dbh->bz_column_info('profiles', 'password')) { + my $dbh = Bugzilla->dbh; + + # 2001-06-12; myk@mozilla.org; bugs 74032, 77473: + # Recrypt passwords using Perl &crypt instead of the mysql equivalent + # and delete plaintext passwords from the database. + if ($dbh->bz_column_info('profiles', 'password')) { - print <<ENDTEXT; + print <<ENDTEXT; Your current installation of Bugzilla stores passwords in plaintext in the database and uses mysql's encrypt function instead of Perl's crypt function to crypt passwords. Passwords are now going to be @@ -1216,299 +1250,315 @@ deleted from the database. This could take a while if your installation has many users. ENDTEXT - # Re-crypt everyone's password. - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); - my $sth = $dbh->prepare("SELECT userid, password FROM profiles"); - $sth->execute(); - - my $i = 1; + # Re-crypt everyone's password. + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); + my $sth = $dbh->prepare("SELECT userid, password FROM profiles"); + $sth->execute(); - print "Fixing passwords...\n"; - while (my ($userid, $password) = $sth->fetchrow_array()) { - my $cryptpassword = $dbh->quote(bz_crypt($password)); - $dbh->do("UPDATE profiles " . - "SET cryptpassword = $cryptpassword " . - "WHERE userid = $userid"); - indicate_progress({ total => $total, current => $i, every => 10 }); - } - print "\n"; + my $i = 1; - # Drop the plaintext password field. - $dbh->bz_drop_column('profiles', 'password'); + print "Fixing passwords...\n"; + while (my ($userid, $password) = $sth->fetchrow_array()) { + my $cryptpassword = $dbh->quote(bz_crypt($password)); + $dbh->do("UPDATE profiles " + . "SET cryptpassword = $cryptpassword " + . "WHERE userid = $userid"); + indicate_progress({total => $total, current => $i, every => 10}); } + print "\n"; + + # Drop the plaintext password field. + $dbh->bz_drop_column('profiles', 'password'); + } } sub _update_bugs_activity_to_only_record_changes { - my $dbh = Bugzilla->dbh; - # 2001-07-20 jake@bugzilla.org - Change bugs_activity to only record changes - # http://bugzilla.mozilla.org/show_bug.cgi?id=55161 - if ($dbh->bz_column_info('bugs_activity', 'oldvalue')) { - $dbh->bz_add_column("bugs_activity", "removed", {TYPE => "TINYTEXT"}); - $dbh->bz_add_column("bugs_activity", "added", {TYPE => "TINYTEXT"}); - - # Need to get field id's for the fields that have multiple values - my @multi; - foreach my $f ("cc", "dependson", "blocked", "keywords") { - my $sth = $dbh->prepare("SELECT id " . - "FROM fielddefs " . - "WHERE name = '$f'"); - $sth->execute(); - my ($fid) = $sth->fetchrow_array(); - push (@multi, $fid); + my $dbh = Bugzilla->dbh; + + # 2001-07-20 jake@bugzilla.org - Change bugs_activity to only record changes + # http://bugzilla.mozilla.org/show_bug.cgi?id=55161 + if ($dbh->bz_column_info('bugs_activity', 'oldvalue')) { + $dbh->bz_add_column("bugs_activity", "removed", {TYPE => "TINYTEXT"}); + $dbh->bz_add_column("bugs_activity", "added", {TYPE => "TINYTEXT"}); + + # Need to get field id's for the fields that have multiple values + my @multi; + foreach my $f ("cc", "dependson", "blocked", "keywords") { + my $sth = $dbh->prepare("SELECT id " . "FROM fielddefs " . "WHERE name = '$f'"); + $sth->execute(); + my ($fid) = $sth->fetchrow_array(); + push(@multi, $fid); + } + + # Now we need to process the bugs_activity table and reformat the data + print "Fixing activity log...\n"; + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs_activity'); + my $sth = $dbh->prepare( + "SELECT bug_id, who, bug_when, fieldid, + oldvalue, newvalue FROM bugs_activity" + ); + $sth->execute; + my $i = 0; + while (my ($bug_id, $who, $bug_when, $fieldid, $oldvalue, $newvalue) + = $sth->fetchrow_array()) + { + $i++; + indicate_progress({total => $total, current => $i, every => 10}); + + # Make sure (old|new)value isn't null (to suppress warnings) + $oldvalue ||= ""; + $newvalue ||= ""; + my ($added, $removed) = ""; + if (grep ($_ eq $fieldid, @multi)) { + $oldvalue =~ s/[\s,]+/ /g; + $newvalue =~ s/[\s,]+/ /g; + my @old = split(" ", $oldvalue); + my @new = split(" ", $newvalue); + my (@add, @remove) = (); + + # Find values that were "added" + foreach my $value (@new) { + if (!grep ($_ eq $value, @old)) { + push(@add, $value); + } } - # Now we need to process the bugs_activity table and reformat the data - print "Fixing activity log...\n"; - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs_activity'); - my $sth = $dbh->prepare("SELECT bug_id, who, bug_when, fieldid, - oldvalue, newvalue FROM bugs_activity"); - $sth->execute; - my $i = 0; - while (my ($bug_id, $who, $bug_when, $fieldid, $oldvalue, $newvalue) - = $sth->fetchrow_array()) - { - $i++; - indicate_progress({ total => $total, current => $i, every => 10 }); - # Make sure (old|new)value isn't null (to suppress warnings) - $oldvalue ||= ""; - $newvalue ||= ""; - my ($added, $removed) = ""; - if (grep ($_ eq $fieldid, @multi)) { - $oldvalue =~ s/[\s,]+/ /g; - $newvalue =~ s/[\s,]+/ /g; - my @old = split(" ", $oldvalue); - my @new = split(" ", $newvalue); - my (@add, @remove) = (); - # Find values that were "added" - foreach my $value(@new) { - if (! grep ($_ eq $value, @old)) { - push (@add, $value); - } - } - # Find values that were removed - foreach my $value(@old) { - if (! grep ($_ eq $value, @new)) { - push (@remove, $value); - } - } - $added = join (", ", @add); - $removed = join (", ", @remove); - # If we can't determine what changed, put a ? in both fields - unless ($added || $removed) { - $added = "?"; - $removed = "?"; - } - # If the original field (old|new)value was full, then this - # could be incomplete data. - if (length($oldvalue) == 255 || length($newvalue) == 255) { - $added = "? $added"; - $removed = "? $removed"; - } - } else { - $removed = $oldvalue; - $added = $newvalue; - } - $added = $dbh->quote($added); - $removed = $dbh->quote($removed); - $dbh->do("UPDATE bugs_activity + # Find values that were removed + foreach my $value (@old) { + if (!grep ($_ eq $value, @new)) { + push(@remove, $value); + } + } + $added = join(", ", @add); + $removed = join(", ", @remove); + + # If we can't determine what changed, put a ? in both fields + unless ($added || $removed) { + $added = "?"; + $removed = "?"; + } + + # If the original field (old|new)value was full, then this + # could be incomplete data. + if (length($oldvalue) == 255 || length($newvalue) == 255) { + $added = "? $added"; + $removed = "? $removed"; + } + } + else { + $removed = $oldvalue; + $added = $newvalue; + } + $added = $dbh->quote($added); + $removed = $dbh->quote($removed); + $dbh->do( + "UPDATE bugs_activity SET removed = $removed, added = $added WHERE bug_id = $bug_id AND who = $who AND bug_when = '$bug_when' - AND fieldid = $fieldid"); - } - print "\n"; - $dbh->bz_drop_column("bugs_activity", "oldvalue"); - $dbh->bz_drop_column("bugs_activity", "newvalue"); + AND fieldid = $fieldid" + ); } + print "\n"; + $dbh->bz_drop_column("bugs_activity", "oldvalue"); + $dbh->bz_drop_column("bugs_activity", "newvalue"); + } } sub _delete_logincookies_cryptpassword_and_handle_invalid_cookies { - my $dbh = Bugzilla->dbh; - # 2002-02-04 bbaetz@student.usyd.edu.au bug 95732 - # Remove logincookies.cryptpassword, and delete entries which become - # invalid - if ($dbh->bz_column_info("logincookies", "cryptpassword")) { - # We need to delete any cookies which are invalid before dropping the - # column - print "Removing invalid login cookies...\n"; - - # mysql doesn't support DELETE with multi-table queries, so we have - # to iterate - my $sth = $dbh->prepare("SELECT cookie FROM logincookies, profiles " . - "WHERE logincookies.cryptpassword != " . - "profiles.cryptpassword AND " . - "logincookies.userid = profiles.userid"); - $sth->execute(); - while (my ($cookie) = $sth->fetchrow_array()) { - $dbh->do("DELETE FROM logincookies WHERE cookie = $cookie"); - } - - $dbh->bz_drop_column("logincookies", "cryptpassword"); + my $dbh = Bugzilla->dbh; + + # 2002-02-04 bbaetz@student.usyd.edu.au bug 95732 + # Remove logincookies.cryptpassword, and delete entries which become + # invalid + if ($dbh->bz_column_info("logincookies", "cryptpassword")) { + + # We need to delete any cookies which are invalid before dropping the + # column + print "Removing invalid login cookies...\n"; + + # mysql doesn't support DELETE with multi-table queries, so we have + # to iterate + my $sth + = $dbh->prepare("SELECT cookie FROM logincookies, profiles " + . "WHERE logincookies.cryptpassword != " + . "profiles.cryptpassword AND " + . "logincookies.userid = profiles.userid"); + $sth->execute(); + while (my ($cookie) = $sth->fetchrow_array()) { + $dbh->do("DELETE FROM logincookies WHERE cookie = $cookie"); } + + $dbh->bz_drop_column("logincookies", "cryptpassword"); + } } sub _use_ip_instead_of_hostname_in_logincookies { - my $dbh = Bugzilla->dbh; - - # 2002-03-15 bbaetz@student.usyd.edu.au - bug 129466 - # 2002-05-13 preed@sigkill.com - bug 129446 patch backported to the - # BUGZILLA-2_14_1-BRANCH as a security blocker for the 2.14.2 release - # - # Use the ip, not the hostname, in the logincookies table - if ($dbh->bz_column_info("logincookies", "hostname")) { - print "Clearing the logincookies table...\n"; - # We've changed what we match against, so all entries are now invalid - $dbh->do("DELETE FROM logincookies"); - - # Now update the logincookies schema - $dbh->bz_drop_column("logincookies", "hostname"); - $dbh->bz_add_column("logincookies", "ipaddr", - {TYPE => 'varchar(40)'}); - } + my $dbh = Bugzilla->dbh; + + # 2002-03-15 bbaetz@student.usyd.edu.au - bug 129466 + # 2002-05-13 preed@sigkill.com - bug 129446 patch backported to the + # BUGZILLA-2_14_1-BRANCH as a security blocker for the 2.14.2 release + # + # Use the ip, not the hostname, in the logincookies table + if ($dbh->bz_column_info("logincookies", "hostname")) { + print "Clearing the logincookies table...\n"; + + # We've changed what we match against, so all entries are now invalid + $dbh->do("DELETE FROM logincookies"); + + # Now update the logincookies schema + $dbh->bz_drop_column("logincookies", "hostname"); + $dbh->bz_add_column("logincookies", "ipaddr", {TYPE => 'varchar(40)'}); + } } sub _move_quips_into_db { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations->{'datadir'}; - # 2002-07-15 davef@tetsubo.com - bug 67950 - # Move quips to the db. - if (-e "$datadir/comments") { - print "Populating quips table from $datadir/comments...\n"; - my $comments = new IO::File("$datadir/comments", 'r') - || die "$datadir/comments: $!"; - $comments->untaint; - while (<$comments>) { - chomp; - $dbh->do("INSERT INTO quips (quip) VALUES (?)", undef, $_); - } - - print "\n", install_string('update_quips', { data => $datadir }), "\n"; - $comments->close; - rename("$datadir/comments", "$datadir/comments.bak") - || warn "Failed to rename: $!"; + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations->{'datadir'}; + + # 2002-07-15 davef@tetsubo.com - bug 67950 + # Move quips to the db. + if (-e "$datadir/comments") { + print "Populating quips table from $datadir/comments...\n"; + my $comments = new IO::File("$datadir/comments", 'r') + || die "$datadir/comments: $!"; + $comments->untaint; + while (<$comments>) { + chomp; + $dbh->do("INSERT INTO quips (quip) VALUES (?)", undef, $_); } + + print "\n", install_string('update_quips', {data => $datadir}), "\n"; + $comments->close; + rename("$datadir/comments", "$datadir/comments.bak") + || warn "Failed to rename: $!"; + } } sub _use_ids_for_products_and_components { - my $dbh = Bugzilla->dbh; - # 2002-08-12 jake@bugzilla.org/bbaetz@student.usyd.edu.au - bug 43600 - # Use integer IDs for products and components. - if ($dbh->bz_column_info("products", "product")) { - print "Updating database to use product IDs.\n"; - - # First, we need to remove possible NULL entries - # NULLs may exist, but won't have been used, since all the uses of them - # are in NOT NULL fields in other tables - $dbh->do("DELETE FROM products WHERE product IS NULL"); - $dbh->do("DELETE FROM components WHERE value IS NULL"); - - $dbh->bz_add_column("products", "id", - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column("components", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("versions", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("milestones", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("bugs", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - - # The attachstatusdefs table was added in version 2.15, but - # removed again in early 2.17. If it exists now, we still need - # to perform this change with product_id because the code later on - # which converts the attachment statuses to flags depends on it. - # But we need to avoid this if the user is upgrading from 2.14 - # or earlier (because it won't be there to convert). - if ($dbh->bz_table_info("attachstatusdefs")) { - $dbh->bz_add_column("attachstatusdefs", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - } - - my %products; - my $sth = $dbh->prepare("SELECT id, product FROM products"); - $sth->execute; - while (my ($product_id, $product) = $sth->fetchrow_array()) { - if (exists $products{$product}) { - print "Ignoring duplicate product $product\n"; - $dbh->do("DELETE FROM products WHERE id = $product_id"); - next; - } - $products{$product} = 1; - $dbh->do("UPDATE components SET product_id = $product_id " . - "WHERE program = " . $dbh->quote($product)); - $dbh->do("UPDATE versions SET product_id = $product_id " . - "WHERE program = " . $dbh->quote($product)); - $dbh->do("UPDATE milestones SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)); - $dbh->do("UPDATE bugs SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)); - $dbh->do("UPDATE attachstatusdefs SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)) - if $dbh->bz_table_info("attachstatusdefs"); - } - - print "Updating the database to use component IDs.\n"; + my $dbh = Bugzilla->dbh; + + # 2002-08-12 jake@bugzilla.org/bbaetz@student.usyd.edu.au - bug 43600 + # Use integer IDs for products and components. + if ($dbh->bz_column_info("products", "product")) { + print "Updating database to use product IDs.\n"; + + # First, we need to remove possible NULL entries + # NULLs may exist, but won't have been used, since all the uses of them + # are in NOT NULL fields in other tables + $dbh->do("DELETE FROM products WHERE product IS NULL"); + $dbh->do("DELETE FROM components WHERE value IS NULL"); + + $dbh->bz_add_column("products", "id", + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column("components", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("versions", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("milestones", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("bugs", "product_id", {TYPE => 'INT2', NOTNULL => 1}, 0); + + # The attachstatusdefs table was added in version 2.15, but + # removed again in early 2.17. If it exists now, we still need + # to perform this change with product_id because the code later on + # which converts the attachment statuses to flags depends on it. + # But we need to avoid this if the user is upgrading from 2.14 + # or earlier (because it won't be there to convert). + if ($dbh->bz_table_info("attachstatusdefs")) { + $dbh->bz_add_column("attachstatusdefs", "product_id", + {TYPE => 'INT2', NOTNULL => 1}, 0); + } - $dbh->bz_add_column("components", "id", - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column("bugs", "component_id", - {TYPE => 'INT3', NOTNULL => 1}, 0); + my %products; + my $sth = $dbh->prepare("SELECT id, product FROM products"); + $sth->execute; + while (my ($product_id, $product) = $sth->fetchrow_array()) { + if (exists $products{$product}) { + print "Ignoring duplicate product $product\n"; + $dbh->do("DELETE FROM products WHERE id = $product_id"); + next; + } + $products{$product} = 1; + $dbh->do("UPDATE components SET product_id = $product_id " + . "WHERE program = " + . $dbh->quote($product)); + $dbh->do("UPDATE versions SET product_id = $product_id " + . "WHERE program = " + . $dbh->quote($product)); + $dbh->do("UPDATE milestones SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)); + $dbh->do("UPDATE bugs SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)); + $dbh->do("UPDATE attachstatusdefs SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)) + if $dbh->bz_table_info("attachstatusdefs"); + } - my %components; - $sth = $dbh->prepare("SELECT id, value, product_id FROM components"); - $sth->execute; - while (my ($component_id, $component, $product_id) - = $sth->fetchrow_array()) - { - if (exists $components{$component}) { - if (exists $components{$component}{$product_id}) { - print "Ignoring duplicate component $component for", - " product $product_id\n"; - $dbh->do("DELETE FROM components WHERE id = $component_id"); - next; - } - } else { - $components{$component} = {}; - } - $components{$component}{$product_id} = 1; - $dbh->do("UPDATE bugs SET component_id = $component_id " . - "WHERE component = " . $dbh->quote($component) . - " AND product_id = $product_id"); + print "Updating the database to use component IDs.\n"; + + $dbh->bz_add_column("components", "id", + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column("bugs", "component_id", {TYPE => 'INT3', NOTNULL => 1}, 0); + + my %components; + $sth = $dbh->prepare("SELECT id, value, product_id FROM components"); + $sth->execute; + while (my ($component_id, $component, $product_id) = $sth->fetchrow_array()) { + if (exists $components{$component}) { + if (exists $components{$component}{$product_id}) { + print "Ignoring duplicate component $component for", " product $product_id\n"; + $dbh->do("DELETE FROM components WHERE id = $component_id"); + next; } - print "Fixing Indexes and Uniqueness.\n"; - $dbh->bz_drop_index('milestones', 'milestones_product_idx'); - - $dbh->bz_add_index('milestones', 'milestones_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); - - $dbh->bz_drop_index('bugs', 'bugs_product_idx'); - $dbh->bz_add_index('bugs', 'bugs_product_id_idx', [qw(product_id)]); - $dbh->bz_drop_index('bugs', 'bugs_component_idx'); - $dbh->bz_add_index('bugs', 'bugs_component_id_idx', [qw(component_id)]); - - print "Removing, renaming, and retyping old product and", - " component fields.\n"; - $dbh->bz_drop_column("components", "program"); - $dbh->bz_drop_column("versions", "program"); - $dbh->bz_drop_column("milestones", "product"); - $dbh->bz_drop_column("bugs", "product"); - $dbh->bz_drop_column("bugs", "component"); - $dbh->bz_drop_column("attachstatusdefs", "product") - if $dbh->bz_table_info("attachstatusdefs"); - $dbh->bz_rename_column("products", "product", "name"); - $dbh->bz_alter_column("products", "name", - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_rename_column("components", "value", "name"); - $dbh->bz_alter_column("components", "name", - {TYPE => 'varchar(64)', NOTNULL => 1}); - - print "Adding indexes for products and components tables.\n"; - $dbh->bz_add_index('products', 'products_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); - $dbh->bz_add_index('components', 'components_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id name)]}); - $dbh->bz_add_index('components', 'components_name_idx', [qw(name)]); + } + else { + $components{$component} = {}; + } + $components{$component}{$product_id} = 1; + $dbh->do("UPDATE bugs SET component_id = $component_id " + . "WHERE component = " + . $dbh->quote($component) + . " AND product_id = $product_id"); } + print "Fixing Indexes and Uniqueness.\n"; + $dbh->bz_drop_index('milestones', 'milestones_product_idx'); + + $dbh->bz_add_index('milestones', 'milestones_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + + $dbh->bz_drop_index('bugs', 'bugs_product_idx'); + $dbh->bz_add_index('bugs', 'bugs_product_id_idx', [qw(product_id)]); + $dbh->bz_drop_index('bugs', 'bugs_component_idx'); + $dbh->bz_add_index('bugs', 'bugs_component_id_idx', [qw(component_id)]); + + print "Removing, renaming, and retyping old product and", + " component fields.\n"; + $dbh->bz_drop_column("components", "program"); + $dbh->bz_drop_column("versions", "program"); + $dbh->bz_drop_column("milestones", "product"); + $dbh->bz_drop_column("bugs", "product"); + $dbh->bz_drop_column("bugs", "component"); + $dbh->bz_drop_column("attachstatusdefs", "product") + if $dbh->bz_table_info("attachstatusdefs"); + $dbh->bz_rename_column("products", "product", "name"); + $dbh->bz_alter_column("products", "name", + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_rename_column("components", "value", "name"); + $dbh->bz_alter_column("components", "name", + {TYPE => 'varchar(64)', NOTNULL => 1}); + + print "Adding indexes for products and components tables.\n"; + $dbh->bz_add_index('products', 'products_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); + $dbh->bz_add_index('components', 'components_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id name)]}); + $dbh->bz_add_index('components', 'components_name_idx', [qw(name)]); + } } # Helper for the below function. @@ -1519,1211 +1569,1341 @@ sub _use_ids_for_products_and_components { # group names, _list_bits is used to fill in a list of references # to groupset bits for groups that no longer exist. sub _list_bits { - my ($num) = @_; - my $dbh = Bugzilla->dbh; - my @res; - my $curr = 1; - while (1) { - # Convert a big integer to a list of bits - my $sth = $dbh->prepare("SELECT ($num & ~$curr) > 0, + my ($num) = @_; + my $dbh = Bugzilla->dbh; + my @res; + my $curr = 1; + while (1) { + + # Convert a big integer to a list of bits + my $sth = $dbh->prepare( + "SELECT ($num & ~$curr) > 0, ($num & $curr), ($num & ~$curr), - $curr << 1"); - $sth->execute; - my ($more, $thisbit, $remain, $nval) = $sth->fetchrow_array; - push @res,"UNKNOWN<$curr>" if ($thisbit); - $curr = $nval; - $num = $remain; - last if !$more; - } - return @res; + $curr << 1" + ); + $sth->execute; + my ($more, $thisbit, $remain, $nval) = $sth->fetchrow_array; + push @res, "UNKNOWN<$curr>" if ($thisbit); + $curr = $nval; + $num = $remain; + last if !$more; + } + return @res; } sub _convert_groups_system_from_groupset { - my $dbh = Bugzilla->dbh; - # 2002-09-22 - bugreport@peshkin.net - bug 157756 - # - # If the whole groups system is new, but the installation isn't, - # convert all the old groupset groups, etc... - # - # This requires: - # 1) define groups ids in group table - # 2) populate user_group_map with grants from old groupsets - # and blessgroupsets - # 3) populate bug_group_map with data converted from old bug groupsets - # 4) convert activity logs to use group names instead of numbers - # 5) identify the admin from the old all-ones groupset - - # The groups system needs to be converted if groupset exists - if ($dbh->bz_column_info("profiles", "groupset")) { - # Some mysql versions will promote any unique key to primary key - # so all unique keys are removed first and then added back in - $dbh->bz_drop_index('groups', 'groups_bit_idx'); - $dbh->bz_drop_index('groups', 'groups_name_idx'); - my @primary_key = $dbh->primary_key(undef, undef, 'groups'); - if (@primary_key) { - $dbh->do("ALTER TABLE groups DROP PRIMARY KEY"); - } + my $dbh = Bugzilla->dbh; + + # 2002-09-22 - bugreport@peshkin.net - bug 157756 + # + # If the whole groups system is new, but the installation isn't, + # convert all the old groupset groups, etc... + # + # This requires: + # 1) define groups ids in group table + # 2) populate user_group_map with grants from old groupsets + # and blessgroupsets + # 3) populate bug_group_map with data converted from old bug groupsets + # 4) convert activity logs to use group names instead of numbers + # 5) identify the admin from the old all-ones groupset + + # The groups system needs to be converted if groupset exists + if ($dbh->bz_column_info("profiles", "groupset")) { + + # Some mysql versions will promote any unique key to primary key + # so all unique keys are removed first and then added back in + $dbh->bz_drop_index('groups', 'groups_bit_idx'); + $dbh->bz_drop_index('groups', 'groups_name_idx'); + my @primary_key = $dbh->primary_key(undef, undef, 'groups'); + if (@primary_key) { + $dbh->do("ALTER TABLE groups DROP PRIMARY KEY"); + } + + $dbh->bz_add_column('groups', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column('groups', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - $dbh->bz_add_index('groups', 'groups_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); - - # Convert all existing groupset records to map entries before removing - # groupset fields or removing "bit" from groups. - my $sth = $dbh->prepare("SELECT bit, id FROM groups WHERE bit > 0"); - $sth->execute(); - while (my ($bit, $gid) = $sth->fetchrow_array) { - # Create user_group_map membership grants for old groupsets. - # Get each user with the old groupset bit set - my $sth2 = $dbh->prepare("SELECT userid FROM profiles - WHERE (groupset & $bit) != 0"); - $sth2->execute(); - while (my ($uid) = $sth2->fetchrow_array) { - # Check to see if the user is already a member of the group - # and, if not, insert a new record. - my $query = "SELECT user_id FROM user_group_map + $dbh->bz_add_index('groups', 'groups_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); + + # Convert all existing groupset records to map entries before removing + # groupset fields or removing "bit" from groups. + my $sth = $dbh->prepare("SELECT bit, id FROM groups WHERE bit > 0"); + $sth->execute(); + while (my ($bit, $gid) = $sth->fetchrow_array) { + + # Create user_group_map membership grants for old groupsets. + # Get each user with the old groupset bit set + my $sth2 = $dbh->prepare( + "SELECT userid FROM profiles + WHERE (groupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($uid) = $sth2->fetchrow_array) { + + # Check to see if the user is already a member of the group + # and, if not, insert a new record. + my $query = "SELECT user_id FROM user_group_map WHERE group_id = $gid AND user_id = $uid AND isbless = 0"; - my $sth3 = $dbh->prepare($query); - $sth3->execute(); - if ( !$sth3->fetchrow_array() ) { - $dbh->do("INSERT INTO user_group_map + my $sth3 = $dbh->prepare($query); + $sth3->execute(); + if (!$sth3->fetchrow_array()) { + $dbh->do( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES ($uid, $gid, 0, " . GRANT_DIRECT . ")"); - } - } - # Create user can bless group grants for old groupsets, but only - # if we're upgrading from a Bugzilla that had blessing. - if($dbh->bz_column_info('profiles', 'blessgroupset')) { - # Get each user with the old blessgroupset bit set - $sth2 = $dbh->prepare("SELECT userid FROM profiles - WHERE (blessgroupset & $bit) != 0"); - $sth2->execute(); - while (my ($uid) = $sth2->fetchrow_array) { - $dbh->do("INSERT INTO user_group_map + VALUES ($uid, $gid, 0, " . GRANT_DIRECT . ")" + ); + } + } + + # Create user can bless group grants for old groupsets, but only + # if we're upgrading from a Bugzilla that had blessing. + if ($dbh->bz_column_info('profiles', 'blessgroupset')) { + + # Get each user with the old blessgroupset bit set + $sth2 = $dbh->prepare( + "SELECT userid FROM profiles + WHERE (blessgroupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($uid) = $sth2->fetchrow_array) { + $dbh->do( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES ($uid, $gid, 1, " . GRANT_DIRECT . ")"); - } - } - # Create bug_group_map records for old groupsets. - # Get each bug with the old group bit set. - $sth2 = $dbh->prepare("SELECT bug_id FROM bugs - WHERE (groupset & $bit) != 0"); - $sth2->execute(); - while (my ($bug_id) = $sth2->fetchrow_array) { - # Insert the bug, group pair into the bug_group_map. - $dbh->do("INSERT INTO bug_group_map (bug_id, group_id) - VALUES ($bug_id, $gid)"); - } + VALUES ($uid, $gid, 1, " . GRANT_DIRECT . ")" + ); } - # Replace old activity log groupset records with lists of names - # of groups. - $sth = $dbh->prepare("SELECT id FROM fielddefs - WHERE name = " . $dbh->quote('bug_group')); - $sth->execute(); - my ($bgfid) = $sth->fetchrow_array; - # Get the field id for the old groupset field - $sth = $dbh->prepare("SELECT id FROM fielddefs - WHERE name = " . $dbh->quote('groupset')); - $sth->execute(); - my ($gsid) = $sth->fetchrow_array; - # Get all bugs_activity records from groupset changes - if ($gsid) { - $sth = $dbh->prepare("SELECT bug_id, bug_when, who, added, removed - FROM bugs_activity WHERE fieldid = $gsid"); - $sth->execute(); - while (my ($bug_id, $bug_when, $who, $added, $removed) = - $sth->fetchrow_array) - { - $added ||= 0; - $removed ||= 0; - # Get names of groups added. - my $sth2 = $dbh->prepare("SELECT name FROM groups - WHERE (bit & $added) != 0 - AND (bit & $removed) = 0"); - $sth2->execute(); - my @logadd; - while (my ($n) = $sth2->fetchrow_array) { - push @logadd, $n; - } - # Get names of groups removed. - $sth2 = $dbh->prepare("SELECT name FROM groups - WHERE (bit & $removed) != 0 - AND (bit & $added) = 0"); - $sth2->execute(); - my @logrem; - while (my ($n) = $sth2->fetchrow_array) { - push @logrem, $n; - } - # Get list of group bits added that correspond to - # missing groups. - $sth2 = $dbh->prepare("SELECT ($added & ~BIT_OR(bit)) - FROM groups"); - $sth2->execute(); - my ($miss) = $sth2->fetchrow_array; - if ($miss) { - push @logadd, _list_bits($miss); - print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", - " CONTAINS DELETED GROUPS\n"; - } - # Get list of group bits deleted that correspond to - # missing groups. - $sth2 = $dbh->prepare("SELECT ($removed & ~BIT_OR(bit)) - FROM groups"); - $sth2->execute(); - ($miss) = $sth2->fetchrow_array; - if ($miss) { - push @logrem, _list_bits($miss); - print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", - " CONTAINS DELETED GROUPS\n"; - } - my $logr = ""; - my $loga = ""; - $logr = join(", ", @logrem) . '?' if @logrem; - $loga = join(", ", @logadd) . '?' if @logadd; - # Replace to old activity record with the converted data. - $dbh->do("UPDATE bugs_activity SET fieldid = $bgfid, added = " . - $dbh->quote($loga) . ", removed = " . - $dbh->quote($logr) . - " WHERE bug_id = $bug_id AND bug_when = " . - $dbh->quote($bug_when) . - " AND who = $who AND fieldid = $gsid"); - } - # Replace groupset changes with group name changes in - # profiles_activity. Get profiles_activity records for groupset. - $sth = $dbh->prepare( - "SELECT userid, profiles_when, who, newvalue, oldvalue " . - "FROM profiles_activity " . - "WHERE fieldid = $gsid"); - $sth->execute(); - while (my ($uid, $uwhen, $uwho, $added, $removed) = - $sth->fetchrow_array) - { - $added ||= 0; - $removed ||= 0; - # Get names of groups added. - my $sth2 = $dbh->prepare("SELECT name FROM groups + } + + # Create bug_group_map records for old groupsets. + # Get each bug with the old group bit set. + $sth2 = $dbh->prepare( + "SELECT bug_id FROM bugs + WHERE (groupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($bug_id) = $sth2->fetchrow_array) { + + # Insert the bug, group pair into the bug_group_map. + $dbh->do( + "INSERT INTO bug_group_map (bug_id, group_id) + VALUES ($bug_id, $gid)" + ); + } + } + + # Replace old activity log groupset records with lists of names + # of groups. + $sth = $dbh->prepare( + "SELECT id FROM fielddefs + WHERE name = " . $dbh->quote('bug_group') + ); + $sth->execute(); + my ($bgfid) = $sth->fetchrow_array; + + # Get the field id for the old groupset field + $sth = $dbh->prepare( + "SELECT id FROM fielddefs + WHERE name = " . $dbh->quote('groupset') + ); + $sth->execute(); + my ($gsid) = $sth->fetchrow_array; + + # Get all bugs_activity records from groupset changes + if ($gsid) { + $sth = $dbh->prepare( + "SELECT bug_id, bug_when, who, added, removed + FROM bugs_activity WHERE fieldid = $gsid" + ); + $sth->execute(); + while (my ($bug_id, $bug_when, $who, $added, $removed) = $sth->fetchrow_array) { + $added ||= 0; + $removed ||= 0; + + # Get names of groups added. + my $sth2 = $dbh->prepare( + "SELECT name FROM groups WHERE (bit & $added) != 0 - AND (bit & $removed) = 0"); - $sth2->execute(); - my @logadd; - while (my ($n) = $sth2->fetchrow_array) { - push @logadd, $n; - } - # Get names of groups removed. - $sth2 = $dbh->prepare("SELECT name FROM groups + AND (bit & $removed) = 0" + ); + $sth2->execute(); + my @logadd; + while (my ($n) = $sth2->fetchrow_array) { + push @logadd, $n; + } + + # Get names of groups removed. + $sth2 = $dbh->prepare( + "SELECT name FROM groups WHERE (bit & $removed) != 0 - AND (bit & $added) = 0"); - $sth2->execute(); - my @logrem; - while (my ($n) = $sth2->fetchrow_array) { - push @logrem, $n; - } - my $ladd = ""; - my $lrem = ""; - $ladd = join(", ", @logadd) . '?' if @logadd; - $lrem = join(", ", @logrem) . '?' if @logrem; - # Replace profiles_activity record for groupset change - # with group list. - $dbh->do("UPDATE profiles_activity " . - "SET fieldid = $bgfid, newvalue = " . - $dbh->quote($ladd) . ", oldvalue = " . - $dbh->quote($lrem) . - " WHERE userid = $uid AND profiles_when = " . - $dbh->quote($uwhen) . - " AND who = $uwho AND fieldid = $gsid"); - } + AND (bit & $added) = 0" + ); + $sth2->execute(); + my @logrem; + while (my ($n) = $sth2->fetchrow_array) { + push @logrem, $n; } - # Identify admin group. - my ($admin_gid) = $dbh->selectrow_array( - "SELECT id FROM groups WHERE name = 'admin'"); - if (!$admin_gid) { - $dbh->do(q{INSERT INTO groups (name, description) - VALUES ('admin', 'Administrators')}); - $admin_gid = $dbh->bz_last_key('groups', 'id'); + # Get list of group bits added that correspond to + # missing groups. + $sth2 = $dbh->prepare( + "SELECT ($added & ~BIT_OR(bit)) + FROM groups" + ); + $sth2->execute(); + my ($miss) = $sth2->fetchrow_array; + if ($miss) { + push @logadd, _list_bits($miss); + print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", + " CONTAINS DELETED GROUPS\n"; } - # Find current admins - my @admins; - # Don't lose admins from DBs where Bug 157704 applies - $sth = $dbh->prepare( - "SELECT userid, (groupset & 65536), login_name " . - "FROM profiles " . - "WHERE (groupset | 65536) = 9223372036854775807"); - $sth->execute(); - while ( my ($userid, $iscomplete, $login_name) - = $sth->fetchrow_array() ) - { - # existing administrators are made members of group "admin" - print "\nWARNING - $login_name IS AN ADMIN IN SPITE OF BUG", - " 157704\n\n" if (!$iscomplete); - push(@admins, $userid) unless grep($_ eq $userid, @admins); + + # Get list of group bits deleted that correspond to + # missing groups. + $sth2 = $dbh->prepare( + "SELECT ($removed & ~BIT_OR(bit)) + FROM groups" + ); + $sth2->execute(); + ($miss) = $sth2->fetchrow_array; + if ($miss) { + push @logrem, _list_bits($miss); + print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", + " CONTAINS DELETED GROUPS\n"; } - # Now make all those users admins directly. They were already - # added to every other group, above, because of their groupset. - foreach my $admin_id (@admins) { - $dbh->do("INSERT INTO user_group_map - (user_id, group_id, isbless, grant_type) - VALUES (?, ?, ?, ?)", - undef, $admin_id, $admin_gid, $_, GRANT_DIRECT) - foreach (0, 1); + my $logr = ""; + my $loga = ""; + $logr = join(", ", @logrem) . '?' if @logrem; + $loga = join(", ", @logadd) . '?' if @logadd; + + # Replace to old activity record with the converted data. + $dbh->do("UPDATE bugs_activity SET fieldid = $bgfid, added = " + . $dbh->quote($loga) + . ", removed = " + . $dbh->quote($logr) + . " WHERE bug_id = $bug_id AND bug_when = " + . $dbh->quote($bug_when) + . " AND who = $who AND fieldid = $gsid"); + } + + # Replace groupset changes with group name changes in + # profiles_activity. Get profiles_activity records for groupset. + $sth + = $dbh->prepare("SELECT userid, profiles_when, who, newvalue, oldvalue " + . "FROM profiles_activity " + . "WHERE fieldid = $gsid"); + $sth->execute(); + while (my ($uid, $uwhen, $uwho, $added, $removed) = $sth->fetchrow_array) { + $added ||= 0; + $removed ||= 0; + + # Get names of groups added. + my $sth2 = $dbh->prepare( + "SELECT name FROM groups + WHERE (bit & $added) != 0 + AND (bit & $removed) = 0" + ); + $sth2->execute(); + my @logadd; + while (my ($n) = $sth2->fetchrow_array) { + push @logadd, $n; } - $dbh->bz_drop_column('profiles','groupset'); - $dbh->bz_drop_column('profiles','blessgroupset'); - $dbh->bz_drop_column('bugs','groupset'); - $dbh->bz_drop_column('groups','bit'); - $dbh->do("DELETE FROM fielddefs WHERE name = " - . $dbh->quote('groupset')); + # Get names of groups removed. + $sth2 = $dbh->prepare( + "SELECT name FROM groups + WHERE (bit & $removed) != 0 + AND (bit & $added) = 0" + ); + $sth2->execute(); + my @logrem; + while (my ($n) = $sth2->fetchrow_array) { + push @logrem, $n; + } + my $ladd = ""; + my $lrem = ""; + $ladd = join(", ", @logadd) . '?' if @logadd; + $lrem = join(", ", @logrem) . '?' if @logrem; + + # Replace profiles_activity record for groupset change + # with group list. + $dbh->do("UPDATE profiles_activity " + . "SET fieldid = $bgfid, newvalue = " + . $dbh->quote($ladd) + . ", oldvalue = " + . $dbh->quote($lrem) + . " WHERE userid = $uid AND profiles_when = " + . $dbh->quote($uwhen) + . " AND who = $uwho AND fieldid = $gsid"); + } } + + # Identify admin group. + my ($admin_gid) + = $dbh->selectrow_array("SELECT id FROM groups WHERE name = 'admin'"); + if (!$admin_gid) { + $dbh->do( + q{INSERT INTO groups (name, description) + VALUES ('admin', 'Administrators')} + ); + $admin_gid = $dbh->bz_last_key('groups', 'id'); + } + + # Find current admins + my @admins; + + # Don't lose admins from DBs where Bug 157704 applies + $sth + = $dbh->prepare("SELECT userid, (groupset & 65536), login_name " + . "FROM profiles " + . "WHERE (groupset | 65536) = 9223372036854775807"); + $sth->execute(); + while (my ($userid, $iscomplete, $login_name) = $sth->fetchrow_array()) { + + # existing administrators are made members of group "admin" + print "\nWARNING - $login_name IS AN ADMIN IN SPITE OF BUG", " 157704\n\n" + if (!$iscomplete); + push(@admins, $userid) unless grep($_ eq $userid, @admins); + } + + # Now make all those users admins directly. They were already + # added to every other group, above, because of their groupset. + foreach my $admin_id (@admins) { + $dbh->do( + "INSERT INTO user_group_map + (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)", undef, $admin_id, $admin_gid, $_, + GRANT_DIRECT + ) foreach (0, 1); + } + + $dbh->bz_drop_column('profiles', 'groupset'); + $dbh->bz_drop_column('profiles', 'blessgroupset'); + $dbh->bz_drop_column('bugs', 'groupset'); + $dbh->bz_drop_column('groups', 'bit'); + $dbh->do("DELETE FROM fielddefs WHERE name = " . $dbh->quote('groupset')); + } } sub _convert_attachment_statuses_to_flags { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + + # September 2002 myk@mozilla.org bug 98801 + # Convert the attachment statuses tables into flags tables. + if ( $dbh->bz_table_info("attachstatuses") + && $dbh->bz_table_info("attachstatusdefs")) + { + print "Converting attachment statuses to flags...\n"; + + # Get IDs for the old attachment status and new flag fields. + my ($old_field_id) + = $dbh->selectrow_array( + "SELECT id FROM fielddefs WHERE name='attachstatusdefs.name'") + || 0; + my ($new_field_id) + = $dbh->selectrow_array( + "SELECT id FROM fielddefs WHERE name = 'flagtypes.name'"); + + # Convert attachment status definitions to flag types. If more than one + # status has the same name and description, it is merged into a single + # status with multiple inclusion records. - # September 2002 myk@mozilla.org bug 98801 - # Convert the attachment statuses tables into flags tables. - if ($dbh->bz_table_info("attachstatuses") - && $dbh->bz_table_info("attachstatusdefs")) - { - print "Converting attachment statuses to flags...\n"; - - # Get IDs for the old attachment status and new flag fields. - my ($old_field_id) = $dbh->selectrow_array( - "SELECT id FROM fielddefs WHERE name='attachstatusdefs.name'") - || 0; - my ($new_field_id) = $dbh->selectrow_array( - "SELECT id FROM fielddefs WHERE name = 'flagtypes.name'"); - - # Convert attachment status definitions to flag types. If more than one - # status has the same name and description, it is merged into a single - # status with multiple inclusion records. - - my $sth = $dbh->prepare( - "SELECT id, name, description, sortkey, product_id - FROM attachstatusdefs"); - - # status definition IDs indexed by name/description - my $def_ids = {}; - - # merged IDs and the IDs they were merged into. The key is the old ID, - # the value is the new one. This allows us to give statuses the right - # ID when we convert them over to flags. This map includes IDs that - # weren't merged (in this case the old and new IDs are the same), since - # it makes the code simpler. - my $def_id_map = {}; - - $sth->execute(); - while (my ($id, $name, $desc, $sortkey, $prod_id) = - $sth->fetchrow_array()) - { - my $key = $name . $desc; - if (!$def_ids->{$key}) { - $def_ids->{$key} = $id; - my $quoted_name = $dbh->quote($name); - my $quoted_desc = $dbh->quote($desc); - $dbh->do("INSERT INTO flagtypes (id, name, description, + my $sth = $dbh->prepare( + "SELECT id, name, description, sortkey, product_id + FROM attachstatusdefs" + ); + + # status definition IDs indexed by name/description + my $def_ids = {}; + + # merged IDs and the IDs they were merged into. The key is the old ID, + # the value is the new one. This allows us to give statuses the right + # ID when we convert them over to flags. This map includes IDs that + # weren't merged (in this case the old and new IDs are the same), since + # it makes the code simpler. + my $def_id_map = {}; + + $sth->execute(); + while (my ($id, $name, $desc, $sortkey, $prod_id) = $sth->fetchrow_array()) { + my $key = $name . $desc; + if (!$def_ids->{$key}) { + $def_ids->{$key} = $id; + my $quoted_name = $dbh->quote($name); + my $quoted_desc = $dbh->quote($desc); + $dbh->do( + "INSERT INTO flagtypes (id, name, description, sortkey, target_type) VALUES ($id, $quoted_name, $quoted_desc, - $sortkey,'a')"); - } - $def_id_map->{$id} = $def_ids->{$key}; - $dbh->do("INSERT INTO flaginclusions (type_id, product_id) - VALUES ($def_id_map->{$id}, $prod_id)"); - } + $sortkey,'a')" + ); + } + $def_id_map->{$id} = $def_ids->{$key}; + $dbh->do( + "INSERT INTO flaginclusions (type_id, product_id) + VALUES ($def_id_map->{$id}, $prod_id)" + ); + } - # Note: even though we've converted status definitions, we still - # can't drop the table because we need it to convert the statuses - # themselves. - - # Convert attachment statuses to flags. To do this we select - # the statuses from the status table and then, for each one, - # figure out who set it and when they set it from the bugs - # activity table. - my $id = 0; - $sth = $dbh->prepare( - "SELECT attachstatuses.attach_id, attachstatusdefs.id, + # Note: even though we've converted status definitions, we still + # can't drop the table because we need it to convert the statuses + # themselves. + + # Convert attachment statuses to flags. To do this we select + # the statuses from the status table and then, for each one, + # figure out who set it and when they set it from the bugs + # activity table. + my $id = 0; + $sth = $dbh->prepare( + "SELECT attachstatuses.attach_id, attachstatusdefs.id, attachstatusdefs.name, attachments.bug_id FROM attachstatuses, attachstatusdefs, attachments WHERE attachstatuses.statusid = attachstatusdefs.id - AND attachstatuses.attach_id = attachments.attach_id"); + AND attachstatuses.attach_id = attachments.attach_id" + ); - # a query to determine when the attachment status was set and who set it - my $sth2 = $dbh->prepare("SELECT added, who, bug_when + # a query to determine when the attachment status was set and who set it + my $sth2 = $dbh->prepare( + "SELECT added, who, bug_when FROM bugs_activity WHERE bug_id = ? AND attach_id = ? AND fieldid = $old_field_id - ORDER BY bug_when DESC"); - - $sth->execute(); - while (my ($attach_id, $def_id, $status, $bug_id) = - $sth->fetchrow_array()) - { - ++$id; - - # Determine when the attachment status was set and who set it. - # We should always be able to find out this info from the bug - # activity, but we fall back to default values just in case. - $sth2->execute($bug_id, $attach_id); - my ($added, $who, $when); - while (($added, $who, $when) = $sth2->fetchrow_array()) { - last if $added =~ /(^|[, ]+)\Q$status\E([, ]+|$)/; - } - $who = $dbh->quote($who); # "NULL" by default if $who is undefined - $when = $when ? $dbh->quote($when) : "NOW()"; - + ORDER BY bug_when DESC" + ); - $dbh->do("INSERT INTO flags (id, type_id, status, bug_id, + $sth->execute(); + while (my ($attach_id, $def_id, $status, $bug_id) = $sth->fetchrow_array()) { + ++$id; + + # Determine when the attachment status was set and who set it. + # We should always be able to find out this info from the bug + # activity, but we fall back to default values just in case. + $sth2->execute($bug_id, $attach_id); + my ($added, $who, $when); + while (($added, $who, $when) = $sth2->fetchrow_array()) { + last if $added =~ /(^|[, ]+)\Q$status\E([, ]+|$)/; + } + $who = $dbh->quote($who); # "NULL" by default if $who is undefined + $when = $when ? $dbh->quote($when) : "NOW()"; + + + $dbh->do( + "INSERT INTO flags (id, type_id, status, bug_id, attach_id, creation_date, modification_date, requestee_id, setter_id) VALUES ($id, $def_id_map->{$def_id}, '+', $bug_id, - $attach_id, $when, $when, NULL, $who)"); - } + $attach_id, $when, $when, NULL, $who)" + ); + } - # Now that we've converted both tables we can drop them. - $dbh->bz_drop_table("attachstatuses"); - $dbh->bz_drop_table("attachstatusdefs"); + # Now that we've converted both tables we can drop them. + $dbh->bz_drop_table("attachstatuses"); + $dbh->bz_drop_table("attachstatusdefs"); - # Convert activity records for attachment statuses into records - # for flags. - $sth = $dbh->prepare("SELECT attach_id, who, bug_when, added, + # Convert activity records for attachment statuses into records + # for flags. + $sth = $dbh->prepare( + "SELECT attach_id, who, bug_when, added, removed FROM bugs_activity - WHERE fieldid = $old_field_id"); - $sth->execute(); - while (my ($attach_id, $who, $when, $old_added, $old_removed) = - $sth->fetchrow_array()) - { - my @additions = split(/[, ]+/, $old_added); - @additions = map("$_+", @additions); - my $new_added = $dbh->quote(join(", ", @additions)); - - my @removals = split(/[, ]+/, $old_removed); - @removals = map("$_+", @removals); - my $new_removed = $dbh->quote(join(", ", @removals)); - - $old_added = $dbh->quote($old_added); - $old_removed = $dbh->quote($old_removed); - $who = $dbh->quote($who); - $when = $dbh->quote($when); - - $dbh->do("UPDATE bugs_activity SET fieldid = $new_field_id, " . - "added = $new_added, removed = $new_removed " . - "WHERE attach_id = $attach_id AND who = $who " . - "AND bug_when = $when AND fieldid = $old_field_id " . - "AND added = $old_added AND removed = $old_removed"); - } + WHERE fieldid = $old_field_id" + ); + $sth->execute(); + while (my ($attach_id, $who, $when, $old_added, $old_removed) + = $sth->fetchrow_array()) + { + my @additions = split(/[, ]+/, $old_added); + @additions = map("$_+", @additions); + my $new_added = $dbh->quote(join(", ", @additions)); + + my @removals = split(/[, ]+/, $old_removed); + @removals = map("$_+", @removals); + my $new_removed = $dbh->quote(join(", ", @removals)); + + $old_added = $dbh->quote($old_added); + $old_removed = $dbh->quote($old_removed); + $who = $dbh->quote($who); + $when = $dbh->quote($when); + + $dbh->do("UPDATE bugs_activity SET fieldid = $new_field_id, " + . "added = $new_added, removed = $new_removed " + . "WHERE attach_id = $attach_id AND who = $who " + . "AND bug_when = $when AND fieldid = $old_field_id " + . "AND added = $old_added AND removed = $old_removed"); + } - # Remove the attachment status field from the field definitions. - $dbh->do("DELETE FROM fielddefs WHERE name='attachstatusdefs.name'"); + # Remove the attachment status field from the field definitions. + $dbh->do("DELETE FROM fielddefs WHERE name='attachstatusdefs.name'"); - print "done.\n"; - } + print "done.\n"; + } } sub _remove_spaces_and_commas_from_flagtypes { - my $dbh = Bugzilla->dbh; - # Get all names and IDs, to find broken ones and to - # check for collisions when renaming. - my $sth = $dbh->prepare("SELECT name, id FROM flagtypes"); - $sth->execute(); - - my %flagtypes; - my @badflagnames; - # find broken flagtype names, and populate a hash table - # to check for collisions. - while (my ($name, $id) = $sth->fetchrow_array()) { - $flagtypes{$name} = $id; - if ($name =~ /[ ,]/) { - push(@badflagnames, $name); - } + my $dbh = Bugzilla->dbh; + + # Get all names and IDs, to find broken ones and to + # check for collisions when renaming. + my $sth = $dbh->prepare("SELECT name, id FROM flagtypes"); + $sth->execute(); + + my %flagtypes; + my @badflagnames; + + # find broken flagtype names, and populate a hash table + # to check for collisions. + while (my ($name, $id) = $sth->fetchrow_array()) { + $flagtypes{$name} = $id; + if ($name =~ /[ ,]/) { + push(@badflagnames, $name); } - if (@badflagnames) { - print "Removing spaces and commas from flag names...\n"; - my ($flagname, $tryflagname); - my $sth = $dbh->prepare("UPDATE flagtypes SET name = ? WHERE id = ?"); - foreach $flagname (@badflagnames) { - print " Bad flag type name \"$flagname\" ...\n"; - # find a new name for this flagtype. - ($tryflagname = $flagname) =~ tr/ ,/__/; - # avoid collisions with existing flagtype names. - while (defined($flagtypes{$tryflagname})) { - print " ... can't rename as \"$tryflagname\" ...\n"; - $tryflagname .= "'"; - if (length($tryflagname) > 50) { - my $lastchanceflagname = (substr $tryflagname, 0, 47) . '...'; - if (defined($flagtypes{$lastchanceflagname})) { - print " ... last attempt as \"$lastchanceflagname\" still failed.'\n"; - die install_string('update_flags_bad_name', - { flag => $flagname }), "\n"; - } - $tryflagname = $lastchanceflagname; - } - } - $sth->execute($tryflagname, $flagtypes{$flagname}); - print " renamed flag type \"$flagname\" as \"$tryflagname\"\n"; - $flagtypes{$tryflagname} = $flagtypes{$flagname}; - delete $flagtypes{$flagname}; + } + if (@badflagnames) { + print "Removing spaces and commas from flag names...\n"; + my ($flagname, $tryflagname); + my $sth = $dbh->prepare("UPDATE flagtypes SET name = ? WHERE id = ?"); + foreach $flagname (@badflagnames) { + print " Bad flag type name \"$flagname\" ...\n"; + + # find a new name for this flagtype. + ($tryflagname = $flagname) =~ tr/ ,/__/; + + # avoid collisions with existing flagtype names. + while (defined($flagtypes{$tryflagname})) { + print " ... can't rename as \"$tryflagname\" ...\n"; + $tryflagname .= "'"; + if (length($tryflagname) > 50) { + my $lastchanceflagname = (substr $tryflagname, 0, 47) . '...'; + if (defined($flagtypes{$lastchanceflagname})) { + print " ... last attempt as \"$lastchanceflagname\" still failed.'\n"; + die install_string('update_flags_bad_name', {flag => $flagname}), "\n"; + } + $tryflagname = $lastchanceflagname; } - print "... done.\n"; + } + $sth->execute($tryflagname, $flagtypes{$flagname}); + print " renamed flag type \"$flagname\" as \"$tryflagname\"\n"; + $flagtypes{$tryflagname} = $flagtypes{$flagname}; + delete $flagtypes{$flagname}; } + print "... done.\n"; + } } sub _setup_usebuggroups_backward_compatibility { - my $dbh = Bugzilla->dbh; - - # Don't run this on newer Bugzillas. This is a reliable test because - # the longdescs table existed in 2.16 (which had usebuggroups) - # but not in 2.18, and this code happens between 2.16 and 2.18. - return if $dbh->bz_column_info('longdescs', 'already_wrapped'); - - # 2002-11-24 - bugreport@peshkin.net - bug 147275 - # - # If group_control_map is empty, backward-compatibility - # usebuggroups-equivalent records should be created. - my ($maps_exist) = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM group_control_map"); - if (!$maps_exist) { - print "Converting old usebuggroups controls...\n"; - # Initially populate group_control_map. - # First, get all the existing products and their groups. - my $sth = $dbh->prepare("SELECT groups.id, products.id, groups.name, + my $dbh = Bugzilla->dbh; + + # Don't run this on newer Bugzillas. This is a reliable test because + # the longdescs table existed in 2.16 (which had usebuggroups) + # but not in 2.18, and this code happens between 2.16 and 2.18. + return if $dbh->bz_column_info('longdescs', 'already_wrapped'); + + # 2002-11-24 - bugreport@peshkin.net - bug 147275 + # + # If group_control_map is empty, backward-compatibility + # usebuggroups-equivalent records should be created. + my ($maps_exist) + = $dbh->selectrow_array("SELECT DISTINCT 1 FROM group_control_map"); + if (!$maps_exist) { + print "Converting old usebuggroups controls...\n"; + + # Initially populate group_control_map. + # First, get all the existing products and their groups. + my $sth = $dbh->prepare( + "SELECT groups.id, products.id, groups.name, products.name FROM groups, products - WHERE isbuggroup != 0"); - $sth->execute(); - while (my ($groupid, $productid, $groupname, $productname) - = $sth->fetchrow_array()) - { - if ($groupname eq $productname) { - # Product and group have same name. - $dbh->do("INSERT INTO group_control_map " . - "(group_id, product_id, membercontrol, othercontrol) " . - "VALUES (?, ?, ?, ?)", undef, - ($groupid, $productid, CONTROLMAPDEFAULT, CONTROLMAPNA)); - } else { - # See if this group is a product group at all. - my $sth2 = $dbh->prepare("SELECT id FROM products - WHERE name = " .$dbh->quote($groupname)); - $sth2->execute(); - my ($id) = $sth2->fetchrow_array(); - if (!$id) { - # If there is no product with the same name as this - # group, then it is permitted for all products. - $dbh->do("INSERT INTO group_control_map " . - "(group_id, product_id, membercontrol, othercontrol) " . - "VALUES (?, ?, ?, ?)", undef, - ($groupid, $productid, CONTROLMAPSHOWN, CONTROLMAPNA)); - } - } + WHERE isbuggroup != 0" + ); + $sth->execute(); + while (my ($groupid, $productid, $groupname, $productname) + = $sth->fetchrow_array()) + { + if ($groupname eq $productname) { + + # Product and group have same name. + $dbh->do( + "INSERT INTO group_control_map " + . "(group_id, product_id, membercontrol, othercontrol) " + . "VALUES (?, ?, ?, ?)", + undef, + ($groupid, $productid, CONTROLMAPDEFAULT, CONTROLMAPNA) + ); + } + else { + # See if this group is a product group at all. + my $sth2 = $dbh->prepare( + "SELECT id FROM products + WHERE name = " . $dbh->quote($groupname) + ); + $sth2->execute(); + my ($id) = $sth2->fetchrow_array(); + if (!$id) { + + # If there is no product with the same name as this + # group, then it is permitted for all products. + $dbh->do( + "INSERT INTO group_control_map " + . "(group_id, product_id, membercontrol, othercontrol) " + . "VALUES (?, ?, ?, ?)", + undef, + ($groupid, $productid, CONTROLMAPSHOWN, CONTROLMAPNA) + ); } + } } + } } sub _remove_user_series_map { - my $dbh = Bugzilla->dbh; - # 2004-07-17 GRM - Remove "subscriptions" concept from charting, and add - # group-based security instead. - if ($dbh->bz_table_info("user_series_map")) { - # Oracle doesn't like "date" as a column name, and apparently some DBs - # don't like 'value' either. We use the changes to subscriptions as - # something to hang these renamings off. - $dbh->bz_rename_column('series_data', 'date', 'series_date'); - $dbh->bz_rename_column('series_data', 'value', 'series_value'); - - # series_categories.category_id produces a too-long column name for the - # auto-incrementing sequence (Oracle again). - $dbh->bz_rename_column('series_categories', 'category_id', 'id'); - - $dbh->bz_add_column("series", "public", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - # Migrate public-ness across from user_series_map to new field - my $sth = $dbh->prepare("SELECT series_id from user_series_map " . - "WHERE user_id = 0"); - $sth->execute(); - while (my ($public_series_id) = $sth->fetchrow_array()) { - $dbh->do("UPDATE series SET public = 1 " . - "WHERE series_id = $public_series_id"); - } + my $dbh = Bugzilla->dbh; + + # 2004-07-17 GRM - Remove "subscriptions" concept from charting, and add + # group-based security instead. + if ($dbh->bz_table_info("user_series_map")) { + + # Oracle doesn't like "date" as a column name, and apparently some DBs + # don't like 'value' either. We use the changes to subscriptions as + # something to hang these renamings off. + $dbh->bz_rename_column('series_data', 'date', 'series_date'); + $dbh->bz_rename_column('series_data', 'value', 'series_value'); + + # series_categories.category_id produces a too-long column name for the + # auto-incrementing sequence (Oracle again). + $dbh->bz_rename_column('series_categories', 'category_id', 'id'); - $dbh->bz_drop_table("user_series_map"); + $dbh->bz_add_column("series", "public", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + # Migrate public-ness across from user_series_map to new field + my $sth = $dbh->prepare( + "SELECT series_id from user_series_map " . "WHERE user_id = 0"); + $sth->execute(); + while (my ($public_series_id) = $sth->fetchrow_array()) { + $dbh->do( + "UPDATE series SET public = 1 " . "WHERE series_id = $public_series_id"); } + + $dbh->bz_drop_table("user_series_map"); + } } sub _copy_old_charts_into_database { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations()->{'datadir'}; - # 2003-06-26 Copy the old charting data into the database, and create the - # queries that will keep it all running. When the old charting system goes - # away, if this code ever runs, it'll just find no files and do nothing. - my $series_exists = $dbh->selectrow_array("SELECT 1 FROM series " . - $dbh->sql_limit(1)); - if (!$series_exists && -d "$datadir/mining" && -e "$datadir/mining/-All-") { - print "Migrating old chart data into database...\n"; - - # We prepare the handle to insert the series data - my $seriesdatasth = $dbh->prepare( - "INSERT INTO series_data (series_id, series_date, series_value) - VALUES (?, ?, ?)"); - - my $deletesth = $dbh->prepare( - "DELETE FROM series_data WHERE series_id = ? AND series_date = ?"); - - my $groupmapsth = $dbh->prepare( - "INSERT INTO category_group_map (category_id, group_id) - VALUES (?, ?)"); - - # Fields in the data file (matches the current collectstats.pl) - my @statuses = - qw(NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED VERIFIED CLOSED); - my @resolutions = - qw(FIXED INVALID WONTFIX LATER REMIND DUPLICATE WORKSFORME MOVED); - my @fields = (@statuses, @resolutions); - - # We have a localization problem here. Where do we get these values? - my $all_name = "-All-"; - my $open_name = "All Open"; - - $dbh->bz_start_transaction(); - my $products = $dbh->selectall_arrayref("SELECT name FROM products"); - - foreach my $product ((map { $_->[0] } @$products), "-All-") { - print "$product:\n"; - # First, create the series - my %queries; - my %seriesids; - - my $query_prod = ""; - if ($product ne "-All-") { - $query_prod = "product=" . html_quote($product) . "&"; - } + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations()->{'datadir'}; + + # 2003-06-26 Copy the old charting data into the database, and create the + # queries that will keep it all running. When the old charting system goes + # away, if this code ever runs, it'll just find no files and do nothing. + my $series_exists + = $dbh->selectrow_array("SELECT 1 FROM series " . $dbh->sql_limit(1)); + if (!$series_exists && -d "$datadir/mining" && -e "$datadir/mining/-All-") { + print "Migrating old chart data into database...\n"; + + # We prepare the handle to insert the series data + my $seriesdatasth = $dbh->prepare( + "INSERT INTO series_data (series_id, series_date, series_value) + VALUES (?, ?, ?)" + ); - # The query for statuses is different to that for resolutions. - $queries{$_} = ($query_prod . "bug_status=$_") foreach (@statuses); - $queries{$_} = ($query_prod . "resolution=$_") - foreach (@resolutions); - - foreach my $field (@fields) { - # Create a Series for each field in this product. - my $series = new Bugzilla::Series(undef, $product, $all_name, - $field, undef, 1, - $queries{$field}, 1); - $series->writeToDatabase(); - $seriesids{$field} = $series->{'series_id'}; - } + my $deletesth = $dbh->prepare( + "DELETE FROM series_data WHERE series_id = ? AND series_date = ?"); - # We also add a new query for "Open", so that migrated products get - # the same set as new products (see editproducts.cgi.) - my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"); - my $query = join("&", map { "bug_status=$_" } @openedstatuses); - my $series = new Bugzilla::Series(undef, $product, $all_name, - $open_name, undef, 1, - $query_prod . $query, 1); - $series->writeToDatabase(); - $seriesids{$open_name} = $series->{'series_id'}; - - # Now, we attempt to read in historical data, if any - # Convert the name in the same way that collectstats.pl does - my $product_file = $product; - $product_file =~ s/\//-/gs; - $product_file = "$datadir/mining/$product_file"; - - # There are many reasons that this might fail (e.g. no stats - # for this product), so we don't worry if it does. - my $in = new IO::File($product_file) or next; - - # The data files should be in a standard format, even for old - # Bugzillas, because of the conversion code further up this file. - my %data; - my $last_date = ""; - - my @lines = <$in>; - while (my $line = shift @lines) { - if ($line =~ /^(\d+\|.*)/) { - my @numbers = split(/\||\r/, $1); - - # Only take the first line for each date; it was possible to - # run collectstats.pl more than once in a day. - next if $numbers[0] eq $last_date; - - for my $i (0 .. $#fields) { - # $numbers[0] is the date - $data{$fields[$i]}{$numbers[0]} = $numbers[$i + 1]; - - # Keep a total of the number of open bugs for this day - if (grep { $_ eq $fields[$i] } @openedstatuses) { - $data{$open_name}{$numbers[0]} += $numbers[$i + 1]; - } - } - - $last_date = $numbers[0]; - } - } + my $groupmapsth = $dbh->prepare( + "INSERT INTO category_group_map (category_id, group_id) + VALUES (?, ?)" + ); - $in->close; - - my $total_items = (scalar(@fields) + 1) - * scalar(keys %{ $data{'NEW'} }); - my $count = 0; - foreach my $field (@fields, $open_name) { - # Insert values into series_data: series_id, date, value - my %fielddata = %{$data{$field}}; - foreach my $date (keys %fielddata) { - # We need to delete in case the text file had duplicate - # entries in it. - $deletesth->execute($seriesids{$field}, $date); - - # We prepared this above - $seriesdatasth->execute($seriesids{$field}, - $date, $fielddata{$date} || 0); - indicate_progress({ total => $total_items, - current => ++$count, every => 100 }); - } - } + # Fields in the data file (matches the current collectstats.pl) + my @statuses = qw(NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED VERIFIED CLOSED); + my @resolutions + = qw(FIXED INVALID WONTFIX LATER REMIND DUPLICATE WORKSFORME MOVED); + my @fields = (@statuses, @resolutions); - # Create the groupsets for the category - my $category_id = - $dbh->selectrow_array("SELECT id FROM series_categories " . - "WHERE name = " . $dbh->quote($product)); - my $product_id = - $dbh->selectrow_array("SELECT id FROM products " . - "WHERE name = " . $dbh->quote($product)); - - if (defined($category_id) && defined($product_id)) { - - # Get all the mandatory groups for this product - my $group_ids = - $dbh->selectcol_arrayref("SELECT group_id " . - "FROM group_control_map " . - "WHERE product_id = $product_id " . - "AND (membercontrol = " . CONTROLMAPMANDATORY . - " OR othercontrol = " . CONTROLMAPMANDATORY . ")"); - - foreach my $group_id (@$group_ids) { - $groupmapsth->execute($category_id, $group_id); - } + # We have a localization problem here. Where do we get these values? + my $all_name = "-All-"; + my $open_name = "All Open"; + + $dbh->bz_start_transaction(); + my $products = $dbh->selectall_arrayref("SELECT name FROM products"); + + foreach my $product ((map { $_->[0] } @$products), "-All-") { + print "$product:\n"; + + # First, create the series + my %queries; + my %seriesids; + + my $query_prod = ""; + if ($product ne "-All-") { + $query_prod = "product=" . html_quote($product) . "&"; + } + + # The query for statuses is different to that for resolutions. + $queries{$_} = ($query_prod . "bug_status=$_") foreach (@statuses); + $queries{$_} = ($query_prod . "resolution=$_") foreach (@resolutions); + + foreach my $field (@fields) { + + # Create a Series for each field in this product. + my $series = new Bugzilla::Series(undef, $product, $all_name, $field, undef, 1, + $queries{$field}, 1); + $series->writeToDatabase(); + $seriesids{$field} = $series->{'series_id'}; + } + + # We also add a new query for "Open", so that migrated products get + # the same set as new products (see editproducts.cgi.) + my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"); + my $query = join("&", map {"bug_status=$_"} @openedstatuses); + my $series + = new Bugzilla::Series(undef, $product, $all_name, $open_name, undef, 1, + $query_prod . $query, 1); + $series->writeToDatabase(); + $seriesids{$open_name} = $series->{'series_id'}; + + # Now, we attempt to read in historical data, if any + # Convert the name in the same way that collectstats.pl does + my $product_file = $product; + $product_file =~ s/\//-/gs; + $product_file = "$datadir/mining/$product_file"; + + # There are many reasons that this might fail (e.g. no stats + # for this product), so we don't worry if it does. + my $in = new IO::File($product_file) or next; + + # The data files should be in a standard format, even for old + # Bugzillas, because of the conversion code further up this file. + my %data; + my $last_date = ""; + + my @lines = <$in>; + while (my $line = shift @lines) { + if ($line =~ /^(\d+\|.*)/) { + my @numbers = split(/\||\r/, $1); + + # Only take the first line for each date; it was possible to + # run collectstats.pl more than once in a day. + next if $numbers[0] eq $last_date; + + for my $i (0 .. $#fields) { + + # $numbers[0] is the date + $data{$fields[$i]}{$numbers[0]} = $numbers[$i + 1]; + + # Keep a total of the number of open bugs for this day + if (grep { $_ eq $fields[$i] } @openedstatuses) { + $data{$open_name}{$numbers[0]} += $numbers[$i + 1]; } + } + + $last_date = $numbers[0]; } + } + + $in->close; + + my $total_items = (scalar(@fields) + 1) * scalar(keys %{$data{'NEW'}}); + my $count = 0; + foreach my $field (@fields, $open_name) { + + # Insert values into series_data: series_id, date, value + my %fielddata = %{$data{$field}}; + foreach my $date (keys %fielddata) { - $dbh->bz_commit_transaction(); + # We need to delete in case the text file had duplicate + # entries in it. + $deletesth->execute($seriesids{$field}, $date); + + # We prepared this above + $seriesdatasth->execute($seriesids{$field}, $date, $fielddata{$date} || 0); + indicate_progress({total => $total_items, current => ++$count, every => 100}); + } + } + + # Create the groupsets for the category + my $category_id = $dbh->selectrow_array( + "SELECT id FROM series_categories " . "WHERE name = " . $dbh->quote($product)); + my $product_id = $dbh->selectrow_array( + "SELECT id FROM products " . "WHERE name = " . $dbh->quote($product)); + + if (defined($category_id) && defined($product_id)) { + + # Get all the mandatory groups for this product + my $group_ids + = $dbh->selectcol_arrayref("SELECT group_id " + . "FROM group_control_map " + . "WHERE product_id = $product_id " + . "AND (membercontrol = " + . CONTROLMAPMANDATORY + . " OR othercontrol = " + . CONTROLMAPMANDATORY + . ")"); + + foreach my $group_id (@$group_ids) { + $groupmapsth->execute($category_id, $group_id); + } + } } + + $dbh->bz_commit_transaction(); + } } sub _add_user_group_map_grant_type { - my $dbh = Bugzilla->dbh; - # 2004-04-12 - Keep regexp-based group permissions up-to-date - Bug 240325 - if ($dbh->bz_column_info("user_group_map", "isderived")) { - $dbh->bz_add_column('user_group_map', 'grant_type', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); - $dbh->do("DELETE FROM user_group_map WHERE isderived != 0"); - $dbh->do("UPDATE user_group_map SET grant_type = " . GRANT_DIRECT); - $dbh->bz_drop_column("user_group_map", "isderived"); - - $dbh->bz_drop_index('user_group_map', 'user_group_map_user_id_idx'); - $dbh->bz_add_index('user_group_map', 'user_group_map_user_id_idx', - {TYPE => 'UNIQUE', - FIELDS => [qw(user_id group_id grant_type isbless)]}); - } + my $dbh = Bugzilla->dbh; + + # 2004-04-12 - Keep regexp-based group permissions up-to-date - Bug 240325 + if ($dbh->bz_column_info("user_group_map", "isderived")) { + $dbh->bz_add_column('user_group_map', 'grant_type', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); + $dbh->do("DELETE FROM user_group_map WHERE isderived != 0"); + $dbh->do("UPDATE user_group_map SET grant_type = " . GRANT_DIRECT); + $dbh->bz_drop_column("user_group_map", "isderived"); + + $dbh->bz_drop_index('user_group_map', 'user_group_map_user_id_idx'); + $dbh->bz_add_index('user_group_map', 'user_group_map_user_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(user_id group_id grant_type isbless)]}); + } } sub _add_group_group_map_grant_type { - my $dbh = Bugzilla->dbh; - # 2004-07-16 - Make it possible to have group-group relationships other than - # membership and bless. - if ($dbh->bz_column_info("group_group_map", "isbless")) { - $dbh->bz_add_column('group_group_map', 'grant_type', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); - $dbh->do("UPDATE group_group_map SET grant_type = " . - "IF(isbless, " . GROUP_BLESS . ", " . - GROUP_MEMBERSHIP . ")"); - $dbh->bz_drop_index('group_group_map', 'group_group_map_member_id_idx'); - $dbh->bz_drop_column("group_group_map", "isbless"); - $dbh->bz_add_index('group_group_map', 'group_group_map_member_id_idx', - {TYPE => 'UNIQUE', - FIELDS => [qw(member_id grantor_id grant_type)]}); - } + my $dbh = Bugzilla->dbh; + + # 2004-07-16 - Make it possible to have group-group relationships other than + # membership and bless. + if ($dbh->bz_column_info("group_group_map", "isbless")) { + $dbh->bz_add_column('group_group_map', 'grant_type', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); + $dbh->do("UPDATE group_group_map SET grant_type = " + . "IF(isbless, " + . GROUP_BLESS . ", " + . GROUP_MEMBERSHIP + . ")"); + $dbh->bz_drop_index('group_group_map', 'group_group_map_member_id_idx'); + $dbh->bz_drop_column("group_group_map", "isbless"); + $dbh->bz_add_index( + 'group_group_map', + 'group_group_map_member_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(member_id grantor_id grant_type)]} + ); + } } sub _add_longdescs_already_wrapped { - my $dbh = Bugzilla->dbh; - # 2005-01-29 - mkanat@bugzilla.org - if (!$dbh->bz_column_info('longdescs', 'already_wrapped')) { - # Old, pre-wrapped comments should not be auto-wrapped - $dbh->bz_add_column('longdescs', 'already_wrapped', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, 1); - # If an old comment doesn't have a newline in the first 81 characters, - # (or doesn't contain a newline at all) and it contains a space, - # then it's probably a mis-wrapped comment and we should wrap it - # at display-time. - print "Fixing old, mis-wrapped comments...\n"; - $dbh->do(q{UPDATE longdescs SET already_wrapped = 0 + my $dbh = Bugzilla->dbh; + + # 2005-01-29 - mkanat@bugzilla.org + if (!$dbh->bz_column_info('longdescs', 'already_wrapped')) { + + # Old, pre-wrapped comments should not be auto-wrapped + $dbh->bz_add_column('longdescs', 'already_wrapped', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, 1); + + # If an old comment doesn't have a newline in the first 81 characters, + # (or doesn't contain a newline at all) and it contains a space, + # then it's probably a mis-wrapped comment and we should wrap it + # at display-time. + print "Fixing old, mis-wrapped comments...\n"; + $dbh->do( + q{UPDATE longdescs SET already_wrapped = 0 WHERE (} . $dbh->sql_position(q{'\n'}, 'thetext') . q{ > 81 OR } . $dbh->sql_position(q{'\n'}, 'thetext') . q{ = 0) - AND SUBSTRING(thetext FROM 1 FOR 80) LIKE '% %'}); - } + AND SUBSTRING(thetext FROM 1 FOR 80) LIKE '% %'} + ); + } } sub _convert_attachments_filename_from_mediumtext { - my $dbh = Bugzilla->dbh; - # 2002 November, myk@mozilla.org, bug 178841: - # - # Convert the "attachments.filename" column from a ridiculously large - # "mediumtext" to a much more sensible "varchar(100)". Also takes - # the opportunity to remove paths from existing filenames, since they - # shouldn't be there for security. Buggy browsers include them, - # and attachment.cgi now takes them out, but old ones need converting. - my $ref = $dbh->bz_column_info("attachments", "filename"); - if ($ref->{TYPE} ne 'varchar(100)' && $ref->{TYPE} ne 'varchar(255)') { - print "Removing paths from filenames in attachments table..."; - - my $sth = $dbh->prepare("SELECT attach_id, filename FROM attachments " . - "WHERE " . $dbh->sql_position(q{'/'}, 'filename') . " > 0 OR " . - $dbh->sql_position(q{'\\\\'}, 'filename') . " > 0"); - $sth->execute; - - while (my ($attach_id, $filename) = $sth->fetchrow_array) { - $filename =~ s/^.*[\/\\]//; - my $quoted_filename = $dbh->quote($filename); - $dbh->do("UPDATE attachments SET filename = $quoted_filename " . - "WHERE attach_id = $attach_id"); - } + my $dbh = Bugzilla->dbh; + + # 2002 November, myk@mozilla.org, bug 178841: + # + # Convert the "attachments.filename" column from a ridiculously large + # "mediumtext" to a much more sensible "varchar(100)". Also takes + # the opportunity to remove paths from existing filenames, since they + # shouldn't be there for security. Buggy browsers include them, + # and attachment.cgi now takes them out, but old ones need converting. + my $ref = $dbh->bz_column_info("attachments", "filename"); + if ($ref->{TYPE} ne 'varchar(100)' && $ref->{TYPE} ne 'varchar(255)') { + print "Removing paths from filenames in attachments table..."; + + my $sth + = $dbh->prepare("SELECT attach_id, filename FROM attachments " + . "WHERE " + . $dbh->sql_position(q{'/'}, 'filename') + . " > 0 OR " + . $dbh->sql_position(q{'\\\\'}, 'filename') + . " > 0"); + $sth->execute; + + while (my ($attach_id, $filename) = $sth->fetchrow_array) { + $filename =~ s/^.*[\/\\]//; + my $quoted_filename = $dbh->quote($filename); + $dbh->do("UPDATE attachments SET filename = $quoted_filename " + . "WHERE attach_id = $attach_id"); + } - print "Done.\n"; + print "Done.\n"; - $dbh->bz_alter_column("attachments", "filename", - {TYPE => 'varchar(100)', NOTNULL => 1}); - } + $dbh->bz_alter_column("attachments", "filename", + {TYPE => 'varchar(100)', NOTNULL => 1}); + } } sub _rename_votes_count_and_force_group_refresh { - my $dbh = Bugzilla->dbh; - # 2003-04-27 - bugzilla@chimpychompy.org (GavinS) - # - # Bug 180086 (http://bugzilla.mozilla.org/show_bug.cgi?id=180086) - # - # Renaming the 'count' column in the votes table because Sybase doesn't - # like it - return if !$dbh->bz_table_info('votes'); - return if $dbh->bz_column_info('votes', 'count'); - $dbh->bz_rename_column('votes', 'count', 'vote_count'); + my $dbh = Bugzilla->dbh; + + # 2003-04-27 - bugzilla@chimpychompy.org (GavinS) + # + # Bug 180086 (http://bugzilla.mozilla.org/show_bug.cgi?id=180086) + # + # Renaming the 'count' column in the votes table because Sybase doesn't + # like it + return if !$dbh->bz_table_info('votes'); + return if $dbh->bz_column_info('votes', 'count'); + $dbh->bz_rename_column('votes', 'count', 'vote_count'); } sub _fix_group_with_empty_name { - my $dbh = Bugzilla->dbh; - # 2005-01-12 Nick Barnes <nb@ravenbrook.com> bug 278010 - # Rename any group which has an empty name. - # Note that there can be at most one such group (because of - # the SQL index on the name column). - my ($emptygroupid) = $dbh->selectrow_array( - "SELECT id FROM groups where name = ''"); - if ($emptygroupid) { - # There is a group with an empty name; find a name to rename it - # as. Must avoid collisions with existing names. Start with - # group_$gid and add _<n> if necessary. - my $trycount = 0; - my $trygroupname; - my $sth = $dbh->prepare("SELECT 1 FROM groups where name = ?"); - my $name_exists = 1; - - while ($name_exists) { - $trygroupname = "group_$emptygroupid"; - if ($trycount > 0) { - $trygroupname .= "_$trycount"; - } - $name_exists = $dbh->selectrow_array($sth, undef, $trygroupname); - $trycount++; - } - $dbh->do("UPDATE groups SET name = ? WHERE id = ?", - undef, $trygroupname, $emptygroupid); - print "Group $emptygroupid had an empty name; renamed as", - " '$trygroupname'.\n"; + my $dbh = Bugzilla->dbh; + + # 2005-01-12 Nick Barnes <nb@ravenbrook.com> bug 278010 + # Rename any group which has an empty name. + # Note that there can be at most one such group (because of + # the SQL index on the name column). + my ($emptygroupid) + = $dbh->selectrow_array("SELECT id FROM groups where name = ''"); + if ($emptygroupid) { + + # There is a group with an empty name; find a name to rename it + # as. Must avoid collisions with existing names. Start with + # group_$gid and add _<n> if necessary. + my $trycount = 0; + my $trygroupname; + my $sth = $dbh->prepare("SELECT 1 FROM groups where name = ?"); + my $name_exists = 1; + + while ($name_exists) { + $trygroupname = "group_$emptygroupid"; + if ($trycount > 0) { + $trygroupname .= "_$trycount"; + } + $name_exists = $dbh->selectrow_array($sth, undef, $trygroupname); + $trycount++; } + $dbh->do("UPDATE groups SET name = ? WHERE id = ?", + undef, $trygroupname, $emptygroupid); + print "Group $emptygroupid had an empty name; renamed as", + " '$trygroupname'.\n"; + } } # A helper for the emailprefs subs below sub _clone_email_event { - my ($source, $target) = @_; - my $dbh = Bugzilla->dbh; + my ($source, $target) = @_; + my $dbh = Bugzilla->dbh; - $dbh->do("INSERT INTO email_setting (user_id, relationship, event) + $dbh->do( + "INSERT INTO email_setting (user_id, relationship, event) SELECT user_id, relationship, $target FROM email_setting - WHERE event = $source"); + WHERE event = $source" + ); } sub _migrate_email_prefs_to_new_table { - my $dbh = Bugzilla->dbh; - # 2005-03-29 - gerv@gerv.net - bug 73665. - # Migrate email preferences to new email prefs table. - if ($dbh->bz_column_info("profiles", "emailflags")) { - print "Migrating email preferences to new table...\n"; - - # These are the "roles" and "reasons" from the original code, mapped to - # the new terminology of relationships and events. - my %relationships = ("Owner" => REL_ASSIGNEE, - "Reporter" => REL_REPORTER, - "QAcontact" => REL_QA, - "CClist" => REL_CC, - # REL_VOTER was "4" before it was moved to an - # extension. - "Voter" => 4); - - my %events = ("Removeme" => EVT_ADDED_REMOVED, - "Comments" => EVT_COMMENT, - "Attachments" => EVT_ATTACHMENT, - "Status" => EVT_PROJ_MANAGEMENT, - "Resolved" => EVT_OPENED_CLOSED, - "Keywords" => EVT_KEYWORD, - "CC" => EVT_CC, - "Other" => EVT_OTHER, - "Unconfirmed" => EVT_UNCONFIRMED); - - # Request preferences - my %requestprefs = ("FlagRequestee" => EVT_FLAG_REQUESTED, - "FlagRequester" => EVT_REQUESTED_FLAG); - - # We run the below code in a transaction to speed things up. - $dbh->bz_start_transaction(); - - # Select all emailflags flag strings - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); - my $sth = $dbh->prepare("SELECT userid, emailflags FROM profiles"); - $sth->execute(); - my $i = 0; - - while (my ($userid, $flagstring) = $sth->fetchrow_array()) { - $i++; - indicate_progress({ total => $total, current => $i, every => 10 }); - # If the user has never logged in since emailprefs arrived, and the - # temporary code to give them a default string never ran, then - # $flagstring will be null. In this case, they just get all mail. - $flagstring ||= ""; - - # The 255 param is here, because without a third param, split will - # trim any trailing null fields, which causes Perl to eject lots of - # warnings. Any suitably large number would do. - my %emailflags = split(/~/, $flagstring, 255); - - my $sth2 = $dbh->prepare("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - "$userid, ?, ?)"); - foreach my $relationship (keys %relationships) { - foreach my $event (keys %events) { - my $key = "email$relationship$event"; - if (!exists($emailflags{$key}) - || $emailflags{$key} eq 'on') - { - $sth2->execute($relationships{$relationship}, - $events{$event}); - } - } - } - # Note that in the old system, the value of "excludeself" is - # assumed to be off if the preference does not exist in the - # user's list, unlike other preferences whose value is - # assumed to be on if they do not exist. - # - # This preference has changed from global to per-relationship. - if (!exists($emailflags{'ExcludeSelf'}) - || $emailflags{'ExcludeSelf'} ne 'on') - { - foreach my $relationship (keys %relationships) { - $dbh->do("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - $userid . ", " . - $relationships{$relationship}. ", " . - EVT_CHANGED_BY_ME . ")"); - } - } + my $dbh = Bugzilla->dbh; + + # 2005-03-29 - gerv@gerv.net - bug 73665. + # Migrate email preferences to new email prefs table. + if ($dbh->bz_column_info("profiles", "emailflags")) { + print "Migrating email preferences to new table...\n"; + + # These are the "roles" and "reasons" from the original code, mapped to + # the new terminology of relationships and events. + my %relationships = ( + "Owner" => REL_ASSIGNEE, + "Reporter" => REL_REPORTER, + "QAcontact" => REL_QA, + "CClist" => REL_CC, + + # REL_VOTER was "4" before it was moved to an + # extension. + "Voter" => 4 + ); - foreach my $key (keys %requestprefs) { - if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { - $dbh->do("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - $userid . ", " . REL_ANY . ", " . - $requestprefs{$key} . ")"); - } - } - } - print "\n"; + my %events = ( + "Removeme" => EVT_ADDED_REMOVED, + "Comments" => EVT_COMMENT, + "Attachments" => EVT_ATTACHMENT, + "Status" => EVT_PROJ_MANAGEMENT, + "Resolved" => EVT_OPENED_CLOSED, + "Keywords" => EVT_KEYWORD, + "CC" => EVT_CC, + "Other" => EVT_OTHER, + "Unconfirmed" => EVT_UNCONFIRMED + ); - # EVT_ATTACHMENT_DATA should initially have identical settings to - # EVT_ATTACHMENT. - _clone_email_event(EVT_ATTACHMENT, EVT_ATTACHMENT_DATA); + # Request preferences + my %requestprefs = ( + "FlagRequestee" => EVT_FLAG_REQUESTED, + "FlagRequester" => EVT_REQUESTED_FLAG + ); - $dbh->bz_commit_transaction(); - $dbh->bz_drop_column("profiles", "emailflags"); + # We run the below code in a transaction to speed things up. + $dbh->bz_start_transaction(); + + # Select all emailflags flag strings + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); + my $sth = $dbh->prepare("SELECT userid, emailflags FROM profiles"); + $sth->execute(); + my $i = 0; + + while (my ($userid, $flagstring) = $sth->fetchrow_array()) { + $i++; + indicate_progress({total => $total, current => $i, every => 10}); + + # If the user has never logged in since emailprefs arrived, and the + # temporary code to give them a default string never ran, then + # $flagstring will be null. In this case, they just get all mail. + $flagstring ||= ""; + + # The 255 param is here, because without a third param, split will + # trim any trailing null fields, which causes Perl to eject lots of + # warnings. Any suitably large number would do. + my %emailflags = split(/~/, $flagstring, 255); + + my $sth2 + = $dbh->prepare("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . "$userid, ?, ?)"); + foreach my $relationship (keys %relationships) { + foreach my $event (keys %events) { + my $key = "email$relationship$event"; + if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { + $sth2->execute($relationships{$relationship}, $events{$event}); + } + } + } + + # Note that in the old system, the value of "excludeself" is + # assumed to be off if the preference does not exist in the + # user's list, unlike other preferences whose value is + # assumed to be on if they do not exist. + # + # This preference has changed from global to per-relationship. + if (!exists($emailflags{'ExcludeSelf'}) || $emailflags{'ExcludeSelf'} ne 'on') { + foreach my $relationship (keys %relationships) { + $dbh->do("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . $userid . ", " + . $relationships{$relationship} . ", " + . EVT_CHANGED_BY_ME + . ")"); + } + } + + foreach my $key (keys %requestprefs) { + if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { + $dbh->do("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . $userid . ", " + . REL_ANY . ", " + . $requestprefs{$key} + . ")"); + } + } } + print "\n"; + + # EVT_ATTACHMENT_DATA should initially have identical settings to + # EVT_ATTACHMENT. + _clone_email_event(EVT_ATTACHMENT, EVT_ATTACHMENT_DATA); + + $dbh->bz_commit_transaction(); + $dbh->bz_drop_column("profiles", "emailflags"); + } } sub _initialize_new_email_prefs { - my $dbh = Bugzilla->dbh; - # Check for any "new" email settings that wouldn't have been ported over - # during the block above. Since these settings would have otherwise - # fallen under EVT_OTHER, we'll just clone those settings. That way if - # folks have already disabled all of that mail, there won't be any change. - my %events = ( - "Dependency Tree Changes" => EVT_DEPEND_BLOCK, - "Product/Component Changes" => EVT_COMPONENT, - ); - - foreach my $desc (keys %events) { - my $event = $events{$desc}; - my $have_events = $dbh->selectrow_array( - "SELECT 1 FROM email_setting WHERE event = $event " - . $dbh->sql_limit(1)); - - if (!$have_events) { - # No settings in the table yet, so we assume that this is the - # first time it's being set. - print "Initializing \"$desc\" email_setting ...\n"; - _clone_email_event(EVT_OTHER, $event); - } + my $dbh = Bugzilla->dbh; + + # Check for any "new" email settings that wouldn't have been ported over + # during the block above. Since these settings would have otherwise + # fallen under EVT_OTHER, we'll just clone those settings. That way if + # folks have already disabled all of that mail, there won't be any change. + my %events = ( + "Dependency Tree Changes" => EVT_DEPEND_BLOCK, + "Product/Component Changes" => EVT_COMPONENT, + ); + + foreach my $desc (keys %events) { + my $event = $events{$desc}; + my $have_events = $dbh->selectrow_array( + "SELECT 1 FROM email_setting WHERE event = $event " . $dbh->sql_limit(1)); + + if (!$have_events) { + + # No settings in the table yet, so we assume that this is the + # first time it's being set. + print "Initializing \"$desc\" email_setting ...\n"; + _clone_email_event(EVT_OTHER, $event); } + } } sub _change_all_mysql_booleans_to_tinyint { - my $dbh = Bugzilla->dbh; - # 2005-03-27: Standardize all boolean fields to plain "tinyint" - if ( $dbh->isa('Bugzilla::DB::Mysql') ) { - # This is a change to make things consistent with Schema, so we use - # direct-database access methods. - my $quip_info_sth = $dbh->column_info(undef, undef, 'quips', '%'); - my $quips_cols = $quip_info_sth->fetchall_hashref("COLUMN_NAME"); - my $approved_col = $quips_cols->{'approved'}; - if ( $approved_col->{TYPE_NAME} eq 'TINYINT' - and $approved_col->{COLUMN_SIZE} == 1 ) - { - # series.public could have been renamed to series.is_public, - # and so wouldn't need to be fixed manually. - if ($dbh->bz_column_info('series', 'public')) { - $dbh->bz_alter_column_raw('series', 'public', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '0'}); - } - $dbh->bz_alter_column_raw('bug_status', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('rep_platform', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('resolution', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('op_sys', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('bug_severity', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('priority', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('quips', 'approved', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - } - } + my $dbh = Bugzilla->dbh; + + # 2005-03-27: Standardize all boolean fields to plain "tinyint" + if ($dbh->isa('Bugzilla::DB::Mysql')) { + + # This is a change to make things consistent with Schema, so we use + # direct-database access methods. + my $quip_info_sth = $dbh->column_info(undef, undef, 'quips', '%'); + my $quips_cols = $quip_info_sth->fetchall_hashref("COLUMN_NAME"); + my $approved_col = $quips_cols->{'approved'}; + if ( $approved_col->{TYPE_NAME} eq 'TINYINT' + and $approved_col->{COLUMN_SIZE} == 1) + { + # series.public could have been renamed to series.is_public, + # and so wouldn't need to be fixed manually. + if ($dbh->bz_column_info('series', 'public')) { + $dbh->bz_alter_column_raw('series', 'public', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '0'}); + } + $dbh->bz_alter_column_raw('bug_status', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('rep_platform', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('resolution', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('op_sys', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('bug_severity', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('priority', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('quips', 'approved', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + } + } } # A helper for the below function. sub _de_dup_version { - my ($product_id, $version) = @_; - my $dbh = Bugzilla->dbh; - print "Fixing duplicate version $version in product_id $product_id...\n"; - $dbh->do('DELETE FROM versions WHERE product_id = ? AND value = ?', - undef, $product_id, $version); - $dbh->do('INSERT INTO versions (product_id, value) VALUES (?,?)', - undef, $product_id, $version); + my ($product_id, $version) = @_; + my $dbh = Bugzilla->dbh; + print "Fixing duplicate version $version in product_id $product_id...\n"; + $dbh->do('DELETE FROM versions WHERE product_id = ? AND value = ?', + undef, $product_id, $version); + $dbh->do('INSERT INTO versions (product_id, value) VALUES (?,?)', + undef, $product_id, $version); } sub _add_versions_product_id_index { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_index_info('versions', 'versions_product_id_idx')) { - my $dup_versions = $dbh->selectall_arrayref( - 'SELECT product_id, value FROM versions - GROUP BY product_id, value HAVING COUNT(value) > 1', {Slice=>{}}); - foreach my $dup_version (@$dup_versions) { - _de_dup_version($dup_version->{product_id}, $dup_version->{value}); - } - - $dbh->bz_add_index('versions', 'versions_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_index_info('versions', 'versions_product_id_idx')) { + my $dup_versions = $dbh->selectall_arrayref( + 'SELECT product_id, value FROM versions + GROUP BY product_id, value HAVING COUNT(value) > 1', {Slice => {}} + ); + foreach my $dup_version (@$dup_versions) { + _de_dup_version($dup_version->{product_id}, $dup_version->{value}); } + + $dbh->bz_add_index('versions', 'versions_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + } } sub _fix_whine_queries_title_and_op_sys_value { - my $dbh = Bugzilla->dbh; - if (!exists $dbh->bz_column_info('whine_queries', 'title')->{DEFAULT}) { - # The below change actually has nothing to do with the whine_queries - # change, it just has to be contained within a schema change so that - # it doesn't run every time we run checksetup. - - # Old Bugzillas have "other" as an OS choice, new ones have "Other" - # (capital O). - print "Setting any 'other' op_sys to 'Other'...\n"; - $dbh->do('UPDATE op_sys SET value = ? WHERE value = ?', - undef, "Other", "other"); - $dbh->do('UPDATE bugs SET op_sys = ? WHERE op_sys = ?', - undef, "Other", "other"); - if (Bugzilla->params->{'defaultopsys'} eq 'other') { - # We can't actually fix the param here, because WriteParams() will - # make $datadir/params.json unwriteable to the webservergroup. - # It's too much of an ugly hack to copy the permission-fixing code - # down to here. (It would create more potential future bugs than - # it would solve problems.) - print "WARNING: Your 'defaultopsys' param is set to 'other', but" - . " Bugzilla now\n" - . " uses 'Other' (capital O).\n"; - } - - # Add a DEFAULT to whine_queries stuff so that editwhines.cgi - # works on PostgreSQL. - $dbh->bz_alter_column('whine_queries', 'title', {TYPE => 'varchar(128)', - NOTNULL => 1, DEFAULT => "''"}); + my $dbh = Bugzilla->dbh; + if (!exists $dbh->bz_column_info('whine_queries', 'title')->{DEFAULT}) { + + # The below change actually has nothing to do with the whine_queries + # change, it just has to be contained within a schema change so that + # it doesn't run every time we run checksetup. + + # Old Bugzillas have "other" as an OS choice, new ones have "Other" + # (capital O). + print "Setting any 'other' op_sys to 'Other'...\n"; + $dbh->do('UPDATE op_sys SET value = ? WHERE value = ?', undef, "Other", + "other"); + $dbh->do('UPDATE bugs SET op_sys = ? WHERE op_sys = ?', undef, "Other", + "other"); + if (Bugzilla->params->{'defaultopsys'} eq 'other') { + + # We can't actually fix the param here, because WriteParams() will + # make $datadir/params.json unwriteable to the webservergroup. + # It's too much of an ugly hack to copy the permission-fixing code + # down to here. (It would create more potential future bugs than + # it would solve problems.) + print "WARNING: Your 'defaultopsys' param is set to 'other', but" + . " Bugzilla now\n" + . " uses 'Other' (capital O).\n"; } + + # Add a DEFAULT to whine_queries stuff so that editwhines.cgi + # works on PostgreSQL. + $dbh->bz_alter_column('whine_queries', 'title', + {TYPE => 'varchar(128)', NOTNULL => 1, DEFAULT => "''"}); + } } sub _fix_attachments_submitter_id_idx { - my $dbh = Bugzilla->dbh; - # 2005-06-29 bugreport@peshkin.net, bug 299156 - if ($dbh->bz_index_info('attachments', 'attachments_submitter_id_idx') - && (scalar(@{$dbh->bz_index_info('attachments', - 'attachments_submitter_id_idx' - )->{FIELDS}}) < 2)) - { - $dbh->bz_drop_index('attachments', 'attachments_submitter_id_idx'); - } - $dbh->bz_add_index('attachments', 'attachments_submitter_id_idx', - [qw(submitter_id bug_id)]); + my $dbh = Bugzilla->dbh; + + # 2005-06-29 bugreport@peshkin.net, bug 299156 + if ( + $dbh->bz_index_info('attachments', 'attachments_submitter_id_idx') + && ( + scalar(@{ + $dbh->bz_index_info('attachments', 'attachments_submitter_id_idx')->{FIELDS} + }) < 2 + ) + ) + { + $dbh->bz_drop_index('attachments', 'attachments_submitter_id_idx'); + } + $dbh->bz_add_index('attachments', 'attachments_submitter_id_idx', + [qw(submitter_id bug_id)]); } sub _copy_attachments_thedata_to_attach_data { - my $dbh = Bugzilla->dbh; - # 2005-08-25 - bugreport@peshkin.net - Bug 305333 - if ($dbh->bz_column_info("attachments", "thedata")) { - print "Migrating attachment data to its own table...\n"; - print "(This may take a very long time)\n"; - $dbh->do("INSERT INTO attach_data (id, thedata) - SELECT attach_id, thedata FROM attachments"); - $dbh->bz_drop_column("attachments", "thedata"); - } + my $dbh = Bugzilla->dbh; + + # 2005-08-25 - bugreport@peshkin.net - Bug 305333 + if ($dbh->bz_column_info("attachments", "thedata")) { + print "Migrating attachment data to its own table...\n"; + print "(This may take a very long time)\n"; + $dbh->do( + "INSERT INTO attach_data (id, thedata) + SELECT attach_id, thedata FROM attachments" + ); + $dbh->bz_drop_column("attachments", "thedata"); + } } sub _fix_broken_all_closed_series { - my $dbh = Bugzilla->dbh; - - # 2005-11-26 - wurblzap@gmail.com - Bug 300473 - # Repair broken automatically generated series queries for non-open bugs. - my $broken_series_indicator = - 'field0-0-0=resolution&type0-0-0=notequals&value0-0-0=---'; - my $broken_nonopen_series = - $dbh->selectall_arrayref("SELECT series_id, query FROM series - WHERE query LIKE '$broken_series_indicator%'"); - if (@$broken_nonopen_series) { - print 'Repairing broken series...'; - my $sth_nuke = - $dbh->prepare('DELETE FROM series_data WHERE series_id = ?'); - # This statement is used to repair a series by replacing the broken - # query with the correct one. - my $sth_repair = - $dbh->prepare('UPDATE series SET query = ? WHERE series_id = ?'); - # The corresponding series for open bugs look like one of these two - # variations (bug 225687 changed the order of bug states). - # This depends on the set of bug states representing open bugs not - # to have changed since series creation. - my $open_bugs_query_base_old = - join("&", map { "bug_status=" . url_quote($_) } - ('UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED')); - my $open_bugs_query_base_new = - join("&", map { "bug_status=" . url_quote($_) } - ('NEW', 'REOPENED', 'ASSIGNED', 'UNCONFIRMED')); - my $sth_openbugs_series = - $dbh->prepare("SELECT series_id FROM series WHERE query IN (?, ?)"); - # Statement to find the series which has collected the most data. - my $sth_data_collected = - $dbh->prepare('SELECT count(*) FROM series_data - WHERE series_id = ?'); - # Statement to select a broken non-open bugs count data entry. - my $sth_select_broken_nonopen_data = - $dbh->prepare('SELECT series_date, series_value FROM series_data' . - ' WHERE series_id = ?'); - # Statement to select an open bugs count data entry. - my $sth_select_open_data = - $dbh->prepare('SELECT series_value FROM series_data' . - ' WHERE series_id = ? AND series_date = ?'); - # Statement to fix a broken non-open bugs count data entry. - my $sth_fix_broken_nonopen_data = - $dbh->prepare('UPDATE series_data SET series_value = ?' . - ' WHERE series_id = ? AND series_date = ?'); - # Statement to delete an unfixable broken non-open bugs count data - # entry. - my $sth_delete_broken_nonopen_data = - $dbh->prepare('DELETE FROM series_data' . - ' WHERE series_id = ? AND series_date = ?'); - foreach (@$broken_nonopen_series) { - my ($broken_series_id, $nonopen_bugs_query) = @$_; - - # Determine the product-and-component part of the query. - if ($nonopen_bugs_query =~ /^$broken_series_indicator(.*)$/) { - my $prodcomp = $1; - - # If there is more than one series for the corresponding - # open-bugs series, we pick the one with the most data, - # which should be the one which was generated on creation. - # It's a pity we can't do subselects. - $sth_openbugs_series->execute( - $open_bugs_query_base_old . $prodcomp, - $open_bugs_query_base_new . $prodcomp); - - my ($found_open_series_id, $datacount) = (undef, -1); - foreach my $open_ser_id ($sth_openbugs_series->fetchrow_array) { - $sth_data_collected->execute($open_ser_id); - my ($this_datacount) = $sth_data_collected->fetchrow_array; - if ($this_datacount > $datacount) { - $datacount = $this_datacount; - $found_open_series_id = $open_ser_id; - } - } - - if ($found_open_series_id) { - # Move along corrupted series data and correct it. The - # corruption consists of it being the number of all bugs - # instead of the number of non-open bugs, so we calculate - # the correct count by subtracting the number of open bugs. - # If there is no corresponding open-bugs count for some - # reason (shouldn't happen), we drop the data entry. - print " $broken_series_id..."; - $sth_select_broken_nonopen_data->execute($broken_series_id); - while (my $rowref = - $sth_select_broken_nonopen_data->fetchrow_arrayref) - { - my ($date, $broken_value) = @$rowref; - my ($openbugs_value) = - $dbh->selectrow_array($sth_select_open_data, undef, - $found_open_series_id, $date); - if (defined($openbugs_value)) { - $sth_fix_broken_nonopen_data->execute - ($broken_value - $openbugs_value, - $broken_series_id, $date); - } - else { - print <<EOT; + my $dbh = Bugzilla->dbh; + + # 2005-11-26 - wurblzap@gmail.com - Bug 300473 + # Repair broken automatically generated series queries for non-open bugs. + my $broken_series_indicator + = 'field0-0-0=resolution&type0-0-0=notequals&value0-0-0=---'; + my $broken_nonopen_series = $dbh->selectall_arrayref( + "SELECT series_id, query FROM series + WHERE query LIKE '$broken_series_indicator%'" + ); + if (@$broken_nonopen_series) { + print 'Repairing broken series...'; + my $sth_nuke = $dbh->prepare('DELETE FROM series_data WHERE series_id = ?'); + + # This statement is used to repair a series by replacing the broken + # query with the correct one. + my $sth_repair + = $dbh->prepare('UPDATE series SET query = ? WHERE series_id = ?'); + + # The corresponding series for open bugs look like one of these two + # variations (bug 225687 changed the order of bug states). + # This depends on the set of bug states representing open bugs not + # to have changed since series creation. + my $open_bugs_query_base_old = join("&", + map { "bug_status=" . url_quote($_) } + ('UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED')); + my $open_bugs_query_base_new = join("&", + map { "bug_status=" . url_quote($_) } + ('NEW', 'REOPENED', 'ASSIGNED', 'UNCONFIRMED')); + my $sth_openbugs_series + = $dbh->prepare("SELECT series_id FROM series WHERE query IN (?, ?)"); + + # Statement to find the series which has collected the most data. + my $sth_data_collected = $dbh->prepare( + 'SELECT count(*) FROM series_data + WHERE series_id = ?' + ); + + # Statement to select a broken non-open bugs count data entry. + my $sth_select_broken_nonopen_data = $dbh->prepare( + 'SELECT series_date, series_value FROM series_data' . ' WHERE series_id = ?'); + + # Statement to select an open bugs count data entry. + my $sth_select_open_data = $dbh->prepare('SELECT series_value FROM series_data' + . ' WHERE series_id = ? AND series_date = ?'); + + # Statement to fix a broken non-open bugs count data entry. + my $sth_fix_broken_nonopen_data + = $dbh->prepare('UPDATE series_data SET series_value = ?' + . ' WHERE series_id = ? AND series_date = ?'); + + # Statement to delete an unfixable broken non-open bugs count data + # entry. + my $sth_delete_broken_nonopen_data = $dbh->prepare( + 'DELETE FROM series_data' . ' WHERE series_id = ? AND series_date = ?'); + foreach (@$broken_nonopen_series) { + my ($broken_series_id, $nonopen_bugs_query) = @$_; + + # Determine the product-and-component part of the query. + if ($nonopen_bugs_query =~ /^$broken_series_indicator(.*)$/) { + my $prodcomp = $1; + + # If there is more than one series for the corresponding + # open-bugs series, we pick the one with the most data, + # which should be the one which was generated on creation. + # It's a pity we can't do subselects. + $sth_openbugs_series->execute($open_bugs_query_base_old . $prodcomp, + $open_bugs_query_base_new . $prodcomp); + + my ($found_open_series_id, $datacount) = (undef, -1); + foreach my $open_ser_id ($sth_openbugs_series->fetchrow_array) { + $sth_data_collected->execute($open_ser_id); + my ($this_datacount) = $sth_data_collected->fetchrow_array; + if ($this_datacount > $datacount) { + $datacount = $this_datacount; + $found_open_series_id = $open_ser_id; + } + } + + if ($found_open_series_id) { + + # Move along corrupted series data and correct it. The + # corruption consists of it being the number of all bugs + # instead of the number of non-open bugs, so we calculate + # the correct count by subtracting the number of open bugs. + # If there is no corresponding open-bugs count for some + # reason (shouldn't happen), we drop the data entry. + print " $broken_series_id..."; + $sth_select_broken_nonopen_data->execute($broken_series_id); + while (my $rowref = $sth_select_broken_nonopen_data->fetchrow_arrayref) { + my ($date, $broken_value) = @$rowref; + my ($openbugs_value) + = $dbh->selectrow_array($sth_select_open_data, undef, $found_open_series_id, + $date); + if (defined($openbugs_value)) { + $sth_fix_broken_nonopen_data->execute($broken_value - $openbugs_value, + $broken_series_id, $date); + } + else { + print <<EOT; WARNING - During repairs of series $broken_series_id, the irreparable data entry for date $date was encountered and is being deleted. Continuing repairs... EOT - $sth_delete_broken_nonopen_data->execute - ($broken_series_id, $date); - } - } - - # Fix the broken query so that it collects correct data - # in the future. - $nonopen_bugs_query =~ - s/^$broken_series_indicator/field0-0-0=resolution&type0-0-0=regexp&value0-0-0=./; - $sth_repair->execute($nonopen_bugs_query, - $broken_series_id); - } - else { - print <<EOT; + $sth_delete_broken_nonopen_data->execute($broken_series_id, $date); + } + } + + # Fix the broken query so that it collects correct data + # in the future. + $nonopen_bugs_query + =~ s/^$broken_series_indicator/field0-0-0=resolution&type0-0-0=regexp&value0-0-0=./; + $sth_repair->execute($nonopen_bugs_query, $broken_series_id); + } + else { + print <<EOT; WARNING - Series $broken_series_id was meant to collect non-open bug counts, but it has counted all bugs instead. It cannot be repaired @@ -2733,518 +2913,576 @@ series $broken_series_id manually Continuing repairs... EOT - } # if ($found_open_series_id) - } # if ($nonopen_bugs_query =~ - } # foreach (@$broken_nonopen_series) - print " done.\n"; - } # if (@$broken_nonopen_series) + } # if ($found_open_series_id) + } # if ($nonopen_bugs_query =~ + } # foreach (@$broken_nonopen_series) + print " done.\n"; + } # if (@$broken_nonopen_series) } -# This needs to happen at two times: when we upgrade from 2.16 (thus creating +# This needs to happen at two times: when we upgrade from 2.16 (thus creating # user_group_map), and when we kill derived gruops in the DB. sub _rederive_regex_groups { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $regex_groups_exist = $dbh->selectrow_array( - "SELECT 1 FROM groups WHERE userregexp = '' " . $dbh->sql_limit(1)); - return if !$regex_groups_exist; + my $regex_groups_exist = $dbh->selectrow_array( + "SELECT 1 FROM groups WHERE userregexp = '' " . $dbh->sql_limit(1)); + return if !$regex_groups_exist; - my $regex_derivations = $dbh->selectrow_array( - 'SELECT 1 FROM user_group_map WHERE grant_type = ' . GRANT_REGEXP - . ' ' . $dbh->sql_limit(1)); - return if $regex_derivations; + my $regex_derivations + = $dbh->selectrow_array('SELECT 1 FROM user_group_map WHERE grant_type = ' + . GRANT_REGEXP . ' ' + . $dbh->sql_limit(1)); + return if $regex_derivations; - print "Deriving regex group memberships...\n"; + print "Deriving regex group memberships...\n"; - # Re-evaluate all regexps, to keep them up-to-date. - my $sth = $dbh->prepare( - "SELECT profiles.userid, profiles.login_name, groups.id, + # Re-evaluate all regexps, to keep them up-to-date. + my $sth = $dbh->prepare( + "SELECT profiles.userid, profiles.login_name, groups.id, groups.userregexp, user_group_map.group_id FROM (profiles CROSS JOIN groups) LEFT JOIN user_group_map ON user_group_map.user_id = profiles.userid AND user_group_map.group_id = groups.id AND user_group_map.grant_type = ? - WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL"); + WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL" + ); - my $sth_add = $dbh->prepare( - "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, 0, " . GRANT_REGEXP . ")"); + my $sth_add = $dbh->prepare( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, 0, " . GRANT_REGEXP . ")" + ); - my $sth_del = $dbh->prepare( - "DELETE FROM user_group_map + my $sth_del = $dbh->prepare( + "DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = 0 - AND grant_type = " . GRANT_REGEXP); + AND grant_type = " . GRANT_REGEXP + ); - $sth->execute(GRANT_REGEXP); - while (my ($uid, $login, $gid, $rexp, $present) = - $sth->fetchrow_array()) - { - if ($login =~ m/$rexp/i) { - $sth_add->execute($uid, $gid) unless $present; - } else { - $sth_del->execute($uid, $gid) if $present; - } + $sth->execute(GRANT_REGEXP); + while (my ($uid, $login, $gid, $rexp, $present) = $sth->fetchrow_array()) { + if ($login =~ m/$rexp/i) { + $sth_add->execute($uid, $gid) unless $present; + } + else { + $sth_del->execute($uid, $gid) if $present; } + } } sub _clean_control_characters_from_short_desc { - my $dbh = Bugzilla->dbh; - - # Fixup for Bug 101380 - # "Newlines, nulls, leading/trailing spaces are getting into summaries" - - my $controlchar_bugs = - $dbh->selectall_arrayref("SELECT short_desc, bug_id FROM bugs WHERE " . - $dbh->sql_regexp('short_desc', "'[[:cntrl:]]'")); - if (scalar(@$controlchar_bugs)) { - my $msg = 'Cleaning control characters from bug summaries...'; - my $found = 0; - foreach (@$controlchar_bugs) { - my ($short_desc, $bug_id) = @$_; - my $clean_short_desc = clean_text($short_desc); - if ($clean_short_desc ne $short_desc) { - print $msg if !$found; - $found = 1; - print " $bug_id..."; - $dbh->do("UPDATE bugs SET short_desc = ? WHERE bug_id = ?", - undef, $clean_short_desc, $bug_id); - } - } - print " done.\n" if $found; + my $dbh = Bugzilla->dbh; + + # Fixup for Bug 101380 + # "Newlines, nulls, leading/trailing spaces are getting into summaries" + + my $controlchar_bugs + = $dbh->selectall_arrayref("SELECT short_desc, bug_id FROM bugs WHERE " + . $dbh->sql_regexp('short_desc', "'[[:cntrl:]]'")); + if (scalar(@$controlchar_bugs)) { + my $msg = 'Cleaning control characters from bug summaries...'; + my $found = 0; + foreach (@$controlchar_bugs) { + my ($short_desc, $bug_id) = @$_; + my $clean_short_desc = clean_text($short_desc); + if ($clean_short_desc ne $short_desc) { + print $msg if !$found; + $found = 1; + print " $bug_id..."; + $dbh->do("UPDATE bugs SET short_desc = ? WHERE bug_id = ?", + undef, $clean_short_desc, $bug_id); + } } + print " done.\n" if $found; + } } sub _stop_storing_inactive_flags { - my $dbh = Bugzilla->dbh; - # 2006-03-02 LpSolit@gmail.com - Bug 322285 - # Do not store inactive flags in the DB anymore. - if ($dbh->bz_column_info('flags', 'id')->{'TYPE'} eq 'INT3') { - # We first have to remove all existing inactive flags. - if ($dbh->bz_column_info('flags', 'is_active')) { - $dbh->do('DELETE FROM flags WHERE is_active = 0'); - } + my $dbh = Bugzilla->dbh; - # Now we convert the id column to the auto_increment format. - $dbh->bz_alter_column('flags', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2006-03-02 LpSolit@gmail.com - Bug 322285 + # Do not store inactive flags in the DB anymore. + if ($dbh->bz_column_info('flags', 'id')->{'TYPE'} eq 'INT3') { - # And finally, we remove the is_active column. - $dbh->bz_drop_column('flags', 'is_active'); + # We first have to remove all existing inactive flags. + if ($dbh->bz_column_info('flags', 'is_active')) { + $dbh->do('DELETE FROM flags WHERE is_active = 0'); } + + # Now we convert the id column to the auto_increment format. + $dbh->bz_alter_column('flags', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + # And finally, we remove the is_active column. + $dbh->bz_drop_column('flags', 'is_active'); + } } sub _change_short_desc_from_mediumtext_to_varchar { - my $dbh = Bugzilla->dbh; - # short_desc should not be a mediumtext, fix anything longer than 255 chars. - if($dbh->bz_column_info('bugs', 'short_desc')->{TYPE} eq 'MEDIUMTEXT') { - # Move extremely long summaries into a comment ("from" the Reporter), - # and then truncate the summary. - my $long_summary_bugs = $dbh->selectall_arrayref( - 'SELECT bug_id, short_desc, reporter - FROM bugs WHERE CHAR_LENGTH(short_desc) > 255'); - - if (@$long_summary_bugs) { - print "\n", install_string('update_summary_truncated'); - my $comment_sth = $dbh->prepare( - 'INSERT INTO longdescs (bug_id, who, thetext, bug_when) - VALUES (?, ?, ?, NOW())'); - my $desc_sth = $dbh->prepare('UPDATE bugs SET short_desc = ? - WHERE bug_id = ?'); - my @affected_bugs; - foreach my $bug (@$long_summary_bugs) { - my ($bug_id, $summary, $reporter_id) = @$bug; - my $summary_comment = - install_string('update_summary_truncate_comment', - { summary => $summary }); - $comment_sth->execute($bug_id, $reporter_id, $summary_comment); - my $short_summary = substr($summary, 0, 252) . "..."; - $desc_sth->execute($short_summary, $bug_id); - push(@affected_bugs, $bug_id); - } - print join(', ', @affected_bugs) . "\n\n"; - } + my $dbh = Bugzilla->dbh; - $dbh->bz_alter_column('bugs', 'short_desc', {TYPE => 'varchar(255)', - NOTNULL => 1}); + # short_desc should not be a mediumtext, fix anything longer than 255 chars. + if ($dbh->bz_column_info('bugs', 'short_desc')->{TYPE} eq 'MEDIUMTEXT') { + + # Move extremely long summaries into a comment ("from" the Reporter), + # and then truncate the summary. + my $long_summary_bugs = $dbh->selectall_arrayref( + 'SELECT bug_id, short_desc, reporter + FROM bugs WHERE CHAR_LENGTH(short_desc) > 255' + ); + + if (@$long_summary_bugs) { + print "\n", install_string('update_summary_truncated'); + my $comment_sth = $dbh->prepare( + 'INSERT INTO longdescs (bug_id, who, thetext, bug_when) + VALUES (?, ?, ?, NOW())' + ); + my $desc_sth = $dbh->prepare( + 'UPDATE bugs SET short_desc = ? + WHERE bug_id = ?' + ); + my @affected_bugs; + foreach my $bug (@$long_summary_bugs) { + my ($bug_id, $summary, $reporter_id) = @$bug; + my $summary_comment + = install_string('update_summary_truncate_comment', {summary => $summary}); + $comment_sth->execute($bug_id, $reporter_id, $summary_comment); + my $short_summary = substr($summary, 0, 252) . "..."; + $desc_sth->execute($short_summary, $bug_id); + push(@affected_bugs, $bug_id); + } + print join(', ', @affected_bugs) . "\n\n"; } + + $dbh->bz_alter_column('bugs', 'short_desc', + {TYPE => 'varchar(255)', NOTNULL => 1}); + } } sub _move_namedqueries_linkinfooter_to_its_own_table { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info("namedqueries", "linkinfooter")) { - # Move link-in-footer information into a table of its own. - my $sth_read = $dbh->prepare('SELECT id, userid + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info("namedqueries", "linkinfooter")) { + + # Move link-in-footer information into a table of its own. + my $sth_read = $dbh->prepare( + 'SELECT id, userid FROM namedqueries - WHERE linkinfooter = 1'); - my $sth_write = $dbh->prepare('INSERT INTO namedqueries_link_in_footer - (namedquery_id, user_id) VALUES (?, ?)'); - $sth_read->execute(); - while (my ($id, $userid) = $sth_read->fetchrow_array()) { - $sth_write->execute($id, $userid); - } - $dbh->bz_drop_column("namedqueries", "linkinfooter"); + WHERE linkinfooter = 1' + ); + my $sth_write = $dbh->prepare( + 'INSERT INTO namedqueries_link_in_footer + (namedquery_id, user_id) VALUES (?, ?)' + ); + $sth_read->execute(); + while (my ($id, $userid) = $sth_read->fetchrow_array()) { + $sth_write->execute($id, $userid); } + $dbh->bz_drop_column("namedqueries", "linkinfooter"); + } } sub _add_classifications_sortkey { - my $dbh = Bugzilla->dbh; - # 2006-07-07 olav@bkor.dhs.org - Bug 277377 - # Add a sortkey to the classifications - if (!$dbh->bz_column_info('classifications', 'sortkey')) { - $dbh->bz_add_column('classifications', 'sortkey', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - - my $class_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM classifications ORDER BY name'); - my $sth = $dbh->prepare('UPDATE classifications SET sortkey = ? ' . - 'WHERE id = ?'); - my $sortkey = 0; - foreach my $class_id (@$class_ids) { - $sth->execute($sortkey, $class_id); - $sortkey += 100; - } + my $dbh = Bugzilla->dbh; + + # 2006-07-07 olav@bkor.dhs.org - Bug 277377 + # Add a sortkey to the classifications + if (!$dbh->bz_column_info('classifications', 'sortkey')) { + $dbh->bz_add_column('classifications', 'sortkey', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + + my $class_ids + = $dbh->selectcol_arrayref('SELECT id FROM classifications ORDER BY name'); + my $sth + = $dbh->prepare('UPDATE classifications SET sortkey = ? ' . 'WHERE id = ?'); + my $sortkey = 0; + foreach my $class_id (@$class_ids) { + $sth->execute($sortkey, $class_id); + $sortkey += 100; } + } } sub _move_data_nomail_into_db { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations()->{'datadir'}; - # 2006-07-14 karl@kornel.name - Bug 100953 - # If a nomail file exists, move its contents into the DB - $dbh->bz_add_column('profiles', 'disable_mail', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }); - if (-e "$datadir/nomail") { - # We have a data/nomail file, read it in and delete it - my %nomail; - print "Found a data/nomail file. Moving nomail entries into DB...\n"; - my $nomail_file = new IO::File("$datadir/nomail", 'r'); - while (<$nomail_file>) { - $nomail{trim($_)} = 1; - } - $nomail_file->close; + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations()->{'datadir'}; + + # 2006-07-14 karl@kornel.name - Bug 100953 + # If a nomail file exists, move its contents into the DB + $dbh->bz_add_column('profiles', 'disable_mail', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + if (-e "$datadir/nomail") { + + # We have a data/nomail file, read it in and delete it + my %nomail; + print "Found a data/nomail file. Moving nomail entries into DB...\n"; + my $nomail_file = new IO::File("$datadir/nomail", 'r'); + while (<$nomail_file>) { + $nomail{trim($_)} = 1; + } + $nomail_file->close; - # Go through each entry read. If a user exists, set disable_mail. - my $query = $dbh->prepare('UPDATE profiles + # Go through each entry read. If a user exists, set disable_mail. + my $query = $dbh->prepare( + 'UPDATE profiles SET disable_mail = 1 - WHERE userid = ?'); - foreach my $user_to_check (keys %nomail) { - my $uid = $dbh->selectrow_array( - 'SELECT userid FROM profiles WHERE login_name = ?', - undef, $user_to_check); - next if !$uid; - print "\tDisabling email for user $user_to_check\n"; - $query->execute($uid); - delete $nomail{$user_to_check}; - } - - # If there are any nomail entries remaining, move them to nomail.bad - # and say something to the user. - if (scalar(keys %nomail)) { - print "\n", install_string('update_nomail_bad', - { data => $datadir }), "\n"; - my $nomail_bad = new IO::File("$datadir/nomail.bad", '>>'); - foreach my $unknown_user (keys %nomail) { - print "\t$unknown_user\n"; - print $nomail_bad "$unknown_user\n"; - delete $nomail{$unknown_user}; - } - $nomail_bad->close; - print "\n"; - } + WHERE userid = ?' + ); + foreach my $user_to_check (keys %nomail) { + my $uid + = $dbh->selectrow_array('SELECT userid FROM profiles WHERE login_name = ?', + undef, $user_to_check); + next if !$uid; + print "\tDisabling email for user $user_to_check\n"; + $query->execute($uid); + delete $nomail{$user_to_check}; + } - # Now that we don't need it, get rid of the nomail file. - unlink "$datadir/nomail"; + # If there are any nomail entries remaining, move them to nomail.bad + # and say something to the user. + if (scalar(keys %nomail)) { + print "\n", install_string('update_nomail_bad', {data => $datadir}), "\n"; + my $nomail_bad = new IO::File("$datadir/nomail.bad", '>>'); + foreach my $unknown_user (keys %nomail) { + print "\t$unknown_user\n"; + print $nomail_bad "$unknown_user\n"; + delete $nomail{$unknown_user}; + } + $nomail_bad->close; + print "\n"; } + + # Now that we don't need it, get rid of the nomail file. + unlink "$datadir/nomail"; + } } sub _update_longdescs_who_index { - my $dbh = Bugzilla->dbh; - # When doing a search on who posted a comment, longdescs is joined - # against the bugs table. So we need an index on both of these, - # not just on "who". - my $who_index = $dbh->bz_index_info('longdescs', 'longdescs_who_idx'); - if (!$who_index || scalar @{$who_index->{FIELDS}} == 1) { - # If the index doesn't exist, this will harmlessly do nothing. - $dbh->bz_drop_index('longdescs', 'longdescs_who_idx'); - $dbh->bz_add_index('longdescs', 'longdescs_who_idx', [qw(who bug_id)]); - } + my $dbh = Bugzilla->dbh; + + # When doing a search on who posted a comment, longdescs is joined + # against the bugs table. So we need an index on both of these, + # not just on "who". + my $who_index = $dbh->bz_index_info('longdescs', 'longdescs_who_idx'); + if (!$who_index || scalar @{$who_index->{FIELDS}} == 1) { + + # If the index doesn't exist, this will harmlessly do nothing. + $dbh->bz_drop_index('longdescs', 'longdescs_who_idx'); + $dbh->bz_add_index('longdescs', 'longdescs_who_idx', [qw(who bug_id)]); + } } sub _fix_uppercase_custom_field_names { - # Before the final release of 3.0, custom fields could be - # created with mixed-case names. - my $dbh = Bugzilla->dbh; - my $fields = $dbh->selectall_arrayref( - 'SELECT name, type FROM fielddefs WHERE custom = 1'); - foreach my $row (@$fields) { - my ($name, $type) = @$row; - if ($name ne lc($name)) { - $dbh->bz_rename_column('bugs', $name, lc($name)); - $dbh->bz_rename_table($name, lc($name)) - if $type == FIELD_TYPE_SINGLE_SELECT; - $dbh->do('UPDATE fielddefs SET name = ? WHERE name = ?', - undef, lc($name), $name); - } + + # Before the final release of 3.0, custom fields could be + # created with mixed-case names. + my $dbh = Bugzilla->dbh; + my $fields = $dbh->selectall_arrayref( + 'SELECT name, type FROM fielddefs WHERE custom = 1'); + foreach my $row (@$fields) { + my ($name, $type) = @$row; + if ($name ne lc($name)) { + $dbh->bz_rename_column('bugs', $name, lc($name)); + $dbh->bz_rename_table($name, lc($name)) if $type == FIELD_TYPE_SINGLE_SELECT; + $dbh->do('UPDATE fielddefs SET name = ? WHERE name = ?', + undef, lc($name), $name); } + } } sub _fix_uppercase_index_names { - # We forgot to fix indexes in the above code. - my $dbh = Bugzilla->dbh; - my $fields = $dbh->selectcol_arrayref( - 'SELECT name FROM fielddefs WHERE type = ? AND custom = 1', - undef, FIELD_TYPE_SINGLE_SELECT); - foreach my $field (@$fields) { - my $indexes = $dbh->bz_table_indexes($field); - foreach my $name (keys %$indexes) { - next if $name eq lc($name); - my $index = $indexes->{$name}; - # Lowercase the name and everything in the definition. - my $new_name = lc($name); - my @new_fields = map {lc($_)} @{$index->{FIELDS}}; - my $new_def = {FIELDS => \@new_fields, TYPE => $index->{TYPE}}; - $new_def = \@new_fields if !$index->{TYPE}; - $dbh->bz_drop_index($field, $name); - $dbh->bz_add_index($field, $new_name, $new_def); - } + + # We forgot to fix indexes in the above code. + my $dbh = Bugzilla->dbh; + my $fields + = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs WHERE type = ? AND custom = 1', + undef, FIELD_TYPE_SINGLE_SELECT); + foreach my $field (@$fields) { + my $indexes = $dbh->bz_table_indexes($field); + foreach my $name (keys %$indexes) { + next if $name eq lc($name); + my $index = $indexes->{$name}; + + # Lowercase the name and everything in the definition. + my $new_name = lc($name); + my @new_fields = map { lc($_) } @{$index->{FIELDS}}; + my $new_def = {FIELDS => \@new_fields, TYPE => $index->{TYPE}}; + $new_def = \@new_fields if !$index->{TYPE}; + $dbh->bz_drop_index($field, $name); + $dbh->bz_add_index($field, $new_name, $new_def); } + } } sub _initialize_workflow_for_upgrade { - my $old_params = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_add_column('bug_status', 'is_open', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - # Till now, bug statuses were not customizable. Nevertheless, local - # changes are possible and so we will try to respect these changes. - # This means: get the status of bugs having a resolution different from '' - # and mark these statuses as 'closed', even if some of these statuses are - # expected to be open statuses. Bug statuses we have no information about - # are left as 'open'. - # - # We append the default list of closed statuses *unless* we detect at least - # one closed state in the DB (i.e. with is_open = 0). This would mean that - # the DB has already been updated at least once and maybe the admin decided - # that e.g. 'RESOLVED' is now an open state, in which case we don't want to - # override this attribute. At least one bug status has to be a closed state - # anyway (due to the 'duplicate_or_move_bug_status' parameter) so it's safe - # to use this criteria. - my $num_closed_states = $dbh->selectrow_array('SELECT COUNT(*) FROM bug_status - WHERE is_open = 0'); - - if (!$num_closed_states) { - my @closed_statuses = - @{$dbh->selectcol_arrayref('SELECT DISTINCT bug_status FROM bugs - WHERE resolution != ?', undef, '')}; - @closed_statuses = - map {$dbh->quote($_)} (@closed_statuses, qw(RESOLVED VERIFIED CLOSED)); - - print "Marking closed bug statuses as such...\n"; - $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value IN (' . - join(', ', @closed_statuses) . ')'); - } - - # We only populate the workflow here if we're upgrading from a version - # before 4.0 (which is where init_workflow was added). This was the - # first schema change done for 4.0, so we check this. - return if $dbh->bz_column_info('bugs_activity', 'comment_id'); - - # Populate the status_workflow table. We do nothing if the table already - # has entries. If all bug status transitions have been deleted, the - # workflow will be restored to its default schema. - my $count = $dbh->selectrow_array('SELECT COUNT(*) FROM status_workflow'); - - if (!$count) { - # Make sure the variables below are defined as - # status_workflow.require_comment cannot be NULL. - my $create = $old_params->{'commentoncreate'} || 0; - my $confirm = $old_params->{'commentonconfirm'} || 0; - my $accept = $old_params->{'commentonaccept'} || 0; - my $resolve = $old_params->{'commentonresolve'} || 0; - my $verify = $old_params->{'commentonverify'} || 0; - my $close = $old_params->{'commentonclose'} || 0; - my $reopen = $old_params->{'commentonreopen'} || 0; - # This was till recently the only way to get back to NEW for - # confirmed bugs, so we use this parameter here. - my $reassign = $old_params->{'commentonreassign'} || 0; - - # This is the default workflow for upgrading installations. - my @workflow = ([undef, 'UNCONFIRMED', $create], - [undef, 'NEW', $create], - [undef, 'ASSIGNED', $create], - ['UNCONFIRMED', 'NEW', $confirm], - ['UNCONFIRMED', 'ASSIGNED', $accept], - ['UNCONFIRMED', 'RESOLVED', $resolve], - ['NEW', 'ASSIGNED', $accept], - ['NEW', 'RESOLVED', $resolve], - ['ASSIGNED', 'NEW', $reassign], - ['ASSIGNED', 'RESOLVED', $resolve], - ['REOPENED', 'NEW', $reassign], - ['REOPENED', 'ASSIGNED', $accept], - ['REOPENED', 'RESOLVED', $resolve], - ['RESOLVED', 'UNCONFIRMED', $reopen], - ['RESOLVED', 'REOPENED', $reopen], - ['RESOLVED', 'VERIFIED', $verify], - ['RESOLVED', 'CLOSED', $close], - ['VERIFIED', 'UNCONFIRMED', $reopen], - ['VERIFIED', 'REOPENED', $reopen], - ['VERIFIED', 'CLOSED', $close], - ['CLOSED', 'UNCONFIRMED', $reopen], - ['CLOSED', 'REOPENED', $reopen]); - - print "Now filling the 'status_workflow' table with valid bug status transitions...\n"; - my $sth_select = $dbh->prepare('SELECT id FROM bug_status WHERE value = ?'); - my $sth = $dbh->prepare('INSERT INTO status_workflow (old_status, new_status, - require_comment) VALUES (?, ?, ?)'); - - foreach my $transition (@workflow) { - my ($from, $to); - # If it's an initial state, there is no "old" value. - $from = $dbh->selectrow_array($sth_select, undef, $transition->[0]) - if $transition->[0]; - $to = $dbh->selectrow_array($sth_select, undef, $transition->[1]); - # If one of the bug statuses doesn't exist, the transition is invalid. - next if (($transition->[0] && !$from) || !$to); - - $sth->execute($from, $to, $transition->[2] ? 1 : 0); - } - } + my $old_params = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_add_column('bug_status', 'is_open', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + # Till now, bug statuses were not customizable. Nevertheless, local + # changes are possible and so we will try to respect these changes. + # This means: get the status of bugs having a resolution different from '' + # and mark these statuses as 'closed', even if some of these statuses are + # expected to be open statuses. Bug statuses we have no information about + # are left as 'open'. + # + # We append the default list of closed statuses *unless* we detect at least + # one closed state in the DB (i.e. with is_open = 0). This would mean that + # the DB has already been updated at least once and maybe the admin decided + # that e.g. 'RESOLVED' is now an open state, in which case we don't want to + # override this attribute. At least one bug status has to be a closed state + # anyway (due to the 'duplicate_or_move_bug_status' parameter) so it's safe + # to use this criteria. + my $num_closed_states = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM bug_status + WHERE is_open = 0' + ); + + if (!$num_closed_states) { + my @closed_statuses = @{ + $dbh->selectcol_arrayref( + 'SELECT DISTINCT bug_status FROM bugs + WHERE resolution != ?', undef, '' + ) + }; + @closed_statuses + = map { $dbh->quote($_) } (@closed_statuses, qw(RESOLVED VERIFIED CLOSED)); + + print "Marking closed bug statuses as such...\n"; + $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value IN (' + . join(', ', @closed_statuses) + . ')'); + } + + # We only populate the workflow here if we're upgrading from a version + # before 4.0 (which is where init_workflow was added). This was the + # first schema change done for 4.0, so we check this. + return if $dbh->bz_column_info('bugs_activity', 'comment_id'); + + # Populate the status_workflow table. We do nothing if the table already + # has entries. If all bug status transitions have been deleted, the + # workflow will be restored to its default schema. + my $count = $dbh->selectrow_array('SELECT COUNT(*) FROM status_workflow'); + + if (!$count) { + + # Make sure the variables below are defined as + # status_workflow.require_comment cannot be NULL. + my $create = $old_params->{'commentoncreate'} || 0; + my $confirm = $old_params->{'commentonconfirm'} || 0; + my $accept = $old_params->{'commentonaccept'} || 0; + my $resolve = $old_params->{'commentonresolve'} || 0; + my $verify = $old_params->{'commentonverify'} || 0; + my $close = $old_params->{'commentonclose'} || 0; + my $reopen = $old_params->{'commentonreopen'} || 0; + + # This was till recently the only way to get back to NEW for + # confirmed bugs, so we use this parameter here. + my $reassign = $old_params->{'commentonreassign'} || 0; + + # This is the default workflow for upgrading installations. + my @workflow = ( + [undef, 'UNCONFIRMED', $create], + [undef, 'NEW', $create], + [undef, 'ASSIGNED', $create], + ['UNCONFIRMED', 'NEW', $confirm], + ['UNCONFIRMED', 'ASSIGNED', $accept], + ['UNCONFIRMED', 'RESOLVED', $resolve], + ['NEW', 'ASSIGNED', $accept], + ['NEW', 'RESOLVED', $resolve], + ['ASSIGNED', 'NEW', $reassign], + ['ASSIGNED', 'RESOLVED', $resolve], + ['REOPENED', 'NEW', $reassign], + ['REOPENED', 'ASSIGNED', $accept], + ['REOPENED', 'RESOLVED', $resolve], + ['RESOLVED', 'UNCONFIRMED', $reopen], + ['RESOLVED', 'REOPENED', $reopen], + ['RESOLVED', 'VERIFIED', $verify], + ['RESOLVED', 'CLOSED', $close], + ['VERIFIED', 'UNCONFIRMED', $reopen], + ['VERIFIED', 'REOPENED', $reopen], + ['VERIFIED', 'CLOSED', $close], + ['CLOSED', 'UNCONFIRMED', $reopen], + ['CLOSED', 'REOPENED', $reopen] + ); - # Make sure the bug status used by the 'duplicate_or_move_bug_status' - # parameter has all the required transitions set. - my $dup_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; - my $status_id = $dbh->selectrow_array( - 'SELECT id FROM bug_status WHERE value = ?', undef, $dup_status); - # There's a minor chance that this status isn't in the DB. - $status_id || return; + print + "Now filling the 'status_workflow' table with valid bug status transitions...\n"; + my $sth_select = $dbh->prepare('SELECT id FROM bug_status WHERE value = ?'); + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow (old_status, new_status, + require_comment) VALUES (?, ?, ?)' + ); - my $missing_statuses = $dbh->selectcol_arrayref( - 'SELECT id FROM bug_status - LEFT JOIN status_workflow ON old_status = id - AND new_status = ? - WHERE old_status IS NULL', undef, $status_id); + foreach my $transition (@workflow) { + my ($from, $to); - my $sth = $dbh->prepare('INSERT INTO status_workflow - (old_status, new_status) VALUES (?, ?)'); + # If it's an initial state, there is no "old" value. + $from = $dbh->selectrow_array($sth_select, undef, $transition->[0]) + if $transition->[0]; + $to = $dbh->selectrow_array($sth_select, undef, $transition->[1]); - foreach my $old_status_id (@$missing_statuses) { - next if ($old_status_id == $status_id); - $sth->execute($old_status_id, $status_id); + # If one of the bug statuses doesn't exist, the transition is invalid. + next if (($transition->[0] && !$from) || !$to); + + $sth->execute($from, $to, $transition->[2] ? 1 : 0); } + } + + # Make sure the bug status used by the 'duplicate_or_move_bug_status' + # parameter has all the required transitions set. + my $dup_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; + my $status_id + = $dbh->selectrow_array('SELECT id FROM bug_status WHERE value = ?', + undef, $dup_status); + + # There's a minor chance that this status isn't in the DB. + $status_id || return; + + my $missing_statuses = $dbh->selectcol_arrayref( + 'SELECT id FROM bug_status + LEFT JOIN status_workflow ON old_status = id + AND new_status = ? + WHERE old_status IS NULL', undef, $status_id + ); + + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow + (old_status, new_status) VALUES (?, ?)' + ); + + foreach my $old_status_id (@$missing_statuses) { + next if ($old_status_id == $status_id); + $sth->execute($old_status_id, $status_id); + } } sub _make_lang_setting_dynamic { - my $dbh = Bugzilla->dbh; - my $count = $dbh->selectrow_array(q{SELECT 1 FROM setting + my $dbh = Bugzilla->dbh; + my $count = $dbh->selectrow_array( + q{SELECT 1 FROM setting WHERE name = 'lang' - AND subclass IS NULL}); - if ($count) { - $dbh->do(q{UPDATE setting SET subclass = 'Lang' WHERE name = 'lang'}); - $dbh->do(q{DELETE FROM setting_value WHERE name = 'lang'}); - } + AND subclass IS NULL} + ); + if ($count) { + $dbh->do(q{UPDATE setting SET subclass = 'Lang' WHERE name = 'lang'}); + $dbh->do(q{DELETE FROM setting_value WHERE name = 'lang'}); + } } sub _fix_attachment_modification_date { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('attachments', 'modification_time')) { - # Allow NULL values till the modification time has been set. - $dbh->bz_add_column('attachments', 'modification_time', {TYPE => 'DATETIME'}); + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('attachments', 'modification_time')) { - print "Setting the modification time for attachments...\n"; - $dbh->do('UPDATE attachments SET modification_time = creation_ts'); + # Allow NULL values till the modification time has been set. + $dbh->bz_add_column('attachments', 'modification_time', {TYPE => 'DATETIME'}); - # Now force values to be always defined. - $dbh->bz_alter_column('attachments', 'modification_time', - {TYPE => 'DATETIME', NOTNULL => 1}); + print "Setting the modification time for attachments...\n"; + $dbh->do('UPDATE attachments SET modification_time = creation_ts'); - # Update the modification time for attachments which have been modified. - my $attachments = - $dbh->selectall_arrayref('SELECT attach_id, MAX(bug_when) FROM bugs_activity - WHERE attach_id IS NOT NULL ' . - $dbh->sql_group_by('attach_id')); + # Now force values to be always defined. + $dbh->bz_alter_column('attachments', 'modification_time', + {TYPE => 'DATETIME', NOTNULL => 1}); - my $sth = $dbh->prepare('UPDATE attachments SET modification_time = ? - WHERE attach_id = ?'); - $sth->execute($_->[1], $_->[0]) foreach (@$attachments); - } - # We add this here to be sure to have the index being added, due to the original - # patch omitting it. - $dbh->bz_add_index('attachments', 'attachments_modification_time_idx', - [qw(modification_time)]); + # Update the modification time for attachments which have been modified. + my $attachments = $dbh->selectall_arrayref( + 'SELECT attach_id, MAX(bug_when) FROM bugs_activity + WHERE attach_id IS NOT NULL ' + . $dbh->sql_group_by('attach_id') + ); + + my $sth = $dbh->prepare( + 'UPDATE attachments SET modification_time = ? + WHERE attach_id = ?' + ); + $sth->execute($_->[1], $_->[0]) foreach (@$attachments); + } + + # We add this here to be sure to have the index being added, due to the original + # patch omitting it. + $dbh->bz_add_index('attachments', 'attachments_modification_time_idx', + [qw(modification_time)]); } sub _change_text_types { - my $dbh = Bugzilla->dbh; - return if - $dbh->bz_column_info('namedqueries', 'query')->{TYPE} eq 'LONGTEXT'; - _check_content_length('attachments', 'mimetype', 255, 'attach_id'); - _check_content_length('fielddefs', 'description', 255, 'id'); - _check_content_length('attachments', 'description', 255, 'attach_id'); - - $dbh->bz_alter_column('bugs', 'bug_file_loc', - { TYPE => 'MEDIUMTEXT'}); - $dbh->bz_alter_column('longdescs', 'thetext', - { TYPE => 'LONGTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('attachments', 'description', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('attachments', 'mimetype', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - # This also changes NULL to NOT NULL. - $dbh->bz_alter_column('flagtypes', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); - $dbh->bz_alter_column('fielddefs', 'description', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('groups', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('namedqueries', 'query', - { TYPE => 'LONGTEXT', NOTNULL => 1 }); - -} + my $dbh = Bugzilla->dbh; + return if $dbh->bz_column_info('namedqueries', 'query')->{TYPE} eq 'LONGTEXT'; + _check_content_length('attachments', 'mimetype', 255, 'attach_id'); + _check_content_length('fielddefs', 'description', 255, 'id'); + _check_content_length('attachments', 'description', 255, 'attach_id'); + + $dbh->bz_alter_column('bugs', 'bug_file_loc', {TYPE => 'MEDIUMTEXT'}); + $dbh->bz_alter_column('longdescs', 'thetext', + {TYPE => 'LONGTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('attachments', 'description', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('attachments', 'mimetype', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + + # This also changes NULL to NOT NULL. + $dbh->bz_alter_column('flagtypes', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + $dbh->bz_alter_column('fielddefs', 'description', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('groups', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('namedqueries', 'query', + {TYPE => 'LONGTEXT', NOTNULL => 1}); + +} sub _check_content_length { - my ($table_name, $field_name, $max_length, $id_field) = @_; - my $dbh = Bugzilla->dbh; - my %contents = @{ $dbh->selectcol_arrayref( - "SELECT $id_field, $field_name FROM $table_name - WHERE CHAR_LENGTH($field_name) > ?", {Columns=>[1,2]}, $max_length) }; - - if (scalar keys %contents) { - my $error = install_string('install_data_too_long', - { column => $field_name, - id_column => $id_field, - table => $table_name, - max_length => $max_length }); - foreach my $id (keys %contents) { - my $string = $contents{$id}; - # Don't dump the whole string--it could be 16MB. - if (length($string) > 80) { - $string = substr($string, 0, 30) . "..." - . substr($string, -30) . "\n"; - } - $error .= "$id: $string\n"; - } - die $error; + my ($table_name, $field_name, $max_length, $id_field) = @_; + my $dbh = Bugzilla->dbh; + my %contents = @{ + $dbh->selectcol_arrayref( + "SELECT $id_field, $field_name FROM $table_name + WHERE CHAR_LENGTH($field_name) > ?", {Columns => [1, 2]}, $max_length + ) + }; + + if (scalar keys %contents) { + my $error = install_string( + 'install_data_too_long', + { + column => $field_name, + id_column => $id_field, + table => $table_name, + max_length => $max_length + } + ); + foreach my $id (keys %contents) { + my $string = $contents{$id}; + + # Don't dump the whole string--it could be 16MB. + if (length($string) > 80) { + $string = substr($string, 0, 30) . "..." . substr($string, -30) . "\n"; + } + $error .= "$id: $string\n"; } + die $error; + } } sub _add_foreign_keys_to_multiselects { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $names = $dbh->selectcol_arrayref( - 'SELECT name + my $names = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs - WHERE type = ' . FIELD_TYPE_MULTI_SELECT); + WHERE type = ' . FIELD_TYPE_MULTI_SELECT + ); - foreach my $name (@$names) { - $dbh->bz_add_fk("bug_$name", "bug_id", - {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}); - - $dbh->bz_add_fk("bug_$name", "value", - {TABLE => $name, COLUMN => 'value', DELETE => 'RESTRICT'}); - } + foreach my $name (@$names) { + $dbh->bz_add_fk("bug_$name", "bug_id", + {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}); + + $dbh->bz_add_fk("bug_$name", "value", + {TABLE => $name, COLUMN => 'value', DELETE => 'RESTRICT'}); + } } # This subroutine is used in multiple places (for times when we update @@ -3252,693 +3490,736 @@ sub _add_foreign_keys_to_multiselects { # it to update bugs_fulltext for those bug_ids instead of populating the # whole table. sub _populate_bugs_fulltext { - my $bug_ids = shift; - my $dbh = Bugzilla->dbh; - my $fulltext = $dbh->selectrow_array('SELECT 1 FROM bugs_fulltext ' - . $dbh->sql_limit(1)); - # We only populate the table if it's empty or if we've been given a - # set of bug ids. - if ($bug_ids or !$fulltext) { - $bug_ids ||= $dbh->selectcol_arrayref('SELECT bug_id FROM bugs'); - # If there are no bugs in the bugs table, there's nothing to populate. - return if !@$bug_ids; - my $num_bugs = scalar @$bug_ids; - - my $command = "INSERT"; - my $where = ""; - if ($fulltext) { - print "Updating bugs_fulltext for $num_bugs bugs...\n"; - $where = "WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids); - # It turns out that doing a REPLACE INTO is up to 10x faster - # than any other possible method of updating the table, in MySQL, - # which matters a LOT for large installations. - if ($dbh->isa('Bugzilla::DB::Mysql')) { - $command = "REPLACE"; - } - else { - $dbh->do("DELETE FROM bugs_fulltext WHERE " - . $dbh->sql_in('bug_id', $bug_ids)); - } - } - else { - print "Populating bugs_fulltext with $num_bugs entries..."; - print " (this can take a long time.)\n"; - } - my $newline = $dbh->quote("\n"); - $dbh->do( - qq{$command INTO bugs_fulltext (bug_id, short_desc, comments, + my $bug_ids = shift; + my $dbh = Bugzilla->dbh; + my $fulltext + = $dbh->selectrow_array('SELECT 1 FROM bugs_fulltext ' . $dbh->sql_limit(1)); + + # We only populate the table if it's empty or if we've been given a + # set of bug ids. + if ($bug_ids or !$fulltext) { + $bug_ids ||= $dbh->selectcol_arrayref('SELECT bug_id FROM bugs'); + + # If there are no bugs in the bugs table, there's nothing to populate. + return if !@$bug_ids; + my $num_bugs = scalar @$bug_ids; + + my $command = "INSERT"; + my $where = ""; + if ($fulltext) { + print "Updating bugs_fulltext for $num_bugs bugs...\n"; + $where = "WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids); + + # It turns out that doing a REPLACE INTO is up to 10x faster + # than any other possible method of updating the table, in MySQL, + # which matters a LOT for large installations. + if ($dbh->isa('Bugzilla::DB::Mysql')) { + $command = "REPLACE"; + } + else { + $dbh->do("DELETE FROM bugs_fulltext WHERE " . $dbh->sql_in('bug_id', $bug_ids)); + } + } + else { + print "Populating bugs_fulltext with $num_bugs entries..."; + print " (this can take a long time.)\n"; + } + my $newline = $dbh->quote("\n"); + $dbh->do( + qq{$command INTO bugs_fulltext (bug_id, short_desc, comments, comments_noprivate) SELECT bugs.bug_id, bugs.short_desc, } - . $dbh->sql_group_concat('longdescs.thetext', $newline, 0) - . ', ' . $dbh->sql_group_concat('nopriv.thetext', $newline, 0) . - qq{ FROM bugs + . $dbh->sql_group_concat('longdescs.thetext', $newline, 0) . ', ' + . $dbh->sql_group_concat('nopriv.thetext', $newline, 0) + . qq{ FROM bugs LEFT JOIN longdescs ON bugs.bug_id = longdescs.bug_id LEFT JOIN longdescs AS nopriv ON longdescs.comment_id = nopriv.comment_id AND nopriv.isprivate = 0 $where } - . $dbh->sql_group_by('bugs.bug_id', 'bugs.short_desc')); - } + . $dbh->sql_group_by('bugs.bug_id', 'bugs.short_desc') + ); + } } sub _fix_illegal_flag_modification_dates { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $rows = $dbh->do('UPDATE flags SET modification_date = creation_date - WHERE modification_date < creation_date'); - # If no rows are affected, $dbh->do returns 0E0 instead of 0. - print "$rows flags had an illegal modification date. Fixed!\n" if ($rows =~ /^\d+$/); + my $rows = $dbh->do( + 'UPDATE flags SET modification_date = creation_date + WHERE modification_date < creation_date' + ); + + # If no rows are affected, $dbh->do returns 0E0 instead of 0. + print "$rows flags had an illegal modification date. Fixed!\n" + if ($rows =~ /^\d+$/); } sub _add_visiblity_value_to_value_tables { - my $dbh = Bugzilla->dbh; - my @standard_fields = - qw(bug_status resolution priority bug_severity op_sys rep_platform); - my $custom_fields = $dbh->selectcol_arrayref( - 'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)', - undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT); - foreach my $field (@standard_fields, @$custom_fields) { - $dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'}); - $dbh->bz_add_index($field, "${field}_visibility_value_id_idx", - ['visibility_value_id']); - } + my $dbh = Bugzilla->dbh; + my @standard_fields + = qw(bug_status resolution priority bug_severity op_sys rep_platform); + my $custom_fields + = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)', + undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT); + foreach my $field (@standard_fields, @$custom_fields) { + $dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'}); + $dbh->bz_add_index($field, "${field}_visibility_value_id_idx", + ['visibility_value_id']); + } } sub _add_extern_id_index { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_index_info('profiles', 'profiles_extern_id_idx')) { - # Some Bugzillas have a multiple empty strings in extern_id, - # which need to be converted to NULLs before we add the index. - $dbh->do("UPDATE profiles SET extern_id = NULL WHERE extern_id = ''"); - $dbh->bz_add_index('profiles', 'profiles_extern_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]}); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_index_info('profiles', 'profiles_extern_id_idx')) { + + # Some Bugzillas have a multiple empty strings in extern_id, + # which need to be converted to NULLs before we add the index. + $dbh->do("UPDATE profiles SET extern_id = NULL WHERE extern_id = ''"); + $dbh->bz_add_index('profiles', 'profiles_extern_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]}); + } } sub _convert_disallownew_to_isactive { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('products', 'disallownew')){ - $dbh->bz_add_column('products', 'isactive', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - # isactive is the boolean reverse of disallownew. - $dbh->do('UPDATE products SET isactive = 0 WHERE disallownew = 1'); - $dbh->do('UPDATE products SET isactive = 1 WHERE disallownew = 0'); - - $dbh->bz_drop_column('products','disallownew'); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info('products', 'disallownew')) { + $dbh->bz_add_column('products', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + # isactive is the boolean reverse of disallownew. + $dbh->do('UPDATE products SET isactive = 0 WHERE disallownew = 1'); + $dbh->do('UPDATE products SET isactive = 1 WHERE disallownew = 0'); + + $dbh->bz_drop_column('products', 'disallownew'); + } } sub _fix_logincookies_ipaddr { - my $dbh = Bugzilla->dbh; - return if !$dbh->bz_column_info('logincookies', 'ipaddr')->{NOTNULL}; + my $dbh = Bugzilla->dbh; + return if !$dbh->bz_column_info('logincookies', 'ipaddr')->{NOTNULL}; - $dbh->bz_alter_column('logincookies', 'ipaddr', {TYPE => 'varchar(40)'}); - $dbh->do('UPDATE logincookies SET ipaddr = NULL WHERE ipaddr = ?', - undef, '0.0.0.0'); + $dbh->bz_alter_column('logincookies', 'ipaddr', {TYPE => 'varchar(40)'}); + $dbh->do('UPDATE logincookies SET ipaddr = NULL WHERE ipaddr = ?', + undef, '0.0.0.0'); } sub _fix_invalid_custom_field_names { - my $fields = Bugzilla->fields({ custom => 1 }); + my $fields = Bugzilla->fields({custom => 1}); - foreach my $field (@$fields) { - next if $field->name =~ /^[a-zA-Z0-9_]+$/; - # The field name is illegal and can break the DB. Kill the field! - $field->set_obsolete(1); - print install_string('update_cf_invalid_name', - { field => $field->name }), "\n"; - eval { $field->remove_from_db(); }; - warn $@ if $@; - } + foreach my $field (@$fields) { + next if $field->name =~ /^[a-zA-Z0-9_]+$/; + + # The field name is illegal and can break the DB. Kill the field! + $field->set_obsolete(1); + print install_string('update_cf_invalid_name', {field => $field->name}), "\n"; + eval { $field->remove_from_db(); }; + warn $@ if $@; + } } sub _set_attachment_comment_type { - my ($type, $string) = @_; - my $dbh = Bugzilla->dbh; - # We check if there are any comments of this type already, first, - # because this is faster than a full LIKE search on the comments, - # and currently this will run every time we run checksetup. - my $test = $dbh->selectrow_array( - "SELECT 1 FROM longdescs WHERE type = $type " . $dbh->sql_limit(1)); - return [] if $test; - my %comments = @{ $dbh->selectcol_arrayref( - "SELECT comment_id, thetext FROM longdescs - WHERE thetext LIKE '$string%'", - {Columns=>[1,2]}) }; - my @comment_ids = keys %comments; - return [] if !scalar @comment_ids; - my $what = "update"; + my ($type, $string) = @_; + my $dbh = Bugzilla->dbh; + + # We check if there are any comments of this type already, first, + # because this is faster than a full LIKE search on the comments, + # and currently this will run every time we run checksetup. + my $test = $dbh->selectrow_array( + "SELECT 1 FROM longdescs WHERE type = $type " . $dbh->sql_limit(1)); + return [] if $test; + my %comments = @{ + $dbh->selectcol_arrayref( + "SELECT comment_id, thetext FROM longdescs + WHERE thetext LIKE '$string%'", {Columns => [1, 2]} + ) + }; + my @comment_ids = keys %comments; + return [] if !scalar @comment_ids; + my $what = "update"; + if ($type == CMT_ATTACHMENT_CREATED) { + $what = "creation"; + } + print "Setting the type field on attachment $what comments...\n"; + my $sth = $dbh->prepare( + 'UPDATE longdescs SET thetext = ?, type = ?, extra_data = ? + WHERE comment_id = ?' + ); + my $count = 0; + my $total = scalar @comment_ids; + foreach my $id (@comment_ids) { + $count++; + my $text = $comments{$id}; + next if $text !~ /^\Q$string\E(\d+)/; + my $attachment_id = $1; + my @lines = split("\n", $text); if ($type == CMT_ATTACHMENT_CREATED) { - $what = "creation"; + + # Now we have to remove the text up until we find a line that's + # just a single newline, because the old "Created an attachment" + # text included the attachment description underneath it, and in + # Bugzillas before 2.20, that could be wrapped into multiple lines, + # in the database. + while (1) { + my $line = shift @lines; + last if (!defined $line or trim($line) eq ''); + } } - print "Setting the type field on attachment $what comments...\n"; - my $sth = $dbh->prepare( - 'UPDATE longdescs SET thetext = ?, type = ?, extra_data = ? - WHERE comment_id = ?'); - my $count = 0; - my $total = scalar @comment_ids; - foreach my $id (@comment_ids) { - $count++; - my $text = $comments{$id}; - next if $text !~ /^\Q$string\E(\d+)/; - my $attachment_id = $1; - my @lines = split("\n", $text); - if ($type == CMT_ATTACHMENT_CREATED) { - # Now we have to remove the text up until we find a line that's - # just a single newline, because the old "Created an attachment" - # text included the attachment description underneath it, and in - # Bugzillas before 2.20, that could be wrapped into multiple lines, - # in the database. - while (1) { - my $line = shift @lines; - last if (!defined $line or trim($line) eq ''); - } - } - else { - # However, the "From update of attachment" line is always just - # one line--the first line of the comment. - shift @lines; - } - $text = join("\n", @lines); - $sth->execute($text, $type, $attachment_id, $id); - indicate_progress({ total => $total, current => $count, - every => 25 }); + else { + # However, the "From update of attachment" line is always just + # one line--the first line of the comment. + shift @lines; } - return \@comment_ids; + $text = join("\n", @lines); + $sth->execute($text, $type, $attachment_id, $id); + indicate_progress({total => $total, current => $count, every => 25}); + } + return \@comment_ids; } sub _set_attachment_comment_types { - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $created_ids = _set_attachment_comment_type( - CMT_ATTACHMENT_CREATED, 'Created an attachment (id='); - my $updated_ids = _set_attachment_comment_type( - CMT_ATTACHMENT_UPDATED, '(From update of attachment '); - $dbh->bz_commit_transaction(); - return unless (@$created_ids or @$updated_ids); - - my @comment_ids = (@$created_ids, @$updated_ids); - - my $bug_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT bug_id FROM longdescs WHERE ' - . $dbh->sql_in('comment_id', \@comment_ids)); - _populate_bugs_fulltext($bug_ids); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $created_ids = _set_attachment_comment_type(CMT_ATTACHMENT_CREATED, + 'Created an attachment (id='); + my $updated_ids = _set_attachment_comment_type(CMT_ATTACHMENT_UPDATED, + '(From update of attachment '); + $dbh->bz_commit_transaction(); + return unless (@$created_ids or @$updated_ids); + + my @comment_ids = (@$created_ids, @$updated_ids); + + my $bug_ids + = $dbh->selectcol_arrayref('SELECT DISTINCT bug_id FROM longdescs WHERE ' + . $dbh->sql_in('comment_id', \@comment_ids)); + _populate_bugs_fulltext($bug_ids); } sub _add_allows_unconfirmed_to_product_table { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('products', 'allows_unconfirmed')) { - $dbh->bz_add_column('products', 'allows_unconfirmed', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }); - if ($dbh->bz_column_info('products', 'votestoconfirm')) { - $dbh->do('UPDATE products SET allows_unconfirmed = 1 - WHERE votestoconfirm > 0'); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('products', 'allows_unconfirmed')) { + $dbh->bz_add_column('products', 'allows_unconfirmed', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + if ($dbh->bz_column_info('products', 'votestoconfirm')) { + $dbh->do( + 'UPDATE products SET allows_unconfirmed = 1 + WHERE votestoconfirm > 0' + ); } + } } sub _convert_flagtypes_fks_to_set_null { - my $dbh = Bugzilla->dbh; - foreach my $column (qw(request_group_id grant_group_id)) { - my $fk = $dbh->bz_fk_info('flagtypes', $column); - if ($fk and !defined $fk->{DELETE}) { - $fk->{DELETE} = 'SET NULL'; - $dbh->bz_alter_fk('flagtypes', $column, $fk); - } + my $dbh = Bugzilla->dbh; + foreach my $column (qw(request_group_id grant_group_id)) { + my $fk = $dbh->bz_fk_info('flagtypes', $column); + if ($fk and !defined $fk->{DELETE}) { + $fk->{DELETE} = 'SET NULL'; + $dbh->bz_alter_fk('flagtypes', $column, $fk); } + } } sub _fix_decimal_types { - my $dbh = Bugzilla->dbh; - my $type = {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}; - $dbh->bz_alter_column('bugs', 'estimated_time', $type); - $dbh->bz_alter_column('bugs', 'remaining_time', $type); - $dbh->bz_alter_column('longdescs', 'work_time', $type); + my $dbh = Bugzilla->dbh; + my $type = {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}; + $dbh->bz_alter_column('bugs', 'estimated_time', $type); + $dbh->bz_alter_column('bugs', 'remaining_time', $type); + $dbh->bz_alter_column('longdescs', 'work_time', $type); } sub _fix_series_creator_fk { - my $dbh = Bugzilla->dbh; - my $fk = $dbh->bz_fk_info('series', 'creator'); - if ($fk and $fk->{DELETE} eq 'SET NULL') { - $fk->{DELETE} = 'CASCADE'; - $dbh->bz_alter_fk('series', 'creator', $fk); - } + my $dbh = Bugzilla->dbh; + my $fk = $dbh->bz_fk_info('series', 'creator'); + if ($fk and $fk->{DELETE} eq 'SET NULL') { + $fk->{DELETE} = 'CASCADE'; + $dbh->bz_alter_fk('series', 'creator', $fk); + } } sub _remove_attachment_isurl { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('attachments', 'isurl')) { - # Now all attachments must have a filename. - $dbh->do('UPDATE attachments SET filename = ? WHERE isurl = 1', - undef, 'url.txt'); - $dbh->bz_drop_column('attachments', 'isurl'); - $dbh->do("DELETE FROM fielddefs WHERE name='attachments.isurl'"); - } + if ($dbh->bz_column_info('attachments', 'isurl')) { + + # Now all attachments must have a filename. + $dbh->do('UPDATE attachments SET filename = ? WHERE isurl = 1', + undef, 'url.txt'); + $dbh->bz_drop_column('attachments', 'isurl'); + $dbh->do("DELETE FROM fielddefs WHERE name='attachments.isurl'"); + } } sub _add_isactive_to_product_fields { - my $dbh = Bugzilla->dbh; - - # If we add the isactive column all values should start off as active - if (!$dbh->bz_column_info('components', 'isactive')) { - $dbh->bz_add_column('components', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } - - if (!$dbh->bz_column_info('versions', 'isactive')) { - $dbh->bz_add_column('versions', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } - - if (!$dbh->bz_column_info('milestones', 'isactive')) { - $dbh->bz_add_column('milestones', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } + my $dbh = Bugzilla->dbh; + + # If we add the isactive column all values should start off as active + if (!$dbh->bz_column_info('components', 'isactive')) { + $dbh->bz_add_column('components', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } + + if (!$dbh->bz_column_info('versions', 'isactive')) { + $dbh->bz_add_column('versions', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } + + if (!$dbh->bz_column_info('milestones', 'isactive')) { + $dbh->bz_add_column('milestones', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } } sub _migrate_field_visibility_value { - my $dbh = Bugzilla->dbh; - - if ($dbh->bz_column_info('fielddefs', 'visibility_value_id')) { - print "Populating new field_visibility table...\n"; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + if ($dbh->bz_column_info('fielddefs', 'visibility_value_id')) { + print "Populating new field_visibility table...\n"; - my %results = - @{ $dbh->selectcol_arrayref( - "SELECT id, visibility_value_id FROM fielddefs - WHERE visibility_value_id IS NOT NULL", - { Columns => [1,2] }) }; + $dbh->bz_start_transaction(); - my $insert_sth = - $dbh->prepare("INSERT INTO field_visibility (field_id, value_id) - VALUES (?, ?)"); + my %results = @{ + $dbh->selectcol_arrayref( + "SELECT id, visibility_value_id FROM fielddefs + WHERE visibility_value_id IS NOT NULL", {Columns => [1, 2]} + ) + }; - foreach my $id (keys %results) { - $insert_sth->execute($id, $results{$id}); - } + my $insert_sth = $dbh->prepare( + "INSERT INTO field_visibility (field_id, value_id) + VALUES (?, ?)" + ); - $dbh->bz_commit_transaction(); - $dbh->bz_drop_column('fielddefs', 'visibility_value_id'); + foreach my $id (keys %results) { + $insert_sth->execute($id, $results{$id}); } + + $dbh->bz_commit_transaction(); + $dbh->bz_drop_column('fielddefs', 'visibility_value_id'); + } } sub _fix_series_indexes { - my $dbh = Bugzilla->dbh; - return if $dbh->bz_index_info('series', 'series_category_idx'); + my $dbh = Bugzilla->dbh; + return if $dbh->bz_index_info('series', 'series_category_idx'); - $dbh->bz_drop_index('series', 'series_creator_idx'); + $dbh->bz_drop_index('series', 'series_creator_idx'); - # Fix duplicated names under the same category/subcategory before - # adding the more restrictive index. - my $duplicated_series = $dbh->selectall_arrayref( - 'SELECT s1.series_id, s1.category, s1.subcategory, s1.name + # Fix duplicated names under the same category/subcategory before + # adding the more restrictive index. + my $duplicated_series = $dbh->selectall_arrayref( + 'SELECT s1.series_id, s1.category, s1.subcategory, s1.name FROM series AS s1 INNER JOIN series AS s2 ON s1.category = s2.category AND s1.subcategory = s2.subcategory AND s1.name = s2.name - WHERE s1.series_id != s2.series_id'); - my $sth_series_update = $dbh->prepare('UPDATE series SET name = ? WHERE series_id = ?'); - my $sth_series_query = $dbh->prepare('SELECT 1 FROM series WHERE name = ? - AND category = ? AND subcategory = ?'); - - my %renamed_series; - foreach my $series (@$duplicated_series) { - my ($series_id, $category, $subcategory, $name) = @$series; - # Leave the first series alone, then rename duplicated ones. - if ($renamed_series{"${category}_${subcategory}_${name}"}++) { - print "Renaming series ${category}/${subcategory}/${name}...\n"; - my $c = 0; - my $exists = 1; - while ($exists) { - $sth_series_query->execute($name . ++$c, $category, $subcategory); - $exists = $sth_series_query->fetchrow_array; - } - $sth_series_update->execute($name . $c, $series_id); - } + WHERE s1.series_id != s2.series_id' + ); + my $sth_series_update + = $dbh->prepare('UPDATE series SET name = ? WHERE series_id = ?'); + my $sth_series_query = $dbh->prepare( + 'SELECT 1 FROM series WHERE name = ? + AND category = ? AND subcategory = ?' + ); + + my %renamed_series; + foreach my $series (@$duplicated_series) { + my ($series_id, $category, $subcategory, $name) = @$series; + + # Leave the first series alone, then rename duplicated ones. + if ($renamed_series{"${category}_${subcategory}_${name}"}++) { + print "Renaming series ${category}/${subcategory}/${name}...\n"; + my $c = 0; + my $exists = 1; + while ($exists) { + $sth_series_query->execute($name . ++$c, $category, $subcategory); + $exists = $sth_series_query->fetchrow_array; + } + $sth_series_update->execute($name . $c, $series_id); } + } - $dbh->bz_add_index('series', 'series_creator_idx', ['creator']); - $dbh->bz_add_index('series', 'series_category_idx', - {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}); + $dbh->bz_add_index('series', 'series_creator_idx', ['creator']); + $dbh->bz_add_index('series', 'series_category_idx', + {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}); } sub _migrate_user_tags { - my $dbh = Bugzilla->dbh; - return unless $dbh->bz_column_info('namedqueries', 'query_type'); + my $dbh = Bugzilla->dbh; + return unless $dbh->bz_column_info('namedqueries', 'query_type'); - my $tags = $dbh->selectall_arrayref('SELECT id, userid, name, query + my $tags = $dbh->selectall_arrayref( + 'SELECT id, userid, name, query FROM namedqueries - WHERE query_type != 0'); - - my $sth_tags = $dbh->prepare( - 'INSERT INTO tag (user_id, name) VALUES (?, ?)'); - my $sth_tag_id = $dbh->prepare( - 'SELECT id FROM tag WHERE user_id = ? AND name = ?'); - my $sth_bug_tag = $dbh->prepare('INSERT INTO bug_tag (bug_id, tag_id) - VALUES (?, ?)'); - my $sth_nq = $dbh->prepare('UPDATE namedqueries SET query = ? - WHERE id = ?'); - - if (scalar @$tags) { - print install_string('update_queries_to_tags'), "\n"; + WHERE query_type != 0' + ); + + my $sth_tags = $dbh->prepare('INSERT INTO tag (user_id, name) VALUES (?, ?)'); + my $sth_tag_id + = $dbh->prepare('SELECT id FROM tag WHERE user_id = ? AND name = ?'); + my $sth_bug_tag = $dbh->prepare( + 'INSERT INTO bug_tag (bug_id, tag_id) + VALUES (?, ?)' + ); + my $sth_nq = $dbh->prepare( + 'UPDATE namedqueries SET query = ? + WHERE id = ?' + ); + + if (scalar @$tags) { + print install_string('update_queries_to_tags'), "\n"; + } + + my $total = scalar(@$tags); + my $current = 0; + + $dbh->bz_start_transaction(); + foreach my $tag (@$tags) { + my ($query_id, $user_id, $name, $query) = @$tag; + + # Tags are all lowercase. + my $tag_name = lc($name); + + $sth_tags->execute($user_id, $tag_name); + + my $tag_id = $dbh->selectrow_array($sth_tag_id, undef, $user_id, $tag_name); + + indicate_progress({current => ++$current, total => $total, every => 25}); + + my $uri = URI->new("buglist.cgi?$query", 'http'); + my $bug_id_list = $uri->query_param_delete('bug_id'); + if (!$bug_id_list) { + warn "No bug_id param for tag $name from user $user_id: $query"; + next; } + my @bug_ids = split(/[\s,]+/, $bug_id_list); - my $total = scalar(@$tags); - my $current = 0; - - $dbh->bz_start_transaction(); - foreach my $tag (@$tags) { - my ($query_id, $user_id, $name, $query) = @$tag; - # Tags are all lowercase. - my $tag_name = lc($name); - - $sth_tags->execute($user_id, $tag_name); + # Make sure that things like "001" get converted to "1" + @bug_ids = map { int($_) } @bug_ids; - my $tag_id = $dbh->selectrow_array($sth_tag_id, - undef, $user_id, $tag_name); + # And remove duplicates + @bug_ids = uniq @bug_ids; + foreach my $bug_id (@bug_ids) { - indicate_progress({ current => ++$current, total => $total, - every => 25 }); - - my $uri = URI->new("buglist.cgi?$query", 'http'); - my $bug_id_list = $uri->query_param_delete('bug_id'); - if (!$bug_id_list) { - warn "No bug_id param for tag $name from user $user_id: $query"; - next; - } - my @bug_ids = split(/[\s,]+/, $bug_id_list); - # Make sure that things like "001" get converted to "1" - @bug_ids = map { int($_) } @bug_ids; - # And remove duplicates - @bug_ids = uniq @bug_ids; - foreach my $bug_id (@bug_ids) { - # If "int" above failed this might be undef. We also - # don't want to accept bug 0. - next if !$bug_id; - $sth_bug_tag->execute($bug_id, $tag_id); - } - - # Existing tags may be used in whines, or shared with - # other users. So we convert them rather than delete them. - $uri->query_param('tag', $tag_name); - $sth_nq->execute($uri->query, $query_id); + # If "int" above failed this might be undef. We also + # don't want to accept bug 0. + next if !$bug_id; + $sth_bug_tag->execute($bug_id, $tag_id); } - $dbh->bz_commit_transaction(); + # Existing tags may be used in whines, or shared with + # other users. So we convert them rather than delete them. + $uri->query_param('tag', $tag_name); + $sth_nq->execute($uri->query, $query_id); + } - $dbh->bz_drop_column('namedqueries', 'query_type'); + $dbh->bz_commit_transaction(); + + $dbh->bz_drop_column('namedqueries', 'query_type'); } sub _populate_bug_see_also_class { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('bug_see_also', 'class')) { - # The length was incorrectly set to 64 instead of 255. - $dbh->bz_alter_column('bug_see_also', 'class', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); - return; - } + if ($dbh->bz_column_info('bug_see_also', 'class')) { - $dbh->bz_add_column('bug_see_also', 'class', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); + # The length was incorrectly set to 64 instead of 255. + $dbh->bz_alter_column('bug_see_also', 'class', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); + return; + } - my $result = $dbh->selectall_arrayref( - "SELECT id, value FROM bug_see_also"); + $dbh->bz_add_column('bug_see_also', 'class', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); - my $update_sth = - $dbh->prepare("UPDATE bug_see_also SET class = ? WHERE id = ?"); - - $dbh->bz_start_transaction(); - foreach my $see_also (@$result) { - my ($id, $value) = @$see_also; - my $class = Bugzilla::BugUrl->class_for($value); - $update_sth->execute($class, $id); - } - $dbh->bz_commit_transaction(); + my $result = $dbh->selectall_arrayref("SELECT id, value FROM bug_see_also"); + + my $update_sth + = $dbh->prepare("UPDATE bug_see_also SET class = ? WHERE id = ?"); + + $dbh->bz_start_transaction(); + foreach my $see_also (@$result) { + my ($id, $value) = @$see_also; + my $class = Bugzilla::BugUrl->class_for($value); + $update_sth->execute($class, $id); + } + $dbh->bz_commit_transaction(); } sub _migrate_disabledtext_boolean { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('profiles', 'is_enabled')) { - $dbh->bz_add_column("profiles", 'is_enabled', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->do("UPDATE profiles SET is_enabled = 0 - WHERE disabledtext != ''"); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('profiles', 'is_enabled')) { + $dbh->bz_add_column("profiles", 'is_enabled', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + $dbh->do( + "UPDATE profiles SET is_enabled = 0 + WHERE disabledtext != ''" + ); + } } sub _rename_tags_to_tag { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_table_info('tags')) { - # If we get here, it's because the schema created "tag" as an empty - # table while "tags" still exists. We get rid of the empty - # tag table so we can do the rename over the top of it. - $dbh->bz_drop_table('tag'); - $dbh->bz_drop_index('tags', 'tags_user_id_idx'); - $dbh->bz_rename_table('tags','tag'); - $dbh->bz_add_index('tag', 'tag_user_id_idx', - {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}); - } - if (my $bug_tag_fk = $dbh->bz_fk_info('bug_tag', 'tag_id')) { - # bz_rename_table() didn't handle FKs correctly. - if ($bug_tag_fk->{TABLE} eq 'tags') { - $bug_tag_fk->{TABLE} = 'tag'; - $dbh->bz_alter_fk('bug_tag', 'tag_id', $bug_tag_fk); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_table_info('tags')) { + + # If we get here, it's because the schema created "tag" as an empty + # table while "tags" still exists. We get rid of the empty + # tag table so we can do the rename over the top of it. + $dbh->bz_drop_table('tag'); + $dbh->bz_drop_index('tags', 'tags_user_id_idx'); + $dbh->bz_rename_table('tags', 'tag'); + $dbh->bz_add_index('tag', 'tag_user_id_idx', + {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}); + } + if (my $bug_tag_fk = $dbh->bz_fk_info('bug_tag', 'tag_id')) { + + # bz_rename_table() didn't handle FKs correctly. + if ($bug_tag_fk->{TABLE} eq 'tags') { + $bug_tag_fk->{TABLE} = 'tag'; + $dbh->bz_alter_fk('bug_tag', 'tag_id', $bug_tag_fk); } + } } sub _on_delete_set_null_for_audit_log_userid { - my $dbh = Bugzilla->dbh; - my $fk = $dbh->bz_fk_info('audit_log', 'user_id'); - if ($fk and !defined $fk->{DELETE}) { - $fk->{DELETE} = 'SET NULL'; - $dbh->bz_alter_fk('audit_log', 'user_id', $fk); - } + my $dbh = Bugzilla->dbh; + my $fk = $dbh->bz_fk_info('audit_log', 'user_id'); + if ($fk and !defined $fk->{DELETE}) { + $fk->{DELETE} = 'SET NULL'; + $dbh->bz_alter_fk('audit_log', 'user_id', $fk); + } } sub _fix_notnull_defaults { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - $dbh->bz_alter_column('bugs', 'bug_file_loc', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, ''); + $dbh->bz_alter_column('bugs', 'bug_file_loc', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, ''); - my $custom_fields = Bugzilla::Field->match({ - custom => 1, type => [ FIELD_TYPE_FREETEXT, FIELD_TYPE_TEXTAREA ] - }); + my $custom_fields = Bugzilla::Field->match( + {custom => 1, type => [FIELD_TYPE_FREETEXT, FIELD_TYPE_TEXTAREA]}); - foreach my $field (@$custom_fields) { - if ($field->type == FIELD_TYPE_FREETEXT) { - $dbh->bz_alter_column('bugs', $field->name, - {TYPE => 'varchar(255)', NOTNULL => 1, - DEFAULT => "''"}, ''); - } - if ($field->type == FIELD_TYPE_TEXTAREA) { - $dbh->bz_alter_column('bugs', $field->name, - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, ''); - } + foreach my $field (@$custom_fields) { + if ($field->type == FIELD_TYPE_FREETEXT) { + $dbh->bz_alter_column('bugs', $field->name, + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); + } + if ($field->type == FIELD_TYPE_TEXTAREA) { + $dbh->bz_alter_column('bugs', $field->name, + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, ''); } + } } sub _fix_longdescs_primary_key { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') { - $dbh->bz_drop_related_fks('longdescs', 'comment_id'); - $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); - $dbh->bz_alter_column('longdescs', 'comment_id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') { + $dbh->bz_drop_related_fks('longdescs', 'comment_id'); + $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); + $dbh->bz_alter_column('longdescs', 'comment_id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + } } sub _fix_longdescs_indexes { - my $dbh = Bugzilla->dbh; - my $bug_id_idx = $dbh->bz_index_info('longdescs', 'longdescs_bug_id_idx'); - if ($bug_id_idx && scalar @{$bug_id_idx->{'FIELDS'}} < 2) { - $dbh->bz_drop_index('longdescs', 'longdescs_bug_id_idx'); - $dbh->bz_add_index('longdescs', 'longdescs_bug_id_idx', [qw(bug_id work_time)]); - } + my $dbh = Bugzilla->dbh; + my $bug_id_idx = $dbh->bz_index_info('longdescs', 'longdescs_bug_id_idx'); + if ($bug_id_idx && scalar @{$bug_id_idx->{'FIELDS'}} < 2) { + $dbh->bz_drop_index('longdescs', 'longdescs_bug_id_idx'); + $dbh->bz_add_index('longdescs', 'longdescs_bug_id_idx', [qw(bug_id work_time)]); + } } sub _fix_dependencies_dupes { - my $dbh = Bugzilla->dbh; - my $blocked_idx = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx'); - if ($blocked_idx && scalar @{$blocked_idx->{'FIELDS'}} < 2) { - # Remove duplicated entries - my $dupes = $dbh->selectall_arrayref(" + my $dbh = Bugzilla->dbh; + my $blocked_idx + = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx'); + if ($blocked_idx && scalar @{$blocked_idx->{'FIELDS'}} < 2) { + + # Remove duplicated entries + my $dupes = $dbh->selectall_arrayref(" SELECT blocked, dependson, COUNT(*) AS count - FROM dependencies " . - $dbh->sql_group_by('blocked, dependson') . " - HAVING COUNT(*) > 1", - { Slice => {} }); - print "Removing duplicated entries from the 'dependencies' table...\n" if @$dupes; - foreach my $dupe (@$dupes) { - $dbh->do("DELETE FROM dependencies - WHERE blocked = ? AND dependson = ?", - undef, $dupe->{blocked}, $dupe->{dependson}); - $dbh->do("INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)", - undef, $dupe->{blocked}, $dupe->{dependson}); - } - $dbh->bz_drop_index('dependencies', 'dependencies_blocked_idx'); - $dbh->bz_add_index('dependencies', 'dependencies_blocked_idx', - { FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE' }); - } + FROM dependencies " . $dbh->sql_group_by('blocked, dependson') . " + HAVING COUNT(*) > 1", {Slice => {}}); + print "Removing duplicated entries from the 'dependencies' table...\n" + if @$dupes; + foreach my $dupe (@$dupes) { + $dbh->do( + "DELETE FROM dependencies + WHERE blocked = ? AND dependson = ?", undef, $dupe->{blocked}, + $dupe->{dependson} + ); + $dbh->do("INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)", + undef, $dupe->{blocked}, $dupe->{dependson}); + } + $dbh->bz_drop_index('dependencies', 'dependencies_blocked_idx'); + $dbh->bz_add_index('dependencies', 'dependencies_blocked_idx', + {FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE'}); + } } sub _shorten_long_quips { - my $dbh = Bugzilla->dbh; - my $quips = $dbh->selectall_arrayref("SELECT quipid, quip FROM quips - WHERE CHAR_LENGTH(quip) > 512"); - - if (@$quips) { - print "Shortening quips longer than 512 characters:"; - - my $query = $dbh->prepare("UPDATE quips SET quip = ? WHERE quipid = ?"); - - foreach my $quip (@$quips) { - my ($quipid, $quip_str) = @$quip; - $quip_str = substr($quip_str, 0, 509) . "..."; - print " $quipid"; - $query->execute($quip_str, $quipid); - } - print "\n"; + my $dbh = Bugzilla->dbh; + my $quips = $dbh->selectall_arrayref( + "SELECT quipid, quip FROM quips + WHERE CHAR_LENGTH(quip) > 512" + ); + + if (@$quips) { + print "Shortening quips longer than 512 characters:"; + + my $query = $dbh->prepare("UPDATE quips SET quip = ? WHERE quipid = ?"); + + foreach my $quip (@$quips) { + my ($quipid, $quip_str) = @$quip; + $quip_str = substr($quip_str, 0, 509) . "..."; + print " $quipid"; + $query->execute($quip_str, $quipid); } - $dbh->bz_alter_column('quips', 'quip', { TYPE => 'varchar(512)', NOTNULL => 1}); + print "\n"; + } + $dbh->bz_alter_column('quips', 'quip', {TYPE => 'varchar(512)', NOTNULL => 1}); } sub _add_password_salt_separator { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my $profiles = $dbh->selectall_arrayref("SELECT userid, cryptpassword FROM profiles WHERE (" - . $dbh->sql_regexp("cryptpassword", "'^[^,]+{'") . ")"); + my $profiles + = $dbh->selectall_arrayref( + "SELECT userid, cryptpassword FROM profiles WHERE (" + . $dbh->sql_regexp("cryptpassword", "'^[^,]+{'") + . ")"); - if (@$profiles) { - say "Adding salt separator to password hashes..."; + if (@$profiles) { + say "Adding salt separator to password hashes..."; - my $query = $dbh->prepare("UPDATE profiles SET cryptpassword = ? WHERE userid = ?"); - my %algo_sizes; + my $query + = $dbh->prepare("UPDATE profiles SET cryptpassword = ? WHERE userid = ?"); + my %algo_sizes; - foreach my $profile (@$profiles) { - my ($userid, $hash) = @$profile; - my ($algorithm) = $hash =~ /{([^}]+)}$/; + foreach my $profile (@$profiles) { + my ($userid, $hash) = @$profile; + my ($algorithm) = $hash =~ /{([^}]+)}$/; - $algo_sizes{$algorithm} ||= length(Digest->new($algorithm)->b64digest); + $algo_sizes{$algorithm} ||= length(Digest->new($algorithm)->b64digest); - # Calculate the salt length by taking the stored hash and - # subtracting the combined lengths of the hash size, the - # algorithm name, and 2 for the {} surrounding the name. - my $not_salt_len = $algo_sizes{$algorithm} + length($algorithm) + 2; - my $salt_len = length($hash) - $not_salt_len; + # Calculate the salt length by taking the stored hash and + # subtracting the combined lengths of the hash size, the + # algorithm name, and 2 for the {} surrounding the name. + my $not_salt_len = $algo_sizes{$algorithm} + length($algorithm) + 2; + my $salt_len = length($hash) - $not_salt_len; - substr($hash, $salt_len, 0, ','); - $query->execute($hash, $userid); - } + substr($hash, $salt_len, 0, ','); + $query->execute($hash, $userid); } - $dbh->bz_commit_transaction(); + } + $dbh->bz_commit_transaction(); } sub _fix_flagclusions_indexes { - my $dbh = Bugzilla->dbh; - foreach my $table ('flaginclusions', 'flagexclusions') { - my $index = $table . '_type_id_idx'; - my $idx_info = $dbh->bz_index_info($table, $index); - if ($idx_info && $idx_info->{'TYPE'} ne 'UNIQUE') { - # Remove duplicated entries - my $dupes = $dbh->selectall_arrayref(" + my $dbh = Bugzilla->dbh; + foreach my $table ('flaginclusions', 'flagexclusions') { + my $index = $table . '_type_id_idx'; + my $idx_info = $dbh->bz_index_info($table, $index); + if ($idx_info && $idx_info->{'TYPE'} ne 'UNIQUE') { + + # Remove duplicated entries + my $dupes = $dbh->selectall_arrayref(" SELECT type_id, product_id, component_id, COUNT(*) AS count - FROM $table " . - $dbh->sql_group_by('type_id, product_id, component_id') . " - HAVING COUNT(*) > 1", - { Slice => {} }); - say "Removing duplicated entries from the '$table' table..." if @$dupes; - foreach my $dupe (@$dupes) { - $dbh->do("DELETE FROM $table + FROM $table " + . $dbh->sql_group_by('type_id, product_id, component_id') . " + HAVING COUNT(*) > 1", {Slice => {}}); + say "Removing duplicated entries from the '$table' table..." if @$dupes; + foreach my $dupe (@$dupes) { + $dbh->do( + "DELETE FROM $table WHERE type_id = ? AND product_id = ? AND component_id = ?", - undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); - $dbh->do("INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)", - undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); - } - $dbh->bz_drop_index($table, $index); - $dbh->bz_add_index($table, $index, - { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }); - } + undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id} + ); + $dbh->do( + "INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)", + undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); + } + $dbh->bz_drop_index($table, $index); + $dbh->bz_add_index($table, $index, + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}); } + } } sub _fix_components_primary_key { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('components', 'id')->{TYPE} ne 'MEDIUMSERIAL') { - $dbh->bz_drop_related_fks('components', 'id'); - $dbh->bz_alter_column("components", "id", - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_alter_column("flaginclusions", "component_id", - {TYPE => 'INT3'}); - $dbh->bz_alter_column("flagexclusions", "component_id", - {TYPE => 'INT3'}); - $dbh->bz_alter_column("bugs", "component_id", - {TYPE => 'INT3', NOTNULL => 1}); - $dbh->bz_alter_column("component_cc", "component_id", - {TYPE => 'INT3', NOTNULL => 1}); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info('components', 'id')->{TYPE} ne 'MEDIUMSERIAL') { + $dbh->bz_drop_related_fks('components', 'id'); + $dbh->bz_alter_column("components", "id", + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_alter_column("flaginclusions", "component_id", {TYPE => 'INT3'}); + $dbh->bz_alter_column("flagexclusions", "component_id", {TYPE => 'INT3'}); + $dbh->bz_alter_column("bugs", "component_id", {TYPE => 'INT3', NOTNULL => 1}); + $dbh->bz_alter_column("component_cc", "component_id", + {TYPE => 'INT3', NOTNULL => 1}); + } } sub _fix_user_api_keys_indexes { - my $dbh = Bugzilla->dbh; - - if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_key')) { - $dbh->bz_drop_index('user_api_keys', 'user_api_keys_key'); - $dbh->bz_add_index('user_api_keys', 'user_api_keys_api_key_idx', - { FIELDS => ['api_key'], TYPE => 'UNIQUE' }); - } - if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_user_id')) { - $dbh->bz_drop_index('user_api_keys', 'user_api_keys_user_id'); - $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_idx', ['user_id']); - } + my $dbh = Bugzilla->dbh; + + if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_key')) { + $dbh->bz_drop_index('user_api_keys', 'user_api_keys_key'); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_api_key_idx', + {FIELDS => ['api_key'], TYPE => 'UNIQUE'}); + } + if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_user_id')) { + $dbh->bz_drop_index('user_api_keys', 'user_api_keys_user_id'); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_idx', ['user_id']); + } } sub _update_alias { - my $dbh = Bugzilla->dbh; - return unless $dbh->bz_column_info('bugs', 'alias'); + my $dbh = Bugzilla->dbh; + return unless $dbh->bz_column_info('bugs', 'alias'); - # We need to move the aliases from the bugs table to the bugs_aliases table - $dbh->do(q{ + # We need to move the aliases from the bugs table to the bugs_aliases table + $dbh->do( + q{ INSERT INTO bugs_aliases (bug_id, alias) SELECT bug_id, alias FROM bugs WHERE alias IS NOT NULL - }); + } + ); - $dbh->bz_drop_column('bugs', 'alias'); + $dbh->bz_drop_column('bugs', 'alias'); } sub _sanitize_audit_log_table { - my $dbh = Bugzilla->dbh; - - # Replace hashed passwords by a generic comment. - my $class = 'Bugzilla::User'; - my $field = 'cryptpassword'; - - my $hashed_passwd = - $dbh->selectcol_arrayref('SELECT added FROM audit_log WHERE class = ? AND field = ? - AND ' . $dbh->sql_not_ilike('hashed_with_', 'added'), - undef, ($class, $field)); - if (@$hashed_passwd) { - say "Sanitizing hashed passwords stored in the 'audit_log' table..."; - my $sth = $dbh->prepare('UPDATE audit_log SET added = ? - WHERE class = ? AND field = ? AND added = ?'); - - foreach my $passwd (@$hashed_passwd) { - my (undef, $sanitized_passwd) = - Bugzilla::Object::_sanitize_audit_log($class, $field, [undef, $passwd]); - $sth->execute($sanitized_passwd, $class, $field, $passwd); - } + my $dbh = Bugzilla->dbh; + + # Replace hashed passwords by a generic comment. + my $class = 'Bugzilla::User'; + my $field = 'cryptpassword'; + + my $hashed_passwd = $dbh->selectcol_arrayref( + 'SELECT added FROM audit_log WHERE class = ? AND field = ? + AND ' + . $dbh->sql_not_ilike('hashed_with_', 'added'), undef, ($class, $field) + ); + if (@$hashed_passwd) { + say "Sanitizing hashed passwords stored in the 'audit_log' table..."; + my $sth = $dbh->prepare( + 'UPDATE audit_log SET added = ? + WHERE class = ? AND field = ? AND added = ?' + ); + + foreach my $passwd (@$hashed_passwd) { + my (undef, $sanitized_passwd) + = Bugzilla::Object::_sanitize_audit_log($class, $field, [undef, $passwd]); + $sth->execute($sanitized_passwd, $class, $field, $passwd); } + } } 1; diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm index a96fd59aa..5aaf01537 100644 --- a/Bugzilla/Install/Filesystem.pm +++ b/Bugzilla/Install/Filesystem.pm @@ -36,11 +36,11 @@ use POSIX (); use parent qw(Exporter); our @EXPORT = qw( - update_filesystem - create_htaccess - fix_all_file_permissions - fix_dir_permissions - fix_file_permissions + update_filesystem + create_htaccess + fix_all_file_permissions + fix_dir_permissions + fix_file_permissions ); use constant HT_DEFAULT_DENY => <<EOT; @@ -64,52 +64,60 @@ EOT ############### # Used by the permissions "constants" below. -sub _suexec { Bugzilla->localconfig->{'use_suexec'} }; -sub _group { Bugzilla->localconfig->{'webservergroup'} }; +sub _suexec { Bugzilla->localconfig->{'use_suexec'} } +sub _group { Bugzilla->localconfig->{'webservergroup'} } # Writeable by the owner only. use constant OWNER_WRITE => 0600; + # Executable by the owner only. use constant OWNER_EXECUTE => 0700; + # A directory which is only writeable by the owner. use constant DIR_OWNER_WRITE => 0700; # A cgi script that the webserver can execute. -sub WS_EXECUTE { _group() ? 0750 : 0755 }; +sub WS_EXECUTE { _group() ? 0750 : 0755 } + # A file that is read by cgi scripts, but is not ever read # directly by the webserver. -sub CGI_READ { _group() ? 0640 : 0644 }; +sub CGI_READ { _group() ? 0640 : 0644 } + # A file that is written to by cgi scripts, but is not ever # read or written directly by the webserver. -sub CGI_WRITE { _group() ? 0660 : 0666 }; +sub CGI_WRITE { _group() ? 0660 : 0666 } + # A file that is served directly by the web server. -sub WS_SERVE { (_group() and !_suexec()) ? 0640 : 0644 }; +sub WS_SERVE { (_group() and !_suexec()) ? 0640 : 0644 } # A directory whose contents can be read or served by the # webserver (so even directories containing cgi scripts # would have this permission). -sub DIR_WS_SERVE { (_group() and !_suexec()) ? 0750 : 0755 }; +sub DIR_WS_SERVE { (_group() and !_suexec()) ? 0750 : 0755 } + # A directory that is read by cgi scripts, but is never accessed # directly by the webserver -sub DIR_CGI_READ { _group() ? 0750 : 0755 }; +sub DIR_CGI_READ { _group() ? 0750 : 0755 } + # A directory that is written to by cgi scripts, but where the # scripts never needs to overwrite files created by other # users. -sub DIR_CGI_WRITE { _group() ? 0770 : 01777 }; +sub DIR_CGI_WRITE { _group() ? 0770 : 01777 } + # A directory that is written to by cgi scripts, where the # scripts need to overwrite files created by other users. -sub DIR_CGI_OVERWRITE { _group() ? 0770 : 0777 }; +sub DIR_CGI_OVERWRITE { _group() ? 0770 : 0777 } -# This can be combined (using "|") with other permissions for +# This can be combined (using "|") with other permissions for # directories that, in addition to their normal permissions (such # as DIR_CGI_WRITE) also have content served directly from them # (or their subdirectories) to the user, via the webserver. -sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 }; +sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 } # This looks like a constant because it effectively is, but # it has to call other subroutines and read the current filesystem, # so it's defined as a sub. This is not exported, so it doesn't have -# a perldoc. However, look at the various hashes defined inside this +# a perldoc. However, look at the various hashes defined inside this # function to understand what it returns. (There are comments throughout.) # # The rationale for the file permissions is that there is a group the @@ -117,196 +125,175 @@ sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 }; # by this group. Otherwise someone may find it possible to change the cgis # when exploiting some security flaw somewhere (not necessarily in Bugzilla!) sub FILESYSTEM { - my $datadir = bz_locations()->{'datadir'}; - my $attachdir = bz_locations()->{'attachdir'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $webdotdir = bz_locations()->{'webdotdir'}; - my $templatedir = bz_locations()->{'templatedir'}; - my $libdir = bz_locations()->{'libpath'}; - my $extlib = bz_locations()->{'ext_libpath'}; - my $skinsdir = bz_locations()->{'skinsdir'}; - my $localconfig = bz_locations()->{'localconfig'}; - my $template_cache = bz_locations()->{'template_cache'}; - my $graphsdir = bz_locations()->{'graphsdir'}; - my $assetsdir = bz_locations()->{'assetsdir'}; - - # We want to set the permissions the same for all localconfig files - # across all PROJECTs, so we do something special with $localconfig, - # lower down in the permissions section. - if ($ENV{PROJECT}) { - $localconfig =~ s/\.\Q$ENV{PROJECT}\E$//; - } - - # Note: When being processed by checksetup, these have their permissions - # set in this order: %all_dirs, %recurse_dirs, %all_files. - # - # Each is processed in alphabetical order of keys, so shorter keys - # will have their permissions set before longer keys (thus setting - # the permissions on parent directories before setting permissions - # on their children). - - # --- FILE PERMISSIONS (Non-created files) --- # - my %files = ( - '*' => { perms => OWNER_WRITE }, - # Some .pl files are WS_EXECUTE because we want - # users to be able to cron them or otherwise run - # them as a secure user, like the webserver owner. - '*.cgi' => { perms => WS_EXECUTE }, - 'whineatnews.pl' => { perms => WS_EXECUTE }, - 'collectstats.pl' => { perms => WS_EXECUTE }, - 'importxml.pl' => { perms => WS_EXECUTE }, - 'testserver.pl' => { perms => WS_EXECUTE }, - 'whine.pl' => { perms => WS_EXECUTE }, - 'email_in.pl' => { perms => WS_EXECUTE }, - 'sanitycheck.pl' => { perms => WS_EXECUTE }, - 'checksetup.pl' => { perms => OWNER_EXECUTE }, - 'runtests.pl' => { perms => OWNER_EXECUTE }, - 'jobqueue.pl' => { perms => WS_EXECUTE }, - 'migrate.pl' => { perms => OWNER_EXECUTE }, - 'install-module.pl' => { perms => OWNER_EXECUTE }, - 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE }, - - 'Bugzilla.pm' => { perms => CGI_READ }, - "$localconfig*" => { perms => CGI_READ }, - 'bugzilla.dtd' => { perms => WS_SERVE }, - 'mod_perl.pl' => { perms => WS_SERVE }, - 'robots.txt' => { perms => WS_SERVE }, - '.htaccess' => { perms => WS_SERVE }, - - 'contrib/README' => { perms => OWNER_WRITE }, - 'contrib/*/README' => { perms => OWNER_WRITE }, - 'contrib/Bugzilla.pm' => { perms => OWNER_WRITE }, - 'docs/bugzilla.ent' => { perms => OWNER_WRITE }, - 'docs/makedocs.pl' => { perms => OWNER_EXECUTE }, - 'docs/style.css' => { perms => WS_SERVE }, - 'docs/*/rel_notes.txt' => { perms => WS_SERVE }, - 'docs/*/README.docs' => { perms => OWNER_WRITE }, - "$datadir/params.json" => { perms => CGI_WRITE }, - "$datadir/old-params.txt" => { perms => OWNER_WRITE }, - "$extensionsdir/create.pl" => { perms => OWNER_EXECUTE }, - "$extensionsdir/*/*.pl" => { perms => WS_EXECUTE }, - ); - - # Directories that we want to set the perms on, but not - # recurse through. These are directories we didn't create - # in checkesetup.pl. - my %non_recurse_dirs = ( - '.' => DIR_WS_SERVE, - docs => DIR_WS_SERVE, - ); - - # This sets the permissions for each item inside each of these - # directories, including the directory itself. - # 'CVS' directories are special, though, and are never readable by - # the webserver. - my %recurse_dirs = ( - # Writeable directories - $template_cache => { files => CGI_READ, - dirs => DIR_CGI_OVERWRITE }, - $attachdir => { files => CGI_WRITE, - dirs => DIR_CGI_WRITE }, - $webdotdir => { files => WS_SERVE, - dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE }, - $graphsdir => { files => WS_SERVE, - dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE }, - "$datadir/db" => { files => CGI_WRITE, - dirs => DIR_CGI_WRITE }, - $assetsdir => { files => WS_SERVE, - dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE }, - - # Readable directories - "$datadir/mining" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - "$libdir/Bugzilla" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - $extlib => { files => CGI_READ, - dirs => DIR_CGI_READ }, - $templatedir => { files => CGI_READ, - dirs => DIR_CGI_READ }, - # Directories in the extensions/ dir are WS_SERVE so that - # the web/ directories can be served by the web server. - # But, for extra security, we deny direct webserver access to - # the lib/ and template/ directories of extensions. - $extensionsdir => { files => CGI_READ, - dirs => DIR_WS_SERVE }, - "$extensionsdir/*/lib" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - "$extensionsdir/*/template" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - - # Content served directly by the webserver - images => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - js => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - $skinsdir => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/html' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/pdf' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/txt' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/images' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - "$extensionsdir/*/web" => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - - # Directories only for the owner, not for the webserver. - '.bzr' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - t => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - xt => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'docs/lib' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'docs/*/xml' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'contrib' => { files => OWNER_EXECUTE, - dirs => DIR_OWNER_WRITE, }, - ); - - # --- FILES TO CREATE --- # - - # The name of each directory that we should actually *create*, - # pointing at its default permissions. - my %create_dirs = ( - # This is DIR_ALSO_WS_SERVE because it contains $webdotdir and - # $assetsdir. - $datadir => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE, - # Directories that are read-only for cgi scripts - "$datadir/mining" => DIR_CGI_READ, - "$datadir/extensions" => DIR_CGI_READ, - $extensionsdir => DIR_CGI_READ, - # Directories that cgi scripts can write to. - "$datadir/db" => DIR_CGI_WRITE, - $attachdir => DIR_CGI_WRITE, - $graphsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - $webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - $assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - # Directories that contain content served directly by the web server. - "$skinsdir/custom" => DIR_WS_SERVE, - "$skinsdir/contrib" => DIR_WS_SERVE, - ); - - # The name of each file, pointing at its default permissions and - # default contents. - my %create_files = ( - "$datadir/extensions/additional" => { perms => CGI_READ, - contents => '' }, - # We create this file so that it always has the right owner - # and permissions. Otherwise, the webserver creates it as - # owned by itself, which can cause problems if jobqueue.pl - # or something else is not running as the webserver or root. - "$datadir/mailer.testfile" => { perms => CGI_WRITE, - contents => '' }, - ); - - # Because checksetup controls the creation of index.html separately - # from all other files, it gets its very own hash. - my %index_html = ( - 'index.html' => { perms => WS_SERVE, contents => <<EOT + my $datadir = bz_locations()->{'datadir'}; + my $attachdir = bz_locations()->{'attachdir'}; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $webdotdir = bz_locations()->{'webdotdir'}; + my $templatedir = bz_locations()->{'templatedir'}; + my $libdir = bz_locations()->{'libpath'}; + my $extlib = bz_locations()->{'ext_libpath'}; + my $skinsdir = bz_locations()->{'skinsdir'}; + my $localconfig = bz_locations()->{'localconfig'}; + my $template_cache = bz_locations()->{'template_cache'}; + my $graphsdir = bz_locations()->{'graphsdir'}; + my $assetsdir = bz_locations()->{'assetsdir'}; + + # We want to set the permissions the same for all localconfig files + # across all PROJECTs, so we do something special with $localconfig, + # lower down in the permissions section. + if ($ENV{PROJECT}) { + $localconfig =~ s/\.\Q$ENV{PROJECT}\E$//; + } + + # Note: When being processed by checksetup, these have their permissions + # set in this order: %all_dirs, %recurse_dirs, %all_files. + # + # Each is processed in alphabetical order of keys, so shorter keys + # will have their permissions set before longer keys (thus setting + # the permissions on parent directories before setting permissions + # on their children). + + # --- FILE PERMISSIONS (Non-created files) --- # + my %files = ( + '*' => {perms => OWNER_WRITE}, + + # Some .pl files are WS_EXECUTE because we want + # users to be able to cron them or otherwise run + # them as a secure user, like the webserver owner. + '*.cgi' => {perms => WS_EXECUTE}, + 'whineatnews.pl' => {perms => WS_EXECUTE}, + 'collectstats.pl' => {perms => WS_EXECUTE}, + 'importxml.pl' => {perms => WS_EXECUTE}, + 'testserver.pl' => {perms => WS_EXECUTE}, + 'whine.pl' => {perms => WS_EXECUTE}, + 'email_in.pl' => {perms => WS_EXECUTE}, + 'sanitycheck.pl' => {perms => WS_EXECUTE}, + 'checksetup.pl' => {perms => OWNER_EXECUTE}, + 'runtests.pl' => {perms => OWNER_EXECUTE}, + 'jobqueue.pl' => {perms => WS_EXECUTE}, + 'migrate.pl' => {perms => OWNER_EXECUTE}, + 'install-module.pl' => {perms => OWNER_EXECUTE}, + 'clean-bug-user-last-visit.pl' => {perms => WS_EXECUTE}, + + 'Bugzilla.pm' => {perms => CGI_READ}, + "$localconfig*" => {perms => CGI_READ}, + 'bugzilla.dtd' => {perms => WS_SERVE}, + 'mod_perl.pl' => {perms => WS_SERVE}, + 'robots.txt' => {perms => WS_SERVE}, + '.htaccess' => {perms => WS_SERVE}, + + 'contrib/README' => {perms => OWNER_WRITE}, + 'contrib/*/README' => {perms => OWNER_WRITE}, + 'contrib/Bugzilla.pm' => {perms => OWNER_WRITE}, + 'docs/bugzilla.ent' => {perms => OWNER_WRITE}, + 'docs/makedocs.pl' => {perms => OWNER_EXECUTE}, + 'docs/style.css' => {perms => WS_SERVE}, + 'docs/*/rel_notes.txt' => {perms => WS_SERVE}, + 'docs/*/README.docs' => {perms => OWNER_WRITE}, + "$datadir/params.json" => {perms => CGI_WRITE}, + "$datadir/old-params.txt" => {perms => OWNER_WRITE}, + "$extensionsdir/create.pl" => {perms => OWNER_EXECUTE}, + "$extensionsdir/*/*.pl" => {perms => WS_EXECUTE}, + ); + + # Directories that we want to set the perms on, but not + # recurse through. These are directories we didn't create + # in checkesetup.pl. + my %non_recurse_dirs = ('.' => DIR_WS_SERVE, docs => DIR_WS_SERVE,); + + # This sets the permissions for each item inside each of these + # directories, including the directory itself. + # 'CVS' directories are special, though, and are never readable by + # the webserver. + my %recurse_dirs = ( + + # Writeable directories + $template_cache => {files => CGI_READ, dirs => DIR_CGI_OVERWRITE}, + $attachdir => {files => CGI_WRITE, dirs => DIR_CGI_WRITE}, + $webdotdir => {files => WS_SERVE, dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE}, + $graphsdir => {files => WS_SERVE, dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE}, + "$datadir/db" => {files => CGI_WRITE, dirs => DIR_CGI_WRITE}, + $assetsdir => + {files => WS_SERVE, dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE}, + + # Readable directories + "$datadir/mining" => {files => CGI_READ, dirs => DIR_CGI_READ}, + "$libdir/Bugzilla" => {files => CGI_READ, dirs => DIR_CGI_READ}, + $extlib => {files => CGI_READ, dirs => DIR_CGI_READ}, + $templatedir => {files => CGI_READ, dirs => DIR_CGI_READ}, + + # Directories in the extensions/ dir are WS_SERVE so that + # the web/ directories can be served by the web server. + # But, for extra security, we deny direct webserver access to + # the lib/ and template/ directories of extensions. + $extensionsdir => {files => CGI_READ, dirs => DIR_WS_SERVE}, + "$extensionsdir/*/lib" => {files => CGI_READ, dirs => DIR_CGI_READ}, + "$extensionsdir/*/template" => {files => CGI_READ, dirs => DIR_CGI_READ}, + + # Content served directly by the webserver + images => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + js => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + $skinsdir => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/html' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/pdf' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/txt' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/images' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + "$extensionsdir/*/web" => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + + # Directories only for the owner, not for the webserver. + '.bzr' => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + t => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + xt => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'docs/lib' => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'docs/*/xml' => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'contrib' => {files => OWNER_EXECUTE, dirs => DIR_OWNER_WRITE,}, + ); + + # --- FILES TO CREATE --- # + + # The name of each directory that we should actually *create*, + # pointing at its default permissions. + my %create_dirs = ( + + # This is DIR_ALSO_WS_SERVE because it contains $webdotdir and + # $assetsdir. + $datadir => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE, + + # Directories that are read-only for cgi scripts + "$datadir/mining" => DIR_CGI_READ, + "$datadir/extensions" => DIR_CGI_READ, + $extensionsdir => DIR_CGI_READ, + + # Directories that cgi scripts can write to. + "$datadir/db" => DIR_CGI_WRITE, + $attachdir => DIR_CGI_WRITE, + $graphsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + $webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + $assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + + # Directories that contain content served directly by the web server. + "$skinsdir/custom" => DIR_WS_SERVE, + "$skinsdir/contrib" => DIR_WS_SERVE, + ); + + # The name of each file, pointing at its default permissions and + # default contents. + my %create_files = ( + "$datadir/extensions/additional" => {perms => CGI_READ, contents => ''}, + + # We create this file so that it always has the right owner + # and permissions. Otherwise, the webserver creates it as + # owned by itself, which can cause problems if jobqueue.pl + # or something else is not running as the webserver or root. + "$datadir/mailer.testfile" => {perms => CGI_WRITE, contents => ''}, + ); + + # Because checksetup controls the creation of index.html separately + # from all other files, it gets its very own hash. + my %index_html = ( + 'index.html' => { + perms => WS_SERVE, + contents => <<EOT <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> @@ -317,35 +304,30 @@ sub FILESYSTEM { </body> </html> EOT - } - ); - - # Because checksetup controls the .htaccess creation separately - # by a localconfig variable, these go in a separate variable from - # %create_files. - # - # Note that these get WS_SERVE as their permission - # because they're *read* by the webserver, even though they're not - # actually, themselves, served. - my %htaccess = ( - "$attachdir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$libdir/Bugzilla/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$extlib/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$templatedir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 'contrib/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 't/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 'xt/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$datadir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - - "$graphsdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT + } + ); + + # Because checksetup controls the .htaccess creation separately + # by a localconfig variable, these go in a separate variable from + # %create_files. + # + # Note that these get WS_SERVE as their permission + # because they're *read* by the webserver, even though they're not + # actually, themselves, served. + my %htaccess = ( + "$attachdir/.htaccess" => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + "$libdir/Bugzilla/.htaccess" => + {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + "$extlib/.htaccess" => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + "$templatedir/.htaccess" => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + 'contrib/.htaccess' => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + 't/.htaccess' => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + 'xt/.htaccess' => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + "$datadir/.htaccess" => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + + "$graphsdir/.htaccess" => { + perms => WS_SERVE, + contents => <<EOT # Allow access to .png and .gif files. <FilesMatch (\\.gif|\\.png)\$> <IfModule mod_version.c> @@ -374,9 +356,11 @@ EOT Deny from all </IfModule> EOT - }, + }, - "$webdotdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT + "$webdotdir/.htaccess" => { + perms => WS_SERVE, + contents => <<EOT # Restrict access to .dot files to the public webdot server at research.att.com # if research.att.com ever changes their IP, or if you use a different # webdot server, you'll need to edit this @@ -425,9 +409,11 @@ EOT Deny from all </IfModule> EOT - }, + }, - "$assetsdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT + "$assetsdir/.htaccess" => { + perms => WS_SERVE, + contents => <<EOT # Allow access to .css files <FilesMatch \\.(css|js)\$> <IfModule mod_version.c> @@ -456,97 +442,102 @@ EOT Deny from all </IfModule> EOT - }, - - ); - - Bugzilla::Hook::process('install_filesystem', { - files => \%files, - create_dirs => \%create_dirs, - non_recurse_dirs => \%non_recurse_dirs, - recurse_dirs => \%recurse_dirs, - create_files => \%create_files, - htaccess => \%htaccess, - }); - - my %all_files = (%create_files, %htaccess, %index_html, %files); - my %all_dirs = (%create_dirs, %non_recurse_dirs); - - return { - create_dirs => \%create_dirs, - recurse_dirs => \%recurse_dirs, - all_dirs => \%all_dirs, - - create_files => \%create_files, - htaccess => \%htaccess, - index_html => \%index_html, - all_files => \%all_files, - }; -} + }, -sub update_filesystem { - my ($params) = @_; - my $fs = FILESYSTEM(); - my %dirs = %{$fs->{create_dirs}}; - my %files = %{$fs->{create_files}}; - - my $datadir = bz_locations->{'datadir'}; - my $graphsdir = bz_locations->{'graphsdir'}; - my $assetsdir = bz_locations->{'assetsdir'}; - # If the graphs/ directory doesn't exist, we're upgrading from - # a version old enough that we need to update the $datadir/mining - # format. - if (-d "$datadir/mining" && !-d $graphsdir) { - _update_old_charts($datadir); - } + ); - # If there is a file named '-All-' in $datadir/mining, then we're still - # having mining files named by product name, and we need to convert them to - # files named by product ID. - if (-e File::Spec->catfile($datadir, 'mining', '-All-')) { - _update_old_mining_filenames(File::Spec->catdir($datadir, 'mining')); + Bugzilla::Hook::process( + 'install_filesystem', + { + files => \%files, + create_dirs => \%create_dirs, + non_recurse_dirs => \%non_recurse_dirs, + recurse_dirs => \%recurse_dirs, + create_files => \%create_files, + htaccess => \%htaccess, } + ); - # By sorting the dirs, we assure that shorter-named directories - # (meaning parent directories) are always created before their - # child directories. - foreach my $dir (sort keys %dirs) { - unless (-d $dir) { - print "Creating $dir directory...\n"; - mkdir $dir or die "mkdir $dir failed: $!"; - # For some reason, passing in the permissions to "mkdir" - # doesn't work right, but doing a "chmod" does. - chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!"; - } - } + my %all_files = (%create_files, %htaccess, %index_html, %files); + my %all_dirs = (%create_dirs, %non_recurse_dirs); - # Move the testfile if we can't write to it, so that we can re-create - # it with the correct permissions below. - my $testfile = "$datadir/mailer.testfile"; - if (-e $testfile and !-w $testfile) { - _rename_file($testfile, "$testfile.old"); - } + return { + create_dirs => \%create_dirs, + recurse_dirs => \%recurse_dirs, + all_dirs => \%all_dirs, - # If old-params.txt exists in the root directory, move it to datadir. - my $oldparamsfile = "old_params.txt"; - if (-e $oldparamsfile) { - _rename_file($oldparamsfile, "$datadir/$oldparamsfile"); - } + create_files => \%create_files, + htaccess => \%htaccess, + index_html => \%index_html, + all_files => \%all_files, + }; +} - # Remove old assets htaccess file to force recreation with correct values. - if (-e "$assetsdir/.htaccess") { - if (read_text("$assetsdir/.htaccess") =~ /<FilesMatch \\\.css\$>/) { - unlink("$assetsdir/.htaccess"); - } +sub update_filesystem { + my ($params) = @_; + my $fs = FILESYSTEM(); + my %dirs = %{$fs->{create_dirs}}; + my %files = %{$fs->{create_files}}; + + my $datadir = bz_locations->{'datadir'}; + my $graphsdir = bz_locations->{'graphsdir'}; + my $assetsdir = bz_locations->{'assetsdir'}; + + # If the graphs/ directory doesn't exist, we're upgrading from + # a version old enough that we need to update the $datadir/mining + # format. + if (-d "$datadir/mining" && !-d $graphsdir) { + _update_old_charts($datadir); + } + + # If there is a file named '-All-' in $datadir/mining, then we're still + # having mining files named by product name, and we need to convert them to + # files named by product ID. + if (-e File::Spec->catfile($datadir, 'mining', '-All-')) { + _update_old_mining_filenames(File::Spec->catdir($datadir, 'mining')); + } + + # By sorting the dirs, we assure that shorter-named directories + # (meaning parent directories) are always created before their + # child directories. + foreach my $dir (sort keys %dirs) { + unless (-d $dir) { + print "Creating $dir directory...\n"; + mkdir $dir or die "mkdir $dir failed: $!"; + + # For some reason, passing in the permissions to "mkdir" + # doesn't work right, but doing a "chmod" does. + chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!"; } - - _create_files(%files); - if ($params->{index_html}) { - _create_files(%{$fs->{index_html}}); + } + + # Move the testfile if we can't write to it, so that we can re-create + # it with the correct permissions below. + my $testfile = "$datadir/mailer.testfile"; + if (-e $testfile and !-w $testfile) { + _rename_file($testfile, "$testfile.old"); + } + + # If old-params.txt exists in the root directory, move it to datadir. + my $oldparamsfile = "old_params.txt"; + if (-e $oldparamsfile) { + _rename_file($oldparamsfile, "$datadir/$oldparamsfile"); + } + + # Remove old assets htaccess file to force recreation with correct values. + if (-e "$assetsdir/.htaccess") { + if (read_text("$assetsdir/.htaccess") =~ /<FilesMatch \\\.css\$>/) { + unlink("$assetsdir/.htaccess"); } - elsif (-e 'index.html') { - my $templatedir = bz_locations()->{'templatedir'}; - print <<EOT; + } + + _create_files(%files); + if ($params->{index_html}) { + _create_files(%{$fs->{index_html}}); + } + elsif (-e 'index.html') { + my $templatedir = bz_locations()->{'templatedir'}; + print <<EOT; *** It appears that you still have an old index.html hanging around. Either the contents of this file should be moved into a template and @@ -554,445 +545,477 @@ sub update_filesystem { the file. EOT - } - - # Delete old files that no longer need to exist - - # 2001-04-29 jake@bugzilla.org - Remove oldemailtech - # http://bugzilla.mozilla.org/show_bugs.cgi?id=71552 - if (-d 'shadow') { - print "Removing shadow directory...\n"; - rmtree("shadow"); - } - - if (-e "$datadir/versioncache") { - print "Removing versioncache...\n"; - unlink "$datadir/versioncache"; - } - - if (-e "$datadir/duplicates.rdf") { - print "Removing duplicates.rdf...\n"; - unlink "$datadir/duplicates.rdf"; - unlink "$datadir/duplicates-old.rdf"; - } - - if (-e "$datadir/duplicates") { - print "Removing duplicates directory...\n"; - rmtree("$datadir/duplicates"); - } - - _remove_empty_css_files(); - _convert_single_file_skins(); - _remove_dynamic_assets(); + } + + # Delete old files that no longer need to exist + + # 2001-04-29 jake@bugzilla.org - Remove oldemailtech + # http://bugzilla.mozilla.org/show_bugs.cgi?id=71552 + if (-d 'shadow') { + print "Removing shadow directory...\n"; + rmtree("shadow"); + } + + if (-e "$datadir/versioncache") { + print "Removing versioncache...\n"; + unlink "$datadir/versioncache"; + } + + if (-e "$datadir/duplicates.rdf") { + print "Removing duplicates.rdf...\n"; + unlink "$datadir/duplicates.rdf"; + unlink "$datadir/duplicates-old.rdf"; + } + + if (-e "$datadir/duplicates") { + print "Removing duplicates directory...\n"; + rmtree("$datadir/duplicates"); + } + + _remove_empty_css_files(); + _convert_single_file_skins(); + _remove_dynamic_assets(); } sub _remove_empty_css_files { - my $skinsdir = bz_locations()->{'skinsdir'}; - foreach my $css_file (glob("$skinsdir/custom/*.css"), - glob("$skinsdir/contrib/*/*.css")) - { - _remove_empty_css($css_file); - } + my $skinsdir = bz_locations()->{'skinsdir'}; + foreach my $css_file (glob("$skinsdir/custom/*.css"), + glob("$skinsdir/contrib/*/*.css")) + { + _remove_empty_css($css_file); + } } # A simple helper for the update code that removes "empty" CSS files. sub _remove_empty_css { - my ($file) = @_; - my $basename = basename($file); - my $empty_contents = <<EOT; + my ($file) = @_; + my $basename = basename($file); + my $empty_contents = <<EOT; /* * Custom rules for $basename. * The rules you put here override rules in that stylesheet. */ EOT - if (length($empty_contents) == -s $file) { - open(my $fh, '<', $file) or warn "$file: $!"; - my $file_contents; - { local $/; $file_contents = <$fh>; } - if ($file_contents eq $empty_contents) { - print install_string('file_remove', { name => $file }), "\n"; - unlink $file or warn "$file: $!"; - } - }; + if (length($empty_contents) == -s $file) { + open(my $fh, '<', $file) or warn "$file: $!"; + my $file_contents; + { local $/; $file_contents = <$fh>; } + if ($file_contents eq $empty_contents) { + print install_string('file_remove', {name => $file}), "\n"; + unlink $file or warn "$file: $!"; + } + } } # We used to allow a single css file in the skins/contrib/ directory # to be a whole skin. sub _convert_single_file_skins { - my $skinsdir = bz_locations()->{'skinsdir'}; - foreach my $skin_file (glob "$skinsdir/contrib/*.css") { - my $dir_name = $skin_file; - $dir_name =~ s/\.css$//; - mkdir $dir_name or warn "$dir_name: $!"; - _rename_file($skin_file, "$dir_name/global.css"); - } + my $skinsdir = bz_locations()->{'skinsdir'}; + foreach my $skin_file (glob "$skinsdir/contrib/*.css") { + my $dir_name = $skin_file; + $dir_name =~ s/\.css$//; + mkdir $dir_name or warn "$dir_name: $!"; + _rename_file($skin_file, "$dir_name/global.css"); + } } # delete all automatically generated css/js files to force recreation at the # next request. sub _remove_dynamic_assets { - my @files = ( - glob(bz_locations()->{assetsdir} . '/*.css'), - glob(bz_locations()->{assetsdir} . '/*.js'), - ); - foreach my $file (@files) { - unlink($file); - } - - # remove old skins/assets directory - my $old_path = bz_locations()->{skinsdir} . '/assets'; - if (-d $old_path) { - foreach my $file (glob("$old_path/*.css")) { - unlink($file); - } - rmdir($old_path); + my @files = ( + glob(bz_locations()->{assetsdir} . '/*.css'), + glob(bz_locations()->{assetsdir} . '/*.js'), + ); + foreach my $file (@files) { + unlink($file); + } + + # remove old skins/assets directory + my $old_path = bz_locations()->{skinsdir} . '/assets'; + if (-d $old_path) { + foreach my $file (glob("$old_path/*.css")) { + unlink($file); } + rmdir($old_path); + } } sub create_htaccess { - _create_files(%{FILESYSTEM()->{htaccess}}); - - # Repair old .htaccess files - - my $webdot_dir = bz_locations()->{'webdotdir'}; - # The public webdot IP address changed. - my $webdot = new IO::File("$webdot_dir/.htaccess", 'r') - || die "$webdot_dir/.htaccess: $!"; - my $webdot_data; - { local $/; $webdot_data = <$webdot>; } + _create_files(%{FILESYSTEM()->{htaccess}}); + + # Repair old .htaccess files + + my $webdot_dir = bz_locations()->{'webdotdir'}; + + # The public webdot IP address changed. + my $webdot = new IO::File("$webdot_dir/.htaccess", 'r') + || die "$webdot_dir/.htaccess: $!"; + my $webdot_data; + { local $/; $webdot_data = <$webdot>; } + $webdot->close; + if ($webdot_data =~ /192\.20\.225\.10/) { + print "Repairing $webdot_dir/.htaccess...\n"; + $webdot_data =~ s/192\.20\.225\.10/192.20.225.0\/24/g; + $webdot = new IO::File("$webdot_dir/.htaccess", 'w') || die $!; + print $webdot $webdot_data; $webdot->close; - if ($webdot_data =~ /192\.20\.225\.10/) { - print "Repairing $webdot_dir/.htaccess...\n"; - $webdot_data =~ s/192\.20\.225\.10/192.20.225.0\/24/g; - $webdot = new IO::File("$webdot_dir/.htaccess", 'w') || die $!; - print $webdot $webdot_data; - $webdot->close; - } + } } sub _rename_file { - my ($from, $to) = @_; - print install_string('file_rename', { from => $from, to => $to }), "\n"; - if (-e $to) { - warn "$to already exists, not moving\n"; - } - else { - move($from, $to) or warn $!; - } + my ($from, $to) = @_; + print install_string('file_rename', {from => $from, to => $to}), "\n"; + if (-e $to) { + warn "$to already exists, not moving\n"; + } + else { + move($from, $to) or warn $!; + } } # A helper for the above functions. sub _create_files { - my (%files) = @_; - - # It's not necessary to sort these, but it does make the - # output of checksetup.pl look a bit nicer. - foreach my $file (sort keys %files) { - unless (-e $file) { - print "Creating $file...\n"; - my $info = $files{$file}; - my $fh = new IO::File($file, O_WRONLY | O_CREAT, $info->{perms}) - || die $!; - print $fh $info->{contents} if $info->{contents}; - $fh->close; - } + my (%files) = @_; + + # It's not necessary to sort these, but it does make the + # output of checksetup.pl look a bit nicer. + foreach my $file (sort keys %files) { + unless (-e $file) { + print "Creating $file...\n"; + my $info = $files{$file}; + my $fh = new IO::File($file, O_WRONLY | O_CREAT, $info->{perms}) || die $!; + print $fh $info->{contents} if $info->{contents}; + $fh->close; } + } } # If you ran a REALLY old version of Bugzilla, your chart files are in the # wrong format. This code is a little messy, because it's very old, and -# when moving it into this module, I couldn't test it so I left it almost +# when moving it into this module, I couldn't test it so I left it almost # completely alone. sub _update_old_charts { - my ($datadir) = @_; - print "Updating old chart storage format...\n"; - foreach my $in_file (glob("$datadir/mining/*")) { - # Don't try and upgrade image or db files! - next if (($in_file =~ /\.gif$/i) || - ($in_file =~ /\.png$/i) || - ($in_file =~ /\.db$/i) || - ($in_file =~ /\.orig$/i)); - - rename("$in_file", "$in_file.orig") or next; - open(IN, "<", "$in_file.orig") or next; - open(OUT, '>', $in_file) or next; - - # Fields in the header - my @declared_fields; - - # Fields we changed to half way through by mistake - # This list comes from an old version of collectstats.pl - # This part is only for people who ran later versions of 2.11 (devel) - my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED - RESOLVED VERIFIED CLOSED); - - # Fields we actually want (matches the current collectstats.pl) - my @out_fields = qw(DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED - VERIFIED CLOSED FIXED INVALID WONTFIX LATER REMIND - DUPLICATE WORKSFORME MOVED); - - while (<IN>) { - if (/^# fields?: (.*)\s$/) { - @declared_fields = map uc, (split /\||\r/, $1); - print OUT "# fields: ", join('|', @out_fields), "\n"; - } - elsif (/^(\d+\|.*)/) { - my @data = split(/\||\r/, $1); - my %data; - if (@data == @declared_fields) { - # old format - for my $i (0 .. $#declared_fields) { - $data{$declared_fields[$i]} = $data[$i]; - } - } - elsif (@data == @intermediate_fields) { - # Must have changed over at this point - for my $i (0 .. $#intermediate_fields) { - $data{$intermediate_fields[$i]} = $data[$i]; - } - } - elsif (@data == @out_fields) { - # This line's fine - it has the right number of entries - for my $i (0 .. $#out_fields) { - $data{$out_fields[$i]} = $data[$i]; - } - } - else { - print "Oh dear, input line $. of $in_file had " . - scalar(@data) . " fields\nThis was unexpected.", - " You may want to check your data files.\n"; - } - - print OUT join('|', - map { defined ($data{$_}) ? ($data{$_}) : "" } @out_fields), - "\n"; - } - else { - print OUT; - } + my ($datadir) = @_; + print "Updating old chart storage format...\n"; + foreach my $in_file (glob("$datadir/mining/*")) { + + # Don't try and upgrade image or db files! + next + if (($in_file =~ /\.gif$/i) + || ($in_file =~ /\.png$/i) + || ($in_file =~ /\.db$/i) + || ($in_file =~ /\.orig$/i)); + + rename("$in_file", "$in_file.orig") or next; + open(IN, "<", "$in_file.orig") or next; + open(OUT, '>', $in_file) or next; + + # Fields in the header + my @declared_fields; + + # Fields we changed to half way through by mistake + # This list comes from an old version of collectstats.pl + # This part is only for people who ran later versions of 2.11 (devel) + my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED + RESOLVED VERIFIED CLOSED); + + # Fields we actually want (matches the current collectstats.pl) + my @out_fields = qw(DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED + VERIFIED CLOSED FIXED INVALID WONTFIX LATER REMIND + DUPLICATE WORKSFORME MOVED); + + while (<IN>) { + if (/^# fields?: (.*)\s$/) { + @declared_fields = map uc, (split /\||\r/, $1); + print OUT "# fields: ", join('|', @out_fields), "\n"; + } + elsif (/^(\d+\|.*)/) { + my @data = split(/\||\r/, $1); + my %data; + if (@data == @declared_fields) { + + # old format + for my $i (0 .. $#declared_fields) { + $data{$declared_fields[$i]} = $data[$i]; + } + } + elsif (@data == @intermediate_fields) { + + # Must have changed over at this point + for my $i (0 .. $#intermediate_fields) { + $data{$intermediate_fields[$i]} = $data[$i]; + } + } + elsif (@data == @out_fields) { + + # This line's fine - it has the right number of entries + for my $i (0 .. $#out_fields) { + $data{$out_fields[$i]} = $data[$i]; + } + } + else { + print "Oh dear, input line $. of $in_file had " + . scalar(@data) + . " fields\nThis was unexpected.", + " You may want to check your data files.\n"; } - close(IN); - close(OUT); - } + print OUT join('|', map { defined($data{$_}) ? ($data{$_}) : "" } @out_fields), + "\n"; + } + else { + print OUT; + } + } + + close(IN); + close(OUT); + } } # The old naming scheme has product names as mining file names; we rename them # to product IDs. sub _update_old_mining_filenames { - my ($miningdir) = @_; - my $dbh = Bugzilla->dbh; - my @conversion_errors; - - # We use a dummy product instance with ID 0, representing all products - my $product_all = {id => 0, name => '-All-'}; - - print "Updating old charting data file names..."; - my @products = @{ $dbh->selectall_arrayref('SELECT id, name FROM products - ORDER BY name', {Slice=>{}}) }; - push(@products, $product_all); - foreach my $product (@products) { - if (-e File::Spec->catfile($miningdir, $product->{id})) { - push(@conversion_errors, - { product => $product, - message => 'A file named "' . $product->{id} . - '" already exists.' }); + my ($miningdir) = @_; + my $dbh = Bugzilla->dbh; + my @conversion_errors; + + # We use a dummy product instance with ID 0, representing all products + my $product_all = {id => 0, name => '-All-'}; + + print "Updating old charting data file names..."; + my @products = @{ + $dbh->selectall_arrayref( + 'SELECT id, name FROM products + ORDER BY name', {Slice => {}} + ) + }; + push(@products, $product_all); + foreach my $product (@products) { + if (-e File::Spec->catfile($miningdir, $product->{id})) { + push( + @conversion_errors, + { + product => $product, + message => 'A file named "' . $product->{id} . '" already exists.' } + ); } + } - if (! @conversion_errors) { - # Renaming mining files should work now without a hitch. - foreach my $product (@products) { - if (! rename(File::Spec->catfile($miningdir, $product->{name}), - File::Spec->catfile($miningdir, $product->{id}))) { - push(@conversion_errors, - { product => $product, - message => $! }); - } - } - } + if (!@conversion_errors) { - # Error reporting - if (! @conversion_errors) { - print " done.\n"; + # Renaming mining files should work now without a hitch. + foreach my $product (@products) { + if ( + !rename( + File::Spec->catfile($miningdir, $product->{name}), + File::Spec->catfile($miningdir, $product->{id}) + ) + ) + { + push(@conversion_errors, {product => $product, message => $!}); + } } - else { - print " FAILED:\n"; - foreach my $error (@conversion_errors) { - printf "Cannot rename charting data file for product %d (%s): %s\n", - $error->{product}->{id}, $error->{product}->{name}, - $error->{message}; - } - print "You need to empty the \"$miningdir\" directory, then run\n", - " collectstats.pl --regenerate\n", - "in order to clean this up.\n"; + } + + # Error reporting + if (!@conversion_errors) { + print " done.\n"; + } + else { + print " FAILED:\n"; + foreach my $error (@conversion_errors) { + printf "Cannot rename charting data file for product %d (%s): %s\n", + $error->{product}->{id}, $error->{product}->{name}, $error->{message}; } + print "You need to empty the \"$miningdir\" directory, then run\n", + " collectstats.pl --regenerate\n", "in order to clean this up.\n"; + } } sub fix_dir_permissions { - my ($dir) = @_; - return if ON_WINDOWS; - # Note that _get_owner_and_group is always silent here. - my ($owner_id, $group_id) = _get_owner_and_group(); - - my $perms; - my $fs = FILESYSTEM(); - if ($perms = $fs->{recurse_dirs}->{$dir}) { - _fix_perms_recursively($dir, $owner_id, $group_id, $perms); - } - elsif ($perms = $fs->{all_dirs}->{$dir}) { - _fix_perms($dir, $owner_id, $group_id, $perms); - } - else { - # Do nothing. We know nothing about this directory. - warn "Unknown directory $dir"; - } + my ($dir) = @_; + return if ON_WINDOWS; + + # Note that _get_owner_and_group is always silent here. + my ($owner_id, $group_id) = _get_owner_and_group(); + + my $perms; + my $fs = FILESYSTEM(); + if ($perms = $fs->{recurse_dirs}->{$dir}) { + _fix_perms_recursively($dir, $owner_id, $group_id, $perms); + } + elsif ($perms = $fs->{all_dirs}->{$dir}) { + _fix_perms($dir, $owner_id, $group_id, $perms); + } + else { + # Do nothing. We know nothing about this directory. + warn "Unknown directory $dir"; + } } sub fix_file_permissions { - my ($file) = @_; - return if ON_WINDOWS; - my $perms = FILESYSTEM()->{all_files}->{$file}->{perms}; - # Note that _get_owner_and_group is always silent here. - my ($owner_id, $group_id) = _get_owner_and_group(); - _fix_perms($file, $owner_id, $group_id, $perms); + my ($file) = @_; + return if ON_WINDOWS; + my $perms = FILESYSTEM()->{all_files}->{$file}->{perms}; + + # Note that _get_owner_and_group is always silent here. + my ($owner_id, $group_id) = _get_owner_and_group(); + _fix_perms($file, $owner_id, $group_id, $perms); } sub fix_all_file_permissions { - my ($output) = @_; + my ($output) = @_; - # _get_owner_and_group also checks that the webservergroup is valid. - my ($owner_id, $group_id) = _get_owner_and_group($output); + # _get_owner_and_group also checks that the webservergroup is valid. + my ($owner_id, $group_id) = _get_owner_and_group($output); - return if ON_WINDOWS; + return if ON_WINDOWS; - my $fs = FILESYSTEM(); - my %files = %{$fs->{all_files}}; - my %dirs = %{$fs->{all_dirs}}; - my %recurse_dirs = %{$fs->{recurse_dirs}}; + my $fs = FILESYSTEM(); + my %files = %{$fs->{all_files}}; + my %dirs = %{$fs->{all_dirs}}; + my %recurse_dirs = %{$fs->{recurse_dirs}}; - print get_text('install_file_perms_fix') . "\n" if $output; + print get_text('install_file_perms_fix') . "\n" if $output; - foreach my $dir (sort keys %dirs) { - next unless -d $dir; - _fix_perms($dir, $owner_id, $group_id, $dirs{$dir}); - } + foreach my $dir (sort keys %dirs) { + next unless -d $dir; + _fix_perms($dir, $owner_id, $group_id, $dirs{$dir}); + } - foreach my $pattern (sort keys %recurse_dirs) { - my $perms = $recurse_dirs{$pattern}; - # %recurse_dirs supports globs - foreach my $dir (glob $pattern) { - next unless -d $dir; - _fix_perms_recursively($dir, $owner_id, $group_id, $perms); - } + foreach my $pattern (sort keys %recurse_dirs) { + my $perms = $recurse_dirs{$pattern}; + + # %recurse_dirs supports globs + foreach my $dir (glob $pattern) { + next unless -d $dir; + _fix_perms_recursively($dir, $owner_id, $group_id, $perms); } + } - foreach my $file (sort keys %files) { - # %files supports globs - foreach my $filename (glob $file) { - # Don't touch directories. - next if -d $filename || !-e $filename; - _fix_perms($filename, $owner_id, $group_id, - $files{$file}->{perms}); - } + foreach my $file (sort keys %files) { + + # %files supports globs + foreach my $filename (glob $file) { + + # Don't touch directories. + next if -d $filename || !-e $filename; + _fix_perms($filename, $owner_id, $group_id, $files{$file}->{perms}); } + } - _fix_cvs_dirs($owner_id, '.'); + _fix_cvs_dirs($owner_id, '.'); } sub _get_owner_and_group { - my ($output) = @_; - my $group_id = _check_web_server_group($output); - return () if ON_WINDOWS; + my ($output) = @_; + my $group_id = _check_web_server_group($output); + return () if ON_WINDOWS; - my $owner_id = POSIX::getuid(); - $group_id = POSIX::getgid() unless defined $group_id; - return ($owner_id, $group_id); + my $owner_id = POSIX::getuid(); + $group_id = POSIX::getgid() unless defined $group_id; + return ($owner_id, $group_id); } # A helper for fix_all_file_permissions sub _fix_cvs_dirs { - my ($owner_id, $dir) = @_; - my $owner_gid = POSIX::getgid(); - find({ no_chdir => 1, wanted => sub { + my ($owner_id, $dir) = @_; + my $owner_gid = POSIX::getgid(); + find( + { + no_chdir => 1, + wanted => sub { my $name = $File::Find::name; - if ($File::Find::dir =~ /\/CVS/ || $_ eq '.cvsignore' - || (-d $name && $_ =~ /CVS$/)) + if ( $File::Find::dir =~ /\/CVS/ + || $_ eq '.cvsignore' + || (-d $name && $_ =~ /CVS$/)) { - my $perms = 0600; - if (-d $name) { - $perms = 0700; - } - _fix_perms($name, $owner_id, $owner_gid, $perms); + my $perms = 0600; + if (-d $name) { + $perms = 0700; + } + _fix_perms($name, $owner_id, $owner_gid, $perms); } - }}, $dir); + } + }, + $dir + ); } sub _fix_perms { - my ($name, $owner, $group, $perms) = @_; - #printf ("Changing $name to %o\n", $perms); - - # The webserver should never try to chown files. - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - chown $owner, $group, $name - or warn install_string('chown_failed', { path => $name, - error => $! }) . "\n"; - } - chmod $perms, $name - or warn install_string('chmod_failed', { path => $name, - error => $! }) . "\n"; + my ($name, $owner, $group, $perms) = @_; + + #printf ("Changing $name to %o\n", $perms); + + # The webserver should never try to chown files. + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + chown $owner, $group, $name + or warn install_string('chown_failed', {path => $name, error => $!}) . "\n"; + } + chmod $perms, $name + or warn install_string('chmod_failed', {path => $name, error => $!}) . "\n"; } sub _fix_perms_recursively { - my ($dir, $owner_id, $group_id, $perms) = @_; - # Set permissions on the directory itself. - _fix_perms($dir, $owner_id, $group_id, $perms->{dirs}); - # Now recurse through the directory and set the correct permissions - # on subdirectories and files. - find({ no_chdir => 1, wanted => sub { + my ($dir, $owner_id, $group_id, $perms) = @_; + + # Set permissions on the directory itself. + _fix_perms($dir, $owner_id, $group_id, $perms->{dirs}); + + # Now recurse through the directory and set the correct permissions + # on subdirectories and files. + find( + { + no_chdir => 1, + wanted => sub { my $name = $File::Find::name; if (-d $name) { - _fix_perms($name, $owner_id, $group_id, $perms->{dirs}); + _fix_perms($name, $owner_id, $group_id, $perms->{dirs}); } else { - _fix_perms($name, $owner_id, $group_id, $perms->{files}); + _fix_perms($name, $owner_id, $group_id, $perms->{files}); } - }}, $dir); + } + }, + $dir + ); } sub _check_web_server_group { - my ($output) = @_; - - my $group = Bugzilla->localconfig->{'webservergroup'}; - my $filename = bz_locations()->{'localconfig'}; - my $group_id; - - # If we are on Windows, webservergroup does nothing - if (ON_WINDOWS && $group && $output) { - print "\n\n" . get_text('install_webservergroup_windows') . "\n\n"; - } - - # If we're not on Windows, make sure that webservergroup isn't - # empty. - elsif (!ON_WINDOWS && !$group && $output) { - print "\n\n" . get_text('install_webservergroup_empty') . "\n\n"; - } - - # If we're not on Windows, make sure we are actually a member of - # the webservergroup. - elsif (!ON_WINDOWS && $group) { - $group_id = getgrnam($group); - ThrowCodeError('invalid_webservergroup', { group => $group }) - unless defined $group_id; - - # If on unix, see if we need to print a warning about a webservergroup - # that we can't chgrp to - if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $)))) { - print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n"; - } + my ($output) = @_; + + my $group = Bugzilla->localconfig->{'webservergroup'}; + my $filename = bz_locations()->{'localconfig'}; + my $group_id; + + # If we are on Windows, webservergroup does nothing + if (ON_WINDOWS && $group && $output) { + print "\n\n" . get_text('install_webservergroup_windows') . "\n\n"; + } + + # If we're not on Windows, make sure that webservergroup isn't + # empty. + elsif (!ON_WINDOWS && !$group && $output) { + print "\n\n" . get_text('install_webservergroup_empty') . "\n\n"; + } + + # If we're not on Windows, make sure we are actually a member of + # the webservergroup. + elsif (!ON_WINDOWS && $group) { + $group_id = getgrnam($group); + ThrowCodeError('invalid_webservergroup', {group => $group}) + unless defined $group_id; + + # If on unix, see if we need to print a warning about a webservergroup + # that we can't chgrp to + if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $)))) { + print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n"; } + } - return $group_id; + return $group_id; } 1; diff --git a/Bugzilla/Install/Localconfig.pm b/Bugzilla/Install/Localconfig.pm index 7f473cc77..bc4557309 100644 --- a/Bugzilla/Install/Localconfig.pm +++ b/Bugzilla/Install/Localconfig.pm @@ -31,151 +31,104 @@ use Term::ANSIColor; use parent qw(Exporter); our @EXPORT_OK = qw( - read_localconfig - update_localconfig + read_localconfig + update_localconfig ); use constant LOCALCONFIG_VARS => ( - { - name => 'create_htaccess', - default => 1, - }, - { - name => 'webservergroup', - default => ON_WINDOWS ? '' : 'apache', - }, - { - name => 'use_suexec', - default => 0, - }, - { - name => 'db_driver', - default => 'mysql', - }, - { - name => 'db_host', - default => 'localhost', - }, - { - name => 'db_name', - default => 'bugs', - }, - { - name => 'db_user', - default => 'bugs', - }, - { - name => 'db_pass', - default => '', - }, - { - name => 'db_port', - default => 0, - }, - { - name => 'db_sock', - default => '', - }, - { - name => 'db_check', - default => 1, - }, - { - name => 'db_mysql_ssl_ca_file', - default => '', - }, - { - name => 'db_mysql_ssl_ca_path', - default => '', - }, - { - name => 'db_mysql_ssl_client_cert', - default => '', - }, - { - name => 'db_mysql_ssl_client_key', - default => '', - }, - { - name => 'index_html', - default => 0, - }, - { - name => 'interdiffbin', - default => sub { bin_loc('interdiff') }, - }, - { - name => 'diffpath', - default => sub { dirname(bin_loc('diff')) }, - }, - { - name => 'site_wide_secret', - # 64 characters is roughly the equivalent of a 384-bit key, which - # is larger than anybody would ever be able to brute-force. - default => sub { generate_random_password(64) }, - }, + {name => 'create_htaccess', default => 1,}, + {name => 'webservergroup', default => ON_WINDOWS ? '' : 'apache',}, + {name => 'use_suexec', default => 0,}, + {name => 'db_driver', default => 'mysql',}, + {name => 'db_host', default => 'localhost',}, + {name => 'db_name', default => 'bugs',}, + {name => 'db_user', default => 'bugs',}, + {name => 'db_pass', default => '',}, + {name => 'db_port', default => 0,}, + {name => 'db_sock', default => '',}, + {name => 'db_check', default => 1,}, + {name => 'db_mysql_ssl_ca_file', default => '',}, + {name => 'db_mysql_ssl_ca_path', default => '',}, + {name => 'db_mysql_ssl_client_cert', default => '',}, + {name => 'db_mysql_ssl_client_key', default => '',}, + {name => 'index_html', default => 0,}, + {name => 'interdiffbin', default => sub { bin_loc('interdiff') },}, + {name => 'diffpath', default => sub { dirname(bin_loc('diff')) },}, + { + name => 'site_wide_secret', + + # 64 characters is roughly the equivalent of a 384-bit key, which + # is larger than anybody would ever be able to brute-force. + default => sub { generate_random_password(64) }, + }, ); sub read_localconfig { - my ($include_deprecated) = @_; - my $filename = bz_locations()->{'localconfig'}; - - my %localconfig; - if (-e $filename) { - my $s = new Safe; - # Some people like to store their database password in another file. - $s->permit('dofile'); - - $s->rdo($filename); - if ($@ || $!) { - my $err_msg = $@ ? $@ : $!; - die install_string('error_localconfig_read', - { error => $err_msg, localconfig => $filename }), "\n"; - } + my ($include_deprecated) = @_; + my $filename = bz_locations()->{'localconfig'}; + + my %localconfig; + if (-e $filename) { + my $s = new Safe; + + # Some people like to store their database password in another file. + $s->permit('dofile'); + + $s->rdo($filename); + if ($@ || $!) { + my $err_msg = $@ ? $@ : $!; + die install_string( + 'error_localconfig_read', {error => $err_msg, localconfig => $filename} + ), + "\n"; + } - my @read_symbols; - if ($include_deprecated) { - # First we have to get the whole symbol table - my $safe_root = $s->root; - my %safe_package; - { no strict 'refs'; %safe_package = %{$safe_root . "::"}; } - # And now we read the contents of every var in the symbol table. - # However: - # * We only include symbols that start with an alphanumeric - # character. This excludes symbols like "_<./localconfig" - # that show up in some perls. - # * We ignore the INC symbol, which exists in every package. - # * Perl 5.10 imports a lot of random symbols that all - # contain "::", and we want to ignore those. - @read_symbols = grep { /^[A-Za-z0-1]/ and !/^INC$/ and !/::/ } - (keys %safe_package); - } - else { - @read_symbols = map($_->{name}, LOCALCONFIG_VARS); - } - foreach my $var (@read_symbols) { - my $glob = $s->varglob($var); - # We can't get the type of a variable out of a Safe automatically. - # We can only get the glob itself. So we figure out its type this - # way, by trying first a scalar, then an array, then a hash. - # - # The interesting thing is that this converts all deprecated - # array or hash vars into hashrefs or arrayrefs, but that's - # fine since as I write this all modern localconfig vars are - # actually scalars. - if (defined $$glob) { - $localconfig{$var} = $$glob; - } - elsif (@$glob) { - $localconfig{$var} = \@$glob; - } - elsif (%$glob) { - $localconfig{$var} = \%$glob; - } - } + my @read_symbols; + if ($include_deprecated) { + + # First we have to get the whole symbol table + my $safe_root = $s->root; + my %safe_package; + { no strict 'refs'; %safe_package = %{$safe_root . "::"}; } + + # And now we read the contents of every var in the symbol table. + # However: + # * We only include symbols that start with an alphanumeric + # character. This excludes symbols like "_<./localconfig" + # that show up in some perls. + # * We ignore the INC symbol, which exists in every package. + # * Perl 5.10 imports a lot of random symbols that all + # contain "::", and we want to ignore those. + @read_symbols + = grep { /^[A-Za-z0-1]/ and !/^INC$/ and !/::/ } (keys %safe_package); } + else { + @read_symbols = map($_->{name}, LOCALCONFIG_VARS); + } + foreach my $var (@read_symbols) { + my $glob = $s->varglob($var); + + # We can't get the type of a variable out of a Safe automatically. + # We can only get the glob itself. So we figure out its type this + # way, by trying first a scalar, then an array, then a hash. + # + # The interesting thing is that this converts all deprecated + # array or hash vars into hashrefs or arrayrefs, but that's + # fine since as I write this all modern localconfig vars are + # actually scalars. + if (defined $$glob) { + $localconfig{$var} = $$glob; + } + elsif (@$glob) { + $localconfig{$var} = \@$glob; + } + elsif (%$glob) { + $localconfig{$var} = \%$glob; + } + } + } - return \%localconfig; + return \%localconfig; } @@ -204,94 +157,97 @@ sub read_localconfig { # Cute, ey? # sub update_localconfig { - my ($params) = @_; - - my $output = $params->{output} || 0; - my $answer = Bugzilla->installation_answers; - my $localconfig = read_localconfig('include deprecated'); - - my @new_vars; - foreach my $var (LOCALCONFIG_VARS) { - my $name = $var->{name}; - my $value = $localconfig->{$name}; - # Regenerate site_wide_secret if it was made by our old, weak - # generate_random_password. Previously we used to generate - # a 256-character string for site_wide_secret. - $value = undef if ($name eq 'site_wide_secret' and defined $value - and length($value) == 256); - - if (!defined $value) { - $var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE'; - if (exists $answer->{$name}) { - $localconfig->{$name} = $answer->{$name}; - } - else { - # If the user did not supply an answers file, then they get - # notified about every variable that gets added. If there was - # an answer file, then we don't notify about site_wide_secret - # because we assume the intent was to auto-generate it anyway. - if (!scalar(keys %$answer) || $name ne 'site_wide_secret') { - push(@new_vars, $name); - } - $localconfig->{$name} = $var->{default}; - } + my ($params) = @_; + + my $output = $params->{output} || 0; + my $answer = Bugzilla->installation_answers; + my $localconfig = read_localconfig('include deprecated'); + + my @new_vars; + foreach my $var (LOCALCONFIG_VARS) { + my $name = $var->{name}; + my $value = $localconfig->{$name}; + + # Regenerate site_wide_secret if it was made by our old, weak + # generate_random_password. Previously we used to generate + # a 256-character string for site_wide_secret. + $value = undef + if ($name eq 'site_wide_secret' and defined $value and length($value) == 256); + + if (!defined $value) { + $var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE'; + if (exists $answer->{$name}) { + $localconfig->{$name} = $answer->{$name}; + } + else { + # If the user did not supply an answers file, then they get + # notified about every variable that gets added. If there was + # an answer file, then we don't notify about site_wide_secret + # because we assume the intent was to auto-generate it anyway. + if (!scalar(keys %$answer) || $name ne 'site_wide_secret') { + push(@new_vars, $name); } + $localconfig->{$name} = $var->{default}; + } } - - if (!$localconfig->{'interdiffbin'} && $output) { - print "\n", install_string('patchutils_missing'), "\n"; + } + + if (!$localconfig->{'interdiffbin'} && $output) { + print "\n", install_string('patchutils_missing'), "\n"; + } + + my @old_vars; + foreach my $var (keys %$localconfig) { + push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS); + } + + my $filename = bz_locations->{'localconfig'}; + + # Move any custom or old variables into a separate file. + if (scalar @old_vars) { + my $filename_old = "$filename.old"; + open(my $old_file, ">>:utf8", $filename_old) or die "$filename_old: $!"; + local $Data::Dumper::Purity = 1; + foreach my $var (@old_vars) { + print $old_file Data::Dumper->Dump([$localconfig->{$var}], ["*$var"]) . "\n\n"; } - - my @old_vars; - foreach my $var (keys %$localconfig) { - push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS); - } - - my $filename = bz_locations->{'localconfig'}; - - # Move any custom or old variables into a separate file. - if (scalar @old_vars) { - my $filename_old = "$filename.old"; - open(my $old_file, ">>:utf8", $filename_old) - or die "$filename_old: $!"; - local $Data::Dumper::Purity = 1; - foreach my $var (@old_vars) { - print $old_file Data::Dumper->Dump([$localconfig->{$var}], - ["*$var"]) . "\n\n"; - } - close $old_file; - my $oldstuff = join(', ', @old_vars); - print install_string('lc_old_vars', - { localconfig => $filename, old_file => $filename_old, - vars => $oldstuff }), "\n"; - } - - # Re-write localconfig - open(my $fh, ">:utf8", $filename) or die "$filename: $!"; - foreach my $var (LOCALCONFIG_VARS) { - my $name = $var->{name}; - my $desc = install_string("localconfig_$name", { root => ROOT_USER }); - chomp($desc); - # Make the description into a comment. - $desc =~ s/^/# /mg; - print $fh $desc, "\n", - Data::Dumper->Dump([$localconfig->{$name}], - ["*$name"]), "\n"; - } - - if (@new_vars) { - my $newstuff = join(', ', @new_vars); - print "\n"; - print colored(install_string('lc_new_vars', { localconfig => $filename, - new_vars => wrap_hard($newstuff, 70) }), - COLOR_ERROR), "\n"; - exit; - } - - # Reset the cache for Bugzilla->localconfig so that it will be re-read - delete Bugzilla->request_cache->{localconfig}; - - return { old_vars => \@old_vars, new_vars => \@new_vars }; + close $old_file; + my $oldstuff = join(', ', @old_vars); + print install_string('lc_old_vars', + {localconfig => $filename, old_file => $filename_old, vars => $oldstuff}), + "\n"; + } + + # Re-write localconfig + open(my $fh, ">:utf8", $filename) or die "$filename: $!"; + foreach my $var (LOCALCONFIG_VARS) { + my $name = $var->{name}; + my $desc = install_string("localconfig_$name", {root => ROOT_USER}); + chomp($desc); + + # Make the description into a comment. + $desc =~ s/^/# /mg; + print $fh $desc, "\n", Data::Dumper->Dump([$localconfig->{$name}], ["*$name"]), + "\n"; + } + + if (@new_vars) { + my $newstuff = join(', ', @new_vars); + print "\n"; + print colored( + install_string( + 'lc_new_vars', {localconfig => $filename, new_vars => wrap_hard($newstuff, 70)} + ), + COLOR_ERROR + ), + "\n"; + exit; + } + + # Reset the cache for Bugzilla->localconfig so that it will be re-read + delete Bugzilla->request_cache->{localconfig}; + + return {old_vars => \@old_vars, new_vars => \@new_vars}; } 1; diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm index 61496d843..4c10263ee 100644 --- a/Bugzilla/Install/Requirements.pm +++ b/Bugzilla/Install/Requirements.pm @@ -19,21 +19,21 @@ use warnings; use Bugzilla::Constants; use Bugzilla::Install::Util qw(install_string bin_loc - extension_requirement_packages); + extension_requirement_packages); use List::Util qw(max); use Term::ANSIColor; use parent qw(Exporter); our @EXPORT = qw( - REQUIRED_MODULES - OPTIONAL_MODULES - FEATURE_FILES - - check_requirements - check_graphviz - have_vers - install_command - map_files_to_features + REQUIRED_MODULES + OPTIONAL_MODULES + FEATURE_FILES + + check_requirements + check_graphviz + have_vers + install_command + map_files_to_features ); # This is how many *'s are in the top of each "box" message printed @@ -45,12 +45,12 @@ use constant TABLE_WIDTH => 71; # # The keys are the names of the modules, the values are what the module # is called in the output of "apachectl -t -D DUMP_MODULES". -use constant APACHE_MODULES => { - mod_headers => 'headers_module', - mod_env => 'env_module', - mod_expires => 'expires_module', - mod_rewrite => 'rewrite_module', - mod_version => 'version_module' +use constant APACHE_MODULES => { + mod_headers => 'headers_module', + mod_env => 'env_module', + mod_expires => 'expires_module', + mod_rewrite => 'rewrite_module', + mod_version => 'version_module' }; # These are all of the binaries that we could possibly use that can @@ -63,12 +63,14 @@ use constant APACHE => qw(apachectl httpd apache2 apache); # If we don't find any of the above binaries in the normal PATH, # these are extra places we look. -use constant APACHE_PATH => [qw( - /usr/sbin +use constant APACHE_PATH => [ + qw( + /usr/sbin /usr/local/sbin /usr/libexec /usr/local/libexec -)]; + ) +]; # The below two constants are subroutines so that they can implement # a hook. Other than that they are actually constants. @@ -82,737 +84,757 @@ use constant APACHE_PATH => [qw( # are 'blacklisted'--that is, even if the version is high enough, Bugzilla # will refuse to say that it's OK to run with that version. sub REQUIRED_MODULES { - my @modules = ( + my @modules = ( { - package => 'CGI.pm', - module => 'CGI', - # 3.51 fixes a security problem that affects Bugzilla. - # (bug 591165) - version => '3.51', - }, - { - package => 'Digest-SHA', - module => 'Digest::SHA', - version => 0 + package => 'CGI.pm', + module => 'CGI', + + # 3.51 fixes a security problem that affects Bugzilla. + # (bug 591165) + version => '3.51', }, + {package => 'Digest-SHA', module => 'Digest::SHA', version => 0}, + # 0.23 fixes incorrect handling of 1/2 & 3/4 timezones. - { - package => 'TimeDate', - module => 'Date::Format', - version => '2.23' - }, + {package => 'TimeDate', module => 'Date::Format', version => '2.23'}, + # 0.75 fixes a warning thrown with Perl 5.17 and newer. - { - package => 'DateTime', - module => 'DateTime', - version => '0.75' - }, + {package => 'DateTime', module => 'DateTime', version => '0.75'}, + # 1.64 fixes a taint issue preventing the local timezone from # being determined on some systems. { - package => 'DateTime-TimeZone', - module => 'DateTime::TimeZone', - version => '1.64' + package => 'DateTime-TimeZone', + module => 'DateTime::TimeZone', + version => '1.64' }, + # 1.54 is required for Perl 5.10+. It also makes DBD::Oracle happy. { - package => 'DBI', - module => 'DBI', - version => ($^V >= v5.13.3) ? '1.614' : '1.54' + package => 'DBI', + module => 'DBI', + version => ($^V >= v5.13.3) ? '1.614' : '1.54' }, + # 2.24 contains several useful text virtual methods. - { - package => 'Template-Toolkit', - module => 'Template', - version => '2.24' - }, + {package => 'Template-Toolkit', module => 'Template', version => '2.24'}, + # 1.300011 has a debug mode for SMTP and automatically pass -i to sendmail. + {package => 'Email-Sender', module => 'Email::Sender', version => '1.300011',}, { - package => 'Email-Sender', - module => 'Email::Sender', - version => '1.300011', - }, - { - package => 'Email-MIME', - module => 'Email::MIME', - # This fixes a memory leak in walk_parts that affected jobqueue.pl. - version => '1.904' + package => 'Email-MIME', + module => 'Email::MIME', + + # This fixes a memory leak in walk_parts that affected jobqueue.pl. + version => '1.904' }, { - package => 'URI', - module => 'URI', - # Follows RFC 3986 to escape characters in URI::Escape. - version => '1.55', + package => 'URI', + module => 'URI', + + # Follows RFC 3986 to escape characters in URI::Escape. + version => '1.55', }, + # 0.32 fixes several memory leaks in the XS version of some functions. + {package => 'List-MoreUtils', module => 'List::MoreUtils', version => 0.32,}, { - package => 'List-MoreUtils', - module => 'List::MoreUtils', - version => 0.32, + package => 'Math-Random-ISAAC', + module => 'Math::Random::ISAAC', + version => '1.0.1', }, { - package => 'Math-Random-ISAAC', - module => 'Math::Random::ISAAC', - version => '1.0.1', - }, - { - package => 'JSON-XS', - module => 'JSON::XS', - # 2.0 is the first version that will work with JSON::RPC. - version => '2.01', + package => 'JSON-XS', + module => 'JSON::XS', + + # 2.0 is the first version that will work with JSON::RPC. + version => '2.01', }, - ); + ); - if (ON_WINDOWS) { - push(@modules, - { - package => 'Win32', - module => 'Win32', - # 0.35 fixes a memory leak in GetOSVersion, which we use. - version => 0.35, - }, - { - package => 'Win32-API', - module => 'Win32::API', - # 0.55 fixes a bug with char* that might affect Bugzilla::RNG. - version => '0.55', - }, - { - package => 'DateTime-TimeZone-Local-Win32', - module => 'DateTime::TimeZone::Local::Win32', - # We require DateTime::TimeZone 1.64, so this version must match. - version => '1.64', - } - ); - } + if (ON_WINDOWS) { + push( + @modules, + { + package => 'Win32', + module => 'Win32', - my $extra_modules = _get_extension_requirements('REQUIRED_MODULES'); - push(@modules, @$extra_modules); - return \@modules; -}; + # 0.35 fixes a memory leak in GetOSVersion, which we use. + version => 0.35, + }, + { + package => 'Win32-API', + module => 'Win32::API', + + # 0.55 fixes a bug with char* that might affect Bugzilla::RNG. + version => '0.55', + }, + { + package => 'DateTime-TimeZone-Local-Win32', + module => 'DateTime::TimeZone::Local::Win32', + + # We require DateTime::TimeZone 1.64, so this version must match. + version => '1.64', + } + ); + } + + my $extra_modules = _get_extension_requirements('REQUIRED_MODULES'); + push(@modules, @$extra_modules); + return \@modules; +} sub OPTIONAL_MODULES { - my @modules = ( + my @modules = ( { - package => 'GD', - module => 'GD', - version => '1.20', - feature => [qw(graphical_reports new_charts old_charts)], + package => 'GD', + module => 'GD', + version => '1.20', + feature => [qw(graphical_reports new_charts old_charts)], }, { - package => 'Chart', - module => 'Chart::Lines', - # Versions below 2.4.1 cannot be compared accurately, see - # https://rt.cpan.org/Public/Bug/Display.html?id=28218. - version => '2.4.1', - feature => [qw(new_charts old_charts)], + package => 'Chart', + module => 'Chart::Lines', + + # Versions below 2.4.1 cannot be compared accurately, see + # https://rt.cpan.org/Public/Bug/Display.html?id=28218. + version => '2.4.1', + feature => [qw(new_charts old_charts)], }, { - package => 'Template-GD', - # This module tells us whether or not Template-GD is installed - # on Template-Toolkits after 2.14, and still works with 2.14 and lower. - module => 'Template::Plugin::GD::Image', - version => 0, - feature => ['graphical_reports'], + package => 'Template-GD', + + # This module tells us whether or not Template-GD is installed + # on Template-Toolkits after 2.14, and still works with 2.14 and lower. + module => 'Template::Plugin::GD::Image', + version => 0, + feature => ['graphical_reports'], }, { - package => 'GDTextUtil', - module => 'GD::Text', - version => 0, - feature => ['graphical_reports'], + package => 'GDTextUtil', + module => 'GD::Text', + version => 0, + feature => ['graphical_reports'], }, { - package => 'GDGraph', - module => 'GD::Graph', - version => 0, - feature => ['graphical_reports'], + package => 'GDGraph', + module => 'GD::Graph', + version => 0, + feature => ['graphical_reports'], }, { - package => 'MIME-tools', - # MIME::Parser is packaged as MIME::Tools on ActiveState Perl - module => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser', - version => '5.406', - feature => ['moving'], + package => 'MIME-tools', + + # MIME::Parser is packaged as MIME::Tools on ActiveState Perl + module => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser', + version => '5.406', + feature => ['moving'], }, { - package => 'libwww-perl', - module => 'LWP::UserAgent', - version => 0, - feature => ['updates'], + package => 'libwww-perl', + module => 'LWP::UserAgent', + version => 0, + feature => ['updates'], }, { - package => 'XML-Twig', - module => 'XML::Twig', - version => 0, - feature => ['moving', 'updates'], + package => 'XML-Twig', + module => 'XML::Twig', + version => 0, + feature => ['moving', 'updates'], }, { - package => 'PatchReader', - module => 'PatchReader', - # 0.9.6 fixes two notable bugs and significantly improves the UX. - version => '0.9.6', - feature => ['patch_viewer'], + package => 'PatchReader', + module => 'PatchReader', + + # 0.9.6 fixes two notable bugs and significantly improves the UX. + version => '0.9.6', + feature => ['patch_viewer'], }, { - package => 'perl-ldap', - module => 'Net::LDAP', - version => 0, - feature => ['auth_ldap'], + package => 'perl-ldap', + module => 'Net::LDAP', + version => 0, + feature => ['auth_ldap'], }, { - package => 'Authen-SASL', - module => 'Authen::SASL', - version => 0, - feature => ['smtp_auth'], + package => 'Authen-SASL', + module => 'Authen::SASL', + version => 0, + feature => ['smtp_auth'], }, { - package => 'Net-SMTP-SSL', - module => 'Net::SMTP::SSL', - version => 1.01, - feature => ['smtp_ssl'], + package => 'Net-SMTP-SSL', + module => 'Net::SMTP::SSL', + version => 1.01, + feature => ['smtp_ssl'], }, { - package => 'RadiusPerl', - module => 'Authen::Radius', - version => 0, - feature => ['auth_radius'], + package => 'RadiusPerl', + module => 'Authen::Radius', + version => 0, + feature => ['auth_radius'], }, + # XXX - Once we require XMLRPC::Lite 0.717 or higher, we can # remove SOAP::Lite from the list. { - package => 'SOAP-Lite', - module => 'SOAP::Lite', - # Fixes various bugs, including 542931 and 552353 + stops - # throwing warnings with Perl 5.12. - version => '0.712', - # SOAP::Transport::HTTP 1.12 is bogus. - blacklist => ['^1\.12$'], - feature => ['xmlrpc'], + package => 'SOAP-Lite', + module => 'SOAP::Lite', + + # Fixes various bugs, including 542931 and 552353 + stops + # throwing warnings with Perl 5.12. + version => '0.712', + + # SOAP::Transport::HTTP 1.12 is bogus. + blacklist => ['^1\.12$'], + feature => ['xmlrpc'], }, + # Since SOAP::Lite 1.0, XMLRPC::Lite is no longer included # and so it must be checked separately. { - package => 'XMLRPC-Lite', - module => 'XMLRPC::Lite', - version => '0.712', - feature => ['xmlrpc'], + package => 'XMLRPC-Lite', + module => 'XMLRPC::Lite', + version => '0.712', + feature => ['xmlrpc'], }, { - package => 'JSON-RPC', - module => 'JSON::RPC', - version => 0, - feature => ['jsonrpc', 'rest'], + package => 'JSON-RPC', + module => 'JSON::RPC', + version => 0, + feature => ['jsonrpc', 'rest'], }, { - package => 'Test-Taint', - module => 'Test::Taint', - # 1.06 no longer throws warnings with Perl 5.10+. - version => 1.06, - feature => ['jsonrpc', 'xmlrpc', 'rest'], + package => 'Test-Taint', + module => 'Test::Taint', + + # 1.06 no longer throws warnings with Perl 5.10+. + version => 1.06, + feature => ['jsonrpc', 'xmlrpc', 'rest'], }, { - # We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber. - package => 'HTML-Parser', - module => 'HTML::Parser', - version => ($^V >= v5.13.3) ? '3.67' : '3.40', - feature => ['html_desc'], + # We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber. + package => 'HTML-Parser', + module => 'HTML::Parser', + version => ($^V >= v5.13.3) ? '3.67' : '3.40', + feature => ['html_desc'], }, { - package => 'HTML-Scrubber', - module => 'HTML::Scrubber', - version => 0, - feature => ['html_desc'], + package => 'HTML-Scrubber', + module => 'HTML::Scrubber', + version => 0, + feature => ['html_desc'], }, { - # we need version 2.21 of Encode for mime_name - package => 'Encode', - module => 'Encode', - version => 2.21, - feature => ['detect_charset'], + # we need version 2.21 of Encode for mime_name + package => 'Encode', + module => 'Encode', + version => 2.21, + feature => ['detect_charset'], }, { - package => 'Encode-Detect', - module => 'Encode::Detect', - version => 0, - feature => ['detect_charset'], + package => 'Encode-Detect', + module => 'Encode::Detect', + version => 0, + feature => ['detect_charset'], }, # Inbound Email { - package => 'Email-Reply', - module => 'Email::Reply', - version => 0, - feature => ['inbound_email'], + package => 'Email-Reply', + module => 'Email::Reply', + version => 0, + feature => ['inbound_email'], }, { - package => 'HTML-FormatText-WithLinks', - module => 'HTML::FormatText::WithLinks', - # We need 0.13 to set the "bold" marker to "*". - version => '0.13', - feature => ['inbound_email'], + package => 'HTML-FormatText-WithLinks', + module => 'HTML::FormatText::WithLinks', + + # We need 0.13 to set the "bold" marker to "*". + version => '0.13', + feature => ['inbound_email'], }, # Mail Queueing { - package => 'TheSchwartz', - module => 'TheSchwartz', - # 1.07 supports the prioritization of jobs. - version => 1.07, - feature => ['jobqueue'], + package => 'TheSchwartz', + module => 'TheSchwartz', + + # 1.07 supports the prioritization of jobs. + version => 1.07, + feature => ['jobqueue'], }, { - package => 'Daemon-Generic', - module => 'Daemon::Generic', - version => 0, - feature => ['jobqueue'], + package => 'Daemon-Generic', + module => 'Daemon::Generic', + version => 0, + feature => ['jobqueue'], }, # mod_perl { - package => 'mod_perl', - module => 'mod_perl2', - version => '1.999022', - feature => ['mod_perl'], + package => 'mod_perl', + module => 'mod_perl2', + version => '1.999022', + feature => ['mod_perl'], }, { - package => 'Apache-SizeLimit', - module => 'Apache2::SizeLimit', - # 0.96 properly determines process size on Linux. - version => '0.96', - feature => ['mod_perl'], + package => 'Apache-SizeLimit', + module => 'Apache2::SizeLimit', + + # 0.96 properly determines process size on Linux. + version => '0.96', + feature => ['mod_perl'], }, # typesniffer { - package => 'File-MimeInfo', - module => 'File::MimeInfo::Magic', - version => '0', - feature => ['typesniffer'], + package => 'File-MimeInfo', + module => 'File::MimeInfo::Magic', + version => '0', + feature => ['typesniffer'], }, { - package => 'IO-stringy', - module => 'IO::Scalar', - version => '0', - feature => ['typesniffer'], + package => 'IO-stringy', + module => 'IO::Scalar', + version => '0', + feature => ['typesniffer'], }, # memcached { - package => 'Cache-Memcached', - module => 'Cache::Memcached', - version => '0', - feature => ['memcached'], + package => 'Cache-Memcached', + module => 'Cache::Memcached', + version => '0', + feature => ['memcached'], }, # Documentation { - package => 'File-Copy-Recursive', - module => 'File::Copy::Recursive', - version => 0, - feature => ['documentation'], + package => 'File-Copy-Recursive', + module => 'File::Copy::Recursive', + version => 0, + feature => ['documentation'], }, { - package => 'File-Which', - module => 'File::Which', - version => 0, - feature => ['documentation'], + package => 'File-Which', + module => 'File::Which', + version => 0, + feature => ['documentation'], }, - ); + ); - my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES'); - push(@modules, @$extra_modules); - return \@modules; -}; + my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES'); + push(@modules, @$extra_modules); + return \@modules; +} # This maps features to the files that require that feature in order # to compile. It is used by t/001compile.t and mod_perl.pl. use constant FEATURE_FILES => ( - jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'], - xmlrpc => ['Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi', - 'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'], - rest => ['Bugzilla/WebService/Server/REST.pm', 'rest.cgi', - 'Bugzilla/WebService/Server/REST/Resources/*.pm'], - moving => ['importxml.pl'], - auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'], - auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'], - documentation => ['docs/makedocs.pl'], - inbound_email => ['email_in.pl'], - jobqueue => ['Bugzilla/Job/*', 'Bugzilla/JobQueue.pm', - 'Bugzilla/JobQueue/*', 'jobqueue.pl'], - patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'], - updates => ['Bugzilla/Update.pm'], - memcached => ['Bugzilla/Memcache.pm'], + jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'], + xmlrpc => [ + 'Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi', + 'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm' + ], + rest => [ + 'Bugzilla/WebService/Server/REST.pm', 'rest.cgi', + 'Bugzilla/WebService/Server/REST/Resources/*.pm' + ], + moving => ['importxml.pl'], + auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'], + auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'], + documentation => ['docs/makedocs.pl'], + inbound_email => ['email_in.pl'], + jobqueue => [ + 'Bugzilla/Job/*', 'Bugzilla/JobQueue.pm', + 'Bugzilla/JobQueue/*', 'jobqueue.pl' + ], + patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'], + updates => ['Bugzilla/Update.pm'], + memcached => ['Bugzilla/Memcache.pm'], ); # This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff # described in in Bugzilla::Extension. sub _get_extension_requirements { - my ($function) = @_; - - my $packages = extension_requirement_packages(); - my @modules; - foreach my $package (@$packages) { - if ($package->can($function)) { - my $extra_modules = $package->$function; - push(@modules, @$extra_modules); - } - } - return \@modules; -}; + my ($function) = @_; -sub check_requirements { - my ($output) = @_; - - print "\n", install_string('checking_modules'), "\n" if $output; - my $root = ROOT_USER; - my $missing = _check_missing(REQUIRED_MODULES, $output); - - print "\n", install_string('checking_dbd'), "\n" if $output; - my $have_one_dbd = 0; - my $db_modules = DB_MODULE; - foreach my $db (keys %$db_modules) { - my $dbd = $db_modules->{$db}->{dbd}; - $have_one_dbd = 1 if have_vers($dbd, $output); + my $packages = extension_requirement_packages(); + my @modules; + foreach my $package (@$packages) { + if ($package->can($function)) { + my $extra_modules = $package->$function; + push(@modules, @$extra_modules); } + } + return \@modules; +} - print "\n", install_string('checking_optional'), "\n" if $output; - my $missing_optional = _check_missing(OPTIONAL_MODULES, $output); - - my $missing_apache = _missing_apache_modules(APACHE_MODULES, $output); - - # If we're running on Windows, reset the input line terminator so that - # console input works properly - loading CGI tends to mess it up - $/ = "\015\012" if ON_WINDOWS; - - my $pass = !scalar(@$missing) && $have_one_dbd; - return { - pass => $pass, - one_dbd => $have_one_dbd, - missing => $missing, - optional => $missing_optional, - apache => $missing_apache, - any_missing => !$pass || scalar(@$missing_optional), - }; +sub check_requirements { + my ($output) = @_; + + print "\n", install_string('checking_modules'), "\n" if $output; + my $root = ROOT_USER; + my $missing = _check_missing(REQUIRED_MODULES, $output); + + print "\n", install_string('checking_dbd'), "\n" if $output; + my $have_one_dbd = 0; + my $db_modules = DB_MODULE; + foreach my $db (keys %$db_modules) { + my $dbd = $db_modules->{$db}->{dbd}; + $have_one_dbd = 1 if have_vers($dbd, $output); + } + + print "\n", install_string('checking_optional'), "\n" if $output; + my $missing_optional = _check_missing(OPTIONAL_MODULES, $output); + + my $missing_apache = _missing_apache_modules(APACHE_MODULES, $output); + + # If we're running on Windows, reset the input line terminator so that + # console input works properly - loading CGI tends to mess it up + $/ = "\015\012" if ON_WINDOWS; + + my $pass = !scalar(@$missing) && $have_one_dbd; + return { + pass => $pass, + one_dbd => $have_one_dbd, + missing => $missing, + optional => $missing_optional, + apache => $missing_apache, + any_missing => !$pass || scalar(@$missing_optional), + }; } # A helper for check_requirements sub _check_missing { - my ($modules, $output) = @_; + my ($modules, $output) = @_; - my @missing; - foreach my $module (@$modules) { - unless (have_vers($module, $output)) { - push(@missing, $module); - } + my @missing; + foreach my $module (@$modules) { + unless (have_vers($module, $output)) { + push(@missing, $module); } + } - return \@missing; + return \@missing; } sub _missing_apache_modules { - my ($modules, $output) = @_; - my $apachectl = _get_apachectl(); - return [] if !$apachectl; - my $command = "$apachectl -t -D DUMP_MODULES"; - my $cmd_info = `$command 2>&1`; - # If apachectl returned a value greater than 0, then there was an - # error parsing Apache's configuration, and we can't check modules. - my $retval = $?; - if ($retval > 0) { - print STDERR install_string('apachectl_failed', - { command => $command, root => ROOT_USER }), "\n"; - return []; - } - my @missing; - foreach my $module (sort keys %$modules) { - my $ok = _check_apache_module($module, $modules->{$module}, - $cmd_info, $output); - push(@missing, $module) if !$ok; - } - return \@missing; + my ($modules, $output) = @_; + my $apachectl = _get_apachectl(); + return [] if !$apachectl; + my $command = "$apachectl -t -D DUMP_MODULES"; + my $cmd_info = `$command 2>&1`; + + # If apachectl returned a value greater than 0, then there was an + # error parsing Apache's configuration, and we can't check modules. + my $retval = $?; + if ($retval > 0) { + print STDERR install_string('apachectl_failed', + {command => $command, root => ROOT_USER}), "\n"; + return []; + } + my @missing; + foreach my $module (sort keys %$modules) { + my $ok = _check_apache_module($module, $modules->{$module}, $cmd_info, $output); + push(@missing, $module) if !$ok; + } + return \@missing; } sub _get_apachectl { - foreach my $bin_name (APACHE) { - my $bin = bin_loc($bin_name); - return $bin if $bin; - } - # Try again with a possibly different path. - foreach my $bin_name (APACHE) { - my $bin = bin_loc($bin_name, APACHE_PATH); - return $bin if $bin; - } - return undef; + foreach my $bin_name (APACHE) { + my $bin = bin_loc($bin_name); + return $bin if $bin; + } + + # Try again with a possibly different path. + foreach my $bin_name (APACHE) { + my $bin = bin_loc($bin_name, APACHE_PATH); + return $bin if $bin; + } + return undef; } sub _check_apache_module { - my ($module, $config_name, $mod_info, $output) = @_; - my $ok; - if ($mod_info =~ /^\s+\Q$config_name\E\b/m) { - $ok = 1; - } - if ($output) { - _checking_for({ package => $module, ok => $ok }); - } - return $ok; + my ($module, $config_name, $mod_info, $output) = @_; + my $ok; + if ($mod_info =~ /^\s+\Q$config_name\E\b/m) { + $ok = 1; + } + if ($output) { + _checking_for({package => $module, ok => $ok}); + } + return $ok; } sub print_module_instructions { - my ($check_results, $output) = @_; - - # First we print the long explanatory messages. - - if (scalar @{$check_results->{missing}}) { - print install_string('modules_message_required'); - } - - if (!$check_results->{one_dbd}) { - print install_string('modules_message_db'); - } - - if (my @missing = @{$check_results->{optional}} and $output) { - print install_string('modules_message_optional'); - # Now we have to determine how large the table cols will be. - my $longest_name = max(map(length($_->{package}), @missing)); - - # The first column header is at least 11 characters long. - $longest_name = 11 if $longest_name < 11; - - # The table is TABLE_WIDTH characters long. There are seven mandatory - # characters (* and space) in the string. So, we have a total - # of TABLE_WIDTH - 7 characters to work with. - my $remaining_space = (TABLE_WIDTH - 7) - $longest_name; - print '*' x TABLE_WIDTH . "\n"; - printf "* \%${longest_name}s * %-${remaining_space}s *\n", - 'MODULE NAME', 'ENABLES FEATURE(S)'; - print '*' x TABLE_WIDTH . "\n"; - foreach my $package (@missing) { - printf "* \%${longest_name}s * %-${remaining_space}s *\n", - $package->{package}, - _translate_feature($package->{feature}); - } - } - - if (my @missing = @{ $check_results->{apache} }) { - print install_string('modules_message_apache'); - my $missing_string = join(', ', @missing); - my $size = TABLE_WIDTH - 7; - printf "* \%-${size}s *\n", $missing_string; - my $spaces = TABLE_WIDTH - 2; - print "*", (' ' x $spaces), "*\n"; - } - - my $need_module_instructions = - ( (!$output and @{$check_results->{missing}}) - or ($output and $check_results->{any_missing}) ) ? 1 : 0; - - if ($need_module_instructions or @{ $check_results->{apache} }) { - # If any output was required, we want to close the "table" - print "*" x TABLE_WIDTH . "\n"; - } - - # And now we print the actual installation commands. - - if (my @missing = @{$check_results->{optional}} and $output) { - print install_string('commands_optional') . "\n\n"; - foreach my $module (@missing) { - my $command = install_command($module); - printf "%15s: $command\n", $module->{package}; - } - print "\n"; - } - - if (!$check_results->{one_dbd}) { - print install_string('commands_dbd') . "\n"; - my %db_modules = %{DB_MODULE()}; - foreach my $db (keys %db_modules) { - my $command = install_command($db_modules{$db}->{dbd}); - printf "%10s: \%s\n", $db_modules{$db}->{name}, $command; - } - print "\n"; - } - - if (my @missing = @{$check_results->{missing}}) { - print colored(install_string('commands_required'), COLOR_ERROR), "\n"; - foreach my $package (@missing) { - my $command = install_command($package); - print " $command\n"; - } - } - - if ($output && $check_results->{any_missing} && !ON_ACTIVESTATE - && !$check_results->{hide_all}) - { - print install_string('install_all', { perl => $^X }); - } - if (!$check_results->{pass}) { - print colored(install_string('installation_failed'), COLOR_ERROR), - "\n\n"; - } + my ($check_results, $output) = @_; + + # First we print the long explanatory messages. + + if (scalar @{$check_results->{missing}}) { + print install_string('modules_message_required'); + } + + if (!$check_results->{one_dbd}) { + print install_string('modules_message_db'); + } + + if (my @missing = @{$check_results->{optional}} and $output) { + print install_string('modules_message_optional'); + + # Now we have to determine how large the table cols will be. + my $longest_name = max(map(length($_->{package}), @missing)); + + # The first column header is at least 11 characters long. + $longest_name = 11 if $longest_name < 11; + + # The table is TABLE_WIDTH characters long. There are seven mandatory + # characters (* and space) in the string. So, we have a total + # of TABLE_WIDTH - 7 characters to work with. + my $remaining_space = (TABLE_WIDTH - 7) - $longest_name; + print '*' x TABLE_WIDTH . "\n"; + printf "* \%${longest_name}s * %-${remaining_space}s *\n", 'MODULE NAME', + 'ENABLES FEATURE(S)'; + print '*' x TABLE_WIDTH . "\n"; + foreach my $package (@missing) { + printf "* \%${longest_name}s * %-${remaining_space}s *\n", $package->{package}, + _translate_feature($package->{feature}); + } + } + + if (my @missing = @{$check_results->{apache}}) { + print install_string('modules_message_apache'); + my $missing_string = join(', ', @missing); + my $size = TABLE_WIDTH - 7; + printf "* \%-${size}s *\n", $missing_string; + my $spaces = TABLE_WIDTH - 2; + print "*", (' ' x $spaces), "*\n"; + } + + my $need_module_instructions = ( + (!$output and @{$check_results->{missing}}) + or ($output and $check_results->{any_missing}) + ) ? 1 : 0; + + if ($need_module_instructions or @{$check_results->{apache}}) { + + # If any output was required, we want to close the "table" + print "*" x TABLE_WIDTH . "\n"; + } + + # And now we print the actual installation commands. + + if (my @missing = @{$check_results->{optional}} and $output) { + print install_string('commands_optional') . "\n\n"; + foreach my $module (@missing) { + my $command = install_command($module); + printf "%15s: $command\n", $module->{package}; + } + print "\n"; + } + + if (!$check_results->{one_dbd}) { + print install_string('commands_dbd') . "\n"; + my %db_modules = %{DB_MODULE()}; + foreach my $db (keys %db_modules) { + my $command = install_command($db_modules{$db}->{dbd}); + printf "%10s: \%s\n", $db_modules{$db}->{name}, $command; + } + print "\n"; + } + + if (my @missing = @{$check_results->{missing}}) { + print colored(install_string('commands_required'), COLOR_ERROR), "\n"; + foreach my $package (@missing) { + my $command = install_command($package); + print " $command\n"; + } + } + + if ( $output + && $check_results->{any_missing} + && !ON_ACTIVESTATE + && !$check_results->{hide_all}) + { + print install_string('install_all', {perl => $^X}); + } + if (!$check_results->{pass}) { + print colored(install_string('installation_failed'), COLOR_ERROR), "\n\n"; + } } sub _translate_feature { - my $features = shift; - my @strings; - foreach my $feature (@$features) { - push(@strings, install_string("feature_$feature")); - } - return join(', ', @strings); + my $features = shift; + my @strings; + foreach my $feature (@$features) { + push(@strings, install_string("feature_$feature")); + } + return join(', ', @strings); } sub check_graphviz { - my ($output) = @_; + my ($output) = @_; - my $webdotbase = Bugzilla->params->{'webdotbase'}; - return 1 if $webdotbase =~ /^https?:/; + my $webdotbase = Bugzilla->params->{'webdotbase'}; + return 1 if $webdotbase =~ /^https?:/; - my $return; - $return = 1 if -x $webdotbase; + my $return; + $return = 1 if -x $webdotbase; - if ($output) { - _checking_for({ package => 'GraphViz', ok => $return }); - } + if ($output) { + _checking_for({package => 'GraphViz', ok => $return}); + } - if (!$return) { - print install_string('bad_executable', { bin => $webdotbase }), "\n"; - } + if (!$return) { + print install_string('bad_executable', {bin => $webdotbase}), "\n"; + } + + my $webdotdir = bz_locations()->{'webdotdir'}; - my $webdotdir = bz_locations()->{'webdotdir'}; - # Check .htaccess allows access to generated images - if (-e "$webdotdir/.htaccess") { - my $htaccess = new IO::File("$webdotdir/.htaccess", 'r') - || die "$webdotdir/.htaccess: " . $!; - if (!grep(/png/, $htaccess->getlines)) { - print STDERR install_string('webdot_bad_htaccess', - { dir => $webdotdir }), "\n"; - } - $htaccess->close; + # Check .htaccess allows access to generated images + if (-e "$webdotdir/.htaccess") { + my $htaccess = new IO::File("$webdotdir/.htaccess", 'r') + || die "$webdotdir/.htaccess: " . $!; + if (!grep(/png/, $htaccess->getlines)) { + print STDERR install_string('webdot_bad_htaccess', {dir => $webdotdir}), "\n"; } + $htaccess->close; + } - return $return; + return $return; } # This was originally clipped from the libnet Makefile.PL, adapted here for # accurate version checking. sub have_vers { - my ($params, $output) = @_; - my $module = $params->{module}; - my $package = $params->{package}; - if (!$package) { - $package = $module; - $package =~ s/::/-/g; - } - my $wanted = $params->{version}; - - eval "require $module;"; - # Don't let loading a module change the output-encoding of STDOUT - # or STDERR. (CGI.pm tries to set "binmode" on these file handles when - # it's loaded, and other modules may do the same in the future.) - Bugzilla::Install::Util::set_output_encoding(); - - # VERSION is provided by UNIVERSAL::, and can be called even if - # the module isn't loaded. We eval'uate ->VERSION because it can die - # when the version is not valid (yes, this happens from time to time). - # In that case, we use an uglier method to get the version. - my $vnum = eval { $module->VERSION }; - if ($@) { - no strict 'refs'; - $vnum = ${"${module}::VERSION"}; - - # If we come here, then the version is not a valid one. - # We try to sanitize it. - if ($vnum =~ /^((\d+)(\.\d+)*)/) { - $vnum = $1; - } - } - $vnum ||= -1; - - # Must do a string comparison as $vnum may be of the form 5.10.1. - my $vok = ($vnum ne '-1' && version->new($vnum) >= version->new($wanted)) ? 1 : 0; - my $blacklisted; - if ($vok && $params->{blacklist}) { - $blacklisted = grep($vnum =~ /$_/, @{$params->{blacklist}}); - $vok = 0 if $blacklisted; - } - - if ($output) { - _checking_for({ - package => $package, ok => $vok, wanted => $wanted, - found => $vnum, blacklisted => $blacklisted - }); - } - - return $vok ? 1 : 0; + my ($params, $output) = @_; + my $module = $params->{module}; + my $package = $params->{package}; + if (!$package) { + $package = $module; + $package =~ s/::/-/g; + } + my $wanted = $params->{version}; + + eval "require $module;"; + + # Don't let loading a module change the output-encoding of STDOUT + # or STDERR. (CGI.pm tries to set "binmode" on these file handles when + # it's loaded, and other modules may do the same in the future.) + Bugzilla::Install::Util::set_output_encoding(); + + # VERSION is provided by UNIVERSAL::, and can be called even if + # the module isn't loaded. We eval'uate ->VERSION because it can die + # when the version is not valid (yes, this happens from time to time). + # In that case, we use an uglier method to get the version. + my $vnum = eval { $module->VERSION }; + if ($@) { + no strict 'refs'; + $vnum = ${"${module}::VERSION"}; + + # If we come here, then the version is not a valid one. + # We try to sanitize it. + if ($vnum =~ /^((\d+)(\.\d+)*)/) { + $vnum = $1; + } + } + $vnum ||= -1; + + # Must do a string comparison as $vnum may be of the form 5.10.1. + my $vok + = ($vnum ne '-1' && version->new($vnum) >= version->new($wanted)) ? 1 : 0; + my $blacklisted; + if ($vok && $params->{blacklist}) { + $blacklisted = grep($vnum =~ /$_/, @{$params->{blacklist}}); + $vok = 0 if $blacklisted; + } + + if ($output) { + _checking_for({ + package => $package, + ok => $vok, + wanted => $wanted, + found => $vnum, + blacklisted => $blacklisted + }); + } + + return $vok ? 1 : 0; } sub _checking_for { - my ($params) = @_; - my ($package, $ok, $wanted, $blacklisted, $found) = - @$params{qw(package ok wanted blacklisted found)}; - - my $ok_string = $ok ? install_string('module_ok') : ''; - - # If we're actually checking versions (like for Perl modules), then - # we have some rather complex logic to determine what we want to - # show. If we're not checking versions (like for GraphViz) we just - # show "ok" or "not found". - if (exists $params->{found}) { - my $found_string; - # We do a string compare in case it's non-numeric. We make sure - # it's not a version object as negative versions are forbidden. - if ($found && !ref($found) && $found eq '-1') { - $found_string = install_string('module_not_found'); - } - elsif ($found) { - $found_string = install_string('module_found', { ver => $found }); - } - else { - $found_string = install_string('module_unknown_version'); - } - $ok_string = $ok ? "$ok_string: $found_string" : $found_string; + my ($params) = @_; + my ($package, $ok, $wanted, $blacklisted, $found) + = @$params{qw(package ok wanted blacklisted found)}; + + my $ok_string = $ok ? install_string('module_ok') : ''; + + # If we're actually checking versions (like for Perl modules), then + # we have some rather complex logic to determine what we want to + # show. If we're not checking versions (like for GraphViz) we just + # show "ok" or "not found". + if (exists $params->{found}) { + my $found_string; + + # We do a string compare in case it's non-numeric. We make sure + # it's not a version object as negative versions are forbidden. + if ($found && !ref($found) && $found eq '-1') { + $found_string = install_string('module_not_found'); + } + elsif ($found) { + $found_string = install_string('module_found', {ver => $found}); } - elsif (!$ok) { - $ok_string = install_string('module_not_found'); + else { + $found_string = install_string('module_unknown_version'); } + $ok_string = $ok ? "$ok_string: $found_string" : $found_string; + } + elsif (!$ok) { + $ok_string = install_string('module_not_found'); + } - my $black_string = $blacklisted ? install_string('blacklisted') : ''; - my $want_string = $wanted ? "v$wanted" : install_string('any'); + my $black_string = $blacklisted ? install_string('blacklisted') : ''; + my $want_string = $wanted ? "v$wanted" : install_string('any'); - my $str = sprintf "%s %20s %-11s $ok_string $black_string\n", - install_string('checking_for'), $package, "($want_string)"; - print $ok ? $str : colored($str, COLOR_ERROR); + my $str = sprintf "%s %20s %-11s $ok_string $black_string\n", + install_string('checking_for'), $package, "($want_string)"; + print $ok ? $str : colored($str, COLOR_ERROR); } sub install_command { - my $module = shift; - my ($command, $package); - - if (ON_ACTIVESTATE) { - $command = 'ppm install %s'; - $package = $module->{package}; - } - else { - $command = "$^X install-module.pl \%s"; - # Non-Windows installations need to use module names, because - # CPAN doesn't understand package names. - $package = $module->{module}; - } - return sprintf $command, $package; + my $module = shift; + my ($command, $package); + + if (ON_ACTIVESTATE) { + $command = 'ppm install %s'; + $package = $module->{package}; + } + else { + $command = "$^X install-module.pl \%s"; + + # Non-Windows installations need to use module names, because + # CPAN doesn't understand package names. + $package = $module->{module}; + } + return sprintf $command, $package; } # This does a reverse mapping for FEATURE_FILES. sub map_files_to_features { - my %features = FEATURE_FILES; - my %files; - foreach my $feature (keys %features) { - my @my_files = @{ $features{$feature} }; - foreach my $pattern (@my_files) { - foreach my $file (glob $pattern) { - $files{$file} = $feature; - } - } - } - return \%files; + my %features = FEATURE_FILES; + my %files; + foreach my $feature (keys %features) { + my @my_files = @{$features{$feature}}; + foreach my $pattern (@my_files) { + foreach my $file (glob $pattern) { + $files{$file} = $feature; + } + } + } + return \%files; } 1; diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm index c05037061..26e58c772 100644 --- a/Bugzilla/Install/Util.pm +++ b/Bugzilla/Install/Util.pm @@ -27,195 +27,206 @@ use PerlIO; use parent qw(Exporter); our @EXPORT_OK = qw( - bin_loc - get_version_and_os - extension_code_files - extension_package_directory - extension_requirement_packages - extension_template_directory - extension_web_directory - indicate_progress - install_string - include_languages - success - template_include_path - init_console + bin_loc + get_version_and_os + extension_code_files + extension_package_directory + extension_requirement_packages + extension_template_directory + extension_web_directory + indicate_progress + install_string + include_languages + success + template_include_path + init_console ); sub bin_loc { - my ($bin, $path) = @_; - # This module is not needed most of the time and is a bit slow, - # so we only load it when calling bin_loc(). - require ExtUtils::MM; - - # If the binary is a full path... - if ($bin =~ m{[/\\]}) { - return MM->maybe_command($bin) || ''; - } - - # Otherwise we look for it in the path in a cross-platform way. - my @path = $path ? @$path : File::Spec->path; - foreach my $dir (@path) { - next if !-d $dir; - my $full_path = File::Spec->catfile($dir, $bin); - # MM is an alias for ExtUtils::MM. maybe_command is nice - # because it checks .com, .bat, .exe (etc.) on Windows. - my $command = MM->maybe_command($full_path); - return $command if $command; - } - - return ''; + my ($bin, $path) = @_; + + # This module is not needed most of the time and is a bit slow, + # so we only load it when calling bin_loc(). + require ExtUtils::MM; + + # If the binary is a full path... + if ($bin =~ m{[/\\]}) { + return MM->maybe_command($bin) || ''; + } + + # Otherwise we look for it in the path in a cross-platform way. + my @path = $path ? @$path : File::Spec->path; + foreach my $dir (@path) { + next if !-d $dir; + my $full_path = File::Spec->catfile($dir, $bin); + + # MM is an alias for ExtUtils::MM. maybe_command is nice + # because it checks .com, .bat, .exe (etc.) on Windows. + my $command = MM->maybe_command($full_path); + return $command if $command; + } + + return ''; } sub get_version_and_os { - # Display version information - my @os_details = POSIX::uname; - # 0 is the name of the OS, 2 is the major version, - my $os_name = $os_details[0] . ' ' . $os_details[2]; - if (ON_WINDOWS) { - require Win32; - $os_name = Win32::GetOSName(); - } - # $os_details[3] is the minor version. - return { bz_ver => BUGZILLA_VERSION, - perl_ver => sprintf('%vd', $^V), - os_name => $os_name, - os_ver => $os_details[3] }; + + # Display version information + my @os_details = POSIX::uname; + + # 0 is the name of the OS, 2 is the major version, + my $os_name = $os_details[0] . ' ' . $os_details[2]; + if (ON_WINDOWS) { + require Win32; + $os_name = Win32::GetOSName(); + } + + # $os_details[3] is the minor version. + return { + bz_ver => BUGZILLA_VERSION, + perl_ver => sprintf('%vd', $^V), + os_name => $os_name, + os_ver => $os_details[3] + }; } sub _extension_paths { - my $dir = bz_locations()->{'extensionsdir'}; - my @extension_items = glob("$dir/*"); - my @paths; - 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 "$item/disabled") { - push(@paths, $item); - } - } - elsif ($item =~ /\.pm$/i) { - push(@paths, $item); - } - } - return @paths; + my $dir = bz_locations()->{'extensionsdir'}; + my @extension_items = glob("$dir/*"); + my @paths; + 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 "$item/disabled") { + push(@paths, $item); + } + } + elsif ($item =~ /\.pm$/i) { + push(@paths, $item); + } + } + return @paths; } sub extension_code_files { - my ($requirements_only) = @_; - my @files; - foreach my $path (_extension_paths()) { - my @load_files; - if (-d $path) { - my $extension_file = "$path/Extension.pm"; - my $config_file = "$path/Config.pm"; - if (-e $extension_file) { - push(@load_files, $extension_file); - } - if (-e $config_file) { - push(@load_files, $config_file); - } - - # Don't load Extension.pm if we just want Config.pm and - # we found both. - if ($requirements_only and scalar(@load_files) == 2) { - shift(@load_files); - } - } - else { - push(@load_files, $path); - } - next if !scalar(@load_files); - # We know that these paths are safe, because they came from - # extensionsdir and we checked them specifically for their format. - # Also, the only thing we ever do with them is pass them to "require". - trick_taint($_) foreach @load_files; - push(@files, \@load_files); - } - - my @additional; - my $datadir = bz_locations()->{'datadir'}; - my $addl_file = "$datadir/extensions/additional"; - if (-e $addl_file) { - open(my $fh, '<', $addl_file) || die "$addl_file: $!"; - @additional = map { trim($_) } <$fh>; - close($fh); + my ($requirements_only) = @_; + my @files; + foreach my $path (_extension_paths()) { + my @load_files; + if (-d $path) { + my $extension_file = "$path/Extension.pm"; + my $config_file = "$path/Config.pm"; + if (-e $extension_file) { + push(@load_files, $extension_file); + } + if (-e $config_file) { + push(@load_files, $config_file); + } + + # Don't load Extension.pm if we just want Config.pm and + # we found both. + if ($requirements_only and scalar(@load_files) == 2) { + shift(@load_files); + } } - return (\@files, \@additional); + else { + push(@load_files, $path); + } + next if !scalar(@load_files); + + # We know that these paths are safe, because they came from + # extensionsdir and we checked them specifically for their format. + # Also, the only thing we ever do with them is pass them to "require". + trick_taint($_) foreach @load_files; + push(@files, \@load_files); + } + + my @additional; + my $datadir = bz_locations()->{'datadir'}; + my $addl_file = "$datadir/extensions/additional"; + if (-e $addl_file) { + open(my $fh, '<', $addl_file) || die "$addl_file: $!"; + @additional = map { trim($_) } <$fh>; + close($fh); + } + return (\@files, \@additional); } # Used by _get_extension_requirements in Bugzilla::Install::Requirements. sub extension_requirement_packages { - # If we're in a .cgi script or some time that's not the requirements phase, - # just use Bugzilla->extensions. This avoids running the below code during - # a normal Bugzilla page, which is important because the below code - # doesn't actually function right if it runs after - # Bugzilla::Extension->load_all (because stuff has already been loaded). - # (This matters because almost every page calls Bugzilla->feature, which - # calls OPTIONAL_MODULES, which calls this method.) - # - # We check if Bugzilla.pm is already loaded, instead of doing a "require", - # because we *do* want the code lower down to run during the Requirements - # phase of checksetup.pl, instead of Bugzilla->extensions, and Bugzilla.pm - # actually *can* be loaded during the Requirements phase if all the - # requirements have already been installed. - if ($INC{'Bugzilla.pm'}) { - return Bugzilla->extensions; - } - my $packages = _cache()->{extension_requirement_packages}; - return $packages if $packages; - $packages = []; - my %package_map; - - my ($file_sets, $extra_packages) = extension_code_files('requirements only'); - foreach my $file_set (@$file_sets) { - my $file = shift @$file_set; - my $name = require $file; - if ($name =~ /^\d+$/) { - die install_string('extension_must_return_name', - { file => $file, returned => $name }); - } - my $package = "Bugzilla::Extension::$name"; - if ($package->can('package_dir')) { - $package->package_dir($file); - } - else { - extension_package_directory($package, $file); - } - $package_map{$file} = $package; - push(@$packages, $package); - } - foreach my $package (@$extra_packages) { - eval("require $package") || die $@; - push(@$packages, $package); - } - _cache()->{extension_requirement_packages} = $packages; - # Used by Bugzilla::Extension->load if it's called after this method - # (which only happens during checksetup.pl, currently). - _cache()->{extension_requirement_package_map} = \%package_map; - return $packages; + # If we're in a .cgi script or some time that's not the requirements phase, + # just use Bugzilla->extensions. This avoids running the below code during + # a normal Bugzilla page, which is important because the below code + # doesn't actually function right if it runs after + # Bugzilla::Extension->load_all (because stuff has already been loaded). + # (This matters because almost every page calls Bugzilla->feature, which + # calls OPTIONAL_MODULES, which calls this method.) + # + # We check if Bugzilla.pm is already loaded, instead of doing a "require", + # because we *do* want the code lower down to run during the Requirements + # phase of checksetup.pl, instead of Bugzilla->extensions, and Bugzilla.pm + # actually *can* be loaded during the Requirements phase if all the + # requirements have already been installed. + if ($INC{'Bugzilla.pm'}) { + return Bugzilla->extensions; + } + my $packages = _cache()->{extension_requirement_packages}; + return $packages if $packages; + $packages = []; + my %package_map; + + my ($file_sets, $extra_packages) = extension_code_files('requirements only'); + foreach my $file_set (@$file_sets) { + my $file = shift @$file_set; + my $name = require $file; + if ($name =~ /^\d+$/) { + die install_string('extension_must_return_name', + {file => $file, returned => $name}); + } + my $package = "Bugzilla::Extension::$name"; + if ($package->can('package_dir')) { + $package->package_dir($file); + } + else { + extension_package_directory($package, $file); + } + $package_map{$file} = $package; + push(@$packages, $package); + } + foreach my $package (@$extra_packages) { + eval("require $package") || die $@; + push(@$packages, $package); + } + + _cache()->{extension_requirement_packages} = $packages; + + # Used by Bugzilla::Extension->load if it's called after this method + # (which only happens during checksetup.pl, currently). + _cache()->{extension_requirement_package_map} = \%package_map; + return $packages; } # Used in this file and in Bugzilla::Extension. sub extension_template_directory { - my $extension = shift; - my $class = ref($extension) || $extension; - my $base_dir = extension_package_directory($class); - if ($base_dir eq bz_locations->{'extensionsdir'}) { - return bz_locations->{'templatedir'}; - } - return "$base_dir/template"; + my $extension = shift; + my $class = ref($extension) || $extension; + my $base_dir = extension_package_directory($class); + if ($base_dir eq bz_locations->{'extensionsdir'}) { + return bz_locations->{'templatedir'}; + } + return "$base_dir/template"; } # Used in this file and in Bugzilla::Extension. sub extension_web_directory { - my $extension = shift; - my $class = ref($extension) || $extension; - my $base_dir = extension_package_directory($class); - return "$base_dir/web"; + my $extension = shift; + my $class = ref($extension) || $extension; + my $base_dir = extension_package_directory($class); + return "$base_dir/web"; } # For extensions that are in the extensions/ dir, this both sets and fetches @@ -223,263 +234,271 @@ sub extension_web_directory { # when determining the template directory for extensions (or other things # that are relative to the extension's base directory). sub extension_package_directory { - my ($invocant, $file) = @_; - my $class = ref($invocant) || $invocant; - - # $file is set on the first invocation, store the value in the extension's - # package for retrieval on subsequent calls - my $var; - { - no warnings 'once'; - no strict 'refs'; - $var = \${"${class}::EXTENSION_PACKAGE_DIR"}; - } - if ($file) { - $$var = dirname($file); - } - my $value = $$var; - - # This is for extensions loaded from data/extensions/additional. - if (!$value) { - my $short_path = $class; - $short_path =~ s/::/\//g; - $short_path .= ".pm"; - my $long_path = $INC{$short_path}; - die "$short_path is not in \%INC" if !$long_path; - $value = $long_path; - $value =~ s/\.pm//; - } - return $value; + my ($invocant, $file) = @_; + my $class = ref($invocant) || $invocant; + + # $file is set on the first invocation, store the value in the extension's + # package for retrieval on subsequent calls + my $var; + { + no warnings 'once'; + no strict 'refs'; + $var = \${"${class}::EXTENSION_PACKAGE_DIR"}; + } + if ($file) { + $$var = dirname($file); + } + my $value = $$var; + + # This is for extensions loaded from data/extensions/additional. + if (!$value) { + my $short_path = $class; + $short_path =~ s/::/\//g; + $short_path .= ".pm"; + my $long_path = $INC{$short_path}; + die "$short_path is not in \%INC" if !$long_path; + $value = $long_path; + $value =~ s/\.pm//; + } + return $value; } sub indicate_progress { - my ($params) = @_; - my $current = $params->{current}; - my $total = $params->{total}; - my $every = $params->{every} || 1; - - print "." if !($current % $every); - if ($current == $total || $current % ($every * 60) == 0) { - print "$current/$total (" . int($current * 100 / $total) . "%)\n"; - } + my ($params) = @_; + my $current = $params->{current}; + my $total = $params->{total}; + my $every = $params->{every} || 1; + + print "." if !($current % $every); + if ($current == $total || $current % ($every * 60) == 0) { + print "$current/$total (" . int($current * 100 / $total) . "%)\n"; + } } sub install_string { - my ($string_id, $vars) = @_; - _cache()->{install_string_path} ||= template_include_path(); - my $path = _cache()->{install_string_path}; - - my $string_template; - # Find the first template that defines this string. - foreach my $dir (@$path) { - my $base = "$dir/setup/strings"; - $string_template = _get_string_from_file($string_id, "$base.txt.pl") - if !defined $string_template; - last if defined $string_template; - } - - die "No language defines the string '$string_id'" - if !defined $string_template; - - utf8::decode($string_template) if !utf8::is_utf8($string_template); - - $vars ||= {}; - my @replace_keys = keys %$vars; - foreach my $key (@replace_keys) { - my $replacement = $vars->{$key}; - die "'$key' in '$string_id' is tainted: '$replacement'" - if tainted($replacement); - # We don't want people to start getting clever and inserting - # ##variable## into their values. So we check if any other - # key is listed in the *replacement* string, before doing - # the replacement. This is mostly to protect programmers from - # making mistakes. - if (grep($replacement =~ /##$key##/, @replace_keys)) { - die "Unsafe replacement for '$key' in '$string_id': '$replacement'"; - } - $string_template =~ s/\Q##$key##\E/$replacement/g; - } - - return $string_template; + my ($string_id, $vars) = @_; + _cache()->{install_string_path} ||= template_include_path(); + my $path = _cache()->{install_string_path}; + + my $string_template; + + # Find the first template that defines this string. + foreach my $dir (@$path) { + my $base = "$dir/setup/strings"; + $string_template = _get_string_from_file($string_id, "$base.txt.pl") + if !defined $string_template; + last if defined $string_template; + } + + die "No language defines the string '$string_id'" if !defined $string_template; + + utf8::decode($string_template) if !utf8::is_utf8($string_template); + + $vars ||= {}; + my @replace_keys = keys %$vars; + foreach my $key (@replace_keys) { + my $replacement = $vars->{$key}; + die "'$key' in '$string_id' is tainted: '$replacement'" + if tainted($replacement); + + # We don't want people to start getting clever and inserting + # ##variable## into their values. So we check if any other + # key is listed in the *replacement* string, before doing + # the replacement. This is mostly to protect programmers from + # making mistakes. + if (grep($replacement =~ /##$key##/, @replace_keys)) { + die "Unsafe replacement for '$key' in '$string_id': '$replacement'"; + } + $string_template =~ s/\Q##$key##\E/$replacement/g; + } + + return $string_template; } sub _wanted_languages { - my ($requested, @wanted); - - # Checking SERVER_SOFTWARE is the same as i_am_cgi() in Bugzilla::Util. - if (exists $ENV{'SERVER_SOFTWARE'}) { - my $cgi = eval { Bugzilla->cgi } || eval { require CGI; return CGI->new() }; - $requested = $cgi->http('Accept-Language') || ''; - my $lang = $cgi->cookie('LANG'); - push(@wanted, $lang) if $lang; - } - else { - $requested = get_console_locale(); - } - - push(@wanted, _sort_accept_language($requested)); - return \@wanted; + my ($requested, @wanted); + + # Checking SERVER_SOFTWARE is the same as i_am_cgi() in Bugzilla::Util. + if (exists $ENV{'SERVER_SOFTWARE'}) { + my $cgi = eval { Bugzilla->cgi } || eval { require CGI; return CGI->new() }; + $requested = $cgi->http('Accept-Language') || ''; + my $lang = $cgi->cookie('LANG'); + push(@wanted, $lang) if $lang; + } + else { + $requested = get_console_locale(); + } + + push(@wanted, _sort_accept_language($requested)); + return \@wanted; } sub _wanted_to_actual_languages { - my ($wanted, $supported) = @_; - - my @actual; - foreach my $lang (@$wanted) { - # If we support the language we want, or *any version* of - # the language we want, it gets pushed into @actual. - # - # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and - # 'en-uk', but not the other way around. (This is unfortunately - # not very clearly stated in those RFC; see comment just over 14.5 - # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) - my @found = grep(/^\Q$lang\E(-.+)?$/i, @$supported); - push(@actual, @found) if @found; - } + my ($wanted, $supported) = @_; - # We always include English at the bottom if it's not there, even if - # it wasn't selected by the user. - if (!grep($_ eq 'en', @actual)) { - push(@actual, 'en'); - } + my @actual; + foreach my $lang (@$wanted) { - return \@actual; + # If we support the language we want, or *any version* of + # the language we want, it gets pushed into @actual. + # + # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and + # 'en-uk', but not the other way around. (This is unfortunately + # not very clearly stated in those RFC; see comment just over 14.5 + # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) + my @found = grep(/^\Q$lang\E(-.+)?$/i, @$supported); + push(@actual, @found) if @found; + } + + # We always include English at the bottom if it's not there, even if + # it wasn't selected by the user. + if (!grep($_ eq 'en', @actual)) { + push(@actual, 'en'); + } + + return \@actual; } sub supported_languages { - my $cache = _cache(); - return $cache->{supported_languages} if $cache->{supported_languages}; - - my @dirs = glob(bz_locations()->{'templatedir'} . "/*"); - my @languages; - foreach my $dir (@dirs) { - # It's a language directory only if it contains "default" or - # "custom". This auto-excludes CVS directories as well. - next if (!-d "$dir/default" and !-d "$dir/custom"); - my $lang = basename($dir); - # Check for language tag format conforming to RFC 1766. - next unless $lang =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/; - push(@languages, $lang); - } + my $cache = _cache(); + return $cache->{supported_languages} if $cache->{supported_languages}; + + my @dirs = glob(bz_locations()->{'templatedir'} . "/*"); + my @languages; + foreach my $dir (@dirs) { + + # It's a language directory only if it contains "default" or + # "custom". This auto-excludes CVS directories as well. + next if (!-d "$dir/default" and !-d "$dir/custom"); + my $lang = basename($dir); + + # Check for language tag format conforming to RFC 1766. + next unless $lang =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/; + push(@languages, $lang); + } - $cache->{supported_languages} = \@languages; - return \@languages; + $cache->{supported_languages} = \@languages; + return \@languages; } sub include_languages { - my ($params) = @_; - - # Basically, the way this works is that we have a list of languages - # that we *want*, and a list of languages that Bugzilla actually - # supports. If there is only one language installed, we take it. - my $supported = supported_languages(); - return @$supported if @$supported == 1; - - my $wanted; - if ($params->{language}) { - # We can pass several languages at once as an arrayref - # or a single language. - $wanted = $params->{language}; - $wanted = [$wanted] unless ref $wanted; - } - else { - $wanted = _wanted_languages(); - } - my $actual = _wanted_to_actual_languages($wanted, $supported); - return @$actual; + my ($params) = @_; + + # Basically, the way this works is that we have a list of languages + # that we *want*, and a list of languages that Bugzilla actually + # supports. If there is only one language installed, we take it. + my $supported = supported_languages(); + return @$supported if @$supported == 1; + + my $wanted; + if ($params->{language}) { + + # We can pass several languages at once as an arrayref + # or a single language. + $wanted = $params->{language}; + $wanted = [$wanted] unless ref $wanted; + } + else { + $wanted = _wanted_languages(); + } + my $actual = _wanted_to_actual_languages($wanted, $supported); + return @$actual; } # Used by template_include_path sub _template_lang_directories { - my ($languages, $templatedir) = @_; - - my @add = qw(custom default); - my $project = bz_locations->{'project'}; - unshift(@add, $project) if $project; - - my @result; - foreach my $lang (@$languages) { - foreach my $dir (@add) { - my $full_dir = "$templatedir/$lang/$dir"; - if (-d $full_dir) { - trick_taint($full_dir); - push(@result, $full_dir); - } - } - } - return @result; + my ($languages, $templatedir) = @_; + + my @add = qw(custom default); + my $project = bz_locations->{'project'}; + unshift(@add, $project) if $project; + + my @result; + foreach my $lang (@$languages) { + foreach my $dir (@add) { + my $full_dir = "$templatedir/$lang/$dir"; + if (-d $full_dir) { + trick_taint($full_dir); + push(@result, $full_dir); + } + } + } + return @result; } # Used by template_include_path. sub _template_base_directories { - # First, we add extension template directories, because extension templates - # override standard templates. Extensions may be localized in the same way - # that Bugzilla templates are localized. - # - # We use extension_requirement_packages instead of Bugzilla->extensions - # because this fucntion is called during the requirements phase of - # installation (so Bugzilla->extensions isn't available). - my $extensions = extension_requirement_packages(); - my @template_dirs; - foreach my $extension (@$extensions) { - my $dir; - # If there's a template_dir method available in the extension - # package, then call it. Note that this has to be defined in - # Config.pm for extensions that have a Config.pm, to be effective - # during the Requirements phase of checksetup.pl. - if ($extension->can('template_dir')) { - $dir = $extension->template_dir; - } - else { - $dir = extension_template_directory($extension); - } - if (-d $dir) { - push(@template_dirs, $dir); - } + + # First, we add extension template directories, because extension templates + # override standard templates. Extensions may be localized in the same way + # that Bugzilla templates are localized. + # + # We use extension_requirement_packages instead of Bugzilla->extensions + # because this fucntion is called during the requirements phase of + # installation (so Bugzilla->extensions isn't available). + my $extensions = extension_requirement_packages(); + my @template_dirs; + foreach my $extension (@$extensions) { + my $dir; + + # If there's a template_dir method available in the extension + # package, then call it. Note that this has to be defined in + # Config.pm for extensions that have a Config.pm, to be effective + # during the Requirements phase of checksetup.pl. + if ($extension->can('template_dir')) { + $dir = $extension->template_dir; + } + else { + $dir = extension_template_directory($extension); } + if (-d $dir) { + push(@template_dirs, $dir); + } + } - # Extensions may also contain *only* templates, in which case they - # won't show up in extension_requirement_packages. - foreach my $path (_extension_paths()) { - next if !-d $path; - if (!-e "$path/Extension.pm" and !-e "$path/Config.pm" - and -d "$path/template") - { - push(@template_dirs, "$path/template"); - } + # Extensions may also contain *only* templates, in which case they + # won't show up in extension_requirement_packages. + foreach my $path (_extension_paths()) { + next if !-d $path; + if (!-e "$path/Extension.pm" and !-e "$path/Config.pm" and -d "$path/template") + { + push(@template_dirs, "$path/template"); } + } - push(@template_dirs, bz_locations()->{'templatedir'}); - return \@template_dirs; + push(@template_dirs, bz_locations()->{'templatedir'}); + return \@template_dirs; } sub template_include_path { - my ($params) = @_; - my @used_languages = include_languages($params); - # Now, we add template directories in the order they will be searched: - my $template_dirs = _template_base_directories(); - - my @include_path; - foreach my $template_dir (@$template_dirs) { - my @lang_dirs = _template_lang_directories(\@used_languages, - $template_dir); - # Hooks get each set of extension directories separately. - if ($params->{hook}) { - push(@include_path, \@lang_dirs); - } - # Whereas everything else just gets a whole INCLUDE_PATH. - else { - push(@include_path, @lang_dirs); - } + my ($params) = @_; + my @used_languages = include_languages($params); + + # Now, we add template directories in the order they will be searched: + my $template_dirs = _template_base_directories(); + + my @include_path; + foreach my $template_dir (@$template_dirs) { + my @lang_dirs = _template_lang_directories(\@used_languages, $template_dir); + + # Hooks get each set of extension directories separately. + if ($params->{hook}) { + push(@include_path, \@lang_dirs); + } + + # Whereas everything else just gets a whole INCLUDE_PATH. + else { + push(@include_path, @lang_dirs); } - return \@include_path; + } + return \@include_path; } sub no_checksetup_from_cgi { - print "Content-Type: text/html; charset=UTF-8\r\n\r\n"; - print install_string('no_checksetup_from_cgi'); - exit; + print "Content-Type: text/html; charset=UTF-8\r\n\r\n"; + print install_string('no_checksetup_from_cgi'); + exit; } ###################### @@ -488,172 +507,183 @@ sub no_checksetup_from_cgi { # Used by install_string sub _get_string_from_file { - my ($string_id, $file) = @_; - # If we already loaded the file, then use its copy from the cache. - if (my $strings = _cache()->{strings_from_file}->{$file}) { - return $strings->{$string_id}; - } - - # This module is only needed by checksetup.pl, - # so only load it when needed. - require Safe; - - return undef if !-e $file; - my $safe = new Safe; - $safe->rdo($file); - my %strings = %{$safe->varglob('strings')}; - _cache()->{strings_from_file}->{$file} = \%strings; - return $strings{$string_id}; + my ($string_id, $file) = @_; + + # If we already loaded the file, then use its copy from the cache. + if (my $strings = _cache()->{strings_from_file}->{$file}) { + return $strings->{$string_id}; + } + + # This module is only needed by checksetup.pl, + # so only load it when needed. + require Safe; + + return undef if !-e $file; + my $safe = new Safe; + $safe->rdo($file); + my %strings = %{$safe->varglob('strings')}; + _cache()->{strings_from_file}->{$file} = \%strings; + return $strings{$string_id}; } # Make an ordered list out of a HTTP Accept-Language header (see RFC 2616, 14.4) # We ignore '*' and <language-range>;q=0 # For languages with the same priority q the order remains unchanged. sub _sort_accept_language { - sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} } - my $accept_language = $_[0]; - - # clean up string. - $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g; - my @qlanguages; - my @languages; - foreach(split /,/, $accept_language) { - if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) { - my $lang = $1; - my $qvalue = $2; - $qvalue = 1 if not defined $qvalue; - next if $qvalue == 0; - $qvalue = 1 if $qvalue > 1; - push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang}); - } - } - - return map($_->{'language'}, (sort sortQvalue @qlanguages)); + sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} } + my $accept_language = $_[0]; + + # clean up string. + $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g; + my @qlanguages; + my @languages; + foreach (split /,/, $accept_language) { + if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) { + my $lang = $1; + my $qvalue = $2; + $qvalue = 1 if not defined $qvalue; + next if $qvalue == 0; + $qvalue = 1 if $qvalue > 1; + push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang}); + } + } + + return map($_->{'language'}, (sort sortQvalue @qlanguages)); } sub get_console_locale { - require Locale::Language; - my $locale = setlocale(LC_CTYPE); - my $language; - # Some distros set e.g. LC_CTYPE = fr_CH.UTF-8. We clean it up. - if ($locale =~ /^([^\.]+)/) { - $locale = $1; - } - $locale =~ s/_/-/; - # It's pretty sure that there is no language pack of the form fr-CH - # installed, so we also include fr as a wanted language. - if ($locale =~ /^(\S+)\-/) { - $language = $1; - $locale .= ",$language"; - } - else { - $language = $locale; - } - - # Some OSs or distributions may have setlocale return a string of the form - # German_Germany.1252 (this example taken from a Windows XP system), which - # is unsuitable for our needs because Bugzilla works on language codes. - # We try and convert them here. - if ($language = Locale::Language::language2code($language)) { - $locale .= ",$language"; - } - - return $locale; + require Locale::Language; + my $locale = setlocale(LC_CTYPE); + my $language; + + # Some distros set e.g. LC_CTYPE = fr_CH.UTF-8. We clean it up. + if ($locale =~ /^([^\.]+)/) { + $locale = $1; + } + $locale =~ s/_/-/; + + # It's pretty sure that there is no language pack of the form fr-CH + # installed, so we also include fr as a wanted language. + if ($locale =~ /^(\S+)\-/) { + $language = $1; + $locale .= ",$language"; + } + else { + $language = $locale; + } + + # Some OSs or distributions may have setlocale return a string of the form + # German_Germany.1252 (this example taken from a Windows XP system), which + # is unsuitable for our needs because Bugzilla works on language codes. + # We try and convert them here. + if ($language = Locale::Language::language2code($language)) { + $locale .= ",$language"; + } + + return $locale; } sub set_output_encoding { - # If we've already set an encoding layer on STDOUT, don't - # add another one. - my @stdout_layers = PerlIO::get_layers(STDOUT); - return if grep(/^encoding/, @stdout_layers); - - my $encoding; - if (ON_WINDOWS and eval { require Win32::Console }) { - # Although setlocale() works on Windows, it doesn't always return - # the current *console's* encoding. So we use OutputCP here instead, - # when we can. - $encoding = Win32::Console::OutputCP(); - } - else { - my $locale = setlocale(LC_CTYPE); - if ($locale =~ /\.([^\.]+)$/) { - $encoding = $1; - } - } - $encoding = "cp$encoding" if ON_WINDOWS; - $encoding = Encode::resolve_alias($encoding) if $encoding; - if ($encoding and $encoding !~ /utf-8/i) { - binmode STDOUT, ":encoding($encoding)"; - binmode STDERR, ":encoding($encoding)"; - } - else { - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - } + # If we've already set an encoding layer on STDOUT, don't + # add another one. + my @stdout_layers = PerlIO::get_layers(STDOUT); + return if grep(/^encoding/, @stdout_layers); + + my $encoding; + if (ON_WINDOWS and eval { require Win32::Console }) { + + # Although setlocale() works on Windows, it doesn't always return + # the current *console's* encoding. So we use OutputCP here instead, + # when we can. + $encoding = Win32::Console::OutputCP(); + } + else { + my $locale = setlocale(LC_CTYPE); + if ($locale =~ /\.([^\.]+)$/) { + $encoding = $1; + } + } + $encoding = "cp$encoding" if ON_WINDOWS; + + $encoding = Encode::resolve_alias($encoding) if $encoding; + if ($encoding and $encoding !~ /utf-8/i) { + binmode STDOUT, ":encoding($encoding)"; + binmode STDERR, ":encoding($encoding)"; + } + else { + binmode STDOUT, ':utf8'; + binmode STDERR, ':utf8'; + } } sub init_console { - eval { ON_WINDOWS && require Win32::Console::ANSI; }; - $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT); - $SIG{__DIE__} = \&_console_die; - prevent_windows_dialog_boxes(); - set_output_encoding(); + eval { ON_WINDOWS && require Win32::Console::ANSI; }; + $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT); + $SIG{__DIE__} = \&_console_die; + prevent_windows_dialog_boxes(); + set_output_encoding(); } sub _console_die { - my ($message) = @_; - # $^S means "we are in an eval" - if ($^S) { - die $message; - } - # Remove newlines from the message before we color it, and then - # add them back in on display. Otherwise the ANSI escape code - # for resetting the color comes after the newline, and Perl thinks - # that it should put "at Bugzilla/Install.pm line 1234" after the - # message. - $message =~ s/\n+$//; - # We put quotes around the message to stringify any object exceptions, - # like Template::Exception. - die colored("$message", COLOR_ERROR) . "\n"; + my ($message) = @_; + + # $^S means "we are in an eval" + if ($^S) { + die $message; + } + + # Remove newlines from the message before we color it, and then + # add them back in on display. Otherwise the ANSI escape code + # for resetting the color comes after the newline, and Perl thinks + # that it should put "at Bugzilla/Install.pm line 1234" after the + # message. + $message =~ s/\n+$//; + + # We put quotes around the message to stringify any object exceptions, + # like Template::Exception. + die colored("$message", COLOR_ERROR) . "\n"; } sub success { - my ($message) = @_; - print colored($message, COLOR_SUCCESS), "\n"; + my ($message) = @_; + print colored($message, COLOR_SUCCESS), "\n"; } sub prevent_windows_dialog_boxes { - # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183 - # and prevents Perl modules from popping up dialog boxes, particularly - # during checksetup (since loading DBD::Oracle during checksetup when - # Oracle isn't installed causes a scary popup and pauses checksetup). - # - # Win32::API ships with ActiveState by default, though there could - # theoretically be a Windows installation without it, I suppose. - if (ON_WINDOWS and eval { require Win32::API }) { - # Call kernel32.SetErrorMode with arguments that mean: - # "The system does not display the critical-error-handler message box. - # Instead, the system sends the error to the calling process." and - # "A child process inherits the error mode of its parent process." - my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode', - 'I', 'I'); - my $SEM_FAILCRITICALERRORS = 0x0001; - my $SEM_NOGPFAULTERRORBOX = 0x0002; - $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX); - } + + # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183 + # and prevents Perl modules from popping up dialog boxes, particularly + # during checksetup (since loading DBD::Oracle during checksetup when + # Oracle isn't installed causes a scary popup and pauses checksetup). + # + # Win32::API ships with ActiveState by default, though there could + # theoretically be a Windows installation without it, I suppose. + if (ON_WINDOWS and eval { require Win32::API }) { + + # Call kernel32.SetErrorMode with arguments that mean: + # "The system does not display the critical-error-handler message box. + # Instead, the system sends the error to the calling process." and + # "A child process inherits the error mode of its parent process." + my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode', 'I', 'I'); + my $SEM_FAILCRITICALERRORS = 0x0001; + my $SEM_NOGPFAULTERRORBOX = 0x0002; + $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX); + } } # This is like request_cache, but it's used only by installation code # for checksetup.pl and things like that. our $_cache = {}; + sub _cache { - # If the normal request_cache is available (which happens any time - # after the requirements phase) then we should use that. - if (eval { Bugzilla->request_cache; }) { - return Bugzilla->request_cache; - } - return $_cache; + + # If the normal request_cache is available (which happens any time + # after the requirements phase) then we should use that. + if (eval { Bugzilla->request_cache; }) { + return Bugzilla->request_cache; + } + return $_cache; } ############################### @@ -661,20 +691,20 @@ sub _cache { ############################## sub trick_taint { - require Carp; - Carp::confess("Undef to trick_taint") unless defined $_[0]; - my $match = $_[0] =~ /^(.*)$/s; - $_[0] = $match ? $1 : undef; - return (defined($_[0])); + require Carp; + Carp::confess("Undef to trick_taint") unless defined $_[0]; + my $match = $_[0] =~ /^(.*)$/s; + $_[0] = $match ? $1 : undef; + return (defined($_[0])); } sub trim { - my ($str) = @_; - if ($str) { - $str =~ s/^\s+//g; - $str =~ s/\s+$//g; - } - return $str; + my ($str) = @_; + if ($str) { + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; } __END__ diff --git a/Bugzilla/Job/BugMail.pm b/Bugzilla/Job/BugMail.pm index e0b7f5448..a6deb5777 100644 --- a/Bugzilla/Job/BugMail.pm +++ b/Bugzilla/Job/BugMail.pm @@ -15,18 +15,18 @@ use Bugzilla::BugMail; BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; } sub work { - my ($class, $job) = @_; - my $success = eval { - Bugzilla::BugMail::dequeue($job->arg->{vars}); - 1; - }; - if (!$success) { - $job->failed($@); - undef $@; - } - else { - $job->completed; - } + my ($class, $job) = @_; + my $success = eval { + Bugzilla::BugMail::dequeue($job->arg->{vars}); + 1; + }; + if (!$success) { + $job->failed($@); + undef $@; + } + else { + $job->completed; + } } 1; diff --git a/Bugzilla/Job/Mailer.pm b/Bugzilla/Job/Mailer.pm index cd1c23445..4a32f0d05 100644 --- a/Bugzilla/Job/Mailer.pm +++ b/Bugzilla/Job/Mailer.pm @@ -16,31 +16,33 @@ BEGIN { eval "use parent qw(TheSchwartz::Worker)"; } # The longest we expect a job to possibly take, in seconds. use constant grab_for => 300; + # We don't want email to fail permanently very easily. Retry for 30 days. use constant max_retries => 725; # The first few retries happen quickly, but after that we wait an hour for # each retry. sub retry_delay { - my ($class, $num_retries) = @_; - if ($num_retries < 5) { - return (10, 30, 60, 300, 600)[$num_retries]; - } - # One hour - return 60*60; + my ($class, $num_retries) = @_; + if ($num_retries < 5) { + return (10, 30, 60, 300, 600)[$num_retries]; + } + + # One hour + return 60 * 60; } sub work { - my ($class, $job) = @_; - my $msg = $job->arg->{msg}; - my $success = eval { MessageToMTA($msg, 1); 1; }; - if (!$success) { - $job->failed($@); - undef $@; - } - else { - $job->completed; - } + my ($class, $job) = @_; + my $msg = $job->arg->{msg}; + my $success = eval { MessageToMTA($msg, 1); 1; }; + if (!$success) { + $job->failed($@); + undef $@; + } + else { + $job->completed; + } } 1; diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm index 6ff85d84f..e48182007 100644 --- a/Bugzilla/JobQueue.pm +++ b/Bugzilla/JobQueue.pm @@ -21,153 +21,155 @@ use fields qw(_worker_pidfile); # This maps job names for Bugzilla::JobQueue to the appropriate modules. # If you add new types of jobs, you should add a mapping here. -use constant JOB_MAP => { - send_mail => 'Bugzilla::Job::Mailer', - bug_mail => 'Bugzilla::Job::BugMail', -}; +use constant JOB_MAP => + {send_mail => 'Bugzilla::Job::Mailer', bug_mail => 'Bugzilla::Job::BugMail',}; # Without a driver cache TheSchwartz opens a new database connection # for each email it sends. This cached connection doesn't persist # across requests. -use constant DRIVER_CACHE_TIME => 300; # 5 minutes +use constant DRIVER_CACHE_TIME => 300; # 5 minutes # To avoid memory leak/fragmentation, a worker process won't process more than # MAX_MESSAGES messages. use constant MAX_MESSAGES => 1000; sub job_map { - if (!defined(Bugzilla->request_cache->{job_map})) { - my $job_map = JOB_MAP; - Bugzilla::Hook::process('job_map', { job_map => $job_map }); - Bugzilla->request_cache->{job_map} = $job_map; - } - - return Bugzilla->request_cache->{job_map}; + if (!defined(Bugzilla->request_cache->{job_map})) { + my $job_map = JOB_MAP; + Bugzilla::Hook::process('job_map', {job_map => $job_map}); + Bugzilla->request_cache->{job_map} = $job_map; + } + + return Bugzilla->request_cache->{job_map}; } sub new { - my $class = shift; - - if (!Bugzilla->feature('jobqueue')) { - ThrowUserError('feature_disabled', { feature => 'jobqueue' }); - } - - my $lc = Bugzilla->localconfig; - # We need to use the main DB as TheSchwartz module is going - # to write to it. - my $self = $class->SUPER::new( - databases => [{ - dsn => Bugzilla->dbh_main->{private_bz_dsn}, - user => $lc->{db_user}, - pass => $lc->{db_pass}, - prefix => 'ts_', - }], - driver_cache_expiration => DRIVER_CACHE_TIME, - prioritize => 1, - ); - - return $self; + my $class = shift; + + if (!Bugzilla->feature('jobqueue')) { + ThrowUserError('feature_disabled', {feature => 'jobqueue'}); + } + + my $lc = Bugzilla->localconfig; + + # We need to use the main DB as TheSchwartz module is going + # to write to it. + my $self = $class->SUPER::new( + databases => [{ + dsn => Bugzilla->dbh_main->{private_bz_dsn}, + user => $lc->{db_user}, + pass => $lc->{db_pass}, + prefix => 'ts_', + }], + driver_cache_expiration => DRIVER_CACHE_TIME, + prioritize => 1, + ); + + return $self; } # A way to get access to the underlying databases directly. sub bz_databases { - my $self = shift; - my @hashes = keys %{ $self->{databases} }; - return map { $self->driver_for($_) } @hashes; + my $self = shift; + my @hashes = keys %{$self->{databases}}; + return map { $self->driver_for($_) } @hashes; } # inserts a job into the queue to be processed and returns immediately sub insert { - my $self = shift; - my $job = shift; - - if (!ref($job)) { - my $mapped_job = Bugzilla::JobQueue->job_map()->{$job}; - ThrowCodeError('jobqueue_no_job_mapping', { job => $job }) - if !$mapped_job; - - $job = new TheSchwartz::Job( - funcname => $mapped_job, - arg => $_[0], - priority => $_[1] || 5 - ); - } - - my $retval = $self->SUPER::insert($job); - # XXX Need to get an error message here if insert fails, but - # I don't see any way to do that in TheSchwartz. - ThrowCodeError('jobqueue_insert_failed', { job => $job, errmsg => $@ }) - if !$retval; - - return $retval; + my $self = shift; + my $job = shift; + + if (!ref($job)) { + my $mapped_job = Bugzilla::JobQueue->job_map()->{$job}; + ThrowCodeError('jobqueue_no_job_mapping', {job => $job}) if !$mapped_job; + + $job = new TheSchwartz::Job( + funcname => $mapped_job, + arg => $_[0], + priority => $_[1] || 5 + ); + } + + my $retval = $self->SUPER::insert($job); + + # XXX Need to get an error message here if insert fails, but + # I don't see any way to do that in TheSchwartz. + ThrowCodeError('jobqueue_insert_failed', {job => $job, errmsg => $@}) + if !$retval; + + return $retval; } # To avoid memory leaks/fragmentation which tends to happen for long running # perl processes; check for jobs, and spawn a new process to empty the queue. sub subprocess_worker { - my $self = shift; - - my $command = "$0 -d -p '" . $self->{_worker_pidfile} . "' onepass"; - - while (1) { - my $time = (time); - my @jobs = $self->list_jobs({ - funcname => $self->{all_abilities}, - run_after => $time, - grabbed_until => $time, - limit => 1, - }); - if (@jobs) { - $self->debug("Spawning queue worker process"); - # Run the worker as a daemon - system $command; - # And poll the PID to detect when the working has finished. - # We do this instead of system() to allow for the INT signal to - # interrup us and trigger kill_worker(). - my $pid = read_text($self->{_worker_pidfile}, err_mode => 'quiet'); - if ($pid) { - sleep(3) while(kill(0, $pid)); - } - $self->debug("Queue worker process completed"); - } else { - $self->debug("No jobs found"); - } - sleep(5); + my $self = shift; + + my $command = "$0 -d -p '" . $self->{_worker_pidfile} . "' onepass"; + + while (1) { + my $time = (time); + my @jobs = $self->list_jobs({ + funcname => $self->{all_abilities}, + run_after => $time, + grabbed_until => $time, + limit => 1, + }); + if (@jobs) { + $self->debug("Spawning queue worker process"); + + # Run the worker as a daemon + system $command; + + # And poll the PID to detect when the working has finished. + # We do this instead of system() to allow for the INT signal to + # interrup us and trigger kill_worker(). + my $pid = read_text($self->{_worker_pidfile}, err_mode => 'quiet'); + if ($pid) { + sleep(3) while (kill(0, $pid)); + } + $self->debug("Queue worker process completed"); } + else { + $self->debug("No jobs found"); + } + sleep(5); + } } sub kill_worker { - my $self = Bugzilla->job_queue(); - if ($self->{_worker_pidfile} && -e $self->{_worker_pidfile}) { - my $worker_pid = read_text($self->{_worker_pidfile}); - if ($worker_pid && kill(0, $worker_pid)) { - $self->debug("Stopping worker process"); - system "$0 -f -p '" . $self->{_worker_pidfile} . "' stop"; - } + my $self = Bugzilla->job_queue(); + if ($self->{_worker_pidfile} && -e $self->{_worker_pidfile}) { + my $worker_pid = read_text($self->{_worker_pidfile}); + if ($worker_pid && kill(0, $worker_pid)) { + $self->debug("Stopping worker process"); + system "$0 -f -p '" . $self->{_worker_pidfile} . "' stop"; } + } } sub set_pidfile { - my ($self, $pidfile) = @_; - $self->{_worker_pidfile} = bz_locations->{'datadir'} . - '/worker-' . basename($pidfile); + my ($self, $pidfile) = @_; + $self->{_worker_pidfile} + = bz_locations->{'datadir'} . '/worker-' . basename($pidfile); } # Clear the request cache at the start of each run. sub work_once { - my $self = shift; - Bugzilla->clear_request_cache(); - return $self->SUPER::work_once(@_); + my $self = shift; + Bugzilla->clear_request_cache(); + return $self->SUPER::work_once(@_); } # Never process more than MAX_MESSAGES in one batch, to avoid memory # leak/fragmentation issues. sub work_until_done { - my $self = shift; - my $count = 0; - while ($count++ < MAX_MESSAGES) { - $self->work_once or last; - } + my $self = shift; + my $count = 0; + while ($count++ < MAX_MESSAGES) { + $self->work_once or last; + } } 1; diff --git a/Bugzilla/JobQueue/Runner.pm b/Bugzilla/JobQueue/Runner.pm index 104a97b0b..a1803be6e 100644 --- a/Bugzilla/JobQueue/Runner.pm +++ b/Bugzilla/JobQueue/Runner.pm @@ -28,8 +28,8 @@ BEGIN { eval "use parent qw(Daemon::Generic)"; } our $VERSION = BUGZILLA_VERSION; # Info we need to install/uninstall the daemon. -our $chkconfig = "/sbin/chkconfig"; -our $initd = "/etc/init.d"; +our $chkconfig = "/sbin/chkconfig"; +our $initd = "/etc/init.d"; our $initscript = "bugzilla-queue"; # The Daemon::Generic docs say that it uses all sorts of @@ -37,187 +37,188 @@ our $initscript = "bugzilla-queue"; # only thing it uses from gd_preconfig is the "pidfile" # config parameter. sub gd_preconfig { - my $self = shift; - - $self->{_run_command} = 'subprocess_worker'; - my $pidfile = $self->{gd_args}{pidfile}; - if (!$pidfile) { - $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} - . ".pid"; - } - return (pidfile => $pidfile); + my $self = shift; + + $self->{_run_command} = 'subprocess_worker'; + my $pidfile = $self->{gd_args}{pidfile}; + if (!$pidfile) { + $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".pid"; + } + return (pidfile => $pidfile); } # All config other than the pidfile has to be done in gd_getopt # in order for it to be set up early enough. sub gd_getopt { - my $self = shift; + my $self = shift; - $self->SUPER::gd_getopt(); + $self->SUPER::gd_getopt(); - if ($self->{gd_args}{progname}) { - $self->{gd_progname} = $self->{gd_args}{progname}; - } - else { - $self->{gd_progname} = basename($0); - } + if ($self->{gd_args}{progname}) { + $self->{gd_progname} = $self->{gd_args}{progname}; + } + else { + $self->{gd_progname} = basename($0); + } - # There are places that Daemon Generic's new() uses $0 instead of - # gd_progname, which it really shouldn't, but this hack fixes it. - $self->{_original_zero} = $0; - $0 = $self->{gd_progname}; + # There are places that Daemon Generic's new() uses $0 instead of + # gd_progname, which it really shouldn't, but this hack fixes it. + $self->{_original_zero} = $0; + $0 = $self->{gd_progname}; } sub gd_postconfig { - my $self = shift; - # See the hack above in gd_getopt. This just reverses it - # in case anything else needs the accurate $0. - $0 = delete $self->{_original_zero}; + my $self = shift; + + # See the hack above in gd_getopt. This just reverses it + # in case anything else needs the accurate $0. + $0 = delete $self->{_original_zero}; } sub gd_more_opt { - my $self = shift; - return ( - 'pidfile=s' => \$self->{gd_args}{pidfile}, - 'n=s' => \$self->{gd_args}{progname}, - ); + my $self = shift; + return ( + 'pidfile=s' => \$self->{gd_args}{pidfile}, + 'n=s' => \$self->{gd_args}{progname}, + ); } sub gd_usage { - pod2usage({ -verbose => 0, -exitval => 'NOEXIT' }); - return 0 + pod2usage({-verbose => 0, -exitval => 'NOEXIT'}); + return 0; } sub gd_can_install { - my $self = shift; + my $self = shift; + + my $source_file; + if (-e "/etc/SuSE-release") { + $source_file = "contrib/$initscript.suse"; + } + else { + $source_file = "contrib/$initscript.rhel"; + } + my $dest_file = "$initd/$initscript"; + my $sysconfig = '/etc/sysconfig'; + my $config_file = "$sysconfig/$initscript"; + + if (!-x $chkconfig or !-d $initd) { + return $self->SUPER::gd_can_install(@_); + } - my $source_file; - if ( -e "/etc/SuSE-release" ) { - $source_file = "contrib/$initscript.suse"; - } else { - $source_file = "contrib/$initscript.rhel"; + return sub { + if (!-w $initd) { + print "You must run the 'install' command as root.\n"; + return; } - my $dest_file = "$initd/$initscript"; - my $sysconfig = '/etc/sysconfig'; - my $config_file = "$sysconfig/$initscript"; - - if (!-x $chkconfig or !-d $initd) { - return $self->SUPER::gd_can_install(@_); + if (-e $dest_file) { + print "$initscript already in $initd.\n"; + } + else { + copy($source_file, $dest_file) + or die "Could not copy $source_file to $dest_file: $!"; + chmod(0755, $dest_file) or die "Could not change permissions on $dest_file: $!"; } - return sub { - if (!-w $initd) { - print "You must run the 'install' command as root.\n"; - return; - } - if (-e $dest_file) { - print "$initscript already in $initd.\n"; - } - else { - copy($source_file, $dest_file) - or die "Could not copy $source_file to $dest_file: $!"; - chmod(0755, $dest_file) - or die "Could not change permissions on $dest_file: $!"; - } - - system($chkconfig, '--add', $initscript); - print "$initscript installed.", - " To start the daemon, do \"$dest_file start\" as root.\n"; - - if (-d $sysconfig and -w $sysconfig) { - if (-e $config_file) { - print "$config_file already exists.\n"; - return; - } - - open(my $config_fh, ">", $config_file) - or die "Could not write to $config_file: $!"; - my $directory = abs_path(dirname($self->{_original_zero})); - my $owner_id = (stat $self->{_original_zero})[4]; - my $owner = getpwuid($owner_id); - print $config_fh <<END; + system($chkconfig, '--add', $initscript); + print "$initscript installed.", + " To start the daemon, do \"$dest_file start\" as root.\n"; + + if (-d $sysconfig and -w $sysconfig) { + if (-e $config_file) { + print "$config_file already exists.\n"; + return; + } + + open(my $config_fh, ">", $config_file) + or die "Could not write to $config_file: $!"; + my $directory = abs_path(dirname($self->{_original_zero})); + my $owner_id = (stat $self->{_original_zero})[4]; + my $owner = getpwuid($owner_id); + print $config_fh <<END; #!/bin/sh BUGZILLA="$directory" # This user must have write access to Bugzilla's data/ directory. USER=$owner END - close($config_fh); - } - else { - print "Please edit $dest_file to configure the daemon.\n"; - } + close($config_fh); + } + else { + print "Please edit $dest_file to configure the daemon.\n"; + } } } sub gd_can_uninstall { - my $self = shift; - - if (-x $chkconfig and -d $initd) { - return sub { - if (!-e "$initd/$initscript") { - print "$initscript not installed.\n"; - return; - } - system($chkconfig, '--del', $initscript); - print "$initscript disabled.", - " To stop it, run: $initd/$initscript stop\n"; - } - } + my $self = shift; - return $self->SUPER::gd_can_install(@_); + if (-x $chkconfig and -d $initd) { + return sub { + if (!-e "$initd/$initscript") { + print "$initscript not installed.\n"; + return; + } + system($chkconfig, '--del', $initscript); + print "$initscript disabled.", " To stop it, run: $initd/$initscript stop\n"; + } + } + + return $self->SUPER::gd_can_install(@_); } sub gd_check { - my $self = shift; - - # Get a count of all the jobs currently in the queue. - my $jq = Bugzilla->job_queue(); - my @dbs = $jq->bz_databases(); - my $count = 0; - foreach my $driver (@dbs) { - $count += $driver->select_one('SELECT COUNT(*) FROM ts_job', []); - } - print get_text('job_queue_depth', { count => $count }) . "\n"; + my $self = shift; + + # Get a count of all the jobs currently in the queue. + my $jq = Bugzilla->job_queue(); + my @dbs = $jq->bz_databases(); + my $count = 0; + foreach my $driver (@dbs) { + $count += $driver->select_one('SELECT COUNT(*) FROM ts_job', []); + } + print get_text('job_queue_depth', {count => $count}) . "\n"; } sub gd_setup_signals { - my $self = shift; - $self->SUPER::gd_setup_signals(); - $SIG{TERM} = sub { $self->gd_quit_event(); } + my $self = shift; + $self->SUPER::gd_setup_signals(); + $SIG{TERM} = sub { $self->gd_quit_event(); } } sub gd_quit_event { - Bugzilla->job_queue->kill_worker(); - exit(1); + Bugzilla->job_queue->kill_worker(); + exit(1); } sub gd_other_cmd { - my ($self, $do, $locked) = @_; - if ($do eq "once") { - $self->{_run_command} = 'work_once'; - } elsif ($do eq "onepass") { - $self->{_run_command} = 'work_until_done'; - } else { - $self->SUPER::gd_other_cmd($do, $locked); - } + my ($self, $do, $locked) = @_; + if ($do eq "once") { + $self->{_run_command} = 'work_once'; + } + elsif ($do eq "onepass") { + $self->{_run_command} = 'work_until_done'; + } + else { + $self->SUPER::gd_other_cmd($do, $locked); + } } sub gd_run { - my $self = shift; - $self->_do_work($self->{_run_command}); + my $self = shift; + $self->_do_work($self->{_run_command}); } sub _do_work { - my ($self, $fn) = @_; - - my $jq = Bugzilla->job_queue(); - $jq->set_verbose($self->{debug}); - $jq->set_pidfile($self->{gd_pidfile}); - foreach my $module (values %{ Bugzilla::JobQueue->job_map() }) { - eval "use $module"; - $jq->can_do($module); - } - $jq->$fn; + my ($self, $fn) = @_; + + my $jq = Bugzilla->job_queue(); + $jq->set_verbose($self->{debug}); + $jq->set_pidfile($self->{gd_pidfile}); + foreach my $module (values %{Bugzilla::JobQueue->job_map()}) { + eval "use $module"; + $jq->can_do($module); + } + $jq->$fn; } 1; diff --git a/Bugzilla/Keyword.pm b/Bugzilla/Keyword.pm index afa93e1e9..f1cb6cadf 100644 --- a/Bugzilla/Keyword.pm +++ b/Bugzilla/Keyword.pm @@ -23,44 +23,42 @@ use Bugzilla::Util; use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( - keyworddefs.id - keyworddefs.name - keyworddefs.description + keyworddefs.id + keyworddefs.name + keyworddefs.description ); use constant DB_TABLE => 'keyworddefs'; -use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, -}; +use constant VALIDATORS => + {name => \&_check_name, description => \&_check_description,}; use constant UPDATE_COLUMNS => qw( - name - description + name + description ); ############################### #### Accessors ###### ############################### -sub description { return $_[0]->{'description'}; } +sub description { return $_[0]->{'description'}; } sub bug_count { - my ($self) = @_; - return $self->{'bug_count'} if defined $self->{'bug_count'}; - ($self->{'bug_count'}) = - Bugzilla->dbh->selectrow_array( - 'SELECT COUNT(*) FROM keywords WHERE keywordid = ?', - undef, $self->id); - return $self->{'bug_count'}; + my ($self) = @_; + return $self->{'bug_count'} if defined $self->{'bug_count'}; + ($self->{'bug_count'}) + = Bugzilla->dbh->selectrow_array( + 'SELECT COUNT(*) FROM keywords WHERE keywordid = ?', + undef, $self->id); + return $self->{'bug_count'}; } ############################### #### Mutators ##### ############################### -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } ############################### @@ -68,27 +66,29 @@ sub set_description { $_[0]->set('description', $_[1]); } ############################### sub get_all_with_bug_count { - my $class = shift; - my $dbh = Bugzilla->dbh; - my $keywords = - $dbh->selectall_arrayref('SELECT ' - . join(', ', $class->_get_db_columns) . ', + my $class = shift; + my $dbh = Bugzilla->dbh; + my $keywords = $dbh->selectall_arrayref( + 'SELECT ' . join(', ', $class->_get_db_columns) . ', COUNT(keywords.bug_id) AS bug_count FROM keyworddefs LEFT JOIN keywords - ON keyworddefs.id = keywords.keywordid ' . - $dbh->sql_group_by('keyworddefs.id', - 'keyworddefs.name, - keyworddefs.description') . ' - ORDER BY keyworddefs.name', {'Slice' => {}}); - if (!$keywords) { - return []; - } - - foreach my $keyword (@$keywords) { - bless($keyword, $class); - } - return $keywords; + ON keyworddefs.id = keywords.keywordid ' + . $dbh->sql_group_by( + 'keyworddefs.id', 'keyworddefs.name, + keyworddefs.description' + ) . ' + ORDER BY keyworddefs.name', + {'Slice' => {}} + ); + if (!$keywords) { + return []; + } + + foreach my $keyword (@$keywords) { + bless($keyword, $class); + } + return $keywords; } ############################### @@ -96,33 +96,33 @@ sub get_all_with_bug_count { ############################### sub _check_name { - my ($self, $name) = @_; - - $name = trim($name); - if (!defined $name or $name eq "") { - ThrowUserError("keyword_blank_name"); - } - if ($name =~ /[\s,]/) { - ThrowUserError("keyword_invalid_name"); - } - - # We only want to validate the non-existence of the name if - # we're creating a new Keyword or actually renaming the keyword. - if (!ref($self) || lc($self->name) ne lc($name)) { - my $keyword = new Bugzilla::Keyword({ name => $name }); - ThrowUserError("keyword_already_exists", { name => $name }) if $keyword; - } - - return $name; + my ($self, $name) = @_; + + $name = trim($name); + if (!defined $name or $name eq "") { + ThrowUserError("keyword_blank_name"); + } + if ($name =~ /[\s,]/) { + ThrowUserError("keyword_invalid_name"); + } + + # We only want to validate the non-existence of the name if + # we're creating a new Keyword or actually renaming the keyword. + if (!ref($self) || lc($self->name) ne lc($name)) { + my $keyword = new Bugzilla::Keyword({name => $name}); + ThrowUserError("keyword_already_exists", {name => $name}) if $keyword; + } + + return $name; } sub _check_description { - my ($self, $desc) = @_; - $desc = trim($desc); - if (!defined $desc or $desc eq '') { - ThrowUserError("keyword_blank_description"); - } - return $desc; + my ($self, $desc) = @_; + $desc = trim($desc); + if (!defined $desc or $desc eq '') { + ThrowUserError("keyword_blank_description"); + } + return $desc; } 1; diff --git a/Bugzilla/MIME.pm b/Bugzilla/MIME.pm index 8c6c141bb..660799e66 100644 --- a/Bugzilla/MIME.pm +++ b/Bugzilla/MIME.pm @@ -14,91 +14,91 @@ use warnings; use parent qw(Email::MIME); sub new { - my ($class, $msg) = @_; - state $use_utf8 = Bugzilla->params->{'utf8'}; - - # Template-Toolkit trims trailing newlines, which is problematic when - # parsing headers. - $msg =~ s/\n*$/\n/; - - # Because the encoding headers are not present in our email templates, we - # need to treat them as binary UTF-8 when parsing. - my ($in_header, $has_type, $has_encoding, $has_body) = (1); - foreach my $line (split(/\n/, $msg)) { - if ($line eq '') { - $in_header = 0; - next; - } - if (!$in_header) { - $has_body = 1; - last; - } - $has_type = 1 if $line =~ /^Content-Type:/i; - $has_encoding = 1 if $line =~ /^Content-Transfer-Encoding:/i; + my ($class, $msg) = @_; + state $use_utf8 = Bugzilla->params->{'utf8'}; + + # Template-Toolkit trims trailing newlines, which is problematic when + # parsing headers. + $msg =~ s/\n*$/\n/; + + # Because the encoding headers are not present in our email templates, we + # need to treat them as binary UTF-8 when parsing. + my ($in_header, $has_type, $has_encoding, $has_body) = (1); + foreach my $line (split(/\n/, $msg)) { + if ($line eq '') { + $in_header = 0; + next; } - if ($has_body) { - if (!$has_type && $use_utf8) { - $msg = qq#Content-Type: text/plain; charset="UTF-8"\n# . $msg; - } - if (!$has_encoding) { - $msg = qq#Content-Transfer-Encoding: binary\n# . $msg; - } + if (!$in_header) { + $has_body = 1; + last; } - if ($use_utf8 && utf8::is_utf8($msg)) { - utf8::encode($msg); + $has_type = 1 if $line =~ /^Content-Type:/i; + $has_encoding = 1 if $line =~ /^Content-Transfer-Encoding:/i; + } + if ($has_body) { + if (!$has_type && $use_utf8) { + $msg = qq#Content-Type: text/plain; charset="UTF-8"\n# . $msg; } - - # RFC 2822 requires us to have CRLF for our line endings and - # Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF) - # directly because Perl translates "\n" depending on what platform - # you're running on. See http://perldoc.perl.org/perlport.html#Newlines - $msg =~ s/(?:\015+)?\012/\015\012/msg; - - return $class->SUPER::new($msg); + if (!$has_encoding) { + $msg = qq#Content-Transfer-Encoding: binary\n# . $msg; + } + } + if ($use_utf8 && utf8::is_utf8($msg)) { + utf8::encode($msg); + } + + # RFC 2822 requires us to have CRLF for our line endings and + # Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF) + # directly because Perl translates "\n" depending on what platform + # you're running on. See http://perldoc.perl.org/perlport.html#Newlines + $msg =~ s/(?:\015+)?\012/\015\012/msg; + + return $class->SUPER::new($msg); } sub as_string { - my $self = shift; - state $use_utf8 = Bugzilla->params->{'utf8'}; - - # We add this header to uniquely identify all email that we - # send as coming from this Bugzilla installation. - # - # We don't use correct_urlbase, because we want this URL to - # *always* be the same for this Bugzilla, in every email, - # even if the admin changes the "ssl_redirect" parameter some day. - $self->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'}); - - # We add this header to mark the mail as "auto-generated" and - # thus to hopefully avoid auto replies. - $self->header_set('Auto-Submitted', 'auto-generated'); - - # MIME-Version must be set otherwise some mailsystems ignore the charset - $self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version'); - - # Encode the headers correctly. - foreach my $header ($self->header_names) { - my @values = $self->header($header); - map { utf8::decode($_) if defined($_) && !utf8::is_utf8($_) } @values; - - $self->header_str_set($header, @values); - } - - # Ensure the character-set and encoding is set correctly on single part - # emails. Multipart emails should have these already set when the parts - # are assembled. - if (scalar($self->parts) == 1) { - $self->charset_set('UTF-8') if $use_utf8; - $self->encoding_set('quoted-printable'); - } - - # Ensure we always return the encoded string - my $value = $self->SUPER::as_string(); - if ($use_utf8 && utf8::is_utf8($value)) { - utf8::encode($value); - } - - return $value; + my $self = shift; + state $use_utf8 = Bugzilla->params->{'utf8'}; + + # We add this header to uniquely identify all email that we + # send as coming from this Bugzilla installation. + # + # We don't use correct_urlbase, because we want this URL to + # *always* be the same for this Bugzilla, in every email, + # even if the admin changes the "ssl_redirect" parameter some day. + $self->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'}); + + # We add this header to mark the mail as "auto-generated" and + # thus to hopefully avoid auto replies. + $self->header_set('Auto-Submitted', 'auto-generated'); + + # MIME-Version must be set otherwise some mailsystems ignore the charset + $self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version'); + + # Encode the headers correctly. + foreach my $header ($self->header_names) { + my @values = $self->header($header); + map { utf8::decode($_) if defined($_) && !utf8::is_utf8($_) } @values; + + $self->header_str_set($header, @values); + } + + # Ensure the character-set and encoding is set correctly on single part + # emails. Multipart emails should have these already set when the parts + # are assembled. + if (scalar($self->parts) == 1) { + $self->charset_set('UTF-8') if $use_utf8; + $self->encoding_set('quoted-printable'); + } + + # Ensure we always return the encoded string + my $value = $self->SUPER::as_string(); + if ($use_utf8 && utf8::is_utf8($value)) { + utf8::encode($value); + } + + return $value; } 1; diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm index 5ccf2d1ed..a5f79b9bc 100644 --- a/Bugzilla/Mailer.pm +++ b/Bugzilla/Mailer.pm @@ -28,199 +28,209 @@ use Email::Sender::Transport::SMTP::Persistent; use Bugzilla::Sender::Transport::Sendmail; sub generate_email { - my ($vars, $templates) = @_; - my ($lang, $email_format, $msg_text, $msg_html, $msg_header); - state $use_utf8 = Bugzilla->params->{'utf8'}; - - if ($vars->{to_user}) { - $lang = $vars->{to_user}->setting('lang'); - $email_format = $vars->{to_user}->setting('email_format'); - } else { - # If there are users in the CC list who don't have an account, - # use the default language for email notifications. - $lang = Bugzilla::User->new()->setting('lang'); - # However we cannot fall back to the default email_format, since - # it may be HTML, and many of the includes used in the HTML - # template require a valid user object. Instead we fall back to - # the plaintext template. - $email_format = 'text_only'; - } - - my $template = Bugzilla->template_inner($lang); - - $template->process($templates->{header}, $vars, \$msg_header) - || ThrowTemplateError($template->error()); - $template->process($templates->{text}, $vars, \$msg_text) - || ThrowTemplateError($template->error()); - - my @parts = ( - Bugzilla::MIME->create( - attributes => { - content_type => 'text/plain', - charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', - encoding => 'quoted-printable', - }, - body_str => $msg_text, - ) - ); - if ($templates->{html} && $email_format eq 'html') { - $template->process($templates->{html}, $vars, \$msg_html) - || ThrowTemplateError($template->error()); - push @parts, Bugzilla::MIME->create( - attributes => { - content_type => 'text/html', - charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', - encoding => 'quoted-printable', - }, - body_str => $msg_html, - ); - } - - my $email = Bugzilla::MIME->new($msg_header); - if (scalar(@parts) == 1) { - $email->content_type_set($parts[0]->content_type); - } else { - $email->content_type_set('multipart/alternative'); - # Some mail clients need same encoding for each part, even empty ones. - $email->charset_set('UTF-8') if $use_utf8; - } - $email->parts_set(\@parts); - return $email; + my ($vars, $templates) = @_; + my ($lang, $email_format, $msg_text, $msg_html, $msg_header); + state $use_utf8 = Bugzilla->params->{'utf8'}; + + if ($vars->{to_user}) { + $lang = $vars->{to_user}->setting('lang'); + $email_format = $vars->{to_user}->setting('email_format'); + } + else { + # If there are users in the CC list who don't have an account, + # use the default language for email notifications. + $lang = Bugzilla::User->new()->setting('lang'); + + # However we cannot fall back to the default email_format, since + # it may be HTML, and many of the includes used in the HTML + # template require a valid user object. Instead we fall back to + # the plaintext template. + $email_format = 'text_only'; + } + + my $template = Bugzilla->template_inner($lang); + + $template->process($templates->{header}, $vars, \$msg_header) + || ThrowTemplateError($template->error()); + $template->process($templates->{text}, $vars, \$msg_text) + || ThrowTemplateError($template->error()); + + my @parts = (Bugzilla::MIME->create( + attributes => { + content_type => 'text/plain', + charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', + encoding => 'quoted-printable', + }, + body_str => $msg_text, + )); + if ($templates->{html} && $email_format eq 'html') { + $template->process($templates->{html}, $vars, \$msg_html) + || ThrowTemplateError($template->error()); + push @parts, + Bugzilla::MIME->create( + attributes => { + content_type => 'text/html', + charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', + encoding => 'quoted-printable', + }, + body_str => $msg_html, + ); + } + + my $email = Bugzilla::MIME->new($msg_header); + if (scalar(@parts) == 1) { + $email->content_type_set($parts[0]->content_type); + } + else { + $email->content_type_set('multipart/alternative'); + + # Some mail clients need same encoding for each part, even empty ones. + $email->charset_set('UTF-8') if $use_utf8; + } + $email->parts_set(\@parts); + return $email; } sub MessageToMTA { - my ($msg, $send_now) = (@_); - my $method = Bugzilla->params->{'mail_delivery_method'}; - return if $method eq 'None'; - - if (Bugzilla->params->{'use_mailer_queue'} - && ! $send_now - && ! Bugzilla->dbh->bz_in_transaction() - ) { - Bugzilla->job_queue->insert('send_mail', { msg => $msg }); - return; - } - - my $dbh = Bugzilla->dbh; - - my $email = ref($msg) ? $msg : Bugzilla::MIME->new($msg); - - # If we're called from within a transaction, we don't want to send the - # email immediately, in case the transaction is rolled back. Instead we - # insert it into the mail_staging table, and bz_commit_transaction calls - # send_staged_mail() after the transaction is committed. - if (! $send_now && $dbh->bz_in_transaction()) { - # The e-mail string may contain tainted values. - my $string = $email->as_string; - trick_taint($string); - - my $sth = $dbh->prepare("INSERT INTO mail_staging (message) VALUES (?)"); - $sth->bind_param(1, $string, $dbh->BLOB_TYPE); - $sth->execute; - return; - } - - my $from = $email->header('From'); - - my $hostname; - my $transport; - if ($method eq "Sendmail") { - if (ON_WINDOWS) { - $transport = Bugzilla::Sender::Transport::Sendmail->new({ sendmail => SENDMAIL_EXE }); - } - else { - $transport = Bugzilla::Sender::Transport::Sendmail->new(); - } + my ($msg, $send_now) = (@_); + my $method = Bugzilla->params->{'mail_delivery_method'}; + return if $method eq 'None'; + + if ( Bugzilla->params->{'use_mailer_queue'} + && !$send_now + && !Bugzilla->dbh->bz_in_transaction()) + { + Bugzilla->job_queue->insert('send_mail', {msg => $msg}); + return; + } + + my $dbh = Bugzilla->dbh; + + my $email = ref($msg) ? $msg : Bugzilla::MIME->new($msg); + + # If we're called from within a transaction, we don't want to send the + # email immediately, in case the transaction is rolled back. Instead we + # insert it into the mail_staging table, and bz_commit_transaction calls + # send_staged_mail() after the transaction is committed. + if (!$send_now && $dbh->bz_in_transaction()) { + + # The e-mail string may contain tainted values. + my $string = $email->as_string; + trick_taint($string); + + my $sth = $dbh->prepare("INSERT INTO mail_staging (message) VALUES (?)"); + $sth->bind_param(1, $string, $dbh->BLOB_TYPE); + $sth->execute; + return; + } + + my $from = $email->header('From'); + + my $hostname; + my $transport; + if ($method eq "Sendmail") { + if (ON_WINDOWS) { + $transport + = Bugzilla::Sender::Transport::Sendmail->new({sendmail => SENDMAIL_EXE}); } else { - # Sendmail will automatically append our hostname to the From - # address, but other mailers won't. - my $urlbase = Bugzilla->params->{'urlbase'}; - $urlbase =~ m|//([^:/]+)[:/]?|; - $hostname = $1 || 'localhost'; - $from .= "\@$hostname" if $from !~ /@/; - $email->header_set('From', $from); - - # Sendmail adds a Date: header also, but others may not. - if (!defined $email->header('Date')) { - $email->header_set('Date', time2str("%a, %d %b %Y %T %z", time())); - } - } - - if ($method eq "SMTP") { - my ($host, $port) = split(/:/, Bugzilla->params->{'smtpserver'}, 2); - $transport = Bugzilla->request_cache->{smtp} //= - Email::Sender::Transport::SMTP::Persistent->new({ - host => $host, - defined($port) ? (port => $port) : (), - sasl_username => Bugzilla->params->{'smtp_username'}, - sasl_password => Bugzilla->params->{'smtp_password'}, - helo => $hostname, - ssl => Bugzilla->params->{'smtp_ssl'}, - debug => Bugzilla->params->{'smtp_debug'} }); + $transport = Bugzilla::Sender::Transport::Sendmail->new(); } - - Bugzilla::Hook::process('mailer_before_send', { email => $email }); - - return if $email->header('to') eq ''; - - if ($method eq "Test") { - my $filename = bz_locations()->{'datadir'} . '/mailer.testfile'; - open TESTFILE, '>>', $filename; - # From - <date> is required to be a valid mbox file. - print TESTFILE "\n\nFrom - " . $email->header('Date') . "\n" . $email->as_string; - close TESTFILE; + } + else { + # Sendmail will automatically append our hostname to the From + # address, but other mailers won't. + my $urlbase = Bugzilla->params->{'urlbase'}; + $urlbase =~ m|//([^:/]+)[:/]?|; + $hostname = $1 || 'localhost'; + $from .= "\@$hostname" if $from !~ /@/; + $email->header_set('From', $from); + + # Sendmail adds a Date: header also, but others may not. + if (!defined $email->header('Date')) { + $email->header_set('Date', time2str("%a, %d %b %Y %T %z", time())); } - else { - # This is useful for Sendmail, so we put it out here. - local $ENV{PATH} = SENDMAIL_PATH; - eval { sendmail($email, { transport => $transport }) }; - if ($@) { - ThrowCodeError('mail_send_error', { msg => $@->message, mail => $email }); - } + } + + if ($method eq "SMTP") { + my ($host, $port) = split(/:/, Bugzilla->params->{'smtpserver'}, 2); + $transport = Bugzilla->request_cache->{smtp} + //= Email::Sender::Transport::SMTP::Persistent->new({ + host => $host, + defined($port) ? (port => $port) : (), + sasl_username => Bugzilla->params->{'smtp_username'}, + sasl_password => Bugzilla->params->{'smtp_password'}, + helo => $hostname, + ssl => Bugzilla->params->{'smtp_ssl'}, + debug => Bugzilla->params->{'smtp_debug'} + }); + } + + Bugzilla::Hook::process('mailer_before_send', {email => $email}); + + return if $email->header('to') eq ''; + + if ($method eq "Test") { + my $filename = bz_locations()->{'datadir'} . '/mailer.testfile'; + open TESTFILE, '>>', $filename; + + # From - <date> is required to be a valid mbox file. + print TESTFILE "\n\nFrom - " + . $email->header('Date') . "\n" + . $email->as_string; + close TESTFILE; + } + else { + # This is useful for Sendmail, so we put it out here. + local $ENV{PATH} = SENDMAIL_PATH; + eval { sendmail($email, {transport => $transport}) }; + if ($@) { + ThrowCodeError('mail_send_error', {msg => $@->message, mail => $email}); } + } } # Builds header suitable for use as a threading marker in email notifications sub build_thread_marker { - my ($bug_id, $user_id, $is_new) = @_; - - if (!defined $user_id) { - $user_id = Bugzilla->user->id; - } - - my $sitespec = '@' . Bugzilla->params->{'urlbase'}; - $sitespec =~ s/:\/\//\./; # Make the protocol look like part of the domain - $sitespec =~ s/^([^:\/]+):(\d+)/$1/; # Remove a port number, to relocate - if ($2) { - $sitespec = "-$2$sitespec"; # Put the port number back in, before the '@' - } - - my $threadingmarker; - if ($is_new) { - $threadingmarker = "Message-ID: <bug-$bug_id-$user_id$sitespec>"; - } - else { - my $rand_bits = generate_random_password(10); - $threadingmarker = "Message-ID: <bug-$bug_id-$user_id-$rand_bits$sitespec>" . - "\nIn-Reply-To: <bug-$bug_id-$user_id$sitespec>" . - "\nReferences: <bug-$bug_id-$user_id$sitespec>"; - } - - return $threadingmarker; + my ($bug_id, $user_id, $is_new) = @_; + + if (!defined $user_id) { + $user_id = Bugzilla->user->id; + } + + my $sitespec = '@' . Bugzilla->params->{'urlbase'}; + $sitespec =~ s/:\/\//\./; # Make the protocol look like part of the domain + $sitespec =~ s/^([^:\/]+):(\d+)/$1/; # Remove a port number, to relocate + if ($2) { + $sitespec = "-$2$sitespec"; # Put the port number back in, before the '@' + } + + my $threadingmarker; + if ($is_new) { + $threadingmarker = "Message-ID: <bug-$bug_id-$user_id$sitespec>"; + } + else { + my $rand_bits = generate_random_password(10); + $threadingmarker + = "Message-ID: <bug-$bug_id-$user_id-$rand_bits$sitespec>" + . "\nIn-Reply-To: <bug-$bug_id-$user_id$sitespec>" + . "\nReferences: <bug-$bug_id-$user_id$sitespec>"; + } + + return $threadingmarker; } sub send_staged_mail { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $emails = $dbh->selectall_arrayref('SELECT id, message FROM mail_staging'); - my $sth = $dbh->prepare('DELETE FROM mail_staging WHERE id = ?'); + my $emails = $dbh->selectall_arrayref('SELECT id, message FROM mail_staging'); + my $sth = $dbh->prepare('DELETE FROM mail_staging WHERE id = ?'); - foreach my $email (@$emails) { - my ($id, $message) = @$email; - MessageToMTA($message); - $sth->execute($id); - } + foreach my $email (@$emails) { + my ($id, $message) = @$email; + MessageToMTA($message); + $sth->execute($id); + } } 1; diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm index df90fef93..2f30c186a 100644 --- a/Bugzilla/Memcached.pm +++ b/Bugzilla/Memcached.pm @@ -20,281 +20,278 @@ use URI::Escape; use constant MAX_KEY_LENGTH => 250; sub _new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $self = {}; - - # always return an object to simplify calling code when memcached is - # disabled. - if (Bugzilla->feature('memcached') - && Bugzilla->params->{memcached_servers}) - { - require Cache::Memcached; - $self->{namespace} = Bugzilla->params->{memcached_namespace} || ''; - $self->{memcached} = - Cache::Memcached->new({ - servers => [ split(/[, ]+/, Bugzilla->params->{memcached_servers}) ], - namespace => $self->{namespace}, - }); - } - return bless($self, $class); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $self = {}; + + # always return an object to simplify calling code when memcached is + # disabled. + if (Bugzilla->feature('memcached') && Bugzilla->params->{memcached_servers}) { + require Cache::Memcached; + $self->{namespace} = Bugzilla->params->{memcached_namespace} || ''; + $self->{memcached} = Cache::Memcached->new({ + servers => [split(/[, ]+/, Bugzilla->params->{memcached_servers})], + namespace => $self->{namespace}, + }); + } + return bless($self, $class); } sub enabled { - return $_[0]->{memcached} ? 1 : 0; + return $_[0]->{memcached} ? 1 : 0; } sub set { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key, value => $value } - if (exists $args->{key}) { - $self->_set($args->{key}, $args->{value}); + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key, value => $value } + if (exists $args->{key}) { + $self->_set($args->{key}, $args->{value}); + } + + # { table => $table, id => $id, name => $name, data => $data } + elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) { + + # For caching of Bugzilla::Object, we have to be able to clear the + # cached values when given either the object's id or name. + my ($table, $id, $name, $data) = @$args{qw(table id name data)}; + $self->_set("$table.id.$id", $data); + if (defined $name) { + $self->_set("$table.name_id.$name", $id); + $self->_set("$table.id_name.$id", $name); } + } - # { table => $table, id => $id, name => $name, data => $data } - elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) { - # For caching of Bugzilla::Object, we have to be able to clear the - # cached values when given either the object's id or name. - my ($table, $id, $name, $data) = @$args{qw(table id name data)}; - $self->_set("$table.id.$id", $data); - if (defined $name) { - $self->_set("$table.name_id.$name", $id); - $self->_set("$table.id_name.$id", $name); - } - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set", - params => [ 'key', 'table' ] }); - } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set", params => ['key', 'table']}); + } } sub get { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key } - if (exists $args->{key}) { - return $self->_get($args->{key}); - } - - # { table => $table, id => $id } - elsif (exists $args->{table} && exists $args->{id}) { - my ($table, $id) = @$args{qw(table id)}; - return $self->_get("$table.id.$id"); - } - - # { table => $table, name => $name } - elsif (exists $args->{table} && exists $args->{name}) { - my ($table, $name) = @$args{qw(table name)}; - return unless my $id = $self->_get("$table.name_id.$name"); - return $self->_get("$table.id.$id"); - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get", - params => [ 'key', 'table' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key } + if (exists $args->{key}) { + return $self->_get($args->{key}); + } + + # { table => $table, id => $id } + elsif (exists $args->{table} && exists $args->{id}) { + my ($table, $id) = @$args{qw(table id)}; + return $self->_get("$table.id.$id"); + } + + # { table => $table, name => $name } + elsif (exists $args->{table} && exists $args->{name}) { + my ($table, $name) = @$args{qw(table name)}; + return unless my $id = $self->_get("$table.name_id.$name"); + return $self->_get("$table.id.$id"); + } + + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::get", params => ['key', 'table']}); + } } sub set_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - - if (exists $args->{key}) { - return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_config", - params => [ 'key' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + if (exists $args->{key}) { + return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set_config", params => ['key']}); + } } sub get_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - - if (exists $args->{key}) { - return $self->_get($self->_config_prefix . '.' . $args->{key}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get_config", - params => [ 'key' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + if (exists $args->{key}) { + return $self->_get($self->_config_prefix . '.' . $args->{key}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::get_config", params => ['key']}); + } } sub clear { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key } - if (exists $args->{key}) { - $self->_delete($args->{key}); - } - - # { table => $table, id => $id } - elsif (exists $args->{table} && exists $args->{id}) { - my ($table, $id) = @$args{qw(table id)}; - my $name = $self->_get("$table.id_name.$id"); - $self->_delete("$table.id.$id"); - $self->_delete("$table.name_id.$name") if defined $name; - $self->_delete("$table.id_name.$id"); - } - - # { table => $table, name => $name } - elsif (exists $args->{table} && exists $args->{name}) { - my ($table, $name) = @$args{qw(table name)}; - return unless my $id = $self->_get("$table.name_id.$name"); - $self->_delete("$table.id.$id"); - $self->_delete("$table.name_id.$name"); - $self->_delete("$table.id_name.$id"); - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::clear", - params => [ 'key', 'table' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key } + if (exists $args->{key}) { + $self->_delete($args->{key}); + } + + # { table => $table, id => $id } + elsif (exists $args->{table} && exists $args->{id}) { + my ($table, $id) = @$args{qw(table id)}; + my $name = $self->_get("$table.id_name.$id"); + $self->_delete("$table.id.$id"); + $self->_delete("$table.name_id.$name") if defined $name; + $self->_delete("$table.id_name.$id"); + } + + # { table => $table, name => $name } + elsif (exists $args->{table} && exists $args->{name}) { + my ($table, $name) = @$args{qw(table name)}; + return unless my $id = $self->_get("$table.name_id.$name"); + $self->_delete("$table.id.$id"); + $self->_delete("$table.name_id.$name"); + $self->_delete("$table.id_name.$id"); + } + + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::clear", params => ['key', 'table']}); + } } sub clear_all { - my ($self) = @_; - return unless $self->{memcached}; - $self->_inc_prefix("global"); + my ($self) = @_; + return unless $self->{memcached}; + $self->_inc_prefix("global"); } sub clear_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - if ($args && exists $args->{key}) { - $self->_delete($self->_config_prefix . '.' . $args->{key}); - } - else { - $self->_inc_prefix("config"); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + if ($args && exists $args->{key}) { + $self->_delete($self->_config_prefix . '.' . $args->{key}); + } + else { + $self->_inc_prefix("config"); + } } # in order to clear all our keys, we add a prefix to all our keys. when we # need to "clear" all current keys, we increment the prefix. sub _prefix { - my ($self, $name) = @_; - # we don't want to change prefixes in the middle of a request - my $request_cache = Bugzilla->request_cache; - my $request_cache_key = "memcached_prefix_$name"; - if (!$request_cache->{$request_cache_key}) { - my $memcached = $self->{memcached}; - my $prefix = $memcached->get($name); - if (!$prefix) { - $prefix = time(); - if (!$memcached->add($name, $prefix)) { - # if this failed, either another process set the prefix, or - # memcached is down. assume we lost the race, and get the new - # value. if that fails, memcached is down so use a dummy - # prefix for this request. - $prefix = $memcached->get($name) || 0; - } - } - $request_cache->{$request_cache_key} = $prefix; + my ($self, $name) = @_; + + # we don't want to change prefixes in the middle of a request + my $request_cache = Bugzilla->request_cache; + my $request_cache_key = "memcached_prefix_$name"; + if (!$request_cache->{$request_cache_key}) { + my $memcached = $self->{memcached}; + my $prefix = $memcached->get($name); + if (!$prefix) { + $prefix = time(); + if (!$memcached->add($name, $prefix)) { + + # if this failed, either another process set the prefix, or + # memcached is down. assume we lost the race, and get the new + # value. if that fails, memcached is down so use a dummy + # prefix for this request. + $prefix = $memcached->get($name) || 0; + } } - return $request_cache->{$request_cache_key}; + $request_cache->{$request_cache_key} = $prefix; + } + return $request_cache->{$request_cache_key}; } sub _inc_prefix { - my ($self, $name) = @_; - my $memcached = $self->{memcached}; - if (!$memcached->incr($name, 1)) { - $memcached->add($name, time()); - } - delete Bugzilla->request_cache->{"memcached_prefix_$name"}; + my ($self, $name) = @_; + my $memcached = $self->{memcached}; + if (!$memcached->incr($name, 1)) { + $memcached->add($name, time()); + } + delete Bugzilla->request_cache->{"memcached_prefix_$name"}; } sub _global_prefix { - return $_[0]->_prefix("global"); + return $_[0]->_prefix("global"); } sub _config_prefix { - return $_[0]->_prefix("config"); + return $_[0]->_prefix("config"); } sub _encode_key { - my ($self, $key) = @_; - $key = $self->_global_prefix . '.' . uri_escape_utf8($key); - return length($self->{namespace} . $key) > MAX_KEY_LENGTH - ? undef - : $key; + my ($self, $key) = @_; + $key = $self->_global_prefix . '.' . uri_escape_utf8($key); + return length($self->{namespace} . $key) > MAX_KEY_LENGTH ? undef : $key; } sub _set { - my ($self, $key, $value) = @_; - if (blessed($value)) { - # we don't support blessed objects - ThrowCodeError('param_invalid', { function => "Bugzilla::Memcached::set", - param => "value" }); - } + my ($self, $key, $value) = @_; + if (blessed($value)) { + + # we don't support blessed objects + ThrowCodeError('param_invalid', + {function => "Bugzilla::Memcached::set", param => "value"}); + } - $key = $self->_encode_key($key) - or return; - return $self->{memcached}->set($key, $value); + $key = $self->_encode_key($key) or return; + return $self->{memcached}->set($key, $value); } sub _get { - my ($self, $key) = @_; - - $key = $self->_encode_key($key) - or return; - my $value = $self->{memcached}->get($key); - return unless defined $value; - - # detaint returned values - # hashes and arrays are detainted just one level deep - if (ref($value) eq 'HASH') { + my ($self, $key) = @_; + + $key = $self->_encode_key($key) or return; + my $value = $self->{memcached}->get($key); + return unless defined $value; + + # detaint returned values + # hashes and arrays are detainted just one level deep + if (ref($value) eq 'HASH') { + _detaint_hashref($value); + } + elsif (ref($value) eq 'ARRAY') { + foreach my $value (@$value) { + next unless defined $value; + + # arrays of hashes and arrays are common + if (ref($value) eq 'HASH') { _detaint_hashref($value); - } - elsif (ref($value) eq 'ARRAY') { - foreach my $value (@$value) { - next unless defined $value; - # arrays of hashes and arrays are common - if (ref($value) eq 'HASH') { - _detaint_hashref($value); - } - elsif (ref($value) eq 'ARRAY') { - _detaint_arrayref($value); - } - elsif (!ref($value)) { - trick_taint($value); - } - } - } - elsif (!ref($value)) { + } + elsif (ref($value) eq 'ARRAY') { + _detaint_arrayref($value); + } + elsif (!ref($value)) { trick_taint($value); + } } - return $value; + } + elsif (!ref($value)) { + trick_taint($value); + } + return $value; } sub _detaint_hashref { - my ($hashref) = @_; - foreach my $value (values %$hashref) { - if (defined($value) && !ref($value)) { - trick_taint($value); - } + my ($hashref) = @_; + foreach my $value (values %$hashref) { + if (defined($value) && !ref($value)) { + trick_taint($value); } + } } sub _detaint_arrayref { - my ($arrayref) = @_; - foreach my $value (@$arrayref) { - if (defined($value) && !ref($value)) { - trick_taint($value); - } + my ($arrayref) = @_; + foreach my $value (@$arrayref) { + if (defined($value) && !ref($value)) { + trick_taint($value); } + } } sub _delete { - my ($self, $key) = @_; - $key = $self->_encode_key($key) - or return; - return $self->{memcached}->delete($key); + my ($self, $key) = @_; + $key = $self->_encode_key($key) or return; + return $self->{memcached}->delete($key); } 1; diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm index 7865c842d..1aa73a9a0 100644 --- a/Bugzilla/Migrate.pm +++ b/Bugzilla/Migrate.pm @@ -20,7 +20,7 @@ use Bugzilla::Install::Requirements (); use Bugzilla::Install::Util qw(indicate_progress); use Bugzilla::Product; use Bugzilla::Util qw(get_text trim generate_random_password); -use Bugzilla::User (); +use Bugzilla::User (); use Bugzilla::Status (); use Bugzilla::Version; @@ -37,10 +37,10 @@ use constant REQUIRED_MODULES => []; use constant NON_COMMENT_FIELDS => (); use constant CONFIG_VARS => ( - { - name => 'translate_fields', - default => {}, - desc => <<'END', + { + name => 'translate_fields', + default => {}, + desc => <<'END', # This maps field names in your bug-tracker to Bugzilla field names. If a field # has the same name in your bug-tracker and Bugzilla (case-insensitively), it # doesn't need a mapping here. If a field isn't listed here and doesn't have @@ -64,11 +64,11 @@ use constant CONFIG_VARS => ( # variable by default, then that field will be automatically created by # the migrator and you don't have to worry about it. END - }, - { - name => 'translate_values', - default => {}, - desc => <<'END', + }, + { + name => 'translate_values', + default => {}, + desc => <<'END', # This configuration variable allows you to say that a particular field # value in your current bug-tracker should be translated to a different # value when it's imported into Bugzilla. @@ -108,22 +108,22 @@ END # # Values that don't get translated will be imported as-is. END - }, - { - name => 'starting_bug_id', - default => 0, - desc => <<'END', + }, + { + name => 'starting_bug_id', + default => 0, + desc => <<'END', # What bug ID do you want the first imported bug to get? If you set this to # 0, then the imported bug ids will just start right after the current # bug ids. If you use this configuration variable, you must make sure that # nobody else is using your Bugzilla while you run the migration, or a new # bug filed by a user might take this ID instead. END - }, - { - name => 'timezone', - default => 'local', - desc => <<'END', + }, + { + name => 'timezone', + default => 'local', + desc => <<'END', # If migrate.pl comes across any dates without timezones, while doing the # migration, what timezone should we assume those dates are in? # The best format for this variable is something like "America/Los Angeles". @@ -133,7 +133,7 @@ END # The special value "local" means "use the same timezone as the system I # am running this script on now". END - }, + }, ); use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc); @@ -143,43 +143,46 @@ use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc); ######################### sub do_migration { - my $self = shift; - my $dbh = Bugzilla->dbh; - # On MySQL, setting serial values implicitly commits a transaction, - # so we want to do it up here, outside of any transaction. This also - # has the advantage of loading the config before anything else is done. - if ($self->config('starting_bug_id')) { - $dbh->bz_set_next_serial_value('bugs', 'bug_id', - $self->config('starting_bug_id')); - } - $dbh->bz_start_transaction(); - - $self->before_read(); - # Read Other Database - my $users = $self->users; - my $products = $self->products; - my $bugs = $self->bugs; - $self->after_read(); - - $self->translate_all_bugs($bugs); - - Bugzilla->set_user(Bugzilla::User->super_user); - - # Insert into Bugzilla - $self->before_insert(); - $self->insert_users($users); - $self->insert_products($products); - $self->create_custom_fields(); - $self->create_legal_values($bugs); - $self->insert_bugs($bugs); - $self->after_insert(); - if ($self->dry_run) { - $dbh->bz_rollback_transaction(); - $self->reset_serial_values(); - } - else { - $dbh->bz_commit_transaction(); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + # On MySQL, setting serial values implicitly commits a transaction, + # so we want to do it up here, outside of any transaction. This also + # has the advantage of loading the config before anything else is done. + if ($self->config('starting_bug_id')) { + $dbh->bz_set_next_serial_value('bugs', 'bug_id', + $self->config('starting_bug_id')); + } + $dbh->bz_start_transaction(); + + $self->before_read(); + + # Read Other Database + my $users = $self->users; + my $products = $self->products; + my $bugs = $self->bugs; + $self->after_read(); + + $self->translate_all_bugs($bugs); + + Bugzilla->set_user(Bugzilla::User->super_user); + + # Insert into Bugzilla + $self->before_insert(); + $self->insert_users($users); + $self->insert_products($products); + $self->create_custom_fields(); + $self->create_legal_values($bugs); + $self->insert_bugs($bugs); + $self->after_insert(); + + if ($self->dry_run) { + $dbh->bz_rollback_transaction(); + $self->reset_serial_values(); + } + else { + $dbh->bz_commit_transaction(); + } } ################ @@ -187,24 +190,23 @@ sub do_migration { ################ sub new { - my ($class) = @_; - my $self = { }; - bless $self, $class; - return $self; + my ($class) = @_; + my $self = {}; + bless $self, $class; + return $self; } sub load { - my ($class, $from) = @_; - my $libdir = bz_locations()->{libpath}; - my @migration_modules = glob("$libdir/Bugzilla/Migrate/*"); - my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } - @migration_modules; - if (!$module) { - ThrowUserError('migrate_from_invalid', { from => $from }); - } - require $module; - my $canonical_name = _canonical_name($module); - return "Bugzilla::Migrate::$canonical_name"->new; + my ($class, $from) = @_; + my $libdir = bz_locations()->{libpath}; + my @migration_modules = glob("$libdir/Bugzilla/Migrate/*"); + my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } @migration_modules; + if (!$module) { + ThrowUserError('migrate_from_invalid', {from => $from}); + } + require $module; + my $canonical_name = _canonical_name($module); + return "Bugzilla::Migrate::$canonical_name"->new; } ############# @@ -212,67 +214,67 @@ sub load { ############# sub name { - my $self = shift; - return _canonical_name(ref $self); + my $self = shift; + return _canonical_name(ref $self); } sub dry_run { - my ($self, $value) = @_; - if (scalar(@_) > 1) { - $self->{dry_run} = $value; - } - return $self->{dry_run} || 0; + my ($self, $value) = @_; + if (scalar(@_) > 1) { + $self->{dry_run} = $value; + } + return $self->{dry_run} || 0; } sub verbose { - my ($self, $value) = @_; - if (scalar(@_) > 1) { - $self->{verbose} = $value; - } - return $self->{verbose} || 0; + my ($self, $value) = @_; + if (scalar(@_) > 1) { + $self->{verbose} = $value; + } + return $self->{verbose} || 0; } sub debug { - my ($self, $value, $level) = @_; - $level ||= 1; - if ($self->verbose >= $level) { - $value = Dumper($value) if ref $value; - print STDERR $value, "\n"; - } + my ($self, $value, $level) = @_; + $level ||= 1; + if ($self->verbose >= $level) { + $value = Dumper($value) if ref $value; + print STDERR $value, "\n"; + } } sub bug_fields { - my $self = shift; - $self->{bug_fields} ||= Bugzilla->fields({ by_name => 1 }); - return $self->{bug_fields}; + my $self = shift; + $self->{bug_fields} ||= Bugzilla->fields({by_name => 1}); + return $self->{bug_fields}; } sub users { - my $self = shift; - if (!exists $self->{users}) { - say get_text('migrate_reading_users'); - $self->{users} = $self->_read_users(); - } - return $self->{users}; + my $self = shift; + if (!exists $self->{users}) { + say get_text('migrate_reading_users'); + $self->{users} = $self->_read_users(); + } + return $self->{users}; } sub products { - my $self = shift; - if (!exists $self->{products}) { - say get_text('migrate_reading_products'); - $self->{products} = $self->_read_products(); - } - return $self->{products}; + my $self = shift; + if (!exists $self->{products}) { + say get_text('migrate_reading_products'); + $self->{products} = $self->_read_products(); + } + return $self->{products}; } sub bugs { - my $self = shift; - if (!exists $self->{bugs}) { - say get_text('migrate_reading_bugs'); - $self->{bugs} = $self->_read_bugs(); - } - return $self->{bugs}; + my $self = shift; + if (!exists $self->{bugs}) { + say get_text('migrate_reading_bugs'); + $self->{bugs} = $self->_read_bugs(); + } + return $self->{bugs}; } ########### @@ -280,49 +282,49 @@ sub bugs { ########### sub check_requirements { - my $self = shift; - my $missing = Bugzilla::Install::Requirements::_check_missing( - $self->REQUIRED_MODULES, 1); - my %results = ( - apache => [], - pass => @$missing ? 0 : 1, - missing => $missing, - any_missing => @$missing ? 1 : 0, - hide_all => 1, - # These are just for compatibility with print_module_instructions - one_dbd => 1, - optional => [], - ); - Bugzilla::Install::Requirements::print_module_instructions( - \%results, 1); - exit(1) if @$missing; + my $self = shift; + my $missing + = Bugzilla::Install::Requirements::_check_missing($self->REQUIRED_MODULES, 1); + my %results = ( + apache => [], + pass => @$missing ? 0 : 1, + missing => $missing, + any_missing => @$missing ? 1 : 0, + hide_all => 1, + + # These are just for compatibility with print_module_instructions + one_dbd => 1, + optional => [], + ); + Bugzilla::Install::Requirements::print_module_instructions(\%results, 1); + exit(1) if @$missing; } sub reset_serial_values { - my $self = shift; - return if $self->{serial_values_reset}; - my $dbh = Bugzilla->dbh; - my %reset = ( - 'bugs' => 'bug_id', - 'attachments' => 'attach_id', - 'profiles' => 'userid', - 'longdescs' => 'comment_id', - 'products' => 'id', - 'components' => 'id', - 'versions' => 'id', - 'milestones' => 'id', - ); - my @select_fields = grep { $_->is_select } (values %{ $self->bug_fields }); - foreach my $field (@select_fields) { - next if $field->is_abnormal; - $reset{$field->name} = 'id'; - } - - while (my ($table, $column) = each %reset) { - $dbh->bz_set_next_serial_value($table, $column); - } - - $self->{serial_values_reset} = 1; + my $self = shift; + return if $self->{serial_values_reset}; + my $dbh = Bugzilla->dbh; + my %reset = ( + 'bugs' => 'bug_id', + 'attachments' => 'attach_id', + 'profiles' => 'userid', + 'longdescs' => 'comment_id', + 'products' => 'id', + 'components' => 'id', + 'versions' => 'id', + 'milestones' => 'id', + ); + my @select_fields = grep { $_->is_select } (values %{$self->bug_fields}); + foreach my $field (@select_fields) { + next if $field->is_abnormal; + $reset{$field->name} = 'id'; + } + + while (my ($table, $column) = each %reset) { + $dbh->bz_set_next_serial_value($table, $column); + } + + $self->{serial_values_reset} = 1; } ################### @@ -330,160 +332,167 @@ sub reset_serial_values { ################### sub translate_all_bugs { - my ($self, $bugs) = @_; - say get_text('migrate_translating_bugs'); - # We modify the array in place so that $self->bugs will return the - # modified bugs, in case $self->before_insert wants them. - my $num_bugs = scalar(@$bugs); - for (my $i = 0; $i < $num_bugs; $i++) { - $bugs->[$i] = $self->translate_bug($bugs->[$i]); - } + my ($self, $bugs) = @_; + say get_text('migrate_translating_bugs'); + + # We modify the array in place so that $self->bugs will return the + # modified bugs, in case $self->before_insert wants them. + my $num_bugs = scalar(@$bugs); + for (my $i = 0; $i < $num_bugs; $i++) { + $bugs->[$i] = $self->translate_bug($bugs->[$i]); + } } sub translate_bug { - my ($self, $fields) = @_; - my (%bug, %other_fields); - my $original_status; - foreach my $field (keys %$fields) { - my $value = delete $fields->{$field}; - my $bz_field = $self->translate_field($field); - if ($bz_field) { - $bug{$bz_field} = $self->translate_value($bz_field, $value); - if ($bz_field eq 'bug_status') { - $original_status = $value; - } - } - else { - $other_fields{$field} = $value; - } + my ($self, $fields) = @_; + my (%bug, %other_fields); + my $original_status; + foreach my $field (keys %$fields) { + my $value = delete $fields->{$field}; + my $bz_field = $self->translate_field($field); + if ($bz_field) { + $bug{$bz_field} = $self->translate_value($bz_field, $value); + if ($bz_field eq 'bug_status') { + $original_status = $value; + } } - - if (defined $original_status and !defined $bug{resolution} - and $self->map_value('bug_status_resolution', $original_status)) - { - $bug{resolution} = $self->map_value('bug_status_resolution', - $original_status); + else { + $other_fields{$field} = $value; } - - $bug{comment} = $self->_generate_description(\%bug, \%other_fields); - - return wantarray ? (\%bug, \%other_fields) : \%bug; + } + + if ( defined $original_status + and !defined $bug{resolution} + and $self->map_value('bug_status_resolution', $original_status)) + { + $bug{resolution} = $self->map_value('bug_status_resolution', $original_status); + } + + $bug{comment} = $self->_generate_description(\%bug, \%other_fields); + + return wantarray ? (\%bug, \%other_fields) : \%bug; } sub _generate_description { - my ($self, $bug, $fields) = @_; - - my $description = ""; - foreach my $field (sort keys %$fields) { - next if grep($_ eq $field, $self->NON_COMMENT_FIELDS); - my $value = delete $fields->{$field}; - next if $value eq ''; - $description .= "$field: $value\n"; - } - $description .= "\n" if $description; - - return $description . $bug->{comment}; + my ($self, $bug, $fields) = @_; + + my $description = ""; + foreach my $field (sort keys %$fields) { + next if grep($_ eq $field, $self->NON_COMMENT_FIELDS); + my $value = delete $fields->{$field}; + next if $value eq ''; + $description .= "$field: $value\n"; + } + $description .= "\n" if $description; + + return $description . $bug->{comment}; } sub translate_field { - my ($self, $field) = @_; - my $mapped = $self->config('translate_fields')->{$field}; - return $mapped if defined $mapped; - ($mapped) = grep { lc($_) eq lc($field) } (keys %{ $self->bug_fields }); - return $mapped; + my ($self, $field) = @_; + my $mapped = $self->config('translate_fields')->{$field}; + return $mapped if defined $mapped; + ($mapped) = grep { lc($_) eq lc($field) } (keys %{$self->bug_fields}); + return $mapped; } sub parse_date { - my ($self, $date) = @_; - my @time = strptime($date); - # Handle times with timezones that strptime doesn't know about. - if (!scalar @time) { - $date =~ s/\s+\S+$//; - @time = strptime($date); + my ($self, $date) = @_; + my @time = strptime($date); + + # Handle times with timezones that strptime doesn't know about. + if (!scalar @time) { + $date =~ s/\s+\S+$//; + @time = strptime($date); + } + my $tz; + if ($time[6]) { + $tz = DateTime::TimeZone->offset_as_string($time[6]); + } + else { + $tz = $self->config('timezone'); + $tz =~ s/\s/_/g; + if ($tz eq 'local') { + $tz = Bugzilla->local_timezone; } - my $tz; - if ($time[6]) { - $tz = DateTime::TimeZone->offset_as_string($time[6]); - } - else { - $tz = $self->config('timezone'); - $tz =~ s/\s/_/g; - if ($tz eq 'local') { - $tz = Bugzilla->local_timezone; - } - } - my $dt = DateTime->new({ - year => $time[5] + 1900, - month => $time[4] + 1, - day => $time[3], - hour => $time[2], - minute => $time[1], - second => int($time[0]), - time_zone => $tz, - }); - $dt->set_time_zone(Bugzilla->local_timezone); - return $dt->iso8601; + } + my $dt = DateTime->new({ + year => $time[5] + 1900, + month => $time[4] + 1, + day => $time[3], + hour => $time[2], + minute => $time[1], + second => int($time[0]), + time_zone => $tz, + }); + $dt->set_time_zone(Bugzilla->local_timezone); + return $dt->iso8601; } sub translate_value { - my ($self, $field, $value) = @_; - - if (!defined $value) { - warn("Got undefined value for $field\n"); - $value = ''; - } - - if (ref($value) eq 'ARRAY') { - return [ map($self->translate_value($field, $_), @$value) ]; - } + my ($self, $field, $value) = @_; - - if (defined $self->map_value($field, $value)) { - return $self->map_value($field, $value); - } - - if (grep($_ eq $field, USER_FIELDS)) { - if (defined $self->map_value('user', $value)) { - return $self->map_value('user', $value); - } - } + if (!defined $value) { + warn("Got undefined value for $field\n"); + $value = ''; + } + + if (ref($value) eq 'ARRAY') { + return [map($self->translate_value($field, $_), @$value)]; + } + + + if (defined $self->map_value($field, $value)) { + return $self->map_value($field, $value); + } - my $field_obj = $self->bug_fields->{$field}; - if ($field eq 'creation_ts' - or $field eq 'delta_ts' - or ($field_obj and - ($field_obj->type == FIELD_TYPE_DATETIME - or $field_obj->type == FIELD_TYPE_DATE))) - { - $value = trim($value); - return undef if !$value; - return $self->parse_date($value); + if (grep($_ eq $field, USER_FIELDS)) { + if (defined $self->map_value('user', $value)) { + return $self->map_value('user', $value); } - - return $value; + } + + my $field_obj = $self->bug_fields->{$field}; + if ( + $field eq 'creation_ts' + or $field eq 'delta_ts' + or ( + $field_obj + and + ($field_obj->type == FIELD_TYPE_DATETIME or $field_obj->type == FIELD_TYPE_DATE) + ) + ) + { + $value = trim($value); + return undef if !$value; + return $self->parse_date($value); + } + + return $value; } sub map_value { - my ($self, $field, $value) = @_; - return $self->_value_map->{$field}->{lc($value)}; + my ($self, $field, $value) = @_; + return $self->_value_map->{$field}->{lc($value)}; } sub _value_map { - my $self = shift; - if (!defined $self->{_value_map}) { - # Lowercase all values to make them case-insensitive. - my %map; - my $translation = $self->config('translate_values'); - foreach my $field (keys %$translation) { - my $value_mapping = $translation->{$field}; - foreach my $value (keys %$value_mapping) { - $map{$field}->{lc($value)} = $value_mapping->{$value}; - } - } - $self->{_value_map} = \%map; + my $self = shift; + if (!defined $self->{_value_map}) { + + # Lowercase all values to make them case-insensitive. + my %map; + my $translation = $self->config('translate_values'); + foreach my $field (keys %$translation) { + my $value_mapping = $translation->{$field}; + foreach my $value (keys %$value_mapping) { + $map{$field}->{lc($value)} = $value_mapping->{$value}; + } } - return $self->{_value_map}; + $self->{_value_map} = \%map; + } + return $self->{_value_map}; } ################# @@ -491,387 +500,401 @@ sub _value_map { ################# sub config { - my ($self, $var) = @_; - if (!exists $self->{config}) { - $self->{config} = $self->read_config; - } - return $self->{config}->{$var}; + my ($self, $var) = @_; + if (!exists $self->{config}) { + $self->{config} = $self->read_config; + } + return $self->{config}->{$var}; } sub config_file_name { - my $self = shift; - my $name = $self->name; - my $dir = bz_locations()->{datadir}; - return "$dir/migrate-$name.cfg" + my $self = shift; + my $name = $self->name; + my $dir = bz_locations()->{datadir}; + return "$dir/migrate-$name.cfg"; } sub read_config { - my ($self) = @_; - my $file = $self->config_file_name; - if (!-e $file) { - $self->write_config(); - ThrowUserError('migrate_config_created', { file => $file }); - } - open(my $fh, "<", $file) || die "$file: $!"; - my $safe = new Safe; - $safe->rdo($file); - my @read_symbols = map($_->{name}, $self->CONFIG_VARS); - my %config; - foreach my $var (@read_symbols) { - my $glob = $safe->varglob($var); - $config{$var} = $$glob; - } - return \%config; + my ($self) = @_; + my $file = $self->config_file_name; + if (!-e $file) { + $self->write_config(); + ThrowUserError('migrate_config_created', {file => $file}); + } + open(my $fh, "<", $file) || die "$file: $!"; + my $safe = new Safe; + $safe->rdo($file); + my @read_symbols = map($_->{name}, $self->CONFIG_VARS); + my %config; + foreach my $var (@read_symbols) { + my $glob = $safe->varglob($var); + $config{$var} = $$glob; + } + return \%config; } sub write_config { - my ($self) = @_; - my $file = $self->config_file_name; - open(my $fh, ">", $file) || die "$file: $!"; - # Fixed indentation - local $Data::Dumper::Indent = 1; - local $Data::Dumper::Quotekeys = 0; - local $Data::Dumper::Sortkeys = 1; - foreach my $var ($self->CONFIG_VARS) { - print $fh "\n", $var->{desc}, - Data::Dumper->Dump([$var->{default}], [$var->{name}]); - } - close($fh); + my ($self) = @_; + my $file = $self->config_file_name; + open(my $fh, ">", $file) || die "$file: $!"; + + # Fixed indentation + local $Data::Dumper::Indent = 1; + local $Data::Dumper::Quotekeys = 0; + local $Data::Dumper::Sortkeys = 1; + foreach my $var ($self->CONFIG_VARS) { + print $fh "\n", $var->{desc}, + Data::Dumper->Dump([$var->{default}], [$var->{name}]); + } + close($fh); } #################################### # Default Implementations of Hooks # #################################### -sub after_insert {} -sub before_insert {} -sub after_read {} -sub before_read {} +sub after_insert { } +sub before_insert { } +sub after_read { } +sub before_read { } ############# # Inserters # ############# sub insert_users { - my ($self, $users) = @_; - foreach my $user (@$users) { - next if new Bugzilla::User({ name => $user->{login_name} }); - my $generated_password; - if (!defined $user->{cryptpassword}) { - $generated_password = lc(generate_random_password()); - $user->{cryptpassword} = $generated_password; - } - my $created = Bugzilla::User->create($user); - print get_text('migrate_user_created', - { created => $created, - password => $generated_password }), "\n"; + my ($self, $users) = @_; + foreach my $user (@$users) { + next if new Bugzilla::User({name => $user->{login_name}}); + my $generated_password; + if (!defined $user->{cryptpassword}) { + $generated_password = lc(generate_random_password()); + $user->{cryptpassword} = $generated_password; } + my $created = Bugzilla::User->create($user); + print get_text('migrate_user_created', + {created => $created, password => $generated_password}), + "\n"; + } } # XXX This should also insert Classifications. sub insert_products { - my ($self, $products) = @_; - foreach my $product (@$products) { - my $components = delete $product->{components}; - - my $created_prod = new Bugzilla::Product({ name => $product->{name} }); - if (!$created_prod) { - $created_prod = Bugzilla::Product->create($product); - print get_text('migrate_product_created', - { created => $created_prod }), "\n"; - } - - foreach my $component (@$components) { - next if new Bugzilla::Component({ product => $created_prod, - name => $component->{name} }); - my $created_comp = Bugzilla::Component->create( - { %$component, product => $created_prod }); - print ' ', get_text('migrate_component_created', - { comp => $created_comp, - product => $created_prod }), "\n"; - } + my ($self, $products) = @_; + foreach my $product (@$products) { + my $components = delete $product->{components}; + + my $created_prod = new Bugzilla::Product({name => $product->{name}}); + if (!$created_prod) { + $created_prod = Bugzilla::Product->create($product); + print get_text('migrate_product_created', {created => $created_prod}), "\n"; } + + foreach my $component (@$components) { + next + if new Bugzilla::Component( + {product => $created_prod, name => $component->{name}}); + my $created_comp + = Bugzilla::Component->create({%$component, product => $created_prod}); + print ' ', + get_text('migrate_component_created', + {comp => $created_comp, product => $created_prod}), + "\n"; + } + } } sub create_custom_fields { - my $self = shift; - foreach my $field (keys %{ $self->CUSTOM_FIELDS }) { - next if new Bugzilla::Field({ name => $field }); - my %values = %{ $self->CUSTOM_FIELDS->{$field} }; - # We set these all here for the dry-run case. - my $created = { %values, name => $field, custom => 1 }; - if (!$self->dry_run) { - $created = Bugzilla::Field->create($created); - } - say get_text('migrate_field_created', { field => $created }); + my $self = shift; + foreach my $field (keys %{$self->CUSTOM_FIELDS}) { + next if new Bugzilla::Field({name => $field}); + my %values = %{$self->CUSTOM_FIELDS->{$field}}; + + # We set these all here for the dry-run case. + my $created = {%values, name => $field, custom => 1}; + if (!$self->dry_run) { + $created = Bugzilla::Field->create($created); } - delete $self->{bug_fields}; + say get_text('migrate_field_created', {field => $created}); + } + delete $self->{bug_fields}; } sub create_legal_values { - my ($self, $bugs) = @_; - my @select_fields = grep($_->is_select, values %{ $self->bug_fields }); - - # Get all the values in use on all the bugs we're importing. - my (%values, %product_values); - foreach my $bug (@$bugs) { - foreach my $field (@select_fields) { - my $name = $field->name; - next if !defined $bug->{$name}; - $values{$name}->{$bug->{$name}} = 1; - } - foreach my $field (qw(version target_milestone)) { - # Fix per-product bug values here, because it's easier than - # doing it during _insert_bugs. - if (!defined $bug->{$field} or trim($bug->{$field}) eq '') { - my $accessor = $field; - $accessor =~ s/^target_//; $accessor .= "s"; - my $product = Bugzilla::Product->check($bug->{product}); - $bug->{$field} = $product->$accessor->[0]->name; - next; - } - $product_values{$bug->{product}}->{$field}->{$bug->{$field}} = 1; - } - } - + my ($self, $bugs) = @_; + my @select_fields = grep($_->is_select, values %{$self->bug_fields}); + + # Get all the values in use on all the bugs we're importing. + my (%values, %product_values); + foreach my $bug (@$bugs) { foreach my $field (@select_fields) { - next if $field->is_abnormal; - my $name = $field->name; - foreach my $value (keys %{ $values{$name} }) { - next if Bugzilla::Field::Choice->type($field)->new({ name => $value }); - Bugzilla::Field::Choice->type($field)->create({ value => $value }); - print get_text('migrate_value_created', - { field => $field, value => $value }), "\n"; - } + my $name = $field->name; + next if !defined $bug->{$name}; + $values{$name}->{$bug->{$name}} = 1; } - - foreach my $product (keys %product_values) { - my $prod_obj = Bugzilla::Product->check($product); - foreach my $version (keys %{ $product_values{$product}->{version} }) { - next if new Bugzilla::Version({ product => $prod_obj, - name => $version }); - my $created = Bugzilla::Version->create({ product => $prod_obj, - value => $version }); - my $field = $self->bug_fields->{version}; - print get_text('migrate_value_created', { product => $prod_obj, - field => $field, - value => $created->name }), "\n"; - } - foreach my $milestone (keys %{ $product_values{$product}->{target_milestone} }) { - next if new Bugzilla::Milestone({ product => $prod_obj, - name => $milestone }); - my $created = Bugzilla::Milestone->create( - { product => $prod_obj, value => $milestone }); - my $field = $self->bug_fields->{target_milestone}; - print get_text('migrate_value_created', { product => $prod_obj, - field => $field, - value => $created->name }), "\n"; - - } + foreach my $field (qw(version target_milestone)) { + + # Fix per-product bug values here, because it's easier than + # doing it during _insert_bugs. + if (!defined $bug->{$field} or trim($bug->{$field}) eq '') { + my $accessor = $field; + $accessor =~ s/^target_//; + $accessor .= "s"; + my $product = Bugzilla::Product->check($bug->{product}); + $bug->{$field} = $product->$accessor->[0]->name; + next; + } + $product_values{$bug->{product}}->{$field}->{$bug->{$field}} = 1; + } + } + + foreach my $field (@select_fields) { + next if $field->is_abnormal; + my $name = $field->name; + foreach my $value (keys %{$values{$name}}) { + next if Bugzilla::Field::Choice->type($field)->new({name => $value}); + Bugzilla::Field::Choice->type($field)->create({value => $value}); + print get_text('migrate_value_created', {field => $field, value => $value}), + "\n"; + } + } + + foreach my $product (keys %product_values) { + my $prod_obj = Bugzilla::Product->check($product); + foreach my $version (keys %{$product_values{$product}->{version}}) { + next if new Bugzilla::Version({product => $prod_obj, name => $version}); + my $created + = Bugzilla::Version->create({product => $prod_obj, value => $version}); + my $field = $self->bug_fields->{version}; + print get_text('migrate_value_created', + {product => $prod_obj, field => $field, value => $created->name}), + "\n"; + } + foreach my $milestone (keys %{$product_values{$product}->{target_milestone}}) { + next if new Bugzilla::Milestone({product => $prod_obj, name => $milestone}); + my $created + = Bugzilla::Milestone->create({product => $prod_obj, value => $milestone}); + my $field = $self->bug_fields->{target_milestone}; + print get_text('migrate_value_created', + {product => $prod_obj, field => $field, value => $created->name}), + "\n"; + } - + } + } sub insert_bugs { - my ($self, $bugs) = @_; - my $dbh = Bugzilla->dbh; - say get_text('migrate_creating_bugs'); - - my $init_statuses = Bugzilla::Status->can_change_to(); - my %allowed_statuses = map { lc($_->name) => 1 } @$init_statuses; - # Bypass the question of whether or not we can file UNCONFIRMED - # in any product by simply picking a non-UNCONFIRMED status as our - # default for bugs that don't have a status specified. - my $default_status = first { $_->name ne 'UNCONFIRMED' } @$init_statuses; - # Use the first resolution that's not blank. - my $default_resolution = - first { $_->name ne '' } - @{ $self->bug_fields->{resolution}->legal_values }; - - # Set the values of any required drop-down fields that aren't set. - my @standard_drop_downs = grep { !$_->custom and $_->is_select } - (values %{ $self->bug_fields }); - # Make bug_status get set before resolution. - @standard_drop_downs = sort { $a->name cmp $b->name } @standard_drop_downs; - # Cache all statuses for setting the resolution. - my %statuses = map { lc($_->name) => $_ } Bugzilla::Status->get_all; - - my $total = scalar @$bugs; - my $count = 1; - foreach my $bug (@$bugs) { - my $comments = delete $bug->{comments}; - my $history = delete $bug->{history}; - my $attachments = delete $bug->{attachments}; - - $self->debug($bug, 3); - - foreach my $field (@standard_drop_downs) { - next if $field->is_abnormal; - my $field_name = $field->name; - if (!defined $bug->{$field_name}) { - # If there's a default value for this, then just let create() - # pick it. - next if grep($_->is_default, @{ $field->legal_values }); - # Otherwise, pick the first valid value if this is a required - # field. - if ($field_name eq 'bug_status') { - $bug->{bug_status} = $default_status; - } - elsif ($field_name eq 'resolution') { - my $status = $statuses{lc($bug->{bug_status})}; - if (!$status->is_open) { - $bug->{resolution} = $default_resolution; - } - } - else { - $bug->{$field_name} = $field->legal_values->[0]->name; - } - } - } - - my $product = Bugzilla::Product->check($bug->{product}); - - # If this isn't a legal starting status, or if the bug has a - # resolution, then those will have to be set after creating the bug. - # We make them into objects so that we can normalize their names. - my ($set_status, $set_resolution); - if (defined $bug->{resolution}) { - $set_resolution = Bugzilla::Field::Choice->type('resolution') - ->new({ name => delete $bug->{resolution} }); + my ($self, $bugs) = @_; + my $dbh = Bugzilla->dbh; + say get_text('migrate_creating_bugs'); + + my $init_statuses = Bugzilla::Status->can_change_to(); + my %allowed_statuses = map { lc($_->name) => 1 } @$init_statuses; + + # Bypass the question of whether or not we can file UNCONFIRMED + # in any product by simply picking a non-UNCONFIRMED status as our + # default for bugs that don't have a status specified. + my $default_status = first { $_->name ne 'UNCONFIRMED' } @$init_statuses; + + # Use the first resolution that's not blank. + my $default_resolution = first { $_->name ne '' } + @{$self->bug_fields->{resolution}->legal_values}; + + # Set the values of any required drop-down fields that aren't set. + my @standard_drop_downs + = grep { !$_->custom and $_->is_select } (values %{$self->bug_fields}); + + # Make bug_status get set before resolution. + @standard_drop_downs = sort { $a->name cmp $b->name } @standard_drop_downs; + + # Cache all statuses for setting the resolution. + my %statuses = map { lc($_->name) => $_ } Bugzilla::Status->get_all; + + my $total = scalar @$bugs; + my $count = 1; + foreach my $bug (@$bugs) { + my $comments = delete $bug->{comments}; + my $history = delete $bug->{history}; + my $attachments = delete $bug->{attachments}; + + $self->debug($bug, 3); + + foreach my $field (@standard_drop_downs) { + next if $field->is_abnormal; + my $field_name = $field->name; + if (!defined $bug->{$field_name}) { + + # If there's a default value for this, then just let create() + # pick it. + next if grep($_->is_default, @{$field->legal_values}); + + # Otherwise, pick the first valid value if this is a required + # field. + if ($field_name eq 'bug_status') { + $bug->{bug_status} = $default_status; } - if (!$allowed_statuses{lc($bug->{bug_status})}) { - $set_status = new Bugzilla::Status({ name => $bug->{bug_status} }); - # Set the starting status to some status that Bugzilla will - # accept. We're going to overwrite it immediately afterward. - $bug->{bug_status} = $default_status; + elsif ($field_name eq 'resolution') { + my $status = $statuses{lc($bug->{bug_status})}; + if (!$status->is_open) { + $bug->{resolution} = $default_resolution; + } } - - # If we're in dry-run mode, our custom fields haven't been created - # yet, so we shouldn't try to set them on creation. - if ($self->dry_run) { - foreach my $field (keys %{ $self->CUSTOM_FIELDS }) { - delete $bug->{$field}; - } + else { + $bug->{$field_name} = $field->legal_values->[0]->name; } - - # File the bug as the reporter. - my $super_user = Bugzilla->user; - my $reporter = Bugzilla::User->check($bug->{reporter}); - # Allow the user to file a bug in any product, no matter their current - # permissions. - $reporter->{groups} = $super_user->groups; - Bugzilla->set_user($reporter); - my $created = Bugzilla::Bug->create($bug); - $self->debug('Created bug ' . $created->id); - Bugzilla->set_user($super_user); - - if (defined $bug->{creation_ts}) { - $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ? + } + } + + my $product = Bugzilla::Product->check($bug->{product}); + + # If this isn't a legal starting status, or if the bug has a + # resolution, then those will have to be set after creating the bug. + # We make them into objects so that we can normalize their names. + my ($set_status, $set_resolution); + if (defined $bug->{resolution}) { + $set_resolution = Bugzilla::Field::Choice->type('resolution') + ->new({name => delete $bug->{resolution}}); + } + if (!$allowed_statuses{lc($bug->{bug_status})}) { + $set_status = new Bugzilla::Status({name => $bug->{bug_status}}); + + # Set the starting status to some status that Bugzilla will + # accept. We're going to overwrite it immediately afterward. + $bug->{bug_status} = $default_status; + } + + # If we're in dry-run mode, our custom fields haven't been created + # yet, so we shouldn't try to set them on creation. + if ($self->dry_run) { + foreach my $field (keys %{$self->CUSTOM_FIELDS}) { + delete $bug->{$field}; + } + } + + # File the bug as the reporter. + my $super_user = Bugzilla->user; + my $reporter = Bugzilla::User->check($bug->{reporter}); + + # Allow the user to file a bug in any product, no matter their current + # permissions. + $reporter->{groups} = $super_user->groups; + Bugzilla->set_user($reporter); + my $created = Bugzilla::Bug->create($bug); + $self->debug('Created bug ' . $created->id); + Bugzilla->set_user($super_user); + + if (defined $bug->{creation_ts}) { + $dbh->do( + 'UPDATE bugs SET creation_ts = ?, delta_ts = ? WHERE bug_id = ?', undef, $bug->{creation_ts}, - $bug->{creation_ts}, $created->id); - } - if (defined $bug->{delta_ts}) { - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $bug->{delta_ts}, $created->id); - } - # We don't need to send email for imported bugs. - $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?', - undef, $created->id); - - # We don't use set_ and update() because that would create - # a bugs_activity entry that we don't want. - if ($set_status) { - $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_id = ?', - undef, $set_status->name, $created->id); - } - if ($set_resolution) { - $dbh->do('UPDATE bugs SET resolution = ? WHERE bug_id = ?', - undef, $set_resolution->name, $created->id); - } - - $self->_insert_comments($created, $comments); - $self->_insert_history($created, $history); - $self->_insert_attachments($created, $attachments); - - # bugs_fulltext isn't transactional, so if we're in a dry-run we - # need to delete anything that we put in there. - if ($self->dry_run) { - $dbh->do('DELETE FROM bugs_fulltext WHERE bug_id = ?', - undef, $created->id); - } + $bug->{creation_ts}, $created->id + ); + } + if (defined $bug->{delta_ts}) { + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $bug->{delta_ts}, $created->id); + } - if (!$self->verbose) { - indicate_progress({ current => $count++, every => 5, total => $total }); - } + # We don't need to send email for imported bugs. + $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?', + undef, $created->id); + + # We don't use set_ and update() because that would create + # a bugs_activity entry that we don't want. + if ($set_status) { + $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_id = ?', + undef, $set_status->name, $created->id); + } + if ($set_resolution) { + $dbh->do('UPDATE bugs SET resolution = ? WHERE bug_id = ?', + undef, $set_resolution->name, $created->id); } + + $self->_insert_comments($created, $comments); + $self->_insert_history($created, $history); + $self->_insert_attachments($created, $attachments); + + # bugs_fulltext isn't transactional, so if we're in a dry-run we + # need to delete anything that we put in there. + if ($self->dry_run) { + $dbh->do('DELETE FROM bugs_fulltext WHERE bug_id = ?', undef, $created->id); + } + + if (!$self->verbose) { + indicate_progress({current => $count++, every => 5, total => $total}); + } + } } sub _insert_comments { - my ($self, $bug, $comments) = @_; - return if !$comments; - $self->debug(' Inserting comments:', 2); - foreach my $comment (@$comments) { - $self->debug($comment, 3); - my %copy = %$comment; - # XXX In the future, if we have a Bugzilla::Comment->create, this - # should use it. - my $who = Bugzilla::User->check(delete $copy{who}); - $copy{who} = $who->id; - $copy{bug_id} = $bug->id; - $self->_do_table_insert('longdescs', \%copy); - $self->debug(" Inserted comment from " . $who->login, 2); - } - $bug->_sync_fulltext( update_comments => 1 ); + my ($self, $bug, $comments) = @_; + return if !$comments; + $self->debug(' Inserting comments:', 2); + foreach my $comment (@$comments) { + $self->debug($comment, 3); + my %copy = %$comment; + + # XXX In the future, if we have a Bugzilla::Comment->create, this + # should use it. + my $who = Bugzilla::User->check(delete $copy{who}); + $copy{who} = $who->id; + $copy{bug_id} = $bug->id; + $self->_do_table_insert('longdescs', \%copy); + $self->debug(" Inserted comment from " . $who->login, 2); + } + $bug->_sync_fulltext(update_comments => 1); } sub _insert_history { - my ($self, $bug, $history) = @_; - return if !$history; - $self->debug(' Inserting history:', 2); - foreach my $item (@$history) { - $self->debug($item, 3); - my $who = Bugzilla::User->check($item->{who}); - LogActivityEntry($bug->id, $item->{field}, $item->{removed}, - $item->{added}, $who->id, $item->{bug_when}); - $self->debug(" $item->{field} change from " . $who->login, 2); - } + my ($self, $bug, $history) = @_; + return if !$history; + $self->debug(' Inserting history:', 2); + foreach my $item (@$history) { + $self->debug($item, 3); + my $who = Bugzilla::User->check($item->{who}); + LogActivityEntry($bug->id, $item->{field}, $item->{removed}, $item->{added}, + $who->id, $item->{bug_when}); + $self->debug(" $item->{field} change from " . $who->login, 2); + } } sub _insert_attachments { - my ($self, $bug, $attachments) = @_; - return if !$attachments; - $self->debug(' Inserting attachments:', 2); - foreach my $attachment (@$attachments) { - $self->debug($attachment, 3); - # Make sure that our pointer is at the beginning of the file, - # because usually it will be at the end, having just been fully - # written to. - if (ref $attachment->{data}) { - $attachment->{data}->seek(0, SEEK_SET); - } - - my $submitter = Bugzilla::User->check(delete $attachment->{submitter}); - my $super_user = Bugzilla->user; - # Make sure the submitter can attach this attachment no matter what. - $submitter->{groups} = $super_user->groups; - Bugzilla->set_user($submitter); - my $created = - Bugzilla::Attachment->create({ %$attachment, bug => $bug }); - $self->debug(' Attachment ' . $created->description . ' from ' - . $submitter->login, 2); - Bugzilla->set_user($super_user); + my ($self, $bug, $attachments) = @_; + return if !$attachments; + $self->debug(' Inserting attachments:', 2); + foreach my $attachment (@$attachments) { + $self->debug($attachment, 3); + + # Make sure that our pointer is at the beginning of the file, + # because usually it will be at the end, having just been fully + # written to. + if (ref $attachment->{data}) { + $attachment->{data}->seek(0, SEEK_SET); } + + my $submitter = Bugzilla::User->check(delete $attachment->{submitter}); + my $super_user = Bugzilla->user; + + # Make sure the submitter can attach this attachment no matter what. + $submitter->{groups} = $super_user->groups; + Bugzilla->set_user($submitter); + my $created = Bugzilla::Attachment->create({%$attachment, bug => $bug}); + $self->debug( + ' Attachment ' . $created->description . ' from ' . $submitter->login, 2); + Bugzilla->set_user($super_user); + } } sub _do_table_insert { - my ($self, $table, $hash) = @_; - my @fields = keys %$hash; - my @questions = ('?') x @fields; - my @values = map { $hash->{$_} } @fields; - my $field_sql = join(',', @fields); - my $question_sql = join(',', @questions); - Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)", - undef, @values); + my ($self, $table, $hash) = @_; + my @fields = keys %$hash; + my @questions = ('?') x @fields; + my @values = map { $hash->{$_} } @fields; + my $field_sql = join(',', @fields); + my $question_sql = join(',', @questions); + Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)", + undef, @values); } ###################### @@ -879,11 +902,11 @@ sub _do_table_insert { ###################### sub _canonical_name { - my ($module) = @_; - $module =~ s{::}{/}g; - $module = basename($module); - $module =~ s/\.pm$//g; - return $module; + my ($module) = @_; + $module =~ s{::}{/}g; + $module = basename($module); + $module =~ s/\.pm$//g; + return $module; } 1; diff --git a/Bugzilla/Migrate/Gnats.pm b/Bugzilla/Migrate/Gnats.pm index 5feda4b8d..3d51aa60d 100644 --- a/Bugzilla/Migrate/Gnats.pm +++ b/Bugzilla/Migrate/Gnats.pm @@ -25,88 +25,87 @@ use List::MoreUtils qw(firstidx); use List::Util qw(first); use constant REQUIRED_MODULES => [ - { - package => 'Email-Simple-FromHandle', - module => 'Email::Simple::FromHandle', - # This version added seekable handles. - version => 0.050, - }, + { + package => 'Email-Simple-FromHandle', + module => 'Email::Simple::FromHandle', + + # This version added seekable handles. + version => 0.050, + }, ]; use constant FIELD_MAP => { - 'Number' => 'bug_id', - 'Category' => 'product', - 'Synopsis' => 'short_desc', - 'Responsible' => 'assigned_to', - 'State' => 'bug_status', - 'Class' => 'cf_type', - 'Classification' => '', - 'Originator' => 'reporter', - 'Arrival-Date' => 'creation_ts', - 'Last-Modified' => 'delta_ts', - 'Release' => 'version', - 'Severity' => 'bug_severity', - 'Description' => 'comment', + 'Number' => 'bug_id', + 'Category' => 'product', + 'Synopsis' => 'short_desc', + 'Responsible' => 'assigned_to', + 'State' => 'bug_status', + 'Class' => 'cf_type', + 'Classification' => '', + 'Originator' => 'reporter', + 'Arrival-Date' => 'creation_ts', + 'Last-Modified' => 'delta_ts', + 'Release' => 'version', + 'Severity' => 'bug_severity', + 'Description' => 'comment', }; use constant VALUE_MAP => { - bug_severity => { - 'serious' => 'major', - 'cosmetic' => 'trivial', - 'new-feature' => 'enhancement', - 'non-critical' => 'normal', - }, - bug_status => { - 'open' => 'CONFIRMED', - 'analyzed' => 'IN_PROGRESS', - 'suspended' => 'RESOLVED', - 'feedback' => 'RESOLVED', - 'released' => 'VERIFIED', - }, - bug_status_resolution => { - 'feedback' => 'FIXED', - 'released' => 'FIXED', - 'closed' => 'FIXED', - 'suspended' => 'LATER', - }, - priority => { - 'medium' => 'Normal', - }, + bug_severity => { + 'serious' => 'major', + 'cosmetic' => 'trivial', + 'new-feature' => 'enhancement', + 'non-critical' => 'normal', + }, + bug_status => { + 'open' => 'CONFIRMED', + 'analyzed' => 'IN_PROGRESS', + 'suspended' => 'RESOLVED', + 'feedback' => 'RESOLVED', + 'released' => 'VERIFIED', + }, + bug_status_resolution => { + 'feedback' => 'FIXED', + 'released' => 'FIXED', + 'closed' => 'FIXED', + 'suspended' => 'LATER', + }, + priority => {'medium' => 'Normal',}, }; use constant GNATS_CONFIG_VARS => ( - { - name => 'gnats_path', - default => '/var/lib/gnats', - desc => <<END, + { + name => 'gnats_path', + default => '/var/lib/gnats', + desc => <<END, # The path to the directory that contains the GNATS database. END - }, - { - name => 'default_email_domain', - default => 'example.com', - desc => <<'END', + }, + { + name => 'default_email_domain', + default => 'example.com', + desc => <<'END', # Some GNATS users do not have full email addresses, but Bugzilla requires # every user to have an email address. What domain should be appended to # usernames that don't have emails, to make them into email addresses? # (For example, if you leave this at the default, "unknown" would become # "unknown@example.com".) END - }, - { - name => 'component_name', - default => 'General', - desc => <<'END', + }, + { + name => 'component_name', + default => 'General', + desc => <<'END', # GNATS has only "Category" to classify bugs. However, Bugzilla has a # multi-level system of Products that contain Components. When importing # GNATS categories, they become a Product with one Component. What should # the name of that Component be? END - }, - { - name => 'version_regex', - default => '', - desc => <<'END', + }, + { + name => 'version_regex', + default => '', + desc => <<'END', # In GNATS, the "version" field can contain almost anything. However, in # Bugzilla, it's a drop-down, so you don't want too many choices in there. # If you specify a regular expression here, versions will be tested against @@ -115,43 +114,43 @@ END # as the version value for the bug instead of the full version value specified # in GNATS. END - }, - { - name => 'default_originator', - default => 'gnats-admin', - desc => <<'END', + }, + { + name => 'default_originator', + default => 'gnats-admin', + desc => <<'END', # Sometimes, a PR has no valid Originator, so we fall back to the From # header of the email. If the From header also isn't a valid username # (is just a name with spaces in it--we can't convert that to an email # address) then this username (which can either be a GNATS username or an # email address) will be considered to be the Originator of the PR. END - } + } ); sub CONFIG_VARS { - my $self = shift; - my @vars = (GNATS_CONFIG_VARS, $self->SUPER::CONFIG_VARS); - my $field_map = first { $_->{name} eq 'translate_fields' } @vars; - $field_map->{default} = FIELD_MAP; - my $value_map = first { $_->{name} eq 'translate_values' } @vars; - $value_map->{default} = VALUE_MAP; - return @vars; + my $self = shift; + my @vars = (GNATS_CONFIG_VARS, $self->SUPER::CONFIG_VARS); + my $field_map = first { $_->{name} eq 'translate_fields' } @vars; + $field_map->{default} = FIELD_MAP; + my $value_map = first { $_->{name} eq 'translate_values' } @vars; + $value_map->{default} = VALUE_MAP; + return @vars; } # Directories that aren't projects, or that we shouldn't be parsing use constant SKIP_DIRECTORIES => qw( - gnats-adm - gnats-queue - pending + gnats-adm + gnats-queue + pending ); use constant NON_COMMENT_FIELDS => qw( - Audit-Trail - Closed-Date - Confidential - Unformatted - attachments + Audit-Trail + Closed-Date + Confidential + Unformatted + attachments ); # Certain fields can contain things that look like fields in them, @@ -160,20 +159,16 @@ use constant NON_COMMENT_FIELDS => qw( # and wait for the next field to consider that we actually have # a field to parse. use constant END_FIELD_ORDER => qw( - Description - How-To-Repeat - Fix - Release-Note - Audit-Trail - Unformatted + Description + How-To-Repeat + Fix + Release-Note + Audit-Trail + Unformatted ); -use constant CUSTOM_FIELDS => { - cf_type => { - type => FIELD_TYPE_SINGLE_SELECT, - description => 'Type', - }, -}; +use constant CUSTOM_FIELDS => + {cf_type => {type => FIELD_TYPE_SINGLE_SELECT, description => 'Type',},}; use constant FIELD_REGEX => qr/^>(\S+):\s*(.*)$/; @@ -192,24 +187,24 @@ use constant LONG_VERSION_LENGTH => 32; ######### sub before_insert { - my $self = shift; - - # gnats_id isn't a valid User::create field, and we don't need it - # anymore now. - delete $_->{gnats_id} foreach @{ $self->users }; - - # Grab a version out of a bug for each product, so that there is a - # valid "version" argument for Bugzilla::Product->create. - foreach my $product (@{ $self->products }) { - my $bug = first { $_->{product} eq $product->{name} and $_->{version} } - @{ $self->bugs }; - if (defined $bug) { - $product->{version} = $bug->{version}; - } - else { - $product->{version} = 'unspecified'; - } + my $self = shift; + + # gnats_id isn't a valid User::create field, and we don't need it + # anymore now. + delete $_->{gnats_id} foreach @{$self->users}; + + # Grab a version out of a bug for each product, so that there is a + # valid "version" argument for Bugzilla::Product->create. + foreach my $product (@{$self->products}) { + my $bug = first { $_->{product} eq $product->{name} and $_->{version} } + @{$self->bugs}; + if (defined $bug) { + $product->{version} = $bug->{version}; + } + else { + $product->{version} = 'unspecified'; } + } } ######### @@ -217,53 +212,53 @@ sub before_insert { ######### sub _read_users { - my $self = shift; - my $path = $self->config('gnats_path'); - my $file = "$path/gnats-adm/responsible"; - $self->debug("Reading users from $file"); - my $default_domain = $self->config('default_email_domain'); - open(my $users_fh, '<', $file) || die "$file: $!"; - my @users; - foreach my $line (<$users_fh>) { - $line = trim($line); - next if $line =~ /^#/; - my ($id, $name, $email) = split(':', $line, 3); - $email ||= "$id\@$default_domain"; - # We can't call our own translate_value, because that depends on - # the existence of user_map, which doesn't exist until after - # this method. However, we still want to translate any users found. - $email = $self->SUPER::translate_value('user', $email); - push(@users, { realname => $name, login_name => $email, - gnats_id => $id }); - } - close($users_fh); - return \@users; + my $self = shift; + my $path = $self->config('gnats_path'); + my $file = "$path/gnats-adm/responsible"; + $self->debug("Reading users from $file"); + my $default_domain = $self->config('default_email_domain'); + open(my $users_fh, '<', $file) || die "$file: $!"; + my @users; + foreach my $line (<$users_fh>) { + $line = trim($line); + next if $line =~ /^#/; + my ($id, $name, $email) = split(':', $line, 3); + $email ||= "$id\@$default_domain"; + + # We can't call our own translate_value, because that depends on + # the existence of user_map, which doesn't exist until after + # this method. However, we still want to translate any users found. + $email = $self->SUPER::translate_value('user', $email); + push(@users, {realname => $name, login_name => $email, gnats_id => $id}); + } + close($users_fh); + return \@users; } sub user_map { - my $self = shift; - $self->{user_map} ||= { map { $_->{gnats_id} => $_->{login_name} } - @{ $self->users } }; - return $self->{user_map}; + my $self = shift; + $self->{user_map} + ||= {map { $_->{gnats_id} => $_->{login_name} } @{$self->users}}; + return $self->{user_map}; } sub add_user { - my ($self, $id, $email) = @_; - return if defined $self->user_map->{$id}; - $self->user_map->{$id} = $email; - push(@{ $self->users }, { login_name => $email, gnats_id => $id }); + my ($self, $id, $email) = @_; + return if defined $self->user_map->{$id}; + $self->user_map->{$id} = $email; + push(@{$self->users}, {login_name => $email, gnats_id => $id}); } sub user_to_email { - my ($self, $value) = @_; - if (defined $self->user_map->{$value}) { - $value = $self->user_map->{$value}; - } - elsif ($value !~ /@/) { - my $domain = $self->config('default_email_domain'); - $value = "$value\@$domain"; - } - return $value; + my ($self, $value) = @_; + if (defined $self->user_map->{$value}) { + $value = $self->user_map->{$value}; + } + elsif ($value !~ /@/) { + my $domain = $self->config('default_email_domain'); + $value = "$value\@$domain"; + } + return $value; } ############ @@ -271,31 +266,33 @@ sub user_to_email { ############ sub _read_products { - my $self = shift; - my $path = $self->config('gnats_path'); - my $file = "$path/gnats-adm/categories"; - $self->debug("Reading categories from $file"); - - open(my $categories_fh, '<', $file) || die "$file: $!"; - my @products; - foreach my $line (<$categories_fh>) { - $line = trim($line); - next if $line =~ /^#/; - my ($name, $description, $assigned_to, $cc) = split(':', $line, 4); - my %product = ( name => $name, description => $description ); - - my @initial_cc = split(',', $cc); - @initial_cc = @{ $self->translate_value('user', \@initial_cc) }; - $assigned_to = $self->translate_value('user', $assigned_to); - my %component = ( name => $self->config('component_name'), - description => $description, - initialowner => $assigned_to, - initial_cc => \@initial_cc ); - $product{components} = [\%component]; - push(@products, \%product); - } - close($categories_fh); - return \@products; + my $self = shift; + my $path = $self->config('gnats_path'); + my $file = "$path/gnats-adm/categories"; + $self->debug("Reading categories from $file"); + + open(my $categories_fh, '<', $file) || die "$file: $!"; + my @products; + foreach my $line (<$categories_fh>) { + $line = trim($line); + next if $line =~ /^#/; + my ($name, $description, $assigned_to, $cc) = split(':', $line, 4); + my %product = (name => $name, description => $description); + + my @initial_cc = split(',', $cc); + @initial_cc = @{$self->translate_value('user', \@initial_cc)}; + $assigned_to = $self->translate_value('user', $assigned_to); + my %component = ( + name => $self->config('component_name'), + description => $description, + initialowner => $assigned_to, + initial_cc => \@initial_cc + ); + $product{components} = [\%component]; + push(@products, \%product); + } + close($categories_fh); + return \@products; } ################ @@ -303,128 +300,131 @@ sub _read_products { ################ sub _read_bugs { - my $self = shift; - my $path = $self->config('gnats_path'); - my @directories = glob("$path/*"); - my @bugs; - foreach my $directory (@directories) { - next if !-d $directory; - my $name = basename($directory); - next if grep($_ eq $name, SKIP_DIRECTORIES); - push(@bugs, @{ $self->_parse_project($directory) }); - } - @bugs = sort { $a->{Number} <=> $b->{Number} } @bugs; - return \@bugs; + my $self = shift; + my $path = $self->config('gnats_path'); + my @directories = glob("$path/*"); + my @bugs; + foreach my $directory (@directories) { + next if !-d $directory; + my $name = basename($directory); + next if grep($_ eq $name, SKIP_DIRECTORIES); + push(@bugs, @{$self->_parse_project($directory)}); + } + @bugs = sort { $a->{Number} <=> $b->{Number} } @bugs; + return \@bugs; } sub _parse_project { - my ($self, $directory) = @_; - my @files = glob("$directory/*"); - - $self->debug("Reading Project: $directory"); - # Sometimes other files get into gnats directories. - @files = grep { basename($_) =~ /^\d+$/ } @files; - my @bugs; - my $count = 1; - my $total = scalar @files; - print basename($directory) . ":\n"; - foreach my $file (@files) { - push(@bugs, $self->_parse_bug_file($file)); - if (!$self->verbose) { - indicate_progress({ current => $count++, every => 5, - total => $total }); - } + my ($self, $directory) = @_; + my @files = glob("$directory/*"); + + $self->debug("Reading Project: $directory"); + + # Sometimes other files get into gnats directories. + @files = grep { basename($_) =~ /^\d+$/ } @files; + my @bugs; + my $count = 1; + my $total = scalar @files; + print basename($directory) . ":\n"; + foreach my $file (@files) { + push(@bugs, $self->_parse_bug_file($file)); + if (!$self->verbose) { + indicate_progress({current => $count++, every => 5, total => $total}); } - return \@bugs; + } + return \@bugs; } sub _parse_bug_file { - my ($self, $file) = @_; - $self->debug("Reading $file"); - open(my $fh, "<", $file) || die "$file: $!"; - my $email = Email::Simple::FromHandle->new($fh); - my $fields = $self->_get_gnats_field_data($email); - # We parse attachments here instead of during translate_bug, - # because otherwise we'd be taking up huge amounts of memory storing - # all the raw attachment data in memory. - $fields->{attachments} = $self->_parse_attachments($fields); - close($fh); - return $fields; + my ($self, $file) = @_; + $self->debug("Reading $file"); + open(my $fh, "<", $file) || die "$file: $!"; + my $email = Email::Simple::FromHandle->new($fh); + my $fields = $self->_get_gnats_field_data($email); + + # We parse attachments here instead of during translate_bug, + # because otherwise we'd be taking up huge amounts of memory storing + # all the raw attachment data in memory. + $fields->{attachments} = $self->_parse_attachments($fields); + close($fh); + return $fields; } sub _get_gnats_field_data { - my ($self, $email) = @_; - my ($current_field, @value_lines, %fields); - $email->reset_handle(); - my $handle = $email->handle; - foreach my $line (<$handle>) { - # If this line starts a field name - if ($line =~ FIELD_REGEX) { - my ($new_field, $rest_of_line) = ($1, $2); - - # If this is one of the last few PR fields, then make sure - # that we're getting our fields in the right order. - my $new_field_valid = 1; - my $search_for = $current_field || ''; - my $current_field_pos = firstidx { $_ eq $search_for } - END_FIELD_ORDER; - if ($current_field_pos > -1) { - my $new_field_pos = firstidx { $_ eq $new_field } - END_FIELD_ORDER; - # We accept any field, as long as it's later than this one. - $new_field_valid = $new_field_pos > $current_field_pos ? 1 : 0; - } - - if ($new_field_valid) { - if ($current_field) { - $fields{$current_field} = _handle_lines(\@value_lines); - @value_lines = (); - } - $current_field = $new_field; - $line = $rest_of_line; - } + my ($self, $email) = @_; + my ($current_field, @value_lines, %fields); + $email->reset_handle(); + my $handle = $email->handle; + foreach my $line (<$handle>) { + + # If this line starts a field name + if ($line =~ FIELD_REGEX) { + my ($new_field, $rest_of_line) = ($1, $2); + + # If this is one of the last few PR fields, then make sure + # that we're getting our fields in the right order. + my $new_field_valid = 1; + my $search_for = $current_field || ''; + my $current_field_pos = firstidx { $_ eq $search_for } + END_FIELD_ORDER; + if ($current_field_pos > -1) { + my $new_field_pos = firstidx { $_ eq $new_field } + END_FIELD_ORDER; + + # We accept any field, as long as it's later than this one. + $new_field_valid = $new_field_pos > $current_field_pos ? 1 : 0; + } + + if ($new_field_valid) { + if ($current_field) { + $fields{$current_field} = _handle_lines(\@value_lines); + @value_lines = (); } - push(@value_lines, $line) if defined $line; + $current_field = $new_field; + $line = $rest_of_line; + } } - $fields{$current_field} = _handle_lines(\@value_lines); - $fields{cc} = [$email->header('Cc')] if $email->header('Cc'); - - # If the Originator is invalid and we don't have a translation for it, - # use the From header instead. - my $originator = $self->translate_value('reporter', $fields{Originator}, - { check_only => 1 }); - if ($originator !~ Bugzilla->params->{emailregexp}) { - # We use the raw header sometimes, because it looks like "From: user" - # which Email::Address won't parse but we can still use. - my $address = $email->header('From'); - my ($parsed) = Email::Address->parse($address); - if ($parsed) { - $address = $parsed->address; - } - if ($address) { - $self->debug( - "PR $fields{Number} had an Originator that was not a valid" - . " user ($fields{Originator}). Using From ($address)" - . " instead.\n"); - my $address_email = $self->translate_value('reporter', $address, - { check_only => 1 }); - if ($address_email !~ Bugzilla->params->{emailregexp}) { - $self->debug(" From was also invalid, using default_originator.\n"); - $address = $self->config('default_originator'); - } - $fields{Originator} = $address; - } + push(@value_lines, $line) if defined $line; + } + $fields{$current_field} = _handle_lines(\@value_lines); + $fields{cc} = [$email->header('Cc')] if $email->header('Cc'); + + # If the Originator is invalid and we don't have a translation for it, + # use the From header instead. + my $originator + = $self->translate_value('reporter', $fields{Originator}, {check_only => 1}); + if ($originator !~ Bugzilla->params->{emailregexp}) { + + # We use the raw header sometimes, because it looks like "From: user" + # which Email::Address won't parse but we can still use. + my $address = $email->header('From'); + my ($parsed) = Email::Address->parse($address); + if ($parsed) { + $address = $parsed->address; + } + if ($address) { + $self->debug("PR $fields{Number} had an Originator that was not a valid" + . " user ($fields{Originator}). Using From ($address)" + . " instead.\n"); + my $address_email + = $self->translate_value('reporter', $address, {check_only => 1}); + if ($address_email !~ Bugzilla->params->{emailregexp}) { + $self->debug(" From was also invalid, using default_originator.\n"); + $address = $self->config('default_originator'); + } + $fields{Originator} = $address; } + } - $self->debug(\%fields, 3); - return \%fields; + $self->debug(\%fields, 3); + return \%fields; } sub _handle_lines { - my ($lines) = @_; - my $value = join('', @$lines); - $value =~ s/\s+$//; - return $value; + my ($lines) = @_; + my $value = join('', @$lines); + $value =~ s/\s+$//; + return $value; } #################### @@ -432,169 +432,188 @@ sub _handle_lines { #################### sub translate_bug { - my ($self, $fields) = @_; + my ($self, $fields) = @_; - my ($bug, $other_fields) = $self->SUPER::translate_bug($fields); + my ($bug, $other_fields) = $self->SUPER::translate_bug($fields); - $bug->{attachments} = delete $other_fields->{attachments}; + $bug->{attachments} = delete $other_fields->{attachments}; - if (defined $other_fields->{_add_to_comment}) { - $bug->{comment} .= delete $other_fields->{_add_to_comment}; - } + if (defined $other_fields->{_add_to_comment}) { + $bug->{comment} .= delete $other_fields->{_add_to_comment}; + } - my ($changes, $extra_comment) = - $self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'}); - - my @comments; - foreach my $change (@$changes) { - if (exists $change->{comment}) { - push(@comments, { - thetext => $change->{comment}, - who => $change->{who}, - bug_when => $change->{bug_when} }); - delete $change->{comment}; - } - } - $bug->{history} = $changes; + my ($changes, $extra_comment) + = $self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'}); - if (trim($extra_comment)) { - push(@comments, { thetext => $extra_comment, who => $bug->{reporter}, - bug_when => $bug->{delta_ts} || $bug->{creation_ts} }); - } - $bug->{comments} = \@comments; - - $bug->{component} = $self->config('component_name'); - if (!$bug->{short_desc}) { - $bug->{short_desc} = NO_SUBJECT; - } - - foreach my $attachment (@{ $bug->{attachments} || [] }) { - $attachment->{submitter} = $bug->{reporter}; - $attachment->{creation_ts} = $bug->{creation_ts}; + my @comments; + foreach my $change (@$changes) { + if (exists $change->{comment}) { + push( + @comments, + { + thetext => $change->{comment}, + who => $change->{who}, + bug_when => $change->{bug_when} + } + ); + delete $change->{comment}; } - - $self->debug($bug, 3); - return $bug; + } + $bug->{history} = $changes; + + if (trim($extra_comment)) { + push( + @comments, + { + thetext => $extra_comment, + who => $bug->{reporter}, + bug_when => $bug->{delta_ts} || $bug->{creation_ts} + } + ); + } + $bug->{comments} = \@comments; + + $bug->{component} = $self->config('component_name'); + if (!$bug->{short_desc}) { + $bug->{short_desc} = NO_SUBJECT; + } + + foreach my $attachment (@{$bug->{attachments} || []}) { + $attachment->{submitter} = $bug->{reporter}; + $attachment->{creation_ts} = $bug->{creation_ts}; + } + + $self->debug($bug, 3); + return $bug; } sub _parse_audit_trail { - my ($self, $bug, $audit_trail) = @_; - return [] if !trim($audit_trail); - $self->debug(" Parsing audit trail...", 2); - - if ($audit_trail !~ /^\S+-Changed-\S+:/ms) { - # This is just a comment from the bug's creator. - $self->debug(" Audit trail is just a comment.", 2); - return ([], $audit_trail); - } - - my (@changes, %current_data, $current_column, $on_why); - my $extra_comment = ''; - my $current_field; - my @all_lines = split("\n", $audit_trail); - foreach my $line (@all_lines) { - # GNATS history looks like: - # Status-Changed-From-To: open->closed - # Status-Changed-By: jack - # Status-Changed-When: Mon May 12 14:46:59 2003 - # Status-Changed-Why: - # This is some comment here about the change. - if ($line =~ /^(\S+)-Changed-(\S+):(.*)/) { - my ($field, $column, $value) = ($1, $2, $3); - my $bz_field = $self->translate_field($field); - # If it's not a field we're importing, we don't care about - # its history. - next if !$bz_field; - # GNATS doesn't track values for description changes, - # unfortunately, and that's the only information we'd be able to - # use in Bugzilla for the audit trail on that field. - next if $bz_field eq 'comment'; - $current_field = $bz_field if !$current_field; - if ($bz_field ne $current_field) { - $self->_store_audit_change( - \@changes, $current_field, \%current_data); - %current_data = (); - $current_field = $bz_field; - } - $value = trim($value); - $self->debug(" $bz_field $column: $value", 3); - if ($column eq 'From-To') { - my ($from, $to) = split('->', $value, 2); - # Sometimes there's just a - instead of a -> between the values. - if (!defined($to)) { - ($from, $to) = split('-', $value, 2); - } - $current_data{added} = $to; - $current_data{removed} = $from; - } - elsif ($column eq 'By') { - my $email = $self->translate_value('user', $value); - # Sometimes we hit users in the audit trail that we haven't - # seen anywhere else. - $current_data{who} = $email; - } - elsif ($column eq 'When') { - $current_data{bug_when} = $self->parse_date($value); - } - if ($column eq 'Why') { - $value = '' if !defined $value; - $current_data{comment} = $value; - $on_why = 1; - } - else { - $on_why = 0; - } - } - elsif ($on_why) { - # "Why" lines are indented four characters. - $line =~ s/^\s{4}//; - $current_data{comment} .= "$line\n"; - } - else { - $self->debug( - "Extra Audit-Trail line on $bug->{product} $bug->{bug_id}:" - . " $line\n", 2); - $extra_comment .= "$line\n"; + my ($self, $bug, $audit_trail) = @_; + return [] if !trim($audit_trail); + $self->debug(" Parsing audit trail...", 2); + + if ($audit_trail !~ /^\S+-Changed-\S+:/ms) { + + # This is just a comment from the bug's creator. + $self->debug(" Audit trail is just a comment.", 2); + return ([], $audit_trail); + } + + my (@changes, %current_data, $current_column, $on_why); + my $extra_comment = ''; + my $current_field; + my @all_lines = split("\n", $audit_trail); + foreach my $line (@all_lines) { + + # GNATS history looks like: + # Status-Changed-From-To: open->closed + # Status-Changed-By: jack + # Status-Changed-When: Mon May 12 14:46:59 2003 + # Status-Changed-Why: + # This is some comment here about the change. + if ($line =~ /^(\S+)-Changed-(\S+):(.*)/) { + my ($field, $column, $value) = ($1, $2, $3); + my $bz_field = $self->translate_field($field); + + # If it's not a field we're importing, we don't care about + # its history. + next if !$bz_field; + + # GNATS doesn't track values for description changes, + # unfortunately, and that's the only information we'd be able to + # use in Bugzilla for the audit trail on that field. + next if $bz_field eq 'comment'; + $current_field = $bz_field if !$current_field; + if ($bz_field ne $current_field) { + $self->_store_audit_change(\@changes, $current_field, \%current_data); + %current_data = (); + $current_field = $bz_field; + } + $value = trim($value); + $self->debug(" $bz_field $column: $value", 3); + if ($column eq 'From-To') { + my ($from, $to) = split('->', $value, 2); + + # Sometimes there's just a - instead of a -> between the values. + if (!defined($to)) { + ($from, $to) = split('-', $value, 2); } + $current_data{added} = $to; + $current_data{removed} = $from; + } + elsif ($column eq 'By') { + my $email = $self->translate_value('user', $value); + + # Sometimes we hit users in the audit trail that we haven't + # seen anywhere else. + $current_data{who} = $email; + } + elsif ($column eq 'When') { + $current_data{bug_when} = $self->parse_date($value); + } + if ($column eq 'Why') { + $value = '' if !defined $value; + $current_data{comment} = $value; + $on_why = 1; + } + else { + $on_why = 0; + } + } + elsif ($on_why) { + + # "Why" lines are indented four characters. + $line =~ s/^\s{4}//; + $current_data{comment} .= "$line\n"; + } + else { + $self->debug( + "Extra Audit-Trail line on $bug->{product} $bug->{bug_id}:" . " $line\n", 2); + $extra_comment .= "$line\n"; } - $self->_store_audit_change(\@changes, $current_field, \%current_data); - return (\@changes, $extra_comment); + } + $self->_store_audit_change(\@changes, $current_field, \%current_data); + return (\@changes, $extra_comment); } sub _store_audit_change { - my ($self, $changes, $old_field, $current_data) = @_; - - $current_data->{field} = $old_field; - $current_data->{removed} = - $self->translate_value($old_field, $current_data->{removed}); - $current_data->{added} = - $self->translate_value($old_field, $current_data->{added}); - push(@$changes, { %$current_data }); + my ($self, $changes, $old_field, $current_data) = @_; + + $current_data->{field} = $old_field; + $current_data->{removed} + = $self->translate_value($old_field, $current_data->{removed}); + $current_data->{added} + = $self->translate_value($old_field, $current_data->{added}); + push(@$changes, {%$current_data}); } sub _parse_attachments { - my ($self, $fields) = @_; - my $unformatted = delete $fields->{'Unformatted'}; - my $gnats_boundary = GNATS_BOUNDARY; - # A sanity checker to make sure that we're parsing attachments right. - my $num_attachments = 0; - $num_attachments++ while ($unformatted =~ /\Q$gnats_boundary\E/g); - # Sometimes there's a GNATS_BOUNDARY that is on the same line as other data. - $unformatted =~ s/(\S\s*)\Q$gnats_boundary\E$/$1\n$gnats_boundary/mg; - # Often the "Unformatted" section starts with stuff before - # ----gnatsweb-attachment---- that isn't necessary. - $unformatted =~ s/^\s*From:.+?Reply-to:[^\n]+//s; - $unformatted = trim($unformatted); - return [] if !$unformatted; - $self->debug('Reading attachments...', 2); - my $boundary = generate_random_password(48); - $unformatted =~ s/\Q$gnats_boundary\E/--$boundary/g; - # Sometimes the whole Unformatted section is indented by exactly - # one space, and needs to be fixed. - if ($unformatted =~ /--\Q$boundary\E\n /) { - $unformatted =~ s/^ //mg; - } - $unformatted = <<END; + my ($self, $fields) = @_; + my $unformatted = delete $fields->{'Unformatted'}; + my $gnats_boundary = GNATS_BOUNDARY; + + # A sanity checker to make sure that we're parsing attachments right. + my $num_attachments = 0; + $num_attachments++ while ($unformatted =~ /\Q$gnats_boundary\E/g); + + # Sometimes there's a GNATS_BOUNDARY that is on the same line as other data. + $unformatted =~ s/(\S\s*)\Q$gnats_boundary\E$/$1\n$gnats_boundary/mg; + + # Often the "Unformatted" section starts with stuff before + # ----gnatsweb-attachment---- that isn't necessary. + $unformatted =~ s/^\s*From:.+?Reply-to:[^\n]+//s; + $unformatted = trim($unformatted); + return [] if !$unformatted; + $self->debug('Reading attachments...', 2); + my $boundary = generate_random_password(48); + $unformatted =~ s/\Q$gnats_boundary\E/--$boundary/g; + + # Sometimes the whole Unformatted section is indented by exactly + # one space, and needs to be fixed. + if ($unformatted =~ /--\Q$boundary\E\n /) { + $unformatted =~ s/^ //mg; + } + $unformatted = <<END; From: nobody MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="$boundary" @@ -607,96 +626,103 @@ Content-Transfer-Encoding: 7bit $unformatted --$boundary-- END - my $email = new Email::MIME(\$unformatted); - my @parts = $email->parts; - # Remove the fake body. - my $part1 = shift @parts; - if ($part1->body) { - $self->debug(" Additional Unformatted data found on " - . $fields->{Category} . " bug " . $fields->{Number}); - $self->debug($part1->body, 3); - $fields->{_add_comment} .= "\n\nUnformatted:\n" . $part1->body; - } + my $email = new Email::MIME(\$unformatted); + my @parts = $email->parts; + + # Remove the fake body. + my $part1 = shift @parts; + if ($part1->body) { + $self->debug(" Additional Unformatted data found on " + . $fields->{Category} . " bug " + . $fields->{Number}); + $self->debug($part1->body, 3); + $fields->{_add_comment} .= "\n\nUnformatted:\n" . $part1->body; + } + + my @attachments; + foreach my $part (@parts) { + $self->debug(' Parsing attachment: ' . $part->filename); + my $temp_fh = IO::File->new_tmpfile or die("Can't create tempfile: $!"); + $temp_fh->binmode; + print $temp_fh $part->body; + my $content_type = $part->content_type; + $content_type =~ s/; name=.+$//; + my $attachment = { + filename => $part->filename, + description => $part->filename, + mimetype => $content_type, + data => $temp_fh + }; + $self->debug($attachment, 3); + push(@attachments, $attachment); + } + + if (scalar(@attachments) ne $num_attachments) { + warn "WARNING: Expected $num_attachments attachments but got " + . scalar(@attachments) . "\n"; + $self->debug($unformatted, 3); + } + return \@attachments; +} - my @attachments; - foreach my $part (@parts) { - $self->debug(' Parsing attachment: ' . $part->filename); - my $temp_fh = IO::File->new_tmpfile or die ("Can't create tempfile: $!"); - $temp_fh->binmode; - print $temp_fh $part->body; - my $content_type = $part->content_type; - $content_type =~ s/; name=.+$//; - my $attachment = { filename => $part->filename, - description => $part->filename, - mimetype => $content_type, - data => $temp_fh }; - $self->debug($attachment, 3); - push(@attachments, $attachment); +sub translate_value { + my $self = shift; + my ($field, $value, $options) = @_; + my $original_value = $value; + $options ||= {}; + + if (!ref($value) and grep($_ eq $field, $self->USER_FIELDS)) { + if ($value =~ /(\S+\@\S+)/) { + $value = $1; + $value =~ s/^<//; + $value =~ s/>$//; } - - if (scalar(@attachments) ne $num_attachments) { - warn "WARNING: Expected $num_attachments attachments but got " - . scalar(@attachments) . "\n" ; - $self->debug($unformatted, 3); + else { + # Sometimes names have extra stuff on the end like "(Somebody's Name)" + $value =~ s/\s+\(.+\)$//; + + # Sometimes user fields look like "(user)" instead of just "user". + $value =~ s/^\((.+)\)$/$1/; + $value = trim($value); } - return \@attachments; -} + } -sub translate_value { - my $self = shift; - my ($field, $value, $options) = @_; - my $original_value = $value; - $options ||= {}; - - if (!ref($value) and grep($_ eq $field, $self->USER_FIELDS)) { - if ($value =~ /(\S+\@\S+)/) { - $value = $1; - $value =~ s/^<//; - $value =~ s/>$//; - } - else { - # Sometimes names have extra stuff on the end like "(Somebody's Name)" - $value =~ s/\s+\(.+\)$//; - # Sometimes user fields look like "(user)" instead of just "user". - $value =~ s/^\((.+)\)$/$1/; - $value = trim($value); - } + if ($field eq 'version' and $value ne '') { + my $version_re = $self->config('version_regex'); + if ($version_re and $value =~ $version_re) { + $value = $1; } - if ($field eq 'version' and $value ne '') { - my $version_re = $self->config('version_regex'); - if ($version_re and $value =~ $version_re) { - $value = $1; - } - # In the GNATS that I tested this with, there were many extremely long - # values for "version" that caused some import problems (they were - # longer than the max allowed version value). So if the version value - # is longer than 32 characters, pull out the first thing that looks - # like a version number. - elsif (length($value) > LONG_VERSION_LENGTH) { - $value =~ s/^.+?\b(\d[\w\.]+)\b.+$/$1/; - } + # In the GNATS that I tested this with, there were many extremely long + # values for "version" that caused some import problems (they were + # longer than the max allowed version value). So if the version value + # is longer than 32 characters, pull out the first thing that looks + # like a version number. + elsif (length($value) > LONG_VERSION_LENGTH) { + $value =~ s/^.+?\b(\d[\w\.]+)\b.+$/$1/; } - - my @args = @_; + } + + my @args = @_; + $args[1] = $value; + + $value = $self->SUPER::translate_value(@args); + return $value if ref $value; + + if (grep($_ eq $field, $self->USER_FIELDS)) { + my $from_value = $value; + $value = $self->user_to_email($value); $args[1] = $value; - + + # If we got something new from user_to_email, do any necessary + # translation of it. $value = $self->SUPER::translate_value(@args); - return $value if ref $value; - - if (grep($_ eq $field, $self->USER_FIELDS)) { - my $from_value = $value; - $value = $self->user_to_email($value); - $args[1] = $value; - # If we got something new from user_to_email, do any necessary - # translation of it. - $value = $self->SUPER::translate_value(@args); - if (!$options->{check_only}) { - $self->add_user($from_value, $value); - } + if (!$options->{check_only}) { + $self->add_user($from_value, $value); } - - return $value; + } + + return $value; } 1; diff --git a/Bugzilla/Milestone.pm b/Bugzilla/Milestone.pm index cf7e3e35f..be49df536 100644 --- a/Bugzilla/Milestone.pm +++ b/Bugzilla/Milestone.pm @@ -25,140 +25,140 @@ use Scalar::Util qw(blessed); use constant DEFAULT_SORTKEY => 0; -use constant DB_TABLE => 'milestones'; +use constant DB_TABLE => 'milestones'; use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'sortkey, value'; use constant DB_COLUMNS => qw( - id - value - product_id - sortkey - isactive + id + value + product_id + sortkey + isactive ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; use constant UPDATE_COLUMNS => qw( - value - sortkey - isactive + value + sortkey + isactive ); use constant VALIDATORS => { - product => \&_check_product, - sortkey => \&_check_sortkey, - value => \&_check_value, - isactive => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + sortkey => \&_check_sortkey, + value => \&_check_value, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - value => ['product'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['product'],}; ################################ sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param and !defined $param->{id}) { - $product = $param->{product}; - my $name = $param->{name}; - if (!defined $product) { - ThrowCodeError('bad_arg', - {argument => 'product', - function => "${class}::new"}); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - - my $condition = 'product_id = ? AND value = ?'; - my @values = ($product->id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $product; + if (ref $param and !defined $param->{id}) { + $product = $param->{product}; + my $name = $param->{name}; + if (!defined $product) { + ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"}); } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); + } + + my $condition = 'product_id = ? AND value = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } - unshift @_, $param; - return $class->SUPER::new(@_); + unshift @_, $param; + return $class->SUPER::new(@_); } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - return $params; + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + return $params; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my $changes = $self->SUPER::update(@_); - - if (exists $changes->{value}) { - # The milestone value is stored in the bugs table instead of its ID. - $dbh->do('UPDATE bugs SET target_milestone = ? - WHERE target_milestone = ? AND product_id = ?', - undef, ($self->name, $changes->{value}->[0], $self->product_id)); - - # The default milestone also stores the value instead of the ID. - $dbh->do('UPDATE products SET defaultmilestone = ? - WHERE id = ? AND defaultmilestone = ?', - undef, ($self->name, $self->product_id, $changes->{value}->[0])); - Bugzilla->memcached->clear({ table => 'products', id => $self->product_id }); - } - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); + + if (exists $changes->{value}) { + + # The milestone value is stored in the bugs table instead of its ID. + $dbh->do( + 'UPDATE bugs SET target_milestone = ? + WHERE target_milestone = ? AND product_id = ?', undef, + ($self->name, $changes->{value}->[0], $self->product_id) + ); + + # The default milestone also stores the value instead of the ID. + $dbh->do( + 'UPDATE products SET defaultmilestone = ? + WHERE id = ? AND defaultmilestone = ?', undef, + ($self->name, $self->product_id, $changes->{value}->[0]) + ); + Bugzilla->memcached->clear({table => 'products', id => $self->product_id}); + } + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # The default milestone cannot be deleted. - if ($self->name eq $self->product->default_milestone) { - ThrowUserError('milestone_is_default', { milestone => $self }); - } + # The default milestone cannot be deleted. + if ($self->name eq $self->product->default_milestone) { + ThrowUserError('milestone_is_default', {milestone => $self}); + } + + if ($self->bug_count) { - if ($self->bug_count) { - # We don't want to delete bugs when deleting a milestone. - # Bugs concerned are reassigned to the default milestone. - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bug_id FROM bugs + # We don't want to delete bugs when deleting a milestone. + # Bugs concerned are reassigned to the default milestone. + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bug_id FROM bugs WHERE product_id = ? AND target_milestone = ?', - undef, ($self->product->id, $self->name)); - - my $timestamp = $dbh->selectrow_array('SELECT NOW()'); - - $dbh->do('UPDATE bugs SET target_milestone = ?, delta_ts = ? - WHERE ' . $dbh->sql_in('bug_id', $bug_ids), - undef, ($self->product->default_milestone, $timestamp)); - - require Bugzilla::Bug; - import Bugzilla::Bug qw(LogActivityEntry); - foreach my $bug_id (@$bug_ids) { - LogActivityEntry($bug_id, 'target_milestone', - $self->name, - $self->product->default_milestone, - Bugzilla->user->id, $timestamp); - } + undef, ($self->product->id, $self->name) + ); + + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + $dbh->do( + 'UPDATE bugs SET target_milestone = ?, delta_ts = ? + WHERE ' . $dbh->sql_in('bug_id', $bug_ids), undef, + ($self->product->default_milestone, $timestamp) + ); + + require Bugzilla::Bug; + import Bugzilla::Bug qw(LogActivityEntry); + foreach my $bug_id (@$bug_ids) { + LogActivityEntry($bug_id, 'target_milestone', $self->name, + $self->product->default_milestone, + Bugzilla->user->id, $timestamp); } - $self->SUPER::remove_from_db(); + } + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } ################################ @@ -166,78 +166,84 @@ sub remove_from_db { ################################ sub _check_value { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('milestone_blank_name'); - if (length($name) > MAX_MILESTONE_SIZE) { - ThrowUserError('milestone_name_too_long', {name => $name}); - } - - my $milestone = new Bugzilla::Milestone({product => $product, name => $name}); - if ($milestone && (!ref $invocant || $milestone->id != $invocant->id)) { - ThrowUserError('milestone_already_exists', { name => $milestone->name, - product => $product->name }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('milestone_blank_name'); + if (length($name) > MAX_MILESTONE_SIZE) { + ThrowUserError('milestone_name_too_long', {name => $name}); + } + + my $milestone = new Bugzilla::Milestone({product => $product, name => $name}); + if ($milestone && (!ref $invocant || $milestone->id != $invocant->id)) { + ThrowUserError('milestone_already_exists', + {name => $milestone->name, product => $product->name}); + } + return $name; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - - # Keep a copy in case detaint_signed() clears the sortkey - my $stored_sortkey = $sortkey; - - if (!detaint_signed($sortkey) || $sortkey < MIN_SMALLINT || $sortkey > MAX_SMALLINT) { - ThrowUserError('milestone_sortkey_invalid', {sortkey => $stored_sortkey}); - } - return $sortkey; + my ($invocant, $sortkey) = @_; + + # Keep a copy in case detaint_signed() clears the sortkey + my $stored_sortkey = $sortkey; + + if ( !detaint_signed($sortkey) + || $sortkey < MIN_SMALLINT + || $sortkey > MAX_SMALLINT) + { + ThrowUserError('milestone_sortkey_invalid', {sortkey => $stored_sortkey}); + } + return $sortkey; } sub _check_product { - my ($invocant, $product) = @_; - $product || ThrowCodeError('param_required', - { function => "$invocant->create", param => "product" }); - return Bugzilla->user->check_can_admin_product($product->name); + my ($invocant, $product) = @_; + $product + || ThrowCodeError('param_required', + {function => "$invocant->create", param => "product"}); + return Bugzilla->user->check_can_admin_product($product->name); } ################################ # Methods ################################ -sub set_name { $_[0]->set('value', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_name { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } sub set_is_active { $_[0]->set('isactive', $_[1]); } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(q{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + q{ SELECT COUNT(*) FROM bugs - WHERE product_id = ? AND target_milestone = ?}, - undef, $self->product_id, $self->name) || 0; - } - return $self->{'bug_count'}; + WHERE product_id = ? AND target_milestone = ?}, undef, $self->product_id, + $self->name + ) || 0; + } + return $self->{'bug_count'}; } ################################ ##### Accessors ###### ################################ -sub name { return $_[0]->{'value'}; } +sub name { return $_[0]->{'value'}; } sub product_id { return $_[0]->{'product_id'}; } -sub sortkey { return $_[0]->{'sortkey'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'isactive'}; } sub product { - my $self = shift; + my $self = shift; - require Bugzilla::Product; - $self->{'product'} ||= new Bugzilla::Product($self->product_id); - return $self->{'product'}; + require Bugzilla::Product; + $self->{'product'} ||= new Bugzilla::Product($self->product_id); + return $self->{'product'}; } 1; diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm index d43c8ca34..6fc3fecc0 100644 --- a/Bugzilla/Object.pm +++ b/Bugzilla/Object.pm @@ -24,16 +24,17 @@ use constant NAME_FIELD => 'name'; use constant ID_FIELD => 'id'; use constant LIST_ORDER => NAME_FIELD; -use constant UPDATE_VALIDATORS => {}; -use constant NUMERIC_COLUMNS => (); -use constant DATE_COLUMNS => (); +use constant UPDATE_VALIDATORS => {}; +use constant NUMERIC_COLUMNS => (); +use constant DATE_COLUMNS => (); use constant VALIDATOR_DEPENDENCIES => {}; + # XXX At some point, this will be joined with FIELD_MAP. -use constant REQUIRED_FIELD_MAP => {}; +use constant REQUIRED_FIELD_MAP => {}; use constant EXTRA_REQUIRED_FIELDS => (); -use constant AUDIT_CREATES => 1; -use constant AUDIT_UPDATES => 1; -use constant AUDIT_REMOVES => 1; +use constant AUDIT_CREATES => 1; +use constant AUDIT_UPDATES => 1; +use constant AUDIT_REMOVES => 1; # When USE_MEMCACHED is true, the class is suitable for serialisation to # Memcached. See documentation in Bugzilla::Memcached for more information. @@ -47,54 +48,51 @@ use constant IS_CONFIG => 0; # This allows the JSON-RPC interface to return Bugzilla::Object instances # as though they were hashes. In the future, this may be modified to return # less information. -sub TO_JSON { return { %{ $_[0] } }; } +sub TO_JSON { return {%{$_[0]}}; } ############################### #### Initialization #### ############################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $param = shift; - - my $object = $class->_object_cache_get($param); - return $object if $object; - - my ($data, $set_memcached); - if (Bugzilla->memcached->enabled - && $class->USE_MEMCACHED - && ref($param) eq 'HASH' && $param->{cache}) - { - if (defined $param->{id}) { - $data = Bugzilla->memcached->get({ - table => $class->DB_TABLE, - id => $param->{id}, - }); - } - elsif (defined $param->{name}) { - $data = Bugzilla->memcached->get({ - table => $class->DB_TABLE, - name => $param->{name}, - }); - } - $set_memcached = $data ? 0 : 1; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $param = shift; + + my $object = $class->_object_cache_get($param); + return $object if $object; + + my ($data, $set_memcached); + if ( Bugzilla->memcached->enabled + && $class->USE_MEMCACHED + && ref($param) eq 'HASH' + && $param->{cache}) + { + if (defined $param->{id}) { + $data + = Bugzilla->memcached->get({table => $class->DB_TABLE, id => $param->{id},}); } - $data ||= $class->_load_from_db($param); - - if ($data && $set_memcached) { - Bugzilla->memcached->set({ - table => $class->DB_TABLE, - id => $data->{$class->ID_FIELD}, - name => $data->{$class->NAME_FIELD}, - data => $data, - }); + elsif (defined $param->{name}) { + $data = Bugzilla->memcached->get( + {table => $class->DB_TABLE, name => $param->{name},}); } - - $object = $class->new_from_hash($data); - $class->_object_cache_set($param, $object); - - return $object; + $set_memcached = $data ? 0 : 1; + } + $data ||= $class->_load_from_db($param); + + if ($data && $set_memcached) { + Bugzilla->memcached->set({ + table => $class->DB_TABLE, + id => $data->{$class->ID_FIELD}, + name => $data->{$class->NAME_FIELD}, + data => $data, + }); + } + + $object = $class->new_from_hash($data); + $class->_object_cache_set($param, $object); + + return $object; } # Note: Because this uses sql_istrcmp, if you make a new object use @@ -102,326 +100,324 @@ sub new { # in Bugzilla::DB::Pg appropriately, to add the right LOWER # index. You can see examples already there. sub _load_from_db { - my $class = shift; - my ($param) = @_; - my $dbh = Bugzilla->dbh; - my $columns = join(',', $class->_get_db_columns); - my $table = $class->DB_TABLE; - my $name_field = $class->NAME_FIELD; - my $id_field = $class->ID_FIELD; - - my $id = $param; - if (ref $param eq 'HASH') { - $id = $param->{id}; + my $class = shift; + my ($param) = @_; + my $dbh = Bugzilla->dbh; + my $columns = join(',', $class->_get_db_columns); + my $table = $class->DB_TABLE; + my $name_field = $class->NAME_FIELD; + my $id_field = $class->ID_FIELD; + + my $id = $param; + if (ref $param eq 'HASH') { + $id = $param->{id}; + } + + my $object_data; + if (defined $id) { + + # We special-case if somebody specifies an ID, so that we can + # validate it as numeric. + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::_load_from_db'}); + + # Too large integers make PostgreSQL crash. + return if $id > MAX_INT_32; + + $object_data = $dbh->selectrow_hashref( + qq{ + SELECT $columns FROM $table + WHERE $id_field = ?}, undef, $id + ); + } + else { + unless (defined $param->{name} + || (defined $param->{'condition'} && defined $param->{'values'})) + { + ThrowCodeError('bad_arg', {argument => 'param', function => $class . '::new'}); } - my $object_data; - if (defined $id) { - # We special-case if somebody specifies an ID, so that we can - # validate it as numeric. - detaint_natural($id) - || ThrowCodeError('param_must_be_numeric', - {function => $class . '::_load_from_db'}); - - # Too large integers make PostgreSQL crash. - return if $id > MAX_INT_32; - - $object_data = $dbh->selectrow_hashref(qq{ - SELECT $columns FROM $table - WHERE $id_field = ?}, undef, $id); - } else { - unless (defined $param->{name} || (defined $param->{'condition'} - && defined $param->{'values'})) + my ($condition, @values); + if (defined $param->{name}) { + $condition = $dbh->sql_istrcmp($name_field, '?'); + push(@values, $param->{name}); + } + elsif (defined $param->{'condition'} && defined $param->{'values'}) { + caller->isa('Bugzilla::Object') || ThrowCodeError( + 'protection_violation', { - ThrowCodeError('bad_arg', { argument => 'param', - function => $class . '::new' }); - } - - my ($condition, @values); - if (defined $param->{name}) { - $condition = $dbh->sql_istrcmp($name_field, '?'); - push(@values, $param->{name}); - } - elsif (defined $param->{'condition'} && defined $param->{'values'}) { - caller->isa('Bugzilla::Object') - || ThrowCodeError('protection_violation', - { caller => caller, - function => $class . '::new', - argument => 'condition/values' }); - $condition = $param->{'condition'}; - push(@values, @{$param->{'values'}}); + caller => caller, + function => $class . '::new', + argument => 'condition/values' } - - map { trick_taint($_) } @values; - $object_data = $dbh->selectrow_hashref( - "SELECT $columns FROM $table WHERE $condition", undef, @values); + ); + $condition = $param->{'condition'}; + push(@values, @{$param->{'values'}}); } - return $object_data; + + map { trick_taint($_) } @values; + $object_data + = $dbh->selectrow_hashref("SELECT $columns FROM $table WHERE $condition", + undef, @values); + } + return $object_data; } sub new_from_list { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($id_list) = @_; - my $id_field = $class->ID_FIELD; - - my @detainted_ids; - foreach my $id (@$id_list) { - detaint_natural($id) || - ThrowCodeError('param_must_be_numeric', - {function => $class . '::new_from_list'}); - # Too large integers make PostgreSQL crash. - next if $id > MAX_INT_32; - push(@detainted_ids, $id); - } - - # We don't do $invocant->match because some classes have - # their own implementation of match which is not compatible - # with this one. However, match() still needs to have the right $invocant - # in order to do $class->DB_TABLE and so on. - return match($invocant, { $id_field => \@detainted_ids }); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($id_list) = @_; + my $id_field = $class->ID_FIELD; + + my @detainted_ids; + foreach my $id (@$id_list) { + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::new_from_list'}); + + # Too large integers make PostgreSQL crash. + next if $id > MAX_INT_32; + push(@detainted_ids, $id); + } + + # We don't do $invocant->match because some classes have + # their own implementation of match which is not compatible + # with this one. However, match() still needs to have the right $invocant + # in order to do $class->DB_TABLE and so on. + return match($invocant, {$id_field => \@detainted_ids}); } sub new_from_hash { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $object_data = shift || return; - $class->_serialisation_keys($object_data); - bless($object_data, $class); - $object_data->initialize(); - return $object_data; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $object_data = shift || return; + $class->_serialisation_keys($object_data); + bless($object_data, $class); + $object_data->initialize(); + return $object_data; } sub initialize { - # abstract + + # abstract } # Provides a mechanism for objects to be cached in the request_cache sub object_cache_get { - my ($class, $id) = @_; - return $class->_object_cache_get( - { id => $id, cache => 1}, - $class - ); + my ($class, $id) = @_; + return $class->_object_cache_get({id => $id, cache => 1}, $class); } sub object_cache_set { - my $self = shift; - return $self->_object_cache_set( - { id => $self->id, cache => 1 }, - $self - ); + my $self = shift; + return $self->_object_cache_set({id => $self->id, cache => 1}, $self); } sub _object_cache_get { - my $class = shift; - my ($param) = @_; - my $cache_key = $class->object_cache_key($param) - || return; - return Bugzilla->request_cache->{$cache_key}; + my $class = shift; + my ($param) = @_; + my $cache_key = $class->object_cache_key($param) || return; + return Bugzilla->request_cache->{$cache_key}; } sub _object_cache_set { - my $class = shift; - my ($param, $object) = @_; - my $cache_key = $class->object_cache_key($param) - || return; - Bugzilla->request_cache->{$cache_key} = $object; + my $class = shift; + my ($param, $object) = @_; + my $cache_key = $class->object_cache_key($param) || return; + Bugzilla->request_cache->{$cache_key} = $object; } sub _object_cache_remove { - my $class = shift; - my ($param) = @_; - $param->{cache} = 1; - my $cache_key = $class->object_cache_key($param) - || return; - delete Bugzilla->request_cache->{$cache_key}; + my $class = shift; + my ($param) = @_; + $param->{cache} = 1; + my $cache_key = $class->object_cache_key($param) || return; + delete Bugzilla->request_cache->{$cache_key}; } sub object_cache_key { - my $class = shift; - my ($param) = @_; - if (ref($param) && $param->{cache} && ($param->{id} || $param->{name})) { - $class = blessed($class) if blessed($class); - return $class . ',' . ($param->{id} || $param->{name}); - } else { - return; - } + my $class = shift; + my ($param) = @_; + if (ref($param) && $param->{cache} && ($param->{id} || $param->{name})) { + $class = blessed($class) if blessed($class); + return $class . ',' . ($param->{id} || $param->{name}); + } + else { + return; + } } # To support serialisation, we need to capture the keys in an object's default # hashref. sub _serialisation_keys { - my ($class, $object) = @_; - my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {}; - $cache->{$class} = [ keys %$object ] if $object && !exists $cache->{$class}; - return @{ $cache->{$class} }; + my ($class, $object) = @_; + my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {}; + $cache->{$class} = [keys %$object] if $object && !exists $cache->{$class}; + return @{$cache->{$class}}; } sub check { - my ($invocant, $param) = @_; - my $class = ref($invocant) || $invocant; - # If we were just passed a name, then just use the name. - if (!ref $param) { - $param = { name => $param }; + my ($invocant, $param) = @_; + my $class = ref($invocant) || $invocant; + + # If we were just passed a name, then just use the name. + if (!ref $param) { + $param = {name => $param}; + } + + # Don't allow empty names or ids. + my $check_param = exists $param->{id} ? 'id' : 'name'; + $param->{$check_param} = trim($param->{$check_param}); + + # If somebody passes us "0", we want to throw an error like + # "there is no X with the name 0". This is true even for ids. So here, + # we only check if the parameter is undefined or empty. + if (!defined $param->{$check_param} or $param->{$check_param} eq '') { + ThrowUserError('object_not_specified', {class => $class}); + } + + my $obj = $class->new($param); + if (!$obj) { + + # We don't want to override the normal template "user" object if + # "user" is one of the params. + delete $param->{user}; + if (my $error = delete $param->{_error}) { + ThrowUserError($error, {%$param, class => $class}); } - - # Don't allow empty names or ids. - my $check_param = exists $param->{id} ? 'id' : 'name'; - $param->{$check_param} = trim($param->{$check_param}); - # If somebody passes us "0", we want to throw an error like - # "there is no X with the name 0". This is true even for ids. So here, - # we only check if the parameter is undefined or empty. - if (!defined $param->{$check_param} or $param->{$check_param} eq '') { - ThrowUserError('object_not_specified', { class => $class }); + else { + ThrowUserError('object_does_not_exist', {%$param, class => $class}); } - - my $obj = $class->new($param); - if (!$obj) { - # We don't want to override the normal template "user" object if - # "user" is one of the params. - delete $param->{user}; - if (my $error = delete $param->{_error}) { - ThrowUserError($error, { %$param, class => $class }); - } - else { - ThrowUserError('object_does_not_exist', { %$param, class => $class }); - } - } - return $obj; + } + return $obj; } # Note: Future extensions to this could be: # * Add a MATCH_JOIN constant so that we can join against # certain other tables for the WHERE criteria. sub match { - my ($invocant, $criteria) = @_; - my $class = ref($invocant) || $invocant; - my $dbh = Bugzilla->dbh; - - return [$class->get_all] if !$criteria; - - my (@terms, @values, $postamble); - foreach my $field (keys %$criteria) { - my $value = $criteria->{$field}; - - # allow for LIMIT and OFFSET expressions via the criteria. - next if $field eq 'OFFSET'; - if ( $field eq 'LIMIT' ) { - next unless defined $value; - detaint_natural($value) - or ThrowCodeError('param_must_be_numeric', - { param => 'LIMIT', - function => "${class}::match" }); - my $offset; - if (defined $criteria->{OFFSET}) { - $offset = $criteria->{OFFSET}; - detaint_signed($offset) - or ThrowCodeError('param_must_be_numeric', - { param => 'OFFSET', - function => "${class}::match" }); - } - $postamble = $dbh->sql_limit($value, $offset); - next; - } - elsif ( $field eq 'WHERE' ) { - # the WHERE value is a hashref where the keys are - # "column_name operator ?" and values are the placeholder's - # value (either a scalar or an array of values). - foreach my $k (keys %$value) { - push(@terms, $k); - my @this_value = ref($value->{$k}) ? @{ $value->{$k} } - : ($value->{$k}); - push(@values, @this_value); - } - next; - } + my ($invocant, $criteria) = @_; + my $class = ref($invocant) || $invocant; + my $dbh = Bugzilla->dbh; + + return [$class->get_all] if !$criteria; + + my (@terms, @values, $postamble); + foreach my $field (keys %$criteria) { + my $value = $criteria->{$field}; + + # allow for LIMIT and OFFSET expressions via the criteria. + next if $field eq 'OFFSET'; + if ($field eq 'LIMIT') { + next unless defined $value; + detaint_natural($value) + or ThrowCodeError('param_must_be_numeric', + {param => 'LIMIT', function => "${class}::match"}); + my $offset; + if (defined $criteria->{OFFSET}) { + $offset = $criteria->{OFFSET}; + detaint_signed($offset) + or ThrowCodeError('param_must_be_numeric', + {param => 'OFFSET', function => "${class}::match"}); + } + $postamble = $dbh->sql_limit($value, $offset); + next; + } + elsif ($field eq 'WHERE') { + + # the WHERE value is a hashref where the keys are + # "column_name operator ?" and values are the placeholder's + # value (either a scalar or an array of values). + foreach my $k (keys %$value) { + push(@terms, $k); + my @this_value = ref($value->{$k}) ? @{$value->{$k}} : ($value->{$k}); + push(@values, @this_value); + } + next; + } - # It's always safe to use the field defined by classes as being - # their ID field. In particular, this means that new_from_list() - # is exempted from this check. - $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD; + # It's always safe to use the field defined by classes as being + # their ID field. In particular, this means that new_from_list() + # is exempted from this check. + $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD; - if (ref $value eq 'ARRAY') { - # IN () is invalid SQL, and if we have an empty list - # to match against, we're just returning an empty - # array anyhow. - return [] if !scalar @$value; + if (ref $value eq 'ARRAY') { - my @qmarks = ("?") x @$value; - push(@terms, $dbh->sql_in($field, \@qmarks)); - push(@values, @$value); - } - elsif ($value eq NOT_NULL) { - push(@terms, "$field IS NOT NULL"); - } - elsif ($value eq IS_NULL) { - push(@terms, "$field IS NULL"); - } - else { - push(@terms, "$field = ?"); - push(@values, $value); - } + # IN () is invalid SQL, and if we have an empty list + # to match against, we're just returning an empty + # array anyhow. + return [] if !scalar @$value; + + my @qmarks = ("?") x @$value; + push(@terms, $dbh->sql_in($field, \@qmarks)); + push(@values, @$value); + } + elsif ($value eq NOT_NULL) { + push(@terms, "$field IS NOT NULL"); } + elsif ($value eq IS_NULL) { + push(@terms, "$field IS NULL"); + } + else { + push(@terms, "$field = ?"); + push(@values, $value); + } + } - my $where = join(' AND ', @terms) if scalar @terms; - return $class->_do_list_select($where, \@values, $postamble); + my $where = join(' AND ', @terms) if scalar @terms; + return $class->_do_list_select($where, \@values, $postamble); } sub _do_list_select { - my ($class, $where, $values, $postamble) = @_; - my $table = $class->DB_TABLE; - my $cols = join(',', $class->_get_db_columns); - my $order = $class->LIST_ORDER; - - # Unconditional requests for configuration data are cacheable. - my ($objects, $set_memcached, $memcached_key); - if (!defined $where - && Bugzilla->memcached->enabled - && $class->IS_CONFIG) - { - $memcached_key = "$class:get_all"; - $objects = Bugzilla->memcached->get_config({ key => $memcached_key }); - $set_memcached = $objects ? 0 : 1; + my ($class, $where, $values, $postamble) = @_; + my $table = $class->DB_TABLE; + my $cols = join(',', $class->_get_db_columns); + my $order = $class->LIST_ORDER; + + # Unconditional requests for configuration data are cacheable. + my ($objects, $set_memcached, $memcached_key); + if (!defined $where && Bugzilla->memcached->enabled && $class->IS_CONFIG) { + $memcached_key = "$class:get_all"; + $objects = Bugzilla->memcached->get_config({key => $memcached_key}); + $set_memcached = $objects ? 0 : 1; + } + + if (!$objects) { + my $sql = "SELECT $cols FROM $table"; + if (defined $where) { + $sql .= " WHERE $where "; } + $sql .= " ORDER BY $order"; + $sql .= " $postamble" if $postamble; - if (!$objects) { - my $sql = "SELECT $cols FROM $table"; - if (defined $where) { - $sql .= " WHERE $where "; - } - $sql .= " ORDER BY $order"; - $sql .= " $postamble" if $postamble; - - my $dbh = Bugzilla->dbh; - # Sometimes the values are tainted, but we don't want to untaint them - # for the caller. So we copy the array. It's safe to untaint because - # they're only used in placeholders here. - my @untainted = @{ $values || [] }; - trick_taint($_) foreach @untainted; - $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted); - $class->_serialisation_keys($objects->[0]) if @$objects; - } - - if ($objects && $set_memcached) { - Bugzilla->memcached->set_config({ - key => $memcached_key, - data => $objects - }); - } + my $dbh = Bugzilla->dbh; - foreach my $object (@$objects) { - $object = $class->new_from_hash($object); - } - return $objects; + # Sometimes the values are tainted, but we don't want to untaint them + # for the caller. So we copy the array. It's safe to untaint because + # they're only used in placeholders here. + my @untainted = @{$values || []}; + trick_taint($_) foreach @untainted; + $objects = $dbh->selectall_arrayref($sql, {Slice => {}}, @untainted); + $class->_serialisation_keys($objects->[0]) if @$objects; + } + + if ($objects && $set_memcached) { + Bugzilla->memcached->set_config({key => $memcached_key, data => $objects}); + } + + foreach my $object (@$objects) { + $object = $class->new_from_hash($object); + } + return $objects; } ############################### #### Accessors ###### ############################### -sub id { return $_[0]->{$_[0]->ID_FIELD}; } +sub id { return $_[0]->{$_[0]->ID_FIELD}; } sub name { return $_[0]->{$_[0]->NAME_FIELD}; } ############################### @@ -429,204 +425,214 @@ sub name { return $_[0]->{$_[0]->NAME_FIELD}; } ############################### sub set { - my ($self, $field, $value) = @_; - - # This method is protected. It's used to help implement set_ functions. - my $caller = caller; - $caller->isa('Bugzilla::Object') || $caller->isa('Bugzilla::Extension') - || ThrowCodeError('protection_violation', - { caller => caller, - superclass => __PACKAGE__, - function => 'Bugzilla::Object->set' }); - - Bugzilla::Hook::process('object_before_set', - { object => $self, field => $field, - value => $value }); - - my %validators = (%{$self->_get_validators}, %{$self->UPDATE_VALIDATORS}); - if (exists $validators{$field}) { - my $validator = $validators{$field}; - $value = $self->$validator($value, $field); - trick_taint($value) if (defined $value && !ref($value)); - - if ($self->can('_set_global_validator')) { - $self->_set_global_validator($value, $field); - } + my ($self, $field, $value) = @_; + + # This method is protected. It's used to help implement set_ functions. + my $caller = caller; + $caller->isa('Bugzilla::Object') + || $caller->isa('Bugzilla::Extension') + || ThrowCodeError( + 'protection_violation', + { + caller => caller, + superclass => __PACKAGE__, + function => 'Bugzilla::Object->set' } + ); - $self->{$field} = $value; + Bugzilla::Hook::process('object_before_set', + {object => $self, field => $field, value => $value}); - Bugzilla::Hook::process('object_end_of_set', - { object => $self, field => $field }); + my %validators = (%{$self->_get_validators}, %{$self->UPDATE_VALIDATORS}); + if (exists $validators{$field}) { + my $validator = $validators{$field}; + $value = $self->$validator($value, $field); + trick_taint($value) if (defined $value && !ref($value)); + + if ($self->can('_set_global_validator')) { + $self->_set_global_validator($value, $field); + } + } + + $self->{$field} = $value; + + Bugzilla::Hook::process('object_end_of_set', + {object => $self, field => $field}); } sub set_all { - my ($self, $params) = @_; - - # Don't let setters modify the values in $params for the caller. - my %field_values = %$params; - - my @sorted_names = $self->_sort_by_dep(keys %field_values); - - foreach my $key (@sorted_names) { - # It's possible for one set_ method to delete a key from $params - # for another set method, so if that's happened, we don't call the - # other set method. - next if !exists $field_values{$key}; - my $method = "set_$key"; - if (!$self->can($method)) { - my $class = ref($self) || $self; - ThrowCodeError("unknown_method", { method => "${class}::${method}" }); - } - $self->$method($field_values{$key}, \%field_values); + my ($self, $params) = @_; + + # Don't let setters modify the values in $params for the caller. + my %field_values = %$params; + + my @sorted_names = $self->_sort_by_dep(keys %field_values); + + foreach my $key (@sorted_names) { + + # It's possible for one set_ method to delete a key from $params + # for another set method, so if that's happened, we don't call the + # other set method. + next if !exists $field_values{$key}; + my $method = "set_$key"; + if (!$self->can($method)) { + my $class = ref($self) || $self; + ThrowCodeError("unknown_method", {method => "${class}::${method}"}); } - Bugzilla::Hook::process('object_end_of_set_all', - { object => $self, params => \%field_values }); + $self->$method($field_values{$key}, \%field_values); + } + Bugzilla::Hook::process('object_end_of_set_all', + {object => $self, params => \%field_values}); } sub update { - my $self = shift; - - my $dbh = Bugzilla->dbh; - my $table = $self->DB_TABLE; - my $id_field = $self->ID_FIELD; - - $dbh->bz_start_transaction(); - - my $old_self = $self->new($self->id); - - my @all_columns = $self->UPDATE_COLUMNS; - my @hook_columns; - Bugzilla::Hook::process('object_update_columns', - { object => $self, columns => \@hook_columns }); - push(@all_columns, @hook_columns); - - my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS; - my %date = map { $_ => 1 } $self->DATE_COLUMNS; - my (@update_columns, @values, %changes); - foreach my $column (@all_columns) { - my ($old, $new) = ($old_self->{$column}, $self->{$column}); - # This has to be written this way in order to allow us to set a field - # from undef or to undef, and avoid warnings about comparing an undef - # with the "eq" operator. - if (!defined $new || !defined $old) { - next if !defined $new && !defined $old; - } - elsif ( ($numeric{$column} && $old == $new) - || ($date{$column} && str2time($old) == str2time($new)) - || $old eq $new ) { - next; - } + my $self = shift; - trick_taint($new) if defined $new; - push(@values, $new); - push(@update_columns, $column); - # We don't use $new because we don't want to detaint this for - # the caller. - $changes{$column} = [$old, $self->{$column}]; - } + my $dbh = Bugzilla->dbh; + my $table = $self->DB_TABLE; + my $id_field = $self->ID_FIELD; - my $columns = join(', ', map {"$_ = ?"} @update_columns); + $dbh->bz_start_transaction(); - $dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", undef, - @values, $self->id) if @values; + my $old_self = $self->new($self->id); - Bugzilla::Hook::process('object_end_of_update', - { object => $self, old_object => $old_self, - changes => \%changes }); + my @all_columns = $self->UPDATE_COLUMNS; + my @hook_columns; + Bugzilla::Hook::process('object_update_columns', + {object => $self, columns => \@hook_columns}); + push(@all_columns, @hook_columns); - $self->audit_log(\%changes) if $self->AUDIT_UPDATES; + my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS; + my %date = map { $_ => 1 } $self->DATE_COLUMNS; + my (@update_columns, @values, %changes); + foreach my $column (@all_columns) { + my ($old, $new) = ($old_self->{$column}, $self->{$column}); - $dbh->bz_commit_transaction(); - if ($self->USE_MEMCACHED && @values) { - Bugzilla->memcached->clear({ table => $table, id => $self->id }); - Bugzilla->memcached->clear_config() - if $self->IS_CONFIG; + # This has to be written this way in order to allow us to set a field + # from undef or to undef, and avoid warnings about comparing an undef + # with the "eq" operator. + if (!defined $new || !defined $old) { + next if !defined $new && !defined $old; } - $self->_object_cache_remove({ id => $self->id }); - $self->_object_cache_remove({ name => $self->name }) if $self->name; - - if (wantarray) { - return (\%changes, $old_self); + elsif (($numeric{$column} && $old == $new) + || ($date{$column} && str2time($old) == str2time($new)) + || $old eq $new) + { + next; } - return \%changes; + trick_taint($new) if defined $new; + push(@values, $new); + push(@update_columns, $column); + + # We don't use $new because we don't want to detaint this for + # the caller. + $changes{$column} = [$old, $self->{$column}]; + } + + my $columns = join(', ', map {"$_ = ?"} @update_columns); + + $dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", + undef, @values, $self->id) + if @values; + + Bugzilla::Hook::process('object_end_of_update', + {object => $self, old_object => $old_self, changes => \%changes}); + + $self->audit_log(\%changes) if $self->AUDIT_UPDATES; + + $dbh->bz_commit_transaction(); + if ($self->USE_MEMCACHED && @values) { + Bugzilla->memcached->clear({table => $table, id => $self->id}); + Bugzilla->memcached->clear_config() if $self->IS_CONFIG; + } + $self->_object_cache_remove({id => $self->id}); + $self->_object_cache_remove({name => $self->name}) if $self->name; + + if (wantarray) { + return (\%changes, $old_self); + } + + return \%changes; } sub remove_from_db { - my $self = shift; - Bugzilla::Hook::process('object_before_delete', { object => $self }); - my $table = $self->DB_TABLE; - my $id_field = $self->ID_FIELD; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - $self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES; - $dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id); - $dbh->bz_commit_transaction(); - if ($self->USE_MEMCACHED) { - Bugzilla->memcached->clear({ table => $table, id => $self->id }); - Bugzilla->memcached->clear_config() - if $self->IS_CONFIG; - } - $self->_object_cache_remove({ id => $self->id }); - $self->_object_cache_remove({ name => $self->name }) if $self->name; - undef $self; + my $self = shift; + Bugzilla::Hook::process('object_before_delete', {object => $self}); + my $table = $self->DB_TABLE; + my $id_field = $self->ID_FIELD; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + $self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES; + $dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id); + $dbh->bz_commit_transaction(); + + if ($self->USE_MEMCACHED) { + Bugzilla->memcached->clear({table => $table, id => $self->id}); + Bugzilla->memcached->clear_config() if $self->IS_CONFIG; + } + $self->_object_cache_remove({id => $self->id}); + $self->_object_cache_remove({name => $self->name}) if $self->name; + undef $self; } sub audit_log { - my ($self, $changes) = @_; - my $class = ref $self; - my $dbh = Bugzilla->dbh; - my $user_id = Bugzilla->user->id || undef; - my $sth = $dbh->prepare( - 'INSERT INTO audit_log (user_id, class, object_id, field, + my ($self, $changes) = @_; + my $class = ref $self; + my $dbh = Bugzilla->dbh; + my $user_id = Bugzilla->user->id || undef; + my $sth = $dbh->prepare( + 'INSERT INTO audit_log (user_id, class, object_id, field, removed, added, at_time) - VALUES (?,?,?,?,?,?,LOCALTIMESTAMP(0))'); - # During creation or removal, $changes is actually just a string - # indicating whether we're creating or removing the object. - if ($changes eq AUDIT_CREATE or $changes eq AUDIT_REMOVE) { - # We put the object's name in the "added" or "removed" field. - # We do this thing with NAME_FIELD because $self->name returns - # the wrong thing for Bugzilla::User. - my $name = $self->{$self->NAME_FIELD}; - my @added_removed = $changes eq AUDIT_CREATE ? (undef, $name) - : ($name, undef); - $sth->execute($user_id, $class, $self->id, $changes, @added_removed); - return; - } - - # During update, it's the actual %changes hash produced by update(). - foreach my $field (keys %$changes) { - # Skip private changes. - next if $field =~ /^_/; - my ($from, $to) = $self->_sanitize_audit_log($field, $changes->{$field}); - $sth->execute($user_id, $class, $self->id, $field, $from, $to); - } + VALUES (?,?,?,?,?,?,LOCALTIMESTAMP(0))' + ); + + # During creation or removal, $changes is actually just a string + # indicating whether we're creating or removing the object. + if ($changes eq AUDIT_CREATE or $changes eq AUDIT_REMOVE) { + + # We put the object's name in the "added" or "removed" field. + # We do this thing with NAME_FIELD because $self->name returns + # the wrong thing for Bugzilla::User. + my $name = $self->{$self->NAME_FIELD}; + my @added_removed = $changes eq AUDIT_CREATE ? (undef, $name) : ($name, undef); + $sth->execute($user_id, $class, $self->id, $changes, @added_removed); + return; + } + + # During update, it's the actual %changes hash produced by update(). + foreach my $field (keys %$changes) { + + # Skip private changes. + next if $field =~ /^_/; + my ($from, $to) = $self->_sanitize_audit_log($field, $changes->{$field}); + $sth->execute($user_id, $class, $self->id, $field, $from, $to); + } } sub _sanitize_audit_log { - my ($self, $field, $changes) = @_; - my $class = ref($self) || $self; - - # Do not store hashed passwords. Only record the algorithm used to encode them. - if ($class eq 'Bugzilla::User' && $field eq 'cryptpassword') { - foreach my $passwd (@$changes) { - next unless $passwd; - my $algorithm = 'unknown_algorithm'; - if ($passwd =~ /{([^}]+)}$/) { - $algorithm = $1; - } - $passwd = "hashed_with_$algorithm"; - } + my ($self, $field, $changes) = @_; + my $class = ref($self) || $self; + + # Do not store hashed passwords. Only record the algorithm used to encode them. + if ($class eq 'Bugzilla::User' && $field eq 'cryptpassword') { + foreach my $passwd (@$changes) { + next unless $passwd; + my $algorithm = 'unknown_algorithm'; + if ($passwd =~ /{([^}]+)}$/) { + $algorithm = $1; + } + $passwd = "hashed_with_$algorithm"; } - return @$changes; + } + return @$changes; } sub flatten_to_hash { - my $self = shift; - my $class = blessed($self); - my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys; - return \%hash; + my $self = shift; + my $class = blessed($self); + my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys; + return \%hash; } ############################### @@ -634,127 +640,125 @@ sub flatten_to_hash { ############################### sub any_exist { - my $class = shift; - my $table = $class->DB_TABLE; - my $dbh = Bugzilla->dbh; - my $any_exist = $dbh->selectrow_array( - "SELECT 1 FROM $table " . $dbh->sql_limit(1)); - return $any_exist ? 1 : 0; + my $class = shift; + my $table = $class->DB_TABLE; + my $dbh = Bugzilla->dbh; + my $any_exist + = $dbh->selectrow_array("SELECT 1 FROM $table " . $dbh->sql_limit(1)); + return $any_exist ? 1 : 0; } sub create { - my ($class, $params) = @_; - my $dbh = Bugzilla->dbh; + my ($class, $params) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - $class->check_required_create_fields($params); - my $field_values = $class->run_create_validators($params); - my $object = $class->insert_create_data($field_values); - $dbh->bz_commit_transaction(); + $dbh->bz_start_transaction(); + $class->check_required_create_fields($params); + my $field_values = $class->run_create_validators($params); + my $object = $class->insert_create_data($field_values); + $dbh->bz_commit_transaction(); - if (Bugzilla->memcached->enabled - && $class->USE_MEMCACHED - && $class->IS_CONFIG) - { - Bugzilla->memcached->clear_config(); - } + if (Bugzilla->memcached->enabled && $class->USE_MEMCACHED && $class->IS_CONFIG) + { + Bugzilla->memcached->clear_config(); + } - return $object; + return $object; } # Used to validate that a field name is in fact a valid column in the # current table before inserting it into SQL. sub _check_field { - my ($invocant, $field, $function) = @_; - my $class = ref($invocant) || $invocant; - if (!Bugzilla->dbh->bz_column_info($class->DB_TABLE, $field)) { - ThrowCodeError('param_invalid', { param => $field, - function => "${class}::$function" }); - } + my ($invocant, $field, $function) = @_; + my $class = ref($invocant) || $invocant; + if (!Bugzilla->dbh->bz_column_info($class->DB_TABLE, $field)) { + ThrowCodeError('param_invalid', + {param => $field, function => "${class}::$function"}); + } } sub check_required_create_fields { - my ($class, $params) = @_; + my ($class, $params) = @_; - # This hook happens here so that even subclasses that don't call - # SUPER::create are still affected by the hook. - Bugzilla::Hook::process('object_before_create', { class => $class, - params => $params }); + # This hook happens here so that even subclasses that don't call + # SUPER::create are still affected by the hook. + Bugzilla::Hook::process('object_before_create', + {class => $class, params => $params}); - my @check_fields = $class->_required_create_fields(); - foreach my $field (@check_fields) { - $params->{$field} = undef if !exists $params->{$field}; - } + my @check_fields = $class->_required_create_fields(); + foreach my $field (@check_fields) { + $params->{$field} = undef if !exists $params->{$field}; + } } sub run_create_validators { - my ($class, $params, $options) = @_; + my ($class, $params, $options) = @_; - my $validators = $class->_get_validators; - my %field_values = %$params; + my $validators = $class->_get_validators; + my %field_values = %$params; - # Make a hash skiplist for easier searching later - my %skip_list = map { $_ => 1 } @{ $options->{skip} || [] }; + # Make a hash skiplist for easier searching later + my %skip_list = map { $_ => 1 } @{$options->{skip} || []}; - # Get the sorted field names - my @sorted_names = $class->_sort_by_dep(keys %field_values); + # Get the sorted field names + my @sorted_names = $class->_sort_by_dep(keys %field_values); - # Remove the skipped names - my @unskipped = grep { !$skip_list{$_} } @sorted_names; + # Remove the skipped names + my @unskipped = grep { !$skip_list{$_} } @sorted_names; - foreach my $field (@unskipped) { - my $value; - if (exists $validators->{$field}) { - my $validator = $validators->{$field}; - $value = $class->$validator($field_values{$field}, $field, - \%field_values); - } - else { - $value = $field_values{$field}; - } - - # We want people to be able to explicitly set fields to NULL, - # and that means they can be set to undef. - trick_taint($value) if defined $value && !ref($value); - $field_values{$field} = $value; + foreach my $field (@unskipped) { + my $value; + if (exists $validators->{$field}) { + my $validator = $validators->{$field}; + $value = $class->$validator($field_values{$field}, $field, \%field_values); } - - Bugzilla::Hook::process('object_end_of_create_validators', - { class => $class, params => \%field_values }); - - return \%field_values; -} - -sub insert_create_data { - my ($class, $field_values) = @_; - my $dbh = Bugzilla->dbh; - - my (@field_names, @values); - while (my ($field, $value) = each %$field_values) { - $class->_check_field($field, 'create'); - push(@field_names, $field); - push(@values, $value); + else { + $value = $field_values{$field}; } - my $qmarks = '?,' x @field_names; - chop($qmarks); - my $table = $class->DB_TABLE; - $dbh->do("INSERT INTO $table (" . join(', ', @field_names) - . ") VALUES ($qmarks)", undef, @values); - my $id = $dbh->bz_last_key($table, $class->ID_FIELD); + # We want people to be able to explicitly set fields to NULL, + # and that means they can be set to undef. + trick_taint($value) if defined $value && !ref($value); + $field_values{$field} = $value; + } - my $object = $class->new($id); + Bugzilla::Hook::process('object_end_of_create_validators', + {class => $class, params => \%field_values}); - Bugzilla::Hook::process('object_end_of_create', { class => $class, - object => $object }); - $object->audit_log(AUDIT_CREATE) if $object->AUDIT_CREATES; + return \%field_values; +} - return $object; +sub insert_create_data { + my ($class, $field_values) = @_; + my $dbh = Bugzilla->dbh; + + my (@field_names, @values); + while (my ($field, $value) = each %$field_values) { + $class->_check_field($field, 'create'); + push(@field_names, $field); + push(@values, $value); + } + + my $qmarks = '?,' x @field_names; + chop($qmarks); + my $table = $class->DB_TABLE; + $dbh->do( + "INSERT INTO $table (" . join(', ', @field_names) . ") VALUES ($qmarks)", + undef, @values); + my $id = $dbh->bz_last_key($table, $class->ID_FIELD); + + my $object = $class->new($id); + + Bugzilla::Hook::process('object_end_of_create', + {class => $class, object => $object}); + $object->audit_log(AUDIT_CREATE) if $object->AUDIT_CREATES; + + return $object; } sub get_all { - my $class = shift; - return @{ $class->_do_list_select() }; + my $class = shift; + return @{$class->_do_list_select()}; } ############################### @@ -764,20 +768,19 @@ sub get_all { sub check_boolean { return $_[1] ? 1 : 0 } sub check_time { - my ($invocant, $value, $field, $params, $allow_negative) = @_; + my ($invocant, $value, $field, $params, $allow_negative) = @_; - # If we don't have a current value default to zero - my $current = blessed($invocant) ? $invocant->{$field} - : 0; - $current ||= 0; + # If we don't have a current value default to zero + my $current = blessed($invocant) ? $invocant->{$field} : 0; + $current ||= 0; - # Get the new value or zero if it isn't defined - $value = trim($value) || 0; + # Get the new value or zero if it isn't defined + $value = trim($value) || 0; - # Make sure the new value is well formed - _validate_time($value, $field, $allow_negative); + # Make sure the new value is well formed + _validate_time($value, $field, $allow_negative); - return $value; + return $value; } @@ -786,26 +789,25 @@ sub check_time { ################### sub _validate_time { - my ($time, $field, $allow_negative) = @_; - - # regexp verifies one or more digits, optionally followed by a period and - # zero or more digits, OR we have a period followed by one or more digits - # (allow negatives, though, so people can back out errors in time reporting) - if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) { - ThrowUserError("number_not_numeric", - {field => $field, num => "$time"}); - } - - # Callers can optionally allow negative times - if ( ($time < 0) && !$allow_negative ) { - ThrowUserError("number_too_small", - {field => $field, num => "$time", min_num => "0"}); - } - - if ($time > 99999.99) { - ThrowUserError("number_too_large", - {field => $field, num => "$time", max_num => "99999.99"}); - } + my ($time, $field, $allow_negative) = @_; + + # regexp verifies one or more digits, optionally followed by a period and + # zero or more digits, OR we have a period followed by one or more digits + # (allow negatives, though, so people can back out errors in time reporting) + if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) { + ThrowUserError("number_not_numeric", {field => $field, num => "$time"}); + } + + # Callers can optionally allow negative times + if (($time < 0) && !$allow_negative) { + ThrowUserError("number_too_small", + {field => $field, num => "$time", min_num => "0"}); + } + + if ($time > 99999.99) { + ThrowUserError("number_too_large", + {field => $field, num => "$time", max_num => "99999.99"}); + } } # Sorts fields according to VALIDATOR_DEPENDENCIES. This is not a @@ -813,54 +815,55 @@ sub _validate_time { # *have* to be in the list--it just has to be earlier than its dependent # if it *is* in the list. sub _sort_by_dep { - my ($invocant, @fields) = @_; - - my $dependencies = $invocant->VALIDATOR_DEPENDENCIES; - my ($has_deps, $no_deps) = part { $dependencies->{$_} ? 0 : 1 } @fields; - - # For fields with no dependencies, we sort them alphabetically, - # so that validation always happens in a consistent order. - # Fields with no dependencies come at the start of the list. - my @result = sort @{ $no_deps || [] }; - - # Fields with dependencies all go at the end of the list, and if - # they have dependencies on *each other*, then they have to be - # sorted properly. We go through $has_deps in sorted order to be - # sure that fields always validate in a consistent order. - foreach my $field (sort @{ $has_deps || [] }) { - if (!grep { $_ eq $field } @result) { - _insert_dep_field($field, $has_deps, $dependencies, \@result); - } + my ($invocant, @fields) = @_; + + my $dependencies = $invocant->VALIDATOR_DEPENDENCIES; + my ($has_deps, $no_deps) = part { $dependencies->{$_} ? 0 : 1 } @fields; + + # For fields with no dependencies, we sort them alphabetically, + # so that validation always happens in a consistent order. + # Fields with no dependencies come at the start of the list. + my @result = sort @{$no_deps || []}; + + # Fields with dependencies all go at the end of the list, and if + # they have dependencies on *each other*, then they have to be + # sorted properly. We go through $has_deps in sorted order to be + # sure that fields always validate in a consistent order. + foreach my $field (sort @{$has_deps || []}) { + if (!grep { $_ eq $field } @result) { + _insert_dep_field($field, $has_deps, $dependencies, \@result); } - return @result; + } + return @result; } sub _insert_dep_field { - my ($field, $insert_me, $dependencies, $result, $loop_tracking) = @_; + my ($field, $insert_me, $dependencies, $result, $loop_tracking) = @_; - if ($loop_tracking->{$field}) { - ThrowCodeError('object_dep_sort_loop', - { field => $field, - considered => [keys %$loop_tracking] }); - } - $loop_tracking->{$field} = 1; - - my $required_fields = $dependencies->{$field}; - # Imagine Field A requires field B... - foreach my $required_field (@$required_fields) { - # If our dependency is already satisfied, we're good. - next if grep { $_ eq $required_field } @$result; - - # If our dependency is not in the remaining fields to insert, - # then we're also OK. - next if !grep { $_ eq $required_field } @$insert_me; - - # So, at this point, we know that Field B is in $insert_me. - # So let's put the required field into the result. - _insert_dep_field($required_field, $insert_me, $dependencies, - $result, $loop_tracking); - } - push(@$result, $field); + if ($loop_tracking->{$field}) { + ThrowCodeError('object_dep_sort_loop', + {field => $field, considered => [keys %$loop_tracking]}); + } + $loop_tracking->{$field} = 1; + + my $required_fields = $dependencies->{$field}; + + # Imagine Field A requires field B... + foreach my $required_field (@$required_fields) { + + # If our dependency is already satisfied, we're good. + next if grep { $_ eq $required_field } @$result; + + # If our dependency is not in the remaining fields to insert, + # then we're also OK. + next if !grep { $_ eq $required_field } @$insert_me; + + # So, at this point, we know that Field B is in $insert_me. + # So let's put the required field into the result. + _insert_dep_field($required_field, $insert_me, $dependencies, $result, + $loop_tracking); + } + push(@$result, $field); } #################### @@ -873,61 +876,67 @@ sub _insert_dep_field { # page. sub _get_db_columns { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $cache = Bugzilla->request_cache; - my $cache_key = "object_${class}_db_columns"; - return @{ $cache->{$cache_key} } if $cache->{$cache_key}; - # Currently you can only add new columns using object_columns, not - # remove or modify existing columns, because removing columns would - # almost certainly cause Bugzilla to function improperly. - my @add_columns; - Bugzilla::Hook::process('object_columns', - { class => $class, columns => \@add_columns }); - my @columns = ($invocant->DB_COLUMNS, @add_columns); - $cache->{$cache_key} = \@columns; - return @{ $cache->{$cache_key} }; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $cache = Bugzilla->request_cache; + my $cache_key = "object_${class}_db_columns"; + return @{$cache->{$cache_key}} if $cache->{$cache_key}; + + # Currently you can only add new columns using object_columns, not + # remove or modify existing columns, because removing columns would + # almost certainly cause Bugzilla to function improperly. + my @add_columns; + Bugzilla::Hook::process('object_columns', + {class => $class, columns => \@add_columns}); + my @columns = ($invocant->DB_COLUMNS, @add_columns); + $cache->{$cache_key} = \@columns; + return @{$cache->{$cache_key}}; } # This method is private and should only be called by Bugzilla::Object. sub _get_validators { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $cache = Bugzilla->request_cache; - my $cache_key = "object_${class}_validators"; - return $cache->{$cache_key} if $cache->{$cache_key}; - # We copy this into a hash so that the hook doesn't modify the constant. - # (That could be bad in mod_perl.) - my %validators = %{ $invocant->VALIDATORS }; - Bugzilla::Hook::process('object_validators', - { class => $class, validators => \%validators }); - $cache->{$cache_key} = \%validators; - return $cache->{$cache_key}; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $cache = Bugzilla->request_cache; + my $cache_key = "object_${class}_validators"; + return $cache->{$cache_key} if $cache->{$cache_key}; + + # We copy this into a hash so that the hook doesn't modify the constant. + # (That could be bad in mod_perl.) + my %validators = %{$invocant->VALIDATORS}; + Bugzilla::Hook::process('object_validators', + {class => $class, validators => \%validators}); + $cache->{$cache_key} = \%validators; + return $cache->{$cache_key}; } # These are all the fields that need to be checked, always, when # calling create(), because they have no DEFAULT and they are marked # NOT NULL. sub _required_create_fields { - my $class = shift; - my $dbh = Bugzilla->dbh; - my $table = $class->DB_TABLE; - - my @columns = $dbh->bz_table_columns($table); - my @required; - foreach my $column (@columns) { - my $def = $dbh->bz_column_info($table, $column); - if ($def->{NOTNULL} and !defined $def->{DEFAULT} - # SERIAL fields effectively have a DEFAULT, but they're not - # listed as having a DEFAULT in DB::Schema. - and $def->{TYPE} !~ /serial/i) - { - my $field = $class->REQUIRED_FIELD_MAP->{$column} || $column; - push(@required, $field); - } + my $class = shift; + my $dbh = Bugzilla->dbh; + my $table = $class->DB_TABLE; + + my @columns = $dbh->bz_table_columns($table); + my @required; + foreach my $column (@columns) { + my $def = $dbh->bz_column_info($table, $column); + if ( + $def->{NOTNULL} + and !defined $def->{DEFAULT} + + # SERIAL fields effectively have a DEFAULT, but they're not + # listed as having a DEFAULT in DB::Schema. + and $def->{TYPE} !~ /serial/i + ) + { + my $field = $class->REQUIRED_FIELD_MAP->{$column} || $column; + push(@required, $field); } - push(@required, $class->EXTRA_REQUIRED_FIELDS); - return @required; + } + push(@required, $class->EXTRA_REQUIRED_FIELDS); + return @required; } 1; diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm index 0c0cb458d..7be17fc9f 100644 --- a/Bugzilla/Product.pm +++ b/Bugzilla/Product.pm @@ -39,32 +39,32 @@ use constant IS_CONFIG => 1; use constant DB_TABLE => 'products'; use constant DB_COLUMNS => qw( - id - name - classification_id - description - isactive - defaultmilestone - allows_unconfirmed + id + name + classification_id + description + isactive + defaultmilestone + allows_unconfirmed ); use constant UPDATE_COLUMNS => qw( - name - description - defaultmilestone - isactive - allows_unconfirmed + name + description + defaultmilestone + isactive + allows_unconfirmed ); use constant VALIDATORS => { - allows_unconfirmed => \&Bugzilla::Object::check_boolean, - classification => \&_check_classification, - name => \&_check_name, - description => \&_check_description, - version => \&_check_version, - defaultmilestone => \&_check_default_milestone, - isactive => \&Bugzilla::Object::check_boolean, - create_series => \&Bugzilla::Object::check_boolean + allows_unconfirmed => \&Bugzilla::Object::check_boolean, + classification => \&_check_classification, + name => \&_check_name, + description => \&_check_description, + version => \&_check_version, + defaultmilestone => \&_check_default_milestone, + isactive => \&Bugzilla::Object::check_boolean, + create_series => \&Bugzilla::Object::check_boolean }; ############################### @@ -72,258 +72,283 @@ use constant VALIDATORS => { ############################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; + my $class = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - $class->check_required_create_fields(@_); + $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - # Some fields do not exist in the DB as is. - if (defined $params->{classification}) { - $params->{classification_id} = delete $params->{classification}; - } - my $version = delete $params->{version}; - my $create_series = delete $params->{create_series}; + my $params = $class->run_create_validators(@_); + + # Some fields do not exist in the DB as is. + if (defined $params->{classification}) { + $params->{classification_id} = delete $params->{classification}; + } + my $version = delete $params->{version}; + my $create_series = delete $params->{create_series}; - my $product = $class->insert_create_data($params); - Bugzilla->user->clear_product_cache(); + my $product = $class->insert_create_data($params); + Bugzilla->user->clear_product_cache(); - # Add the new version and milestone into the DB as valid values. - Bugzilla::Version->create({ value => $version, product => $product }); - Bugzilla::Milestone->create({ value => $product->default_milestone, - product => $product }); + # Add the new version and milestone into the DB as valid values. + Bugzilla::Version->create({value => $version, product => $product}); + Bugzilla::Milestone->create( + {value => $product->default_milestone, product => $product}); - # Create groups and series for the new product, if requested. - $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'}; - $product->_create_series() if $create_series; + # Create groups and series for the new product, if requested. + $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'}; + $product->_create_series() if $create_series; - Bugzilla::Hook::process('product_end_of_create', { product => $product }); + Bugzilla::Hook::process('product_end_of_create', {product => $product}); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - return $product; + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + return $product; } # This is considerably faster than calling new_from_list three times # for each product in the list, particularly with hundreds or thousands # of products. sub preload { - my ($products, $preload_flagtypes) = @_; - my %prods = map { $_->id => $_ } @$products; - my @prod_ids = keys %prods; - return unless @prod_ids; - - # We cannot |use| it due to a dependency loop with Bugzilla::User. - require Bugzilla::Component; - foreach my $field (qw(component version milestone)) { - my $classname = "Bugzilla::" . ucfirst($field); - my $objects = $classname->match({ product_id => \@prod_ids }); - - # Now populate the products with this set of objects. - foreach my $obj (@$objects) { - my $product_id = $obj->product_id; - $prods{$product_id}->{"${field}s"} ||= []; - push(@{$prods{$product_id}->{"${field}s"}}, $obj); - } - } - if ($preload_flagtypes) { - $_->flag_types foreach @$products; + my ($products, $preload_flagtypes) = @_; + my %prods = map { $_->id => $_ } @$products; + my @prod_ids = keys %prods; + return unless @prod_ids; + + # We cannot |use| it due to a dependency loop with Bugzilla::User. + require Bugzilla::Component; + foreach my $field (qw(component version milestone)) { + my $classname = "Bugzilla::" . ucfirst($field); + my $objects = $classname->match({product_id => \@prod_ids}); + + # Now populate the products with this set of objects. + foreach my $obj (@$objects) { + my $product_id = $obj->product_id; + $prods{$product_id}->{"${field}s"} ||= []; + push(@{$prods{$product_id}->{"${field}s"}}, $obj); } + } + if ($preload_flagtypes) { + $_->flag_types foreach @$products; + } } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - # Don't update the DB if something goes wrong below -> transaction. - $dbh->bz_start_transaction(); - my ($changes, $old_self) = $self->SUPER::update(@_); - - # Also update group settings. - if ($self->{check_group_controls}) { - require Bugzilla::Bug; - import Bugzilla::Bug qw(LogActivityEntry); - - my $old_settings = $old_self->group_controls; - my $new_settings = $self->group_controls; - my $timestamp = $dbh->selectrow_array('SELECT NOW()'); - - foreach my $gid (keys %$new_settings) { - my $old_setting = $old_settings->{$gid} || {}; - my $new_setting = $new_settings->{$gid}; - # If all new settings are 0 for a given group, we delete the entry - # from group_control_map, so we have to track it here. - my $all_zero = 1; - my @fields; - my @values; - - foreach my $field ('entry', 'membercontrol', 'othercontrol', 'canedit', - 'editcomponents', 'editbugs', 'canconfirm') - { - my $old_value = $old_setting->{$field}; - my $new_value = $new_setting->{$field}; - $all_zero = 0 if $new_value; - next if (defined $old_value && $old_value == $new_value); - push(@fields, $field); - # The value has already been validated. - detaint_natural($new_value); - push(@values, $new_value); - } - # Is there anything to update? - next unless scalar @fields; - - if ($all_zero) { - $dbh->do('DELETE FROM group_control_map - WHERE product_id = ? AND group_id = ?', - undef, $self->id, $gid); - } - else { - if (exists $old_setting->{group}) { - # There is already an entry in the DB. - my $set_fields = join(', ', map {"$_ = ?"} @fields); - $dbh->do("UPDATE group_control_map SET $set_fields - WHERE product_id = ? AND group_id = ?", - undef, (@values, $self->id, $gid)); - } - else { - # No entry yet. - my $fields = join(', ', @fields); - # +2 because of the product and group IDs. - my $qmarks = join(',', ('?') x (scalar @fields + 2)); - $dbh->do("INSERT INTO group_control_map (product_id, group_id, $fields) - VALUES ($qmarks)", undef, ($self->id, $gid, @values)); - } - } - - # If the group is mandatory, restrict all bugs to it. - if ($new_setting->{membercontrol} == CONTROLMAPMANDATORY) { - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + my $self = shift; + my $dbh = Bugzilla->dbh; + + # Don't update the DB if something goes wrong below -> transaction. + $dbh->bz_start_transaction(); + my ($changes, $old_self) = $self->SUPER::update(@_); + + # Also update group settings. + if ($self->{check_group_controls}) { + require Bugzilla::Bug; + import Bugzilla::Bug qw(LogActivityEntry); + + my $old_settings = $old_self->group_controls; + my $new_settings = $self->group_controls; + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + foreach my $gid (keys %$new_settings) { + my $old_setting = $old_settings->{$gid} || {}; + my $new_setting = $new_settings->{$gid}; + + # If all new settings are 0 for a given group, we delete the entry + # from group_control_map, so we have to track it here. + my $all_zero = 1; + my @fields; + my @values; + + foreach my $field ( + 'entry', 'membercontrol', 'othercontrol', 'canedit', + 'editcomponents', 'editbugs', 'canconfirm' + ) + { + my $old_value = $old_setting->{$field}; + my $new_value = $new_setting->{$field}; + $all_zero = 0 if $new_value; + next if (defined $old_value && $old_value == $new_value); + push(@fields, $field); + + # The value has already been validated. + detaint_natural($new_value); + push(@values, $new_value); + } + + # Is there anything to update? + next unless scalar @fields; + + if ($all_zero) { + $dbh->do( + 'DELETE FROM group_control_map + WHERE product_id = ? AND group_id = ?', undef, $self->id, $gid + ); + } + else { + if (exists $old_setting->{group}) { + + # There is already an entry in the DB. + my $set_fields = join(', ', map {"$_ = ?"} @fields); + $dbh->do( + "UPDATE group_control_map SET $set_fields + WHERE product_id = ? AND group_id = ?", undef, + (@values, $self->id, $gid) + ); + } + else { + # No entry yet. + my $fields = join(', ', @fields); + + # +2 because of the product and group IDs. + my $qmarks = join(',', ('?') x (scalar @fields + 2)); + $dbh->do( + "INSERT INTO group_control_map (product_id, group_id, $fields) + VALUES ($qmarks)", undef, ($self->id, $gid, @values) + ); + } + } + + # If the group is mandatory, restrict all bugs to it. + if ($new_setting->{membercontrol} == CONTROLMAPMANDATORY) { + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs LEFT JOIN bug_group_map ON bug_group_map.bug_id = bugs.bug_id AND group_id = ? WHERE product_id = ? AND bug_group_map.bug_id IS NULL', - undef, $gid, $self->id); - - if (scalar @$bug_ids) { - my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) - VALUES (?, ?)'); - - foreach my $bug_id (@$bug_ids) { - $sth->execute($bug_id, $gid); - # Add this change to the bug history. - LogActivityEntry($bug_id, 'bug_group', '', - $new_setting->{group}->name, - Bugzilla->user->id, $timestamp); - } - push(@{$changes->{'_group_controls'}->{'now_mandatory'}}, - {name => $new_setting->{group}->name, - bug_count => scalar @$bug_ids}); - } - } - # If the group can no longer be used to restrict bugs, remove them. - elsif ($new_setting->{membercontrol} == CONTROLMAPNA) { - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + undef, $gid, $self->id + ); + + if (scalar @$bug_ids) { + my $sth = $dbh->prepare( + 'INSERT INTO bug_group_map (bug_id, group_id) + VALUES (?, ?)' + ); + + foreach my $bug_id (@$bug_ids) { + $sth->execute($bug_id, $gid); + + # Add this change to the bug history. + LogActivityEntry($bug_id, 'bug_group', '', $new_setting->{group}->name, + Bugzilla->user->id, $timestamp); + } + push( + @{$changes->{'_group_controls'}->{'now_mandatory'}}, + {name => $new_setting->{group}->name, bug_count => scalar @$bug_ids} + ); + } + } + + # If the group can no longer be used to restrict bugs, remove them. + elsif ($new_setting->{membercontrol} == CONTROLMAPNA) { + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs INNER JOIN bug_group_map ON bug_group_map.bug_id = bugs.bug_id WHERE product_id = ? AND group_id = ?', - undef, $self->id, $gid); - - if (scalar @$bug_ids) { - $dbh->do('DELETE FROM bug_group_map WHERE group_id = ? AND ' . - $dbh->sql_in('bug_id', $bug_ids), undef, $gid); - - # Add this change to the bug history. - foreach my $bug_id (@$bug_ids) { - LogActivityEntry($bug_id, 'bug_group', - $old_setting->{group}->name, '', - Bugzilla->user->id, $timestamp); - } - push(@{$changes->{'_group_controls'}->{'now_na'}}, - {name => $old_setting->{group}->name, - bug_count => scalar @$bug_ids}); - } - } + undef, $self->id, $gid + ); + + if (scalar @$bug_ids) { + $dbh->do( + 'DELETE FROM bug_group_map WHERE group_id = ? AND ' + . $dbh->sql_in('bug_id', $bug_ids), + undef, $gid + ); + + # Add this change to the bug history. + foreach my $bug_id (@$bug_ids) { + LogActivityEntry($bug_id, 'bug_group', $old_setting->{group}->name, + '', Bugzilla->user->id, $timestamp); + } + push( + @{$changes->{'_group_controls'}->{'now_na'}}, + {name => $old_setting->{group}->name, bug_count => scalar @$bug_ids} + ); } - - delete $self->{groups_available}; - delete $self->{groups_mandatory}; + } } - $dbh->bz_commit_transaction(); - # Changes have been committed. - delete $self->{check_group_controls}; - Bugzilla->user->clear_product_cache(); - Bugzilla->memcached->clear_config(); - return $changes; + delete $self->{groups_available}; + delete $self->{groups_mandatory}; + } + $dbh->bz_commit_transaction(); + + # Changes have been committed. + delete $self->{check_group_controls}; + Bugzilla->user->clear_product_cache(); + Bugzilla->memcached->clear_config(); + + return $changes; } sub remove_from_db { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - $self->_check_if_controller(); - - if ($self->bug_count) { - if (Bugzilla->params->{'allowbugdeletion'}) { - require Bugzilla::Bug; - foreach my $bug_id (@{$self->bug_ids}) { - # Note that we allow the user to delete bugs they can't see, - # which is okay, because they're deleting the whole Product. - my $bug = new Bugzilla::Bug($bug_id); - $bug->remove_from_db(); - } - } - else { - ThrowUserError('product_has_bugs', { nb => $self->bug_count }); - } + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + $self->_check_if_controller(); + + if ($self->bug_count) { + if (Bugzilla->params->{'allowbugdeletion'}) { + require Bugzilla::Bug; + foreach my $bug_id (@{$self->bug_ids}) { + + # Note that we allow the user to delete bugs they can't see, + # which is okay, because they're deleting the whole Product. + my $bug = new Bugzilla::Bug($bug_id); + $bug->remove_from_db(); + } } + else { + ThrowUserError('product_has_bugs', {nb => $self->bug_count}); + } + } - if ($params->{delete_series}) { - my $series_ids = - $dbh->selectcol_arrayref('SELECT series_id + if ($params->{delete_series}) { + my $series_ids = $dbh->selectcol_arrayref( + 'SELECT series_id FROM series INNER JOIN series_categories ON series_categories.id = series.category - WHERE series_categories.name = ?', - undef, $self->name); + WHERE series_categories.name = ?', undef, + $self->name + ); - if (scalar @$series_ids) { - $dbh->do('DELETE FROM series WHERE ' . $dbh->sql_in('series_id', $series_ids)); - } + if (scalar @$series_ids) { + $dbh->do('DELETE FROM series WHERE ' . $dbh->sql_in('series_id', $series_ids)); + } - # If no subcategory uses this product name, completely purge it. - my $in_use = - $dbh->selectrow_array('SELECT 1 + # If no subcategory uses this product name, completely purge it. + my $in_use = $dbh->selectrow_array( + 'SELECT 1 FROM series INNER JOIN series_categories ON series_categories.id = series.subcategory - WHERE series_categories.name = ? ' . - $dbh->sql_limit(1), - undef, $self->name); - if (!$in_use) { - $dbh->do('DELETE FROM series_categories WHERE name = ?', undef, $self->name); - } + WHERE series_categories.name = ? ' + . $dbh->sql_limit(1), undef, $self->name + ); + if (!$in_use) { + $dbh->do('DELETE FROM series_categories WHERE name = ?', undef, $self->name); } + } - $self->SUPER::remove_from_db(); + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); - # We have to delete these internal variables, else we get - # the old lists of products and classifications again. - delete $user->{selectable_products}; - delete $user->{selectable_classifications}; + # We have to delete these internal variables, else we get + # the old lists of products and classifications again. + delete $user->{selectable_products}; + delete $user->{selectable_classifications}; } @@ -332,91 +357,94 @@ sub remove_from_db { ############################### sub _check_classification { - my ($invocant, $classification_name) = @_; - - my $classification_id = 1; - if (Bugzilla->params->{'useclassification'}) { - my $classification = Bugzilla::Classification->check($classification_name); - $classification_id = $classification->id; - } - return $classification_id; + my ($invocant, $classification_name) = @_; + + my $classification_id = 1; + if (Bugzilla->params->{'useclassification'}) { + my $classification = Bugzilla::Classification->check($classification_name); + $classification_id = $classification->id; + } + return $classification_id; } sub _check_name { - my ($invocant, $name) = @_; + my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError('product_blank_name'); + $name = trim($name); + $name || ThrowUserError('product_blank_name'); - if (length($name) > MAX_PRODUCT_SIZE) { - ThrowUserError('product_name_too_long', {'name' => $name}); - } + if (length($name) > MAX_PRODUCT_SIZE) { + ThrowUserError('product_name_too_long', {'name' => $name}); + } - my $product = new Bugzilla::Product({name => $name}); - if ($product && (!ref $invocant || $product->id != $invocant->id)) { - # Check for exact case sensitive match: - if ($product->name eq $name) { - ThrowUserError('product_name_already_in_use', {'product' => $product->name}); - } - else { - ThrowUserError('product_name_diff_in_case', {'product' => $name, - 'existing_product' => $product->name}); - } + my $product = new Bugzilla::Product({name => $name}); + if ($product && (!ref $invocant || $product->id != $invocant->id)) { + + # Check for exact case sensitive match: + if ($product->name eq $name) { + ThrowUserError('product_name_already_in_use', {'product' => $product->name}); } - return $name; + else { + ThrowUserError('product_name_diff_in_case', + {'product' => $name, 'existing_product' => $product->name}); + } + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('product_must_have_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('product_must_have_description'); + return $description; } sub _check_version { - my ($invocant, $version) = @_; + my ($invocant, $version) = @_; + + $version = trim($version); + $version || ThrowUserError('product_must_have_version'); - $version = trim($version); - $version || ThrowUserError('product_must_have_version'); - # We will check the version length when Bugzilla::Version->create will do it. - return $version; + # We will check the version length when Bugzilla::Version->create will do it. + return $version; } sub _check_default_milestone { - my ($invocant, $milestone) = @_; + my ($invocant, $milestone) = @_; - # Do nothing if target milestones are not in use. - unless (Bugzilla->params->{'usetargetmilestone'}) { - return (ref $invocant) ? $invocant->default_milestone : '---'; - } + # Do nothing if target milestones are not in use. + unless (Bugzilla->params->{'usetargetmilestone'}) { + return (ref $invocant) ? $invocant->default_milestone : '---'; + } - $milestone = trim($milestone); + $milestone = trim($milestone); - if (ref $invocant) { - # The default milestone must be one of the existing milestones. - my $mil_obj = new Bugzilla::Milestone({name => $milestone, product => $invocant}); + if (ref $invocant) { - $mil_obj || ThrowUserError('product_must_define_defaultmilestone', - {product => $invocant->name, - milestone => $milestone}); - } - else { - $milestone ||= '---'; - } - return $milestone; + # The default milestone must be one of the existing milestones. + my $mil_obj + = new Bugzilla::Milestone({name => $milestone, product => $invocant}); + + $mil_obj || ThrowUserError('product_must_define_defaultmilestone', + {product => $invocant->name, milestone => $milestone}); + } + else { + $milestone ||= '---'; + } + return $milestone; } sub _check_milestone_url { - my ($invocant, $url) = @_; + my ($invocant, $url) = @_; - # Do nothing if target milestones are not in use. - unless (Bugzilla->params->{'usetargetmilestone'}) { - return (ref $invocant) ? $invocant->milestone_url : ''; - } + # Do nothing if target milestones are not in use. + unless (Bugzilla->params->{'usetargetmilestone'}) { + return (ref $invocant) ? $invocant->milestone_url : ''; + } - $url = trim($url || ''); - return $url; + $url = trim($url || ''); + return $url; } ##################################### @@ -431,393 +459,427 @@ use constant is_default => 0; ############################### sub _create_bug_group { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - my $group_name = $self->name; - while (new Bugzilla::Group({name => $group_name})) { - $group_name .= '_'; - } - my $group_description = get_text('bug_group_description', {product => $self}); + my $group_name = $self->name; + while (new Bugzilla::Group({name => $group_name})) { + $group_name .= '_'; + } + my $group_description = get_text('bug_group_description', {product => $self}); - my $group = Bugzilla::Group->create({name => $group_name, - description => $group_description, - isbuggroup => 1}); + my $group = Bugzilla::Group->create( + {name => $group_name, description => $group_description, isbuggroup => 1}); - # Associate the new group and new product. - $dbh->do('INSERT INTO group_control_map + # Associate the new group and new product. + $dbh->do( + 'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol) - VALUES (?, ?, ?, ?)', - undef, ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA)); + VALUES (?, ?, ?, ?)', undef, + ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA) + ); } sub _create_series { - my $self = shift; - - my @series; - # We do every status, every resolution, and an "opened" one as well. - foreach my $bug_status (@{get_legal_field_values('bug_status')}) { - push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]); - } - - foreach my $resolution (@{get_legal_field_values('resolution')}) { - next if !$resolution; - push(@series, [$resolution, "resolution=" . url_quote($resolution)]); - } - - my @openedstatuses = BUG_STATE_OPEN; - my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses); - push(@series, [get_text('series_all_open'), $query]); - - foreach my $sdata (@series) { - my $series = new Bugzilla::Series(undef, $self->name, - get_text('series_subcategory'), - $sdata->[0], Bugzilla->user->id, 1, - $sdata->[1] . "&product=" . url_quote($self->name), 1); - $series->writeToDatabase(); - } + my $self = shift; + + my @series; + + # We do every status, every resolution, and an "opened" one as well. + foreach my $bug_status (@{get_legal_field_values('bug_status')}) { + push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]); + } + + foreach my $resolution (@{get_legal_field_values('resolution')}) { + next if !$resolution; + push(@series, [$resolution, "resolution=" . url_quote($resolution)]); + } + + my @openedstatuses = BUG_STATE_OPEN; + my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses); + push(@series, [get_text('series_all_open'), $query]); + + foreach my $sdata (@series) { + my $series + = new Bugzilla::Series(undef, $self->name, get_text('series_subcategory'), + $sdata->[0], Bugzilla->user->id, 1, + $sdata->[1] . "&product=" . url_quote($self->name), 1); + $series->writeToDatabase(); + } } -sub set_name { $_[0]->set('name', $_[1]); } -sub set_description { $_[0]->set('description', $_[1]); } -sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } sub set_allows_unconfirmed { $_[0]->set('allows_unconfirmed', $_[1]); } sub set_group_controls { - my ($self, $group, $settings) = @_; - - $group->is_active_bug_group - || ThrowUserError('product_illegal_group', {group => $group}); - - scalar(keys %$settings) - || ThrowCodeError('product_empty_group_controls', {group => $group}); - - # We store current settings for this group. - my $gs = $self->group_controls->{$group->id}; - # If there is no entry for this group yet, create a default hash. - unless (defined $gs) { - $gs = { entry => 0, - membercontrol => CONTROLMAPNA, - othercontrol => CONTROLMAPNA, - canedit => 0, - editcomponents => 0, - editbugs => 0, - canconfirm => 0, - group => $group }; - } - - # Both settings must be defined, or none of them can be updated. - if (defined $settings->{membercontrol} && defined $settings->{othercontrol}) { - # Legality of control combination is a function of - # membercontrol\othercontrol - # NA SH DE MA - # NA + - - - - # SH + + + + - # DE + - + + - # MA - - - + - foreach my $field ('membercontrol', 'othercontrol') { - my ($is_legal) = grep { $settings->{$field} == $_ } - (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY); - defined $is_legal || ThrowCodeError('product_illegal_group_control', - { field => $field, value => $settings->{$field} }); - } - unless ($settings->{membercontrol} == $settings->{othercontrol} - || $settings->{membercontrol} == CONTROLMAPSHOWN - || ($settings->{membercontrol} == CONTROLMAPDEFAULT - && $settings->{othercontrol} != CONTROLMAPSHOWN)) - { - ThrowUserError('illegal_group_control_combination', {groupname => $group->name}); - } - $gs->{membercontrol} = $settings->{membercontrol}; - $gs->{othercontrol} = $settings->{othercontrol}; + my ($self, $group, $settings) = @_; + + $group->is_active_bug_group + || ThrowUserError('product_illegal_group', {group => $group}); + + scalar(keys %$settings) + || ThrowCodeError('product_empty_group_controls', {group => $group}); + + # We store current settings for this group. + my $gs = $self->group_controls->{$group->id}; + + # If there is no entry for this group yet, create a default hash. + unless (defined $gs) { + $gs = { + entry => 0, + membercontrol => CONTROLMAPNA, + othercontrol => CONTROLMAPNA, + canedit => 0, + editcomponents => 0, + editbugs => 0, + canconfirm => 0, + group => $group + }; + } + + # Both settings must be defined, or none of them can be updated. + if (defined $settings->{membercontrol} && defined $settings->{othercontrol}) { + + # Legality of control combination is a function of + # membercontrol\othercontrol + # NA SH DE MA + # NA + - - - + # SH + + + + + # DE + - + + + # MA - - - + + foreach my $field ('membercontrol', 'othercontrol') { + my ($is_legal) + = grep { $settings->{$field} == $_ } + (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY); + defined $is_legal || ThrowCodeError('product_illegal_group_control', + {field => $field, value => $settings->{$field}}); } - - foreach my $field ('entry', 'canedit', 'editcomponents', 'editbugs', 'canconfirm') { - next unless defined $settings->{$field}; - $gs->{$field} = $settings->{$field} ? 1 : 0; + unless ( + $settings->{membercontrol} == $settings->{othercontrol} + || $settings->{membercontrol} == CONTROLMAPSHOWN + || ( $settings->{membercontrol} == CONTROLMAPDEFAULT + && $settings->{othercontrol} != CONTROLMAPSHOWN) + ) + { + ThrowUserError('illegal_group_control_combination', + {groupname => $group->name}); } - $self->{group_controls}->{$group->id} = $gs; - $self->{check_group_controls} = 1; + $gs->{membercontrol} = $settings->{membercontrol}; + $gs->{othercontrol} = $settings->{othercontrol}; + } + + foreach + my $field ('entry', 'canedit', 'editcomponents', 'editbugs', 'canconfirm') + { + next unless defined $settings->{$field}; + $gs->{$field} = $settings->{$field} ? 1 : 0; + } + $self->{group_controls}->{$group->id} = $gs; + $self->{check_group_controls} = 1; } sub components { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{components}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{components}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM components WHERE product_id = ? - ORDER BY name}, undef, $self->id); + ORDER BY name}, undef, $self->id + ); - require Bugzilla::Component; - $self->{components} = Bugzilla::Component->new_from_list($ids); - } - return $self->{components}; + require Bugzilla::Component; + $self->{components} = Bugzilla::Component->new_from_list($ids); + } + return $self->{components}; } sub group_controls { - my ($self, $full_data) = @_; - my $dbh = Bugzilla->dbh; - - # By default, we don't return groups which are not listed in - # group_control_map. If $full_data is true, then we also - # return groups whose settings could be set for the product. - my $where_or_and = 'WHERE'; - my $and_or_where = 'AND'; - if ($full_data) { - $where_or_and = 'AND'; - $and_or_where = 'WHERE'; - } - - # If $full_data is true, we collect all the data in all cases, - # even if the cache is already populated. - # $full_data is never used except in the very special case where - # all configurable bug groups are displayed to administrators, - # so we don't care about collecting all the data again in this case. - if (!defined $self->{group_controls} || $full_data) { - # Include name to the list, to allow us sorting data more easily. - my $query = qq{SELECT id, name, entry, membercontrol, othercontrol, + my ($self, $full_data) = @_; + my $dbh = Bugzilla->dbh; + + # By default, we don't return groups which are not listed in + # group_control_map. If $full_data is true, then we also + # return groups whose settings could be set for the product. + my $where_or_and = 'WHERE'; + my $and_or_where = 'AND'; + if ($full_data) { + $where_or_and = 'AND'; + $and_or_where = 'WHERE'; + } + + # If $full_data is true, we collect all the data in all cases, + # even if the cache is already populated. + # $full_data is never used except in the very special case where + # all configurable bug groups are displayed to administrators, + # so we don't care about collecting all the data again in this case. + if (!defined $self->{group_controls} || $full_data) { + + # Include name to the list, to allow us sorting data more easily. + my $query = qq{SELECT id, name, entry, membercontrol, othercontrol, canedit, editcomponents, editbugs, canconfirm FROM groups LEFT JOIN group_control_map ON id = group_id $where_or_and product_id = ? $and_or_where isbuggroup = 1}; - $self->{group_controls} = - $dbh->selectall_hashref($query, 'id', undef, $self->id); - - # For each group ID listed above, create and store its group object. - my @gids = keys %{$self->{group_controls}}; - my $groups = Bugzilla::Group->new_from_list(\@gids); - $self->{group_controls}->{$_->id}->{group} = $_ foreach @$groups; - } - - # We never cache bug counts, for the same reason as above. - if ($full_data) { - my $counts = - $dbh->selectall_arrayref('SELECT group_id, COUNT(bugs.bug_id) AS bug_count + $self->{group_controls} + = $dbh->selectall_hashref($query, 'id', undef, $self->id); + + # For each group ID listed above, create and store its group object. + my @gids = keys %{$self->{group_controls}}; + my $groups = Bugzilla::Group->new_from_list(\@gids); + $self->{group_controls}->{$_->id}->{group} = $_ foreach @$groups; + } + + # We never cache bug counts, for the same reason as above. + if ($full_data) { + my $counts = $dbh->selectall_arrayref( + 'SELECT group_id, COUNT(bugs.bug_id) AS bug_count FROM bug_group_map INNER JOIN bugs ON bugs.bug_id = bug_group_map.bug_id - WHERE bugs.product_id = ? ' . - $dbh->sql_group_by('group_id'), - {'Slice' => {}}, $self->id); - foreach my $data (@$counts) { - $self->{group_controls}->{$data->{group_id}}->{bug_count} = $data->{bug_count}; - } + WHERE bugs.product_id = ? ' + . $dbh->sql_group_by('group_id'), {'Slice' => {}}, $self->id + ); + foreach my $data (@$counts) { + $self->{group_controls}->{$data->{group_id}}->{bug_count} = $data->{bug_count}; } - return $self->{group_controls}; + } + return $self->{group_controls}; } sub groups_available { - my ($self) = @_; - return $self->{groups_available} if defined $self->{groups_available}; - my $dbh = Bugzilla->dbh; - my $shown = CONTROLMAPSHOWN; - my $default = CONTROLMAPDEFAULT; - my %member_groups = @{ $dbh->selectcol_arrayref( - "SELECT group_id, membercontrol + my ($self) = @_; + return $self->{groups_available} if defined $self->{groups_available}; + my $dbh = Bugzilla->dbh; + my $shown = CONTROLMAPSHOWN; + my $default = CONTROLMAPDEFAULT; + my %member_groups = @{ + $dbh->selectcol_arrayref( + "SELECT group_id, membercontrol FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ? AND (membercontrol = $shown OR membercontrol = $default) - AND " . Bugzilla->user->groups_in_sql(), - {Columns=>[1,2]}, $self->id) }; - # We don't need to check the group membership here, because we only - # add these groups to the list below if the group isn't already listed - # for membercontrol. - my %other_groups = @{ $dbh->selectcol_arrayref( - "SELECT group_id, othercontrol + AND " . Bugzilla->user->groups_in_sql(), {Columns => [1, 2]}, + $self->id + ) + }; + + # We don't need to check the group membership here, because we only + # add these groups to the list below if the group isn't already listed + # for membercontrol. + my %other_groups = @{ + $dbh->selectcol_arrayref( + "SELECT group_id, othercontrol FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ? - AND (othercontrol = $shown OR othercontrol = $default)", - {Columns=>[1,2]}, $self->id) }; - - # If the user is a member, then we use the membercontrol value. - # Otherwise, we use the othercontrol value. - my %all_groups = %member_groups; - foreach my $id (keys %other_groups) { - if (!defined $all_groups{$id}) { - $all_groups{$id} = $other_groups{$id}; - } + AND (othercontrol = $shown OR othercontrol = $default)", + {Columns => [1, 2]}, $self->id + ) + }; + + # If the user is a member, then we use the membercontrol value. + # Otherwise, we use the othercontrol value. + my %all_groups = %member_groups; + foreach my $id (keys %other_groups) { + if (!defined $all_groups{$id}) { + $all_groups{$id} = $other_groups{$id}; } + } - my $available = Bugzilla::Group->new_from_list([keys %all_groups]); - foreach my $group (@$available) { - $group->{is_default} = 1 if $all_groups{$group->id} == $default; - } + my $available = Bugzilla::Group->new_from_list([keys %all_groups]); + foreach my $group (@$available) { + $group->{is_default} = 1 if $all_groups{$group->id} == $default; + } - $self->{groups_available} = $available; - return $self->{groups_available}; + $self->{groups_available} = $available; + return $self->{groups_available}; } sub groups_mandatory { - my ($self) = @_; - return $self->{groups_mandatory} if $self->{groups_mandatory}; - my $groups = Bugzilla->user->groups_as_string; - my $mandatory = CONTROLMAPMANDATORY; - # For membercontrol we don't check group_id IN, because if membercontrol - # is Mandatory, the group is Mandatory for everybody, regardless of their - # group membership. - my $ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT group_id + my ($self) = @_; + return $self->{groups_mandatory} if $self->{groups_mandatory}; + my $groups = Bugzilla->user->groups_as_string; + my $mandatory = CONTROLMAPMANDATORY; + + # For membercontrol we don't check group_id IN, because if membercontrol + # is Mandatory, the group is Mandatory for everybody, regardless of their + # group membership. + my $ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT group_id FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE product_id = ? AND isactive = 1 AND (membercontrol = $mandatory OR (othercontrol = $mandatory - AND group_id NOT IN ($groups)))", - undef, $self->id); - $self->{groups_mandatory} = Bugzilla::Group->new_from_list($ids); - return $self->{groups_mandatory}; + AND group_id NOT IN ($groups)))", undef, $self->id + ); + $self->{groups_mandatory} = Bugzilla::Group->new_from_list($ids); + return $self->{groups_mandatory}; } # We don't just check groups_valid, because we want to know specifically # if this group can be validly set by the currently-logged-in user. sub group_is_settable { - my ($self, $group) = @_; + my ($self, $group) = @_; - return 0 unless ($group->is_active && $group->is_bug_group); + return 0 unless ($group->is_active && $group->is_bug_group); - my $is_mandatory = grep { $group->id == $_->id } - @{ $self->groups_mandatory }; - my $is_available = grep { $group->id == $_->id } - @{ $self->groups_available }; - return ($is_mandatory or $is_available) ? 1 : 0; + my $is_mandatory = grep { $group->id == $_->id } @{$self->groups_mandatory}; + my $is_available = grep { $group->id == $_->id } @{$self->groups_available}; + return ($is_mandatory or $is_available) ? 1 : 0; } sub group_is_valid { - my ($self, $group) = @_; - return grep($_->id == $group->id, @{ $self->groups_valid }) ? 1 : 0; + my ($self, $group) = @_; + return grep($_->id == $group->id, @{$self->groups_valid}) ? 1 : 0; } sub groups_valid { - my ($self) = @_; - return $self->{groups_valid} if defined $self->{groups_valid}; - - # Note that we don't check OtherControl below, because there is no - # valid NA/* combination. - my $ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT DISTINCT group_id + my ($self) = @_; + return $self->{groups_valid} if defined $self->{groups_valid}; + + # Note that we don't check OtherControl below, because there is no + # valid NA/* combination. + my $ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT DISTINCT group_id FROM group_control_map AS gcm INNER JOIN groups ON gcm.group_id = groups.id WHERE product_id = ? AND isbuggroup = 1 - AND membercontrol != " . CONTROLMAPNA, undef, $self->id); - $self->{groups_valid} = Bugzilla::Group->new_from_list($ids); - return $self->{groups_valid}; + AND membercontrol != " . CONTROLMAPNA, undef, $self->id + ); + $self->{groups_valid} = Bugzilla::Group->new_from_list($ids); + return $self->{groups_valid}; } sub versions { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{versions}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{versions}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM versions - WHERE product_id = ?}, undef, $self->id); + WHERE product_id = ?}, undef, $self->id + ); - $self->{versions} = Bugzilla::Version->new_from_list($ids); - } - return $self->{versions}; + $self->{versions} = Bugzilla::Version->new_from_list($ids); + } + return $self->{versions}; } sub milestones { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{milestones}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{milestones}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM milestones - WHERE product_id = ?}, undef, $self->id); - - $self->{milestones} = Bugzilla::Milestone->new_from_list($ids); - } - return $self->{milestones}; + WHERE product_id = ?}, undef, $self->id + ); + + $self->{milestones} = Bugzilla::Milestone->new_from_list($ids); + } + return $self->{milestones}; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(qq{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + qq{ SELECT COUNT(bug_id) FROM bugs - WHERE product_id = ?}, undef, $self->id); + WHERE product_id = ?}, undef, $self->id + ); - } - return $self->{'bug_count'}; + } + return $self->{'bug_count'}; } sub bug_ids { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'bug_ids'}) { - $self->{'bug_ids'} = - $dbh->selectcol_arrayref(q{SELECT bug_id FROM bugs - WHERE product_id = ?}, - undef, $self->id); - } - return $self->{'bug_ids'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'bug_ids'}) { + $self->{'bug_ids'} = $dbh->selectcol_arrayref( + q{SELECT bug_id FROM bugs + WHERE product_id = ?}, undef, $self->id + ); + } + return $self->{'bug_ids'}; } sub user_has_access { - my ($self, $user) = @_; + my ($self, $user) = @_; - return Bugzilla->dbh->selectrow_array( - 'SELECT CASE WHEN group_id IS NULL THEN 1 ELSE 0 END + return Bugzilla->dbh->selectrow_array( + 'SELECT CASE WHEN group_id IS NULL THEN 1 ELSE 0 END FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id AND group_control_map.entry != 0 AND group_id NOT IN (' . $user->groups_as_string . ') - WHERE products.id = ? ' . Bugzilla->dbh->sql_limit(1), - undef, $self->id); + WHERE products.id = ? ' . Bugzilla->dbh->sql_limit(1), undef, $self->id + ); } sub flag_types { - my $self = shift; - - return $self->{'flag_types'} if defined $self->{'flag_types'}; - - # We cache flag types to avoid useless calls to get_clusions(). - my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {}; - $self->{flag_types} = {}; - my $prod_id = $self->id; - my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id }); - - foreach my $type ('bug', 'attachment') { - my @flags = grep { $_->target_type eq $type } @$flagtypes; - $self->{flag_types}->{$type} = \@flags; - - # Also populate component flag types, while we are here. - foreach my $comp (@{$self->components}) { - $comp->{flag_types} ||= {}; - my $comp_id = $comp->id; - - foreach my $flag (@flags) { - my $flag_id = $flag->id; - $cache->{$flag_id} ||= $flag; - my $i = $cache->{$flag_id}->inclusions_as_hash; - my $e = $cache->{$flag_id}->exclusions_as_hash; - my $included = $i->{0}->{0} || $i->{0}->{$comp_id} - || $i->{$prod_id}->{0} || $i->{$prod_id}->{$comp_id}; - my $excluded = $e->{0}->{0} || $e->{0}->{$comp_id} - || $e->{$prod_id}->{0} || $e->{$prod_id}->{$comp_id}; - push(@{$comp->{flag_types}->{$type}}, $flag) if ($included && !$excluded); - } - } + my $self = shift; + + return $self->{'flag_types'} if defined $self->{'flag_types'}; + + # We cache flag types to avoid useless calls to get_clusions(). + my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {}; + $self->{flag_types} = {}; + my $prod_id = $self->id; + my $flagtypes = Bugzilla::FlagType::match({product_id => $prod_id}); + + foreach my $type ('bug', 'attachment') { + my @flags = grep { $_->target_type eq $type } @$flagtypes; + $self->{flag_types}->{$type} = \@flags; + + # Also populate component flag types, while we are here. + foreach my $comp (@{$self->components}) { + $comp->{flag_types} ||= {}; + my $comp_id = $comp->id; + + foreach my $flag (@flags) { + my $flag_id = $flag->id; + $cache->{$flag_id} ||= $flag; + my $i = $cache->{$flag_id}->inclusions_as_hash; + my $e = $cache->{$flag_id}->exclusions_as_hash; + my $included + = $i->{0}->{0} + || $i->{0}->{$comp_id} + || $i->{$prod_id}->{0} + || $i->{$prod_id}->{$comp_id}; + my $excluded + = $e->{0}->{0} + || $e->{0}->{$comp_id} + || $e->{$prod_id}->{0} + || $e->{$prod_id}->{$comp_id}; + push(@{$comp->{flag_types}->{$type}}, $flag) if ($included && !$excluded); + } } - return $self->{'flag_types'}; + } + return $self->{'flag_types'}; } sub classification { - my $self = shift; - $self->{'classification'} ||= - new Bugzilla::Classification({ id => $self->classification_id, cache => 1 }); - return $self->{'classification'}; + my $self = shift; + $self->{'classification'} ||= new Bugzilla::Classification( + {id => $self->classification_id, cache => 1}); + return $self->{'classification'}; } ############################### @@ -825,29 +887,29 @@ sub classification { ############################### sub allows_unconfirmed { return $_[0]->{'allows_unconfirmed'}; } -sub description { return $_[0]->{'description'}; } -sub is_active { return $_[0]->{'isactive'}; } -sub default_milestone { return $_[0]->{'defaultmilestone'}; } -sub classification_id { return $_[0]->{'classification_id'}; } +sub description { return $_[0]->{'description'}; } +sub is_active { return $_[0]->{'isactive'}; } +sub default_milestone { return $_[0]->{'defaultmilestone'}; } +sub classification_id { return $_[0]->{'classification_id'}; } ############################### #### Subroutines ###### ############################### sub check { - my ($class, $params) = @_; - $params = { name => $params } if !ref $params; - if (!$params->{allow_inaccessible}) { - $params->{_error} = 'product_access_denied'; - } - my $product = $class->SUPER::check($params); - - if (!$params->{allow_inaccessible} - && !Bugzilla->user->can_access_product($product)) - { - ThrowUserError('product_access_denied', $params); - } - return $product; + my ($class, $params) = @_; + $params = {name => $params} if !ref $params; + if (!$params->{allow_inaccessible}) { + $params->{_error} = 'product_access_denied'; + } + my $product = $class->SUPER::check($params); + + if ( !$params->{allow_inaccessible} + && !Bugzilla->user->can_access_product($product)) + { + ThrowUserError('product_access_denied', $params); + } + return $product; } 1; diff --git a/Bugzilla/RNG.pm b/Bugzilla/RNG.pm index 96e442fa0..b92cbd720 100644 --- a/Bugzilla/RNG.pm +++ b/Bugzilla/RNG.pm @@ -27,7 +27,7 @@ our @EXPORT_OK = qw(rand srand irand); use constant DIVIDE_BY => 2**32; # How many bytes of seed to read. -use constant SEED_SIZE => 16; # 128 bits. +use constant SEED_SIZE => 16; # 128 bits. ################# # Windows Stuff # @@ -42,10 +42,11 @@ use constant PROV_RSA_FULL => 1; # Flags for CryptGenRandom: # Don't ever display a UI to the user, just fail if one would be needed. use constant CRYPT_SILENT => 64; + # Don't require existing public/private keypairs. use constant CRYPT_VERIFYCONTEXT => 0xF0000000; -# For some reason, BOOLEAN doesn't work properly as a return type with +# For some reason, BOOLEAN doesn't work properly as a return type with # Win32::API. use constant RTLGENRANDOM_PROTO => <<END; INT SystemFunction036( @@ -59,40 +60,42 @@ END ################# sub rand (;$) { - my ($limit) = @_; - my $int = irand(); - return _to_float($int, $limit); + my ($limit) = @_; + my $int = irand(); + return _to_float($int, $limit); } sub irand (;$) { - my ($limit) = @_; - Bugzilla::RNG::srand() if !defined $RNG; - my $int = $RNG->irand(); - if (defined $limit) { - # We can't just use the mod operator because it will bias - # our output. Search for "modulo bias" on the Internet for - # details. This is slower than mod(), but does not have a bias, - # as demonstrated by Math::Random::Secure's uniform.t test. - return int(_to_float($int, $limit)); - } - return $int; + my ($limit) = @_; + Bugzilla::RNG::srand() if !defined $RNG; + my $int = $RNG->irand(); + if (defined $limit) { + + # We can't just use the mod operator because it will bias + # our output. Search for "modulo bias" on the Internet for + # details. This is slower than mod(), but does not have a bias, + # as demonstrated by Math::Random::Secure's uniform.t test. + return int(_to_float($int, $limit)); + } + return $int; } sub srand (;$) { - my ($value) = @_; - # Remove any RNG that might already have been made. - $RNG = undef; - my %args; - if (defined $value) { - $args{seed} = $value; - } - $RNG = _create_rng(\%args); + my ($value) = @_; + + # Remove any RNG that might already have been made. + $RNG = undef; + my %args; + if (defined $value) { + $args{seed} = $value; + } + $RNG = _create_rng(\%args); } sub _to_float { - my ($integer, $limit) = @_; - $limit ||= 1; - return ($integer / DIVIDE_BY) * $limit; + my ($integer, $limit) = @_; + $limit ||= 1; + return ($integer / DIVIDE_BY) * $limit; } ########################## @@ -100,123 +103,123 @@ sub _to_float { ########################## sub _create_rng { - my ($params) = @_; + my ($params) = @_; - if (!defined $params->{seed}) { - $params->{seed} = _get_seed(); - } + if (!defined $params->{seed}) { + $params->{seed} = _get_seed(); + } - _check_seed($params->{seed}); + _check_seed($params->{seed}); - my @seed_ints = unpack('L*', $params->{seed}); + my @seed_ints = unpack('L*', $params->{seed}); - my $rng = Math::Random::ISAAC->new(@seed_ints); + my $rng = Math::Random::ISAAC->new(@seed_ints); - # It's faster to skip the frontend interface of Math::Random::ISAAC - # and just use the backend directly. However, in case the internal - # code of Math::Random::ISAAC changes at some point, we do make sure - # that the {backend} element actually exists first. - return $rng->{backend} ? $rng->{backend} : $rng; + # It's faster to skip the frontend interface of Math::Random::ISAAC + # and just use the backend directly. However, in case the internal + # code of Math::Random::ISAAC changes at some point, we do make sure + # that the {backend} element actually exists first. + return $rng->{backend} ? $rng->{backend} : $rng; } sub _check_seed { - my ($seed) = @_; - if (length($seed) < 8) { - warn "Your seed is less than 8 bytes (64 bits). It could be" - . " easy to crack"; - } - # If it looks like we were seeded with a 32-bit integer, warn the - # user that they are making a dangerous, easily-crackable mistake. - elsif (length($seed) <= 10 and $seed =~ /^\d+$/) { - warn "RNG seeded with a 32-bit integer, this is easy to crack"; - } + my ($seed) = @_; + if (length($seed) < 8) { + warn "Your seed is less than 8 bytes (64 bits). It could be" . " easy to crack"; + } + + # If it looks like we were seeded with a 32-bit integer, warn the + # user that they are making a dangerous, easily-crackable mistake. + elsif (length($seed) <= 10 and $seed =~ /^\d+$/) { + warn "RNG seeded with a 32-bit integer, this is easy to crack"; + } } sub _get_seed { - return _windows_seed() if ON_WINDOWS; + return _windows_seed() if ON_WINDOWS; - if (-r '/dev/urandom') { - return _read_seed_from('/dev/urandom'); - } + if (-r '/dev/urandom') { + return _read_seed_from('/dev/urandom'); + } - return _read_seed_from('/dev/random'); + return _read_seed_from('/dev/random'); } sub _read_seed_from { - my ($from) = @_; - - open(my $fh, '<', $from) or die "$from: $!"; - my $buffer; - read($fh, $buffer, SEED_SIZE); - if (length($buffer) < SEED_SIZE) { - die "Could not read enough seed bytes from $from, got only " - . length($buffer); - } - close $fh; - return $buffer; + my ($from) = @_; + + open(my $fh, '<', $from) or die "$from: $!"; + my $buffer; + read($fh, $buffer, SEED_SIZE); + if (length($buffer) < SEED_SIZE) { + die "Could not read enough seed bytes from $from, got only " . length($buffer); + } + close $fh; + return $buffer; } sub _windows_seed { - my ($major, $minor) = (Win32::GetOSVersion())[1,2]; - if ($major < 5) { - die "Bugzilla does not support versions of Windows before" - . " Windows 2000"; - } - # This means Windows 2000. - if ($major == 5 and $minor == 0) { - return _win2k_seed(); - } - - my $rtlgenrand = Win32::API->new('advapi32', RTLGENRANDOM_PROTO); - if (!defined $rtlgenrand) { - die "Could not import RtlGenRand: $^E"; - } - my $buffer = chr(0) x SEED_SIZE; - my $result = $rtlgenrand->Call($buffer, SEED_SIZE); - if (!$result) { - die "RtlGenRand failed: $^E"; - } - return $buffer; + my ($major, $minor) = (Win32::GetOSVersion())[1, 2]; + if ($major < 5) { + die "Bugzilla does not support versions of Windows before" . " Windows 2000"; + } + + # This means Windows 2000. + if ($major == 5 and $minor == 0) { + return _win2k_seed(); + } + + my $rtlgenrand = Win32::API->new('advapi32', RTLGENRANDOM_PROTO); + if (!defined $rtlgenrand) { + die "Could not import RtlGenRand: $^E"; + } + my $buffer = chr(0) x SEED_SIZE; + my $result = $rtlgenrand->Call($buffer, SEED_SIZE); + if (!$result) { + die "RtlGenRand failed: $^E"; + } + return $buffer; } sub _win2k_seed { - my $crypt_acquire = Win32::API->new( - "advapi32", 'CryptAcquireContext', 'PPPNN', 'I'); - if (!defined $crypt_acquire) { - die "Could not import CryptAcquireContext: $^E"; - } - - my $crypt_release = Win32::API->new( - "advapi32", 'CryptReleaseContext', 'NN', 'I'); - if (!defined $crypt_release) { - die "Could not import CryptReleaseContext: $^E"; - } - - my $crypt_gen_random = Win32::API->new( - "advapi32", 'CryptGenRandom', 'NNP', 'I'); - if (!defined $crypt_gen_random) { - die "Could not import CryptGenRandom: $^E"; - } - - my $context = chr(0) x Win32::API::Type->sizeof('PULONG'); - my $acquire_result = $crypt_acquire->Call( - $context, 0, 0, PROV_RSA_FULL, CRYPT_SILENT | CRYPT_VERIFYCONTEXT); - if (!defined $acquire_result) { - die "CryptAcquireContext failed: $^E"; - } - - my $pack_type = Win32::API::Type::packing('PULONG'); - $context = unpack($pack_type, $context); - - my $buffer = chr(0) x SEED_SIZE; - my $rand_result = $crypt_gen_random->Call($context, SEED_SIZE, $buffer); - my $rand_error = $^E; - # We don't check this if it fails, we don't care. - $crypt_release->Call($context, 0); - if (!defined $rand_result) { - die "CryptGenRandom failed: $rand_error"; - } - return $buffer; + my $crypt_acquire + = Win32::API->new("advapi32", 'CryptAcquireContext', 'PPPNN', 'I'); + if (!defined $crypt_acquire) { + die "Could not import CryptAcquireContext: $^E"; + } + + my $crypt_release + = Win32::API->new("advapi32", 'CryptReleaseContext', 'NN', 'I'); + if (!defined $crypt_release) { + die "Could not import CryptReleaseContext: $^E"; + } + + my $crypt_gen_random + = Win32::API->new("advapi32", 'CryptGenRandom', 'NNP', 'I'); + if (!defined $crypt_gen_random) { + die "Could not import CryptGenRandom: $^E"; + } + + my $context = chr(0) x Win32::API::Type->sizeof('PULONG'); + my $acquire_result = $crypt_acquire->Call($context, 0, 0, PROV_RSA_FULL, + CRYPT_SILENT | CRYPT_VERIFYCONTEXT); + if (!defined $acquire_result) { + die "CryptAcquireContext failed: $^E"; + } + + my $pack_type = Win32::API::Type::packing('PULONG'); + $context = unpack($pack_type, $context); + + my $buffer = chr(0) x SEED_SIZE; + my $rand_result = $crypt_gen_random->Call($context, SEED_SIZE, $buffer); + my $rand_error = $^E; + + # We don't check this if it fails, we don't care. + $crypt_release->Call($context, 0); + if (!defined $rand_result) { + die "CryptGenRandom failed: $rand_error"; + } + return $buffer; } 1; diff --git a/Bugzilla/Report.pm b/Bugzilla/Report.pm index 10af2ea9e..84ea3b38b 100644 --- a/Bugzilla/Report.pm +++ b/Bugzilla/Report.pm @@ -26,43 +26,40 @@ use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - user_id - name - query + id + user_id + name + query ); use constant UPDATE_COLUMNS => qw( - name - query + name + query ); -use constant VALIDATORS => { - name => \&_check_name, - query => \&_check_query, -}; +use constant VALIDATORS => {name => \&_check_name, query => \&_check_query,}; ############## # Validators # ############## sub _check_name { - my ($invocant, $name) = @_; - $name = clean_text($name); - $name || ThrowUserError("report_name_missing"); - $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); - if (length($name) > MAX_LEN_QUERY_NAME) { - ThrowUserError("query_name_too_long"); - } - return $name; + my ($invocant, $name) = @_; + $name = clean_text($name); + $name || ThrowUserError("report_name_missing"); + $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); + if (length($name) > MAX_LEN_QUERY_NAME) { + ThrowUserError("query_name_too_long"); + } + return $name; } sub _check_query { - my ($invocant, $query) = @_; - $query || ThrowUserError("buglist_parameters_required"); - my $cgi = new Bugzilla::CGI($query); - $cgi->clean_search_url; - return $cgi->query_string; + my ($invocant, $query) = @_; + $query || ThrowUserError("buglist_parameters_required"); + my $cgi = new Bugzilla::CGI($query); + $cgi->clean_search_url; + return $cgi->query_string; } ############# @@ -71,7 +68,7 @@ sub _check_query { sub query { return $_[0]->{'query'}; } -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_query { $_[0]->set('query', $_[1]); } ########### @@ -79,25 +76,26 @@ sub set_query { $_[0]->set('query', $_[1]); } ########### sub create { - my $class = shift; - my $param = shift; + my $class = shift; + my $param = shift; - Bugzilla->login(LOGIN_REQUIRED); - $param->{'user_id'} = Bugzilla->user->id; + Bugzilla->login(LOGIN_REQUIRED); + $param->{'user_id'} = Bugzilla->user->id; - unshift @_, $param; - my $self = $class->SUPER::create(@_); + unshift @_, $param; + my $self = $class->SUPER::create(@_); } sub check { - my $class = shift; - my $report = $class->SUPER::check(@_); - my $user = Bugzilla->user; - if ( grep($_->id eq $report->id, @{$user->reports})) { - return $report; - } else { - ThrowUserError('report_access_denied'); - } + my $class = shift; + my $report = $class->SUPER::check(@_); + my $user = Bugzilla->user; + if (grep($_->id eq $report->id, @{$user->reports})) { + return $report; + } + else { + ThrowUserError('report_access_denied'); + } } 1; diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index 0694dd98c..b15cbabd4 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -13,8 +13,8 @@ use warnings; use parent qw(Exporter); @Bugzilla::Search::EXPORT = qw( - IsValidQueryType - split_order_term + IsValidQueryType + split_order_term ); use Bugzilla::Error; @@ -135,311 +135,275 @@ use constant NUMBER_REGEX => qr/ # If you specify a search type in the boolean charts, this describes # which operator maps to which internal function here. use constant OPERATORS => { - equals => \&_simple_operator, - notequals => \&_simple_operator, - casesubstring => \&_casesubstring, - substring => \&_substring, - substr => \&_substring, - notsubstring => \&_notsubstring, - regexp => \&_regexp, - notregexp => \&_notregexp, - lessthan => \&_simple_operator, - lessthaneq => \&_simple_operator, - matches => sub { ThrowUserError("search_content_without_matches"); }, - notmatches => sub { ThrowUserError("search_content_without_matches"); }, - greaterthan => \&_simple_operator, - greaterthaneq => \&_simple_operator, - anyexact => \&_anyexact, - anywordssubstr => \&_anywordsubstr, - allwordssubstr => \&_allwordssubstr, - nowordssubstr => \&_nowordssubstr, - anywords => \&_anywords, - allwords => \&_allwords, - nowords => \&_nowords, - changedbefore => \&_changedbefore_changedafter, - changedafter => \&_changedbefore_changedafter, - changedfrom => \&_changedfrom_changedto, - changedto => \&_changedfrom_changedto, - changedby => \&_changedby, - isempty => \&_isempty, - isnotempty => \&_isnotempty, + equals => \&_simple_operator, + notequals => \&_simple_operator, + casesubstring => \&_casesubstring, + substring => \&_substring, + substr => \&_substring, + notsubstring => \&_notsubstring, + regexp => \&_regexp, + notregexp => \&_notregexp, + lessthan => \&_simple_operator, + lessthaneq => \&_simple_operator, + matches => sub { ThrowUserError("search_content_without_matches"); }, + notmatches => sub { ThrowUserError("search_content_without_matches"); }, + greaterthan => \&_simple_operator, + greaterthaneq => \&_simple_operator, + anyexact => \&_anyexact, + anywordssubstr => \&_anywordsubstr, + allwordssubstr => \&_allwordssubstr, + nowordssubstr => \&_nowordssubstr, + anywords => \&_anywords, + allwords => \&_allwords, + nowords => \&_nowords, + changedbefore => \&_changedbefore_changedafter, + changedafter => \&_changedbefore_changedafter, + changedfrom => \&_changedfrom_changedto, + changedto => \&_changedfrom_changedto, + changedby => \&_changedby, + isempty => \&_isempty, + isnotempty => \&_isnotempty, }; # Some operators are really just standard SQL operators, and are # all implemented by the _simple_operator function, which uses this # constant. use constant SIMPLE_OPERATORS => { - equals => '=', - notequals => '!=', - greaterthan => '>', - greaterthaneq => '>=', - lessthan => '<', - lessthaneq => "<=", + equals => '=', + notequals => '!=', + greaterthan => '>', + greaterthaneq => '>=', + lessthan => '<', + lessthaneq => "<=", }; # Most operators just reverse by removing or adding "not" from/to them. # However, some operators reverse in a different way, so those are listed # here. use constant OPERATOR_REVERSE => { - nowords => 'anywords', - nowordssubstr => 'anywordssubstr', - anywords => 'nowords', - anywordssubstr => 'nowordssubstr', - lessthan => 'greaterthaneq', - lessthaneq => 'greaterthan', - greaterthan => 'lessthaneq', - greaterthaneq => 'lessthan', - isempty => 'isnotempty', - isnotempty => 'isempty', - # The following don't currently have reversals: - # casesubstring, anyexact, allwords, allwordssubstr + nowords => 'anywords', + nowordssubstr => 'anywordssubstr', + anywords => 'nowords', + anywordssubstr => 'nowordssubstr', + lessthan => 'greaterthaneq', + lessthaneq => 'greaterthan', + greaterthan => 'lessthaneq', + greaterthaneq => 'lessthan', + isempty => 'isnotempty', + isnotempty => 'isempty', + + # The following don't currently have reversals: + # casesubstring, anyexact, allwords, allwordssubstr }; # For these operators, even if a field is numeric (is_numeric returns true), # we won't treat the input like a number. use constant NON_NUMERIC_OPERATORS => qw( - changedafter - changedbefore - changedfrom - changedto - regexp - notregexp + changedafter + changedbefore + changedfrom + changedto + regexp + notregexp ); # These operators ignore the entered value use constant NO_VALUE_OPERATORS => qw( - isempty - isnotempty + isempty + isnotempty ); use constant MULTI_SELECT_OVERRIDE => { - notequals => \&_multiselect_negative, - notregexp => \&_multiselect_negative, - notsubstring => \&_multiselect_negative, - nowords => \&_multiselect_negative, - nowordssubstr => \&_multiselect_negative, - - allwords => \&_multiselect_multiple, - allwordssubstr => \&_multiselect_multiple, - anyexact => \&_multiselect_multiple, - anywords => \&_multiselect_multiple, - anywordssubstr => \&_multiselect_multiple, - - _non_changed => \&_multiselect_nonchanged, + notequals => \&_multiselect_negative, + notregexp => \&_multiselect_negative, + notsubstring => \&_multiselect_negative, + nowords => \&_multiselect_negative, + nowordssubstr => \&_multiselect_negative, + + allwords => \&_multiselect_multiple, + allwordssubstr => \&_multiselect_multiple, + anyexact => \&_multiselect_multiple, + anywords => \&_multiselect_multiple, + anywordssubstr => \&_multiselect_multiple, + + _non_changed => \&_multiselect_nonchanged, }; use constant OPERATOR_FIELD_OVERRIDE => { - # User fields - 'attachments.submitter' => { - _non_changed => \&_user_nonchanged, - }, - assigned_to => { - _non_changed => \&_user_nonchanged, - }, - assigned_to_realname => { - _non_changed => \&_user_nonchanged, - }, - cc => { - _non_changed => \&_user_nonchanged, - }, - commenter => { - _non_changed => \&_user_nonchanged, - }, - reporter => { - _non_changed => \&_user_nonchanged, - }, - reporter_realname => { - _non_changed => \&_user_nonchanged, - }, - 'requestees.login_name' => { - _non_changed => \&_user_nonchanged, - }, - 'setters.login_name' => { - _non_changed => \&_user_nonchanged, - }, - qa_contact => { - _non_changed => \&_user_nonchanged, - }, - qa_contact_realname => { - _non_changed => \&_user_nonchanged, - }, - # General Bug Fields - alias => { _non_changed => \&_alias_nonchanged }, - 'attach_data.thedata' => MULTI_SELECT_OVERRIDE, - # We check all attachment fields against this. - attachments => MULTI_SELECT_OVERRIDE, - blocked => MULTI_SELECT_OVERRIDE, - bug_file_loc => { _non_changed => \&_nullable }, - bug_group => MULTI_SELECT_OVERRIDE, - classification => { - _non_changed => \&_classification_nonchanged, - }, - component => { - _non_changed => \&_component_nonchanged, - }, - content => { - matches => \&_content_matches, - notmatches => \&_content_matches, - _default => sub { ThrowUserError("search_content_without_matches"); }, - }, - days_elapsed => { - _default => \&_days_elapsed, - }, - dependson => MULTI_SELECT_OVERRIDE, - keywords => MULTI_SELECT_OVERRIDE, - 'flagtypes.name' => { - _non_changed => \&_flagtypes_nonchanged, - }, - longdesc => { - changedby => \&_long_desc_changedby, - changedbefore => \&_long_desc_changedbefore_after, - changedafter => \&_long_desc_changedbefore_after, - _non_changed => \&_long_desc_nonchanged, - }, - 'longdescs.count' => { - changedby => \&_long_desc_changedby, - changedbefore => \&_long_desc_changedbefore_after, - changedafter => \&_long_desc_changedbefore_after, - changedfrom => \&_invalid_combination, - changedto => \&_invalid_combination, - _default => \&_long_descs_count, - }, - 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE, - owner_idle_time => { - greaterthan => \&_owner_idle_time_greater_less, - greaterthaneq => \&_owner_idle_time_greater_less, - lessthan => \&_owner_idle_time_greater_less, - lessthaneq => \&_owner_idle_time_greater_less, - _default => \&_invalid_combination, - }, - product => { - _non_changed => \&_product_nonchanged, - }, - tag => MULTI_SELECT_OVERRIDE, - comment_tag => MULTI_SELECT_OVERRIDE, - - # Timetracking Fields - deadline => { _non_changed => \&_deadline }, - percentage_complete => { - _non_changed => \&_percentage_complete, - }, - work_time => { - changedby => \&_work_time_changedby, - changedbefore => \&_work_time_changedbefore_after, - changedafter => \&_work_time_changedbefore_after, - _default => \&_work_time, - }, - last_visit_ts => { - _non_changed => \&_last_visit_ts, - _default => \&_last_visit_ts_invalid_operator, - }, - - # Custom Fields - FIELD_TYPE_FREETEXT, { _non_changed => \&_nullable }, - FIELD_TYPE_BUG_ID, { _non_changed => \&_nullable_int }, - FIELD_TYPE_DATETIME, { _non_changed => \&_nullable_datetime }, - FIELD_TYPE_DATE, { _non_changed => \&_nullable_date }, - FIELD_TYPE_TEXTAREA, { _non_changed => \&_nullable }, - FIELD_TYPE_MULTI_SELECT, MULTI_SELECT_OVERRIDE, - FIELD_TYPE_BUG_URLS, MULTI_SELECT_OVERRIDE, + # User fields + 'attachments.submitter' => {_non_changed => \&_user_nonchanged,}, + assigned_to => {_non_changed => \&_user_nonchanged,}, + assigned_to_realname => {_non_changed => \&_user_nonchanged,}, + cc => {_non_changed => \&_user_nonchanged,}, + commenter => {_non_changed => \&_user_nonchanged,}, + reporter => {_non_changed => \&_user_nonchanged,}, + reporter_realname => {_non_changed => \&_user_nonchanged,}, + 'requestees.login_name' => {_non_changed => \&_user_nonchanged,}, + 'setters.login_name' => {_non_changed => \&_user_nonchanged,}, + qa_contact => {_non_changed => \&_user_nonchanged,}, + qa_contact_realname => {_non_changed => \&_user_nonchanged,}, + + # General Bug Fields + alias => {_non_changed => \&_alias_nonchanged}, + 'attach_data.thedata' => MULTI_SELECT_OVERRIDE, + + # We check all attachment fields against this. + attachments => MULTI_SELECT_OVERRIDE, + blocked => MULTI_SELECT_OVERRIDE, + bug_file_loc => {_non_changed => \&_nullable}, + bug_group => MULTI_SELECT_OVERRIDE, + classification => {_non_changed => \&_classification_nonchanged,}, + component => {_non_changed => \&_component_nonchanged,}, + content => { + matches => \&_content_matches, + notmatches => \&_content_matches, + _default => sub { ThrowUserError("search_content_without_matches"); }, + }, + days_elapsed => {_default => \&_days_elapsed,}, + dependson => MULTI_SELECT_OVERRIDE, + keywords => MULTI_SELECT_OVERRIDE, + 'flagtypes.name' => {_non_changed => \&_flagtypes_nonchanged,}, + longdesc => { + changedby => \&_long_desc_changedby, + changedbefore => \&_long_desc_changedbefore_after, + changedafter => \&_long_desc_changedbefore_after, + _non_changed => \&_long_desc_nonchanged, + }, + 'longdescs.count' => { + changedby => \&_long_desc_changedby, + changedbefore => \&_long_desc_changedbefore_after, + changedafter => \&_long_desc_changedbefore_after, + changedfrom => \&_invalid_combination, + changedto => \&_invalid_combination, + _default => \&_long_descs_count, + }, + 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE, + owner_idle_time => { + greaterthan => \&_owner_idle_time_greater_less, + greaterthaneq => \&_owner_idle_time_greater_less, + lessthan => \&_owner_idle_time_greater_less, + lessthaneq => \&_owner_idle_time_greater_less, + _default => \&_invalid_combination, + }, + product => {_non_changed => \&_product_nonchanged,}, + tag => MULTI_SELECT_OVERRIDE, + comment_tag => MULTI_SELECT_OVERRIDE, + + # Timetracking Fields + deadline => {_non_changed => \&_deadline}, + percentage_complete => {_non_changed => \&_percentage_complete,}, + work_time => { + changedby => \&_work_time_changedby, + changedbefore => \&_work_time_changedbefore_after, + changedafter => \&_work_time_changedbefore_after, + _default => \&_work_time, + }, + last_visit_ts => { + _non_changed => \&_last_visit_ts, + _default => \&_last_visit_ts_invalid_operator, + }, + + # Custom Fields + FIELD_TYPE_FREETEXT, + {_non_changed => \&_nullable}, + FIELD_TYPE_BUG_ID, + {_non_changed => \&_nullable_int}, + FIELD_TYPE_DATETIME, + {_non_changed => \&_nullable_datetime}, + FIELD_TYPE_DATE, + {_non_changed => \&_nullable_date}, + FIELD_TYPE_TEXTAREA, + {_non_changed => \&_nullable}, + FIELD_TYPE_MULTI_SELECT, + MULTI_SELECT_OVERRIDE, + FIELD_TYPE_BUG_URLS, + MULTI_SELECT_OVERRIDE, }; # These are fields where special action is taken depending on the # *value* passed in to the chart, sometimes. # This is a sub because custom fields are dynamic sub SPECIAL_PARSING { - my $map = { - # Pronoun Fields (Ones that can accept %user%, etc.) - assigned_to => \&_contact_pronoun, - cc => \&_contact_pronoun, - commenter => \&_contact_pronoun, - qa_contact => \&_contact_pronoun, - reporter => \&_contact_pronoun, - 'setters.login_name' => \&_contact_pronoun, - 'requestees.login_name' => \&_contact_pronoun, - - # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format. - creation_ts => \&_datetime_translate, - deadline => \&_date_translate, - delta_ts => \&_datetime_translate, - - # last_visit field that accept both a 1d, 1w, 1m, 1y format and the - # %last_changed% pronoun. - last_visit_ts => \&_last_visit_datetime, - }; - foreach my $field (Bugzilla->active_custom_fields) { - if ($field->type == FIELD_TYPE_DATETIME) { - $map->{$field->name} = \&_datetime_translate; - } elsif ($field->type == FIELD_TYPE_DATE) { - $map->{$field->name} = \&_date_translate; - } + my $map = { + + # Pronoun Fields (Ones that can accept %user%, etc.) + assigned_to => \&_contact_pronoun, + cc => \&_contact_pronoun, + commenter => \&_contact_pronoun, + qa_contact => \&_contact_pronoun, + reporter => \&_contact_pronoun, + 'setters.login_name' => \&_contact_pronoun, + 'requestees.login_name' => \&_contact_pronoun, + + # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format. + creation_ts => \&_datetime_translate, + deadline => \&_date_translate, + delta_ts => \&_datetime_translate, + + # last_visit field that accept both a 1d, 1w, 1m, 1y format and the + # %last_changed% pronoun. + last_visit_ts => \&_last_visit_datetime, + }; + foreach my $field (Bugzilla->active_custom_fields) { + if ($field->type == FIELD_TYPE_DATETIME) { + $map->{$field->name} = \&_datetime_translate; } - return $map; -}; + elsif ($field->type == FIELD_TYPE_DATE) { + $map->{$field->name} = \&_date_translate; + } + } + return $map; +} # Information about fields that represent "users", used by _user_nonchanged. # There are other user fields than the ones listed here, but those use # defaults in _user_nonchanged. use constant USER_FIELDS => { - 'attachments.submitter' => { - field => 'submitter_id', - join => { table => 'attachments' }, - isprivate => 1, - }, - cc => { - field => 'who', - join => { table => 'cc' }, - }, - commenter => { - field => 'who', - join => { table => 'longdescs', join => 'INNER' }, - isprivate => 1, - }, - qa_contact => { - nullable => 1, - }, - 'requestees.login_name' => { - nullable => 1, - field => 'requestee_id', - join => { table => 'flags' }, - }, - 'setters.login_name' => { - field => 'setter_id', - join => { table => 'flags' }, - }, + 'attachments.submitter' => + {field => 'submitter_id', join => {table => 'attachments'}, isprivate => 1,}, + cc => {field => 'who', join => {table => 'cc'},}, + commenter => { + field => 'who', + join => {table => 'longdescs', join => 'INNER'}, + isprivate => 1, + }, + qa_contact => {nullable => 1,}, + 'requestees.login_name' => + {nullable => 1, field => 'requestee_id', join => {table => 'flags'},}, + 'setters.login_name' => {field => 'setter_id', join => {table => 'flags'},}, }; # Backwards compatibility for times that we changed the names of fields # or URL parameters. use constant FIELD_MAP => { - 'attachments.thedata' => 'attach_data.thedata', - bugidtype => 'bug_id_type', - changedin => 'days_elapsed', - long_desc => 'longdesc', - tags => 'tag', + 'attachments.thedata' => 'attach_data.thedata', + bugidtype => 'bug_id_type', + changedin => 'days_elapsed', + long_desc => 'longdesc', + tags => 'tag', }; # Some fields are not sorted on themselves, but on other fields. # We need to have a list of these fields and what they map to. use constant SPECIAL_ORDER => { - 'target_milestone' => { - order => ['map_target_milestone.sortkey','map_target_milestone.value'], - join => { - table => 'milestones', - from => 'target_milestone', - to => 'value', - extra => ['bugs.product_id = map_target_milestone.product_id'], - join => 'INNER', - } - }, + 'target_milestone' => { + order => ['map_target_milestone.sortkey', 'map_target_milestone.value'], + join => { + table => 'milestones', + from => 'target_milestone', + to => 'value', + extra => ['bugs.product_id = map_target_milestone.product_id'], + join => 'INNER', + } + }, }; # Certain columns require other columns to come before them # in _select_columns, and should be put there if they're not there. use constant COLUMN_DEPENDS => { - classification => ['product'], - percentage_complete => ['actual_time', 'remaining_time'], + classification => ['product'], + percentage_complete => ['actual_time', 'remaining_time'], }; # This describes tables that must be joined when you want to display @@ -447,109 +411,81 @@ use constant COLUMN_DEPENDS => { # DB::Schema to figure out what needs to be joined, but for some # fields it needs a little help. sub COLUMN_JOINS { - my $invocant = shift; - my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - - my $joins = { - actual_time => { - table => '(SELECT bug_id, SUM(work_time) AS total' - . ' FROM longdescs GROUP BY bug_id)', - join => 'INNER', - }, - alias => { - table => 'bugs_aliases', - as => 'map_alias', - }, - assigned_to => { - from => 'assigned_to', - to => 'userid', - table => 'profiles', - join => 'INNER', - }, - reporter => { - from => 'reporter', - to => 'userid', - table => 'profiles', - join => 'INNER', - }, - qa_contact => { - from => 'qa_contact', - to => 'userid', - table => 'profiles', - }, - component => { - from => 'component_id', - to => 'id', - table => 'components', - join => 'INNER', - }, - product => { - from => 'product_id', - to => 'id', - table => 'products', - join => 'INNER', - }, - classification => { - table => 'classifications', - from => 'map_product.classification_id', - to => 'id', - join => 'INNER', - }, - 'flagtypes.name' => { - as => 'map_flags', - table => 'flags', - extra => ['map_flags.attach_id IS NULL'], - then_to => { - as => 'map_flagtypes', - table => 'flagtypes', - from => 'map_flags.type_id', - to => 'id', - }, - }, - keywords => { - table => 'keywords', - then_to => { - as => 'map_keyworddefs', - table => 'keyworddefs', - from => 'map_keywords.keywordid', - to => 'id', - }, - }, - blocked => { - table => 'dependencies', - to => 'dependson', - }, - dependson => { - table => 'dependencies', - to => 'blocked', - }, - 'longdescs.count' => { - table => 'longdescs', - join => 'INNER', - }, - tag => { - as => 'map_bug_tag', - table => 'bug_tag', - then_to => { - as => 'map_tag', - table => 'tag', - extra => ['map_tag.user_id = ' . $user->id], - from => 'map_bug_tag.tag_id', - to => 'id', - }, - }, - last_visit_ts => { - as => 'bug_user_last_visit', - table => 'bug_user_last_visit', - extra => ['bug_user_last_visit.user_id = ' . $user->id], - from => 'bug_id', - to => 'bug_id', - }, - }; - return $joins; -}; + my $invocant = shift; + my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; + + my $joins = { + actual_time => { + table => '(SELECT bug_id, SUM(work_time) AS total' + . ' FROM longdescs GROUP BY bug_id)', + join => 'INNER', + }, + alias => {table => 'bugs_aliases', as => 'map_alias',}, + assigned_to => { + from => 'assigned_to', + to => 'userid', + table => 'profiles', + join => 'INNER', + }, + reporter => + {from => 'reporter', to => 'userid', table => 'profiles', join => 'INNER',}, + qa_contact => {from => 'qa_contact', to => 'userid', table => 'profiles',}, + component => + {from => 'component_id', to => 'id', table => 'components', join => 'INNER',}, + product => + {from => 'product_id', to => 'id', table => 'products', join => 'INNER',}, + classification => { + table => 'classifications', + from => 'map_product.classification_id', + to => 'id', + join => 'INNER', + }, + 'flagtypes.name' => { + as => 'map_flags', + table => 'flags', + extra => ['map_flags.attach_id IS NULL'], + then_to => { + as => 'map_flagtypes', + table => 'flagtypes', + from => 'map_flags.type_id', + to => 'id', + }, + }, + keywords => { + table => 'keywords', + then_to => { + as => 'map_keyworddefs', + table => 'keyworddefs', + from => 'map_keywords.keywordid', + to => 'id', + }, + }, + blocked => {table => 'dependencies', to => 'dependson',}, + dependson => {table => 'dependencies', to => 'blocked',}, + 'longdescs.count' => {table => 'longdescs', join => 'INNER',}, + tag => { + as => 'map_bug_tag', + table => 'bug_tag', + then_to => { + as => 'map_tag', + table => 'tag', + extra => ['map_tag.user_id = ' . $user->id], + from => 'map_bug_tag.tag_id', + to => 'id', + }, + }, + last_visit_ts => { + as => 'bug_user_last_visit', + table => 'bug_user_last_visit', + extra => ['bug_user_last_visit.user_id = ' . $user->id], + from => 'bug_id', + to => 'bug_id', + }, + }; + return $joins; +} -# This constant defines the columns that can be selected in a query +# This constant defines the columns that can be selected in a query # and/or displayed in a bug list. Column records include the following # fields: # @@ -559,7 +495,7 @@ sub COLUMN_JOINS { # that returns the value of the column); # # 3. title: The title of the column as displayed to users. -# +# # Note: There are a few hacks in the code that deviate from these definitions. # In particular, the redundant short_desc column is removed when the # client requests "all" columns. @@ -570,150 +506,149 @@ sub COLUMN_JOINS { # and we don't want it to happen at compile time, so we have it as a # subroutine. sub COLUMNS { - my $invocant = shift; - my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - my $dbh = Bugzilla->dbh; - my $cache = Bugzilla->request_cache; + my $invocant = shift; + my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; + my $dbh = Bugzilla->dbh; + my $cache = Bugzilla->request_cache; - if (defined $cache->{search_columns}->{$user->id}) { - return $cache->{search_columns}->{$user->id}; - } - - # These are columns that don't exist in fielddefs, but are valid buglist - # columns. (Also see near the bottom of this function for the definition - # of short_short_desc.) - my %columns = ( - relevance => { title => 'Relevance' }, - ); - - # Next we define columns that have special SQL instead of just something - # like "bugs.bug_id". - my $total_time = "(map_actual_time.total + bugs.remaining_time)"; - my %special_sql = ( - alias => $dbh->sql_group_concat('DISTINCT map_alias.alias'), - deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'), - actual_time => 'map_actual_time.total', - - # "FLOOR" is in there to turn this into an integer, making searches - # totally predictable. Otherwise you get floating-point numbers that - # are rather hard to search reliably if you're asking for exact - # numbers. - percentage_complete => - "(CASE WHEN $total_time = 0" - . " THEN 0" - . " ELSE FLOOR(100 * (map_actual_time.total / $total_time))" - . " END)", - - 'flagtypes.name' => $dbh->sql_group_concat('DISTINCT ' - . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status'), - undef, undef, 'map_flagtypes.sortkey, map_flagtypes.name'), - - 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), - - blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'), - dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'), - - 'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)', - - tag => $dbh->sql_group_concat('DISTINCT map_tag.name'), - last_visit_ts => 'bug_user_last_visit.last_visit_ts', - ); - - # Backward-compatibility for old field names. Goes new_name => old_name. - # These are here and not in _translate_old_column because the rest of the - # code actually still uses the old names, while the fielddefs table uses - # the new names (which is not the case for the fields handled by - # _translate_old_column). - my %old_names = ( - creation_ts => 'opendate', - delta_ts => 'changeddate', - work_time => 'actual_time', - ); - - # Fields that are email addresses - my @email_fields = qw(assigned_to reporter qa_contact); - # Other fields that are stored in the bugs table as an id, but - # should be displayed using their name. - my @id_fields = qw(product component classification); - - foreach my $col (@email_fields) { - my $sql = "map_${col}.login_name"; - if (!$user->id) { - $sql = $dbh->sql_string_until($sql, $dbh->quote('@')); - } - $special_sql{$col} = $sql; - $special_sql{"${col}_realname"} = "map_${col}.realname"; - } - - foreach my $col (@id_fields) { - $special_sql{$col} = "map_${col}.name"; + if (defined $cache->{search_columns}->{$user->id}) { + return $cache->{search_columns}->{$user->id}; + } + + # These are columns that don't exist in fielddefs, but are valid buglist + # columns. (Also see near the bottom of this function for the definition + # of short_short_desc.) + my %columns = (relevance => {title => 'Relevance'},); + + # Next we define columns that have special SQL instead of just something + # like "bugs.bug_id". + my $total_time = "(map_actual_time.total + bugs.remaining_time)"; + my %special_sql = ( + alias => $dbh->sql_group_concat('DISTINCT map_alias.alias'), + deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'), + actual_time => 'map_actual_time.total', + + # "FLOOR" is in there to turn this into an integer, making searches + # totally predictable. Otherwise you get floating-point numbers that + # are rather hard to search reliably if you're asking for exact + # numbers. + percentage_complete => "(CASE WHEN $total_time = 0" + . " THEN 0" + . " ELSE FLOOR(100 * (map_actual_time.total / $total_time))" . " END)", + + 'flagtypes.name' => $dbh->sql_group_concat( + 'DISTINCT ' . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status'), + undef, + undef, + 'map_flagtypes.sortkey, map_flagtypes.name' + ), + + 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), + + blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'), + dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'), + + 'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)', + + tag => $dbh->sql_group_concat('DISTINCT map_tag.name'), + last_visit_ts => 'bug_user_last_visit.last_visit_ts', + ); + + # Backward-compatibility for old field names. Goes new_name => old_name. + # These are here and not in _translate_old_column because the rest of the + # code actually still uses the old names, while the fielddefs table uses + # the new names (which is not the case for the fields handled by + # _translate_old_column). + my %old_names = ( + creation_ts => 'opendate', + delta_ts => 'changeddate', + work_time => 'actual_time', + ); + + # Fields that are email addresses + my @email_fields = qw(assigned_to reporter qa_contact); + + # Other fields that are stored in the bugs table as an id, but + # should be displayed using their name. + my @id_fields = qw(product component classification); + + foreach my $col (@email_fields) { + my $sql = "map_${col}.login_name"; + if (!$user->id) { + $sql = $dbh->sql_string_until($sql, $dbh->quote('@')); + } + $special_sql{$col} = $sql; + $special_sql{"${col}_realname"} = "map_${col}.realname"; + } + + foreach my $col (@id_fields) { + $special_sql{$col} = "map_${col}.name"; + } + + # Do the actual column-getting from fielddefs, now. + my @fields = @{Bugzilla->fields({obsolete => 0, buglist => 1})}; + foreach my $field (@fields) { + my $id = $field->name; + $id = $old_names{$id} if exists $old_names{$id}; + my $sql; + if (exists $special_sql{$id}) { + $sql = $special_sql{$id}; + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + $sql = $dbh->sql_group_concat('DISTINCT map_' . $field->name . '.value'); } - - # Do the actual column-getting from fielddefs, now. - my @fields = @{ Bugzilla->fields({ obsolete => 0, buglist => 1 }) }; - foreach my $field (@fields) { - my $id = $field->name; - $id = $old_names{$id} if exists $old_names{$id}; - my $sql; - if (exists $special_sql{$id}) { - $sql = $special_sql{$id}; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - $sql = $dbh->sql_group_concat( - 'DISTINCT map_' . $field->name . '.value'); - } - else { - $sql = 'bugs.' . $field->name; - } - $columns{$id} = { name => $sql, title => $field->description }; + else { + $sql = 'bugs.' . $field->name; } + $columns{$id} = {name => $sql, title => $field->description}; + } - # The short_short_desc column is identical to short_desc - $columns{'short_short_desc'} = $columns{'short_desc'}; + # The short_short_desc column is identical to short_desc + $columns{'short_short_desc'} = $columns{'short_desc'}; - Bugzilla::Hook::process('buglist_columns', { columns => \%columns }); + Bugzilla::Hook::process('buglist_columns', {columns => \%columns}); - $cache->{search_columns}->{$user->id} = \%columns; - return $cache->{search_columns}->{$user->id}; + $cache->{search_columns}->{$user->id} = \%columns; + return $cache->{search_columns}->{$user->id}; } sub REPORT_COLUMNS { - my $invocant = shift; - my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - - my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS); - # There's no reason to support reporting on unique fields. - # Also, some other fields don't make very good reporting axises, - # or simply don't work with the current reporting system. - my @no_report_columns = - qw(bug_id alias short_short_desc opendate changeddate - flagtypes.name relevance); - - # If you're not a time-tracker, you can't use time-tracking - # columns. - if (!$user->is_timetracker) { - push(@no_report_columns, TIMETRACKING_FIELDS); - } + my $invocant = shift; + my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - foreach my $name (@no_report_columns) { - delete $columns->{$name}; - } - return $columns; + my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS); + + # There's no reason to support reporting on unique fields. + # Also, some other fields don't make very good reporting axises, + # or simply don't work with the current reporting system. + my @no_report_columns = qw(bug_id alias short_short_desc opendate changeddate + flagtypes.name relevance); + + # If you're not a time-tracker, you can't use time-tracking + # columns. + if (!$user->is_timetracker) { + push(@no_report_columns, TIMETRACKING_FIELDS); + } + + foreach my $name (@no_report_columns) { + delete $columns->{$name}; + } + return $columns; } # These are fields that never go into the GROUP BY on any DB. bug_id # is here because it *always* goes into the GROUP BY as the first item, # so it should be skipped when determining extra GROUP BY columns. use constant GROUP_BY_SKIP => qw( - alias - blocked - bug_id - dependson - flagtypes.name - keywords - longdescs.count - percentage_complete - tag + alias + blocked + bug_id + dependson + flagtypes.name + keywords + longdescs.count + percentage_complete + tag ); ############### @@ -722,27 +657,27 @@ use constant GROUP_BY_SKIP => qw( # Note that the params argument may be modified by Bugzilla::Search sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - - my $self = { @_ }; - bless($self, $class); - $self->{'user'} ||= Bugzilla->user; - - # There are certain behaviors of the CGI "Vars" hash that we don't want. - # In particular, if you put a single-value arrayref into it, later you - # get back out a string, which breaks anyexact charts (because they - # need arrays even for individual items, or we will re-trigger bug 67036). - # - # We can't just untie the hash--that would give us a hash with no values. - # We have to manually copy the hash into a new one, and we have to always - # do it, because there's no way to know if we were passed a tied hash - # or not. - my $params_in = $self->_params; - my %params = map { $_ => $params_in->{$_} } keys %$params_in; - $self->{params} = \%params; - - return $self; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + my $self = {@_}; + bless($self, $class); + $self->{'user'} ||= Bugzilla->user; + + # There are certain behaviors of the CGI "Vars" hash that we don't want. + # In particular, if you put a single-value arrayref into it, later you + # get back out a string, which breaks anyexact charts (because they + # need arrays even for individual items, or we will re-trigger bug 67036). + # + # We can't just untie the hash--that would give us a hash with no values. + # We have to manually copy the hash into a new one, and we have to always + # do it, because there's no way to know if we were passed a tied hash + # or not. + my $params_in = $self->_params; + my %params = map { $_ => $params_in->{$_} } keys %$params_in; + $self->{params} = \%params; + + return $self; } @@ -751,148 +686,156 @@ sub new { #################### sub data { - my $self = shift; - return $self->{data} if $self->{data}; - my $dbh = Bugzilla->dbh; - - # If all fields belong to the 'bugs' table, there is no need to split - # the original query into two pieces. Else we override the 'fields' - # argument to first get bug IDs based on the search criteria defined - # by the caller, and the desired fields are collected in the 2nd query. - my @orig_fields = $self->_input_columns; - my $all_in_bugs_table = 1; - foreach my $field (@orig_fields) { - next if ($self->COLUMNS->{$field}->{name} // $field) =~ /^bugs\.\w+$/; - $self->{fields} = ['bug_id']; - $all_in_bugs_table = 0; - last; - } - - my $start_time = [gettimeofday()]; - my $sql = $self->_sql; - # Do we just want bug IDs to pass to the 2nd query or all the data immediately? - my $func = $all_in_bugs_table ? 'selectall_arrayref' : 'selectcol_arrayref'; - my $bug_ids = $dbh->$func($sql); - my @extra_data = ({sql => $sql, time => tv_interval($start_time)}); - # Restore the original 'fields' argument, just in case. - $self->{fields} = \@orig_fields unless $all_in_bugs_table; - - # If there are no bugs found, or all fields are in the 'bugs' table, - # there is no need for another query. - if (!scalar @$bug_ids || $all_in_bugs_table) { - $self->{data} = $bug_ids; - return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; - } - - # Make sure the bug_id will be returned. If not, append it to the list. - my $pos = firstidx { $_ eq 'bug_id' } @orig_fields; - if ($pos < 0) { - push(@orig_fields, 'bug_id'); - $pos = $#orig_fields; - } - - # Now create a query with the buglist above as the single criteria - # and the fields that the caller wants. No need to redo security checks; - # the list has already been validated above. - my $search = $self->new('fields' => \@orig_fields, - 'params' => {bug_id => $bug_ids, bug_id_type => 'anyexact'}, - 'sharer' => $self->_sharer_id, - 'user' => $self->_user, - 'allow_unlimited' => 1, - '_no_security_check' => 1); - - $start_time = [gettimeofday()]; - $sql = $search->_sql; - my $unsorted_data = $dbh->selectall_arrayref($sql); - push(@extra_data, {sql => $sql, time => tv_interval($start_time)}); - # Let's sort the data. We didn't do it in the query itself because - # we already know in which order to sort bugs thanks to the first query, - # and this avoids additional table joins in the SQL query. - my %data = map { $_->[$pos] => $_ } @$unsorted_data; - $self->{data} = [map { $data{$_} } @$bug_ids]; + my $self = shift; + return $self->{data} if $self->{data}; + my $dbh = Bugzilla->dbh; + + # If all fields belong to the 'bugs' table, there is no need to split + # the original query into two pieces. Else we override the 'fields' + # argument to first get bug IDs based on the search criteria defined + # by the caller, and the desired fields are collected in the 2nd query. + my @orig_fields = $self->_input_columns; + my $all_in_bugs_table = 1; + foreach my $field (@orig_fields) { + next if ($self->COLUMNS->{$field}->{name} // $field) =~ /^bugs\.\w+$/; + $self->{fields} = ['bug_id']; + $all_in_bugs_table = 0; + last; + } + + my $start_time = [gettimeofday()]; + my $sql = $self->_sql; + + # Do we just want bug IDs to pass to the 2nd query or all the data immediately? + my $func = $all_in_bugs_table ? 'selectall_arrayref' : 'selectcol_arrayref'; + my $bug_ids = $dbh->$func($sql); + my @extra_data = ({sql => $sql, time => tv_interval($start_time)}); + + # Restore the original 'fields' argument, just in case. + $self->{fields} = \@orig_fields unless $all_in_bugs_table; + + # If there are no bugs found, or all fields are in the 'bugs' table, + # there is no need for another query. + if (!scalar @$bug_ids || $all_in_bugs_table) { + $self->{data} = $bug_ids; return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; + } + + # Make sure the bug_id will be returned. If not, append it to the list. + my $pos = firstidx { $_ eq 'bug_id' } @orig_fields; + if ($pos < 0) { + push(@orig_fields, 'bug_id'); + $pos = $#orig_fields; + } + + # Now create a query with the buglist above as the single criteria + # and the fields that the caller wants. No need to redo security checks; + # the list has already been validated above. + my $search = $self->new( + 'fields' => \@orig_fields, + 'params' => {bug_id => $bug_ids, bug_id_type => 'anyexact'}, + 'sharer' => $self->_sharer_id, + 'user' => $self->_user, + 'allow_unlimited' => 1, + '_no_security_check' => 1 + ); + + $start_time = [gettimeofday()]; + $sql = $search->_sql; + my $unsorted_data = $dbh->selectall_arrayref($sql); + push(@extra_data, {sql => $sql, time => tv_interval($start_time)}); + + # Let's sort the data. We didn't do it in the query itself because + # we already know in which order to sort bugs thanks to the first query, + # and this avoids additional table joins in the SQL query. + my %data = map { $_->[$pos] => $_ } @$unsorted_data; + $self->{data} = [map { $data{$_} } @$bug_ids]; + return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; } sub _sql { - my ($self) = @_; - return $self->{sql} if $self->{sql}; - my $dbh = Bugzilla->dbh; - - my ($joins, $clause) = $self->_charts_to_conditions(); - - if (!$clause->as_string - && !Bugzilla->params->{'search_allow_no_criteria'} - && !$self->{allow_unlimited}) - { - ThrowUserError('buglist_parameters_required'); - } - - my $select = join(', ', $self->_sql_select); - my $from = $self->_sql_from($joins); - my $where = $self->_sql_where($clause); - my $group_by = $dbh->sql_group_by($self->_sql_group_by); - my $order_by = $self->_sql_order_by - ? "\nORDER BY " . join(', ', $self->_sql_order_by) : ''; - my $limit = $self->_sql_limit; - $limit = "\n$limit" if $limit; - - my $query = <<END; + my ($self) = @_; + return $self->{sql} if $self->{sql}; + my $dbh = Bugzilla->dbh; + + my ($joins, $clause) = $self->_charts_to_conditions(); + + if ( !$clause->as_string + && !Bugzilla->params->{'search_allow_no_criteria'} + && !$self->{allow_unlimited}) + { + ThrowUserError('buglist_parameters_required'); + } + + my $select = join(', ', $self->_sql_select); + my $from = $self->_sql_from($joins); + my $where = $self->_sql_where($clause); + my $group_by = $dbh->sql_group_by($self->_sql_group_by); + my $order_by + = $self->_sql_order_by + ? "\nORDER BY " . join(', ', $self->_sql_order_by) + : ''; + my $limit = $self->_sql_limit; + $limit = "\n$limit" if $limit; + + my $query = <<END; SELECT $select FROM $from WHERE $where $group_by$order_by$limit END - $self->{sql} = $query; - return $self->{sql}; + $self->{sql} = $query; + return $self->{sql}; } sub search_description { - my ($self, $params) = @_; - my $desc = $self->{'search_description'} ||= []; - if ($params) { - push(@$desc, $params); - } - # Make sure that the description has actually been generated if - # people are asking for the whole thing. - else { - $self->_sql; - } - return $self->{'search_description'}; + my ($self, $params) = @_; + my $desc = $self->{'search_description'} ||= []; + if ($params) { + push(@$desc, $params); + } + + # Make sure that the description has actually been generated if + # people are asking for the whole thing. + else { + $self->_sql; + } + return $self->{'search_description'}; } sub boolean_charts_to_custom_search { - my ($self, $cgi_buffer) = @_; - my $boolean_charts = $self->_boolean_charts; - my @as_params = $boolean_charts ? $boolean_charts->as_params : (); - - # We need to start our new ids after the last custom search "f" id. - # We can just pick the last id in the array because they are sorted - # numerically. - my $last_id = ($self->_field_ids)[-1]; - my $count = defined($last_id) ? $last_id + 1 : 0; - foreach my $param_set (@as_params) { - foreach my $name (keys %$param_set) { - my $value = $param_set->{$name}; - next if !defined $value; - $cgi_buffer->param($name . $count, $value); - } - $count++; + my ($self, $cgi_buffer) = @_; + my $boolean_charts = $self->_boolean_charts; + my @as_params = $boolean_charts ? $boolean_charts->as_params : (); + + # We need to start our new ids after the last custom search "f" id. + # We can just pick the last id in the array because they are sorted + # numerically. + my $last_id = ($self->_field_ids)[-1]; + my $count = defined($last_id) ? $last_id + 1 : 0; + foreach my $param_set (@as_params) { + foreach my $name (keys %$param_set) { + my $value = $param_set->{$name}; + next if !defined $value; + $cgi_buffer->param($name . $count, $value); } + $count++; + } } sub invalid_order_columns { - my ($self) = @_; - my @invalid_columns; - foreach my $order ($self->_input_order) { - next if defined $self->_validate_order_column($order); - push(@invalid_columns, $order); - } - return \@invalid_columns; + my ($self) = @_; + my @invalid_columns; + foreach my $order ($self->_input_order) { + next if defined $self->_validate_order_column($order); + push(@invalid_columns, $order); + } + return \@invalid_columns; } sub order { - my ($self) = @_; - return $self->_valid_order; + my ($self) = @_; + return $self->_valid_order; } ###################### @@ -901,49 +844,48 @@ sub order { # Fields that are legal for boolean charts of any kind. sub _chart_fields { - my ($self) = @_; + my ($self) = @_; - if (!$self->{chart_fields}) { - my $chart_fields = Bugzilla->fields({ by_name => 1 }); + if (!$self->{chart_fields}) { + my $chart_fields = Bugzilla->fields({by_name => 1}); - if (!$self->_user->is_timetracker) { - foreach my $tt_field (TIMETRACKING_FIELDS) { - delete $chart_fields->{$tt_field}; - } - } - $self->{chart_fields} = $chart_fields; + if (!$self->_user->is_timetracker) { + foreach my $tt_field (TIMETRACKING_FIELDS) { + delete $chart_fields->{$tt_field}; + } } - return $self->{chart_fields}; + $self->{chart_fields} = $chart_fields; + } + return $self->{chart_fields}; } # There are various places in Search.pm that we need to know the list of # valid multi-select fields--or really, fields that are stored like # multi-selects, which includes BUG_URLS fields. sub _multi_select_fields { - my ($self) = @_; - $self->{multi_select_fields} ||= Bugzilla->fields({ - by_name => 1, - type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]}); - return $self->{multi_select_fields}; + my ($self) = @_; + $self->{multi_select_fields} ||= Bugzilla->fields( + {by_name => 1, type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]}); + return $self->{multi_select_fields}; } # $self->{params} contains values that could be undef, could be a string, # or could be an arrayref. Sometimes we want that value as an array, # always. sub _param_array { - my ($self, $name) = @_; - my $value = $self->_params->{$name}; - if (!defined $value) { - return (); - } - if (ref($value) eq 'ARRAY') { - return @$value; - } - return ($value); -} - -sub _params { $_[0]->{params} } -sub _user { return $_[0]->{user} } + my ($self, $name) = @_; + my $value = $self->_params->{$name}; + if (!defined $value) { + return (); + } + if (ref($value) eq 'ARRAY') { + return @$value; + } + return ($value); +} + +sub _params { $_[0]->{params} } +sub _user { return $_[0]->{user} } sub _sharer_id { $_[0]->{sharer} } ############################## @@ -952,81 +894,84 @@ sub _sharer_id { $_[0]->{sharer} } # These are the fields the user has chosen to display on the buglist, # exactly as they were passed to new(). -sub _input_columns { @{ $_[0]->{'fields'} || [] } } +sub _input_columns { @{$_[0]->{'fields'} || []} } # These are columns that are also going to be in the SELECT for one reason # or another, but weren't actually requested by the caller. sub _extra_columns { - my ($self) = @_; - # Everything that's going to be in the ORDER BY must also be - # in the SELECT. - push(@{ $self->{extra_columns} }, $self->_valid_order_columns); - return @{ $self->{extra_columns} }; + my ($self) = @_; + + # Everything that's going to be in the ORDER BY must also be + # in the SELECT. + push(@{$self->{extra_columns}}, $self->_valid_order_columns); + return @{$self->{extra_columns}}; } # For search functions to modify extra_columns. It doesn't matter if # people push the same column onto this array multiple times, because # _select_columns will call "uniq" on its final result. sub _add_extra_column { - my ($self, $column) = @_; - push(@{ $self->{extra_columns} }, $column); + my ($self, $column) = @_; + push(@{$self->{extra_columns}}, $column); } # These are the columns that we're going to be actually SELECTing. sub _display_columns { - my ($self) = @_; - return @{ $self->{display_columns} } if $self->{display_columns}; - - # Do not alter the list from _input_columns at all, even if there are - # duplicated columns. Those are passed by the caller, and the caller - # expects to get them back in the exact same order. - my @columns = $self->_input_columns; - - # Only add columns which are not already listed. - my %list = map { $_ => 1 } @columns; - foreach my $column ($self->_extra_columns) { - push(@columns, $column) unless $list{$column}++; - } - $self->{display_columns} = \@columns; - return @{ $self->{display_columns} }; + my ($self) = @_; + return @{$self->{display_columns}} if $self->{display_columns}; + + # Do not alter the list from _input_columns at all, even if there are + # duplicated columns. Those are passed by the caller, and the caller + # expects to get them back in the exact same order. + my @columns = $self->_input_columns; + + # Only add columns which are not already listed. + my %list = map { $_ => 1 } @columns; + foreach my $column ($self->_extra_columns) { + push(@columns, $column) unless $list{$column}++; + } + $self->{display_columns} = \@columns; + return @{$self->{display_columns}}; } # These are the columns that are involved in the query. sub _select_columns { - my ($self) = @_; - return @{ $self->{select_columns} } if $self->{select_columns}; - - my @select_columns; - foreach my $column ($self->_display_columns) { - if (my $add_first = COLUMN_DEPENDS->{$column}) { - push(@select_columns, @$add_first); - } - push(@select_columns, $column); + my ($self) = @_; + return @{$self->{select_columns}} if $self->{select_columns}; + + my @select_columns; + foreach my $column ($self->_display_columns) { + if (my $add_first = COLUMN_DEPENDS->{$column}) { + push(@select_columns, @$add_first); } - # Remove duplicated columns. - $self->{select_columns} = [uniq @select_columns]; - return @{ $self->{select_columns} }; + push(@select_columns, $column); + } + + # Remove duplicated columns. + $self->{select_columns} = [uniq @select_columns]; + return @{$self->{select_columns}}; } # This takes _display_columns and translates it into the actual SQL that # will go into the SELECT clause. sub _sql_select { - my ($self) = @_; - my @sql_fields; - foreach my $column ($self->_display_columns) { - my $sql = $self->COLUMNS->{$column}->{name} // ''; - if ($sql) { - my $alias = $column; - # Aliases cannot contain dots in them. We convert them to underscores. - $alias =~ tr/./_/; - $sql .= " AS $alias"; - } - else { - $sql = $column; - } - push(@sql_fields, $sql); + my ($self) = @_; + my @sql_fields; + foreach my $column ($self->_display_columns) { + my $sql = $self->COLUMNS->{$column}->{name} // ''; + if ($sql) { + my $alias = $column; + + # Aliases cannot contain dots in them. We convert them to underscores. + $alias =~ tr/./_/; + $sql .= " AS $alias"; + } + else { + $sql = $column; } - return @sql_fields; + push(@sql_fields, $sql); + } + return @sql_fields; } ################################ @@ -1035,85 +980,83 @@ sub _sql_select { # The "order" that was requested by the consumer, exactly as it was # requested. -sub _input_order { @{ $_[0]->{'order'} || [] } } +sub _input_order { @{$_[0]->{'order'} || []} } + # Requested order with invalid values removed and old names translated sub _valid_order { - my ($self) = @_; - return map { ($self->_validate_order_column($_)) } $self->_input_order; + my ($self) = @_; + return map { ($self->_validate_order_column($_)) } $self->_input_order; } + # The valid order with just the column names, and no ASC or DESC. sub _valid_order_columns { - my ($self) = @_; - return map { (split_order_term($_))[0] } $self->_valid_order; + my ($self) = @_; + return map { (split_order_term($_))[0] } $self->_valid_order; } sub _validate_order_column { - my ($self, $order_item) = @_; + my ($self, $order_item) = @_; - # Translate old column names - my ($field, $direction) = split_order_term($order_item); - $field = $self->_translate_old_column($field); + # Translate old column names + my ($field, $direction) = split_order_term($order_item); + $field = $self->_translate_old_column($field); - # Only accept valid columns - return if (!exists $self->COLUMNS->{$field}); + # Only accept valid columns + return if (!exists $self->COLUMNS->{$field}); - # Relevance column can be used only with one or more fulltext searches - return if ($field eq 'relevance' && !$self->COLUMNS->{$field}->{name}); + # Relevance column can be used only with one or more fulltext searches + return if ($field eq 'relevance' && !$self->COLUMNS->{$field}->{name}); - $direction = " $direction" if $direction; - return "$field$direction"; + $direction = " $direction" if $direction; + return "$field$direction"; } # A hashref that describes all the special stuff that has to be done # for various fields if they go into the ORDER BY clause. sub _special_order { - my ($self) = @_; - return $self->{special_order} if $self->{special_order}; - - my %special_order = %{ SPECIAL_ORDER() }; - my $select_fields = Bugzilla->fields({ type => FIELD_TYPE_SINGLE_SELECT }); - foreach my $field (@$select_fields) { - next if $field->is_abnormal; - my $name = $field->name; - $special_order{$name} = { - order => ["map_$name.sortkey", "map_$name.value"], - join => { - table => $name, - from => "bugs.$name", - to => "value", - join => 'INNER', - } - }; - } - $self->{special_order} = \%special_order; - return $self->{special_order}; + my ($self) = @_; + return $self->{special_order} if $self->{special_order}; + + my %special_order = %{SPECIAL_ORDER()}; + my $select_fields = Bugzilla->fields({type => FIELD_TYPE_SINGLE_SELECT}); + foreach my $field (@$select_fields) { + next if $field->is_abnormal; + my $name = $field->name; + $special_order{$name} = { + order => ["map_$name.sortkey", "map_$name.value"], + join => {table => $name, from => "bugs.$name", to => "value", join => 'INNER',} + }; + } + $self->{special_order} = \%special_order; + return $self->{special_order}; } sub _sql_order_by { - my ($self) = @_; - if (!$self->{sql_order_by}) { - my @order_by = map { $self->_translate_order_by_column($_) } - $self->_valid_order; - $self->{sql_order_by} = \@order_by; - } - return @{ $self->{sql_order_by} }; + my ($self) = @_; + if (!$self->{sql_order_by}) { + my @order_by + = map { $self->_translate_order_by_column($_) } $self->_valid_order; + $self->{sql_order_by} = \@order_by; + } + return @{$self->{sql_order_by}}; } sub _translate_order_by_column { - my ($self, $order_by_item) = @_; - - my ($field, $direction) = split_order_term($order_by_item); - - $direction = '' if lc($direction) eq 'asc'; - my $special_order = $self->_special_order->{$field}->{order}; - # Standard fields have underscores in their SELECT alias instead - # of a period (because aliases can't have periods). - $field =~ s/\./_/g; - my @items = $special_order ? @$special_order : $field; - if (lc($direction) eq 'desc') { - @items = map { "$_ DESC" } @items; - } - return @items; + my ($self, $order_by_item) = @_; + + my ($field, $direction) = split_order_term($order_by_item); + + $direction = '' if lc($direction) eq 'asc'; + my $special_order = $self->_special_order->{$field}->{order}; + + # Standard fields have underscores in their SELECT alias instead + # of a period (because aliases can't have periods). + $field =~ s/\./_/g; + my @items = $special_order ? @$special_order : $field; + if (lc($direction) eq 'desc') { + @items = map {"$_ DESC"} @items; + } + return @items; } ############################# @@ -1121,32 +1064,30 @@ sub _translate_order_by_column { ############################# sub _sql_limit { - my ($self) = @_; - my $limit = $self->_params->{limit}; - my $offset = $self->_params->{offset}; - - my $max_results = Bugzilla->params->{'max_search_results'}; - if (!$self->{allow_unlimited} && (!$limit || $limit > $max_results)) { - $limit = $max_results; - } - - if (defined($offset) && !$limit) { - $limit = INT_MAX; - } - if (defined $limit) { - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::Search::new', - param => 'limit' }); - if (defined $offset) { - detaint_natural($offset) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::Search::new', - param => 'offset' }); - } - return Bugzilla->dbh->sql_limit($limit, $offset); - } - return ''; + my ($self) = @_; + my $limit = $self->_params->{limit}; + my $offset = $self->_params->{offset}; + + my $max_results = Bugzilla->params->{'max_search_results'}; + if (!$self->{allow_unlimited} && (!$limit || $limit > $max_results)) { + $limit = $max_results; + } + + if (defined($offset) && !$limit) { + $limit = INT_MAX; + } + if (defined $limit) { + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::Search::new', param => 'limit'}); + if (defined $offset) { + detaint_natural($offset) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::Search::new', param => 'offset'}); + } + return Bugzilla->dbh->sql_limit($limit, $offset); + } + return ''; } ############################ @@ -1154,176 +1095,176 @@ sub _sql_limit { ############################ sub _column_join { - my ($self, $field) = @_; - # The _realname fields require the same join as the username fields. - $field =~ s/_realname$//; - my $column_joins = $self->_get_column_joins(); - my $join_info = $column_joins->{$field}; - if ($join_info) { - # Don't allow callers to modify the constant. - $join_info = dclone($join_info); - } - else { - if ($self->_multi_select_fields->{$field}) { - $join_info = { table => "bug_$field" }; - } - } - if ($join_info and !$join_info->{as}) { - $join_info = dclone($join_info); - $join_info->{as} = "map_$field"; + my ($self, $field) = @_; + + # The _realname fields require the same join as the username fields. + $field =~ s/_realname$//; + my $column_joins = $self->_get_column_joins(); + my $join_info = $column_joins->{$field}; + if ($join_info) { + + # Don't allow callers to modify the constant. + $join_info = dclone($join_info); + } + else { + if ($self->_multi_select_fields->{$field}) { + $join_info = {table => "bug_$field"}; } - return $join_info ? $join_info : (); + } + if ($join_info and !$join_info->{as}) { + $join_info = dclone($join_info); + $join_info->{as} = "map_$field"; + } + return $join_info ? $join_info : (); } # Sometimes we join the same table more than once. In this case, we # want to AND all the various critiera that were used in both joins. sub _combine_joins { - my ($self, $joins) = @_; - my @result; - while(my $join = shift @$joins) { - my $name = $join->{as}; - my ($others_like_me, $the_rest) = part { $_->{as} eq $name ? 0 : 1 } - @$joins; - if ($others_like_me) { - my $from = $join->{from}; - my $to = $join->{to}; - # Sanity check to make sure that we have the same from and to - # for all the same-named joins. - if ($from) { - all { $_->{from} eq $from } @$others_like_me - or die "Not all same-named joins have identical 'from': " - . Dumper($join, $others_like_me); - } - if ($to) { - all { $_->{to} eq $to } @$others_like_me - or die "Not all same-named joins have identical 'to': " - . Dumper($join, $others_like_me); - } - - # We don't need to call uniq here--translate_join will do that - # for us. - my @conditions = map { @{ $_->{extra} || [] } } - ($join, @$others_like_me); - $join->{extra} = \@conditions; - $joins = $the_rest; - } - push(@result, $join); - } - - return @result; + my ($self, $joins) = @_; + my @result; + while (my $join = shift @$joins) { + my $name = $join->{as}; + my ($others_like_me, $the_rest) = part { $_->{as} eq $name ? 0 : 1 } + @$joins; + if ($others_like_me) { + my $from = $join->{from}; + my $to = $join->{to}; + + # Sanity check to make sure that we have the same from and to + # for all the same-named joins. + if ($from) { + all { $_->{from} eq $from } @$others_like_me + or die "Not all same-named joins have identical 'from': " + . Dumper($join, $others_like_me); + } + if ($to) { + all { $_->{to} eq $to } @$others_like_me + or die "Not all same-named joins have identical 'to': " + . Dumper($join, $others_like_me); + } + + # We don't need to call uniq here--translate_join will do that + # for us. + my @conditions = map { @{$_->{extra} || []} } ($join, @$others_like_me); + $join->{extra} = \@conditions; + $joins = $the_rest; + } + push(@result, $join); + } + + return @result; } # Takes all the "then_to" items and just puts them as the next item in # the array. Right now this only does one level of "then_to", but we # could re-write this to handle then_to recursively if we need more levels. sub _extract_then_to { - my ($self, $joins) = @_; - my @result; - foreach my $join (@$joins) { - push(@result, $join); - if (my $then_to = $join->{then_to}) { - push(@result, $then_to); - } + my ($self, $joins) = @_; + my @result; + foreach my $join (@$joins) { + push(@result, $join); + if (my $then_to = $join->{then_to}) { + push(@result, $then_to); } - return @result; + } + return @result; } # JOIN statements for the SELECT and ORDER BY columns. This should not be # called until the moment it is needed, because _select_columns might be # modified by the charts. sub _select_order_joins { - my ($self) = @_; - my @joins; - foreach my $field ($self->_select_columns) { - my @column_join = $self->_column_join($field); - push(@joins, @column_join); - } - foreach my $field ($self->_valid_order_columns) { - my $join_info = $self->_special_order->{$field}->{join}; - if ($join_info) { - # Don't let callers modify SPECIAL_ORDER. - $join_info = dclone($join_info); - if (!$join_info->{as}) { - $join_info->{as} = "map_$field"; - } - push(@joins, $join_info); - } + my ($self) = @_; + my @joins; + foreach my $field ($self->_select_columns) { + my @column_join = $self->_column_join($field); + push(@joins, @column_join); + } + foreach my $field ($self->_valid_order_columns) { + my $join_info = $self->_special_order->{$field}->{join}; + if ($join_info) { + + # Don't let callers modify SPECIAL_ORDER. + $join_info = dclone($join_info); + if (!$join_info->{as}) { + $join_info->{as} = "map_$field"; + } + push(@joins, $join_info); } - return @joins; + } + return @joins; } # These are the joins that are *always* in the FROM clause. sub _standard_joins { - my ($self) = @_; - my $user = $self->_user; - my @joins; - return () if $self->{_no_security_check}; - - my $security_join = { - table => 'bug_group_map', - as => 'security_map', - }; - push(@joins, $security_join); + my ($self) = @_; + my $user = $self->_user; + my @joins; + return () if $self->{_no_security_check}; - if ($user->id) { - # See also _standard_joins for the other half of the below statement - if (!Bugzilla->params->{'or_groups'}) { - $security_join->{extra} = - ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"]; - } - - my $security_cc_join = { - table => 'cc', - as => 'security_cc', - extra => ['security_cc.who = ' . $user->id], - }; - push(@joins, $security_cc_join); + my $security_join = {table => 'bug_group_map', as => 'security_map',}; + push(@joins, $security_join); + + if ($user->id) { + + # See also _standard_joins for the other half of the below statement + if (!Bugzilla->params->{'or_groups'}) { + $security_join->{extra} + = ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"]; } - - return @joins; + + my $security_cc_join = { + table => 'cc', + as => 'security_cc', + extra => ['security_cc.who = ' . $user->id], + }; + push(@joins, $security_cc_join); + } + + return @joins; } sub _sql_from { - my ($self, $joins_input) = @_; - my @joins = ($self->_standard_joins, $self->_select_order_joins, - @$joins_input); - @joins = $self->_extract_then_to(\@joins); - @joins = $self->_combine_joins(\@joins); - my @join_sql = map { $self->_translate_join($_) } @joins; - return "bugs\n" . join("\n", @join_sql); + my ($self, $joins_input) = @_; + my @joins = ($self->_standard_joins, $self->_select_order_joins, @$joins_input); + @joins = $self->_extract_then_to(\@joins); + @joins = $self->_combine_joins(\@joins); + my @join_sql = map { $self->_translate_join($_) } @joins; + return "bugs\n" . join("\n", @join_sql); } # This takes a join data structure and turns it into actual JOIN SQL. sub _translate_join { - my ($self, $join_info) = @_; - - die "join with no table: " . Dumper($join_info) if !$join_info->{table}; - die "join with no 'as': " . Dumper($join_info) if !$join_info->{as}; - - my $from_table = $join_info->{bugs_table} || "bugs"; - my $from = $join_info->{from} || "bug_id"; - if ($from =~ /^(\w+)\.(\w+)$/) { - ($from_table, $from) = ($1, $2); - } - my $table = $join_info->{table}; - my $name = $join_info->{as}; - my $to = $join_info->{to} || "bug_id"; - my $join = $join_info->{join} || 'LEFT'; - my @extra = @{ $join_info->{extra} || [] }; - $name =~ s/\./_/g; - - # If a term contains ORs, we need to put parens around the condition. - # This is a pretty weak test, but it's actually OK to put parens - # around too many things. - @extra = map { $_ =~ /\bOR\b/i ? "($_)" : $_ } @extra; - my $extra_condition = join(' AND ', uniq @extra); - if ($extra_condition) { - $extra_condition = " AND $extra_condition"; - } - - my @join_sql = "$join JOIN $table AS $name" - . " ON $from_table.$from = $name.$to$extra_condition"; - return @join_sql; + my ($self, $join_info) = @_; + + die "join with no table: " . Dumper($join_info) if !$join_info->{table}; + die "join with no 'as': " . Dumper($join_info) if !$join_info->{as}; + + my $from_table = $join_info->{bugs_table} || "bugs"; + my $from = $join_info->{from} || "bug_id"; + if ($from =~ /^(\w+)\.(\w+)$/) { + ($from_table, $from) = ($1, $2); + } + my $table = $join_info->{table}; + my $name = $join_info->{as}; + my $to = $join_info->{to} || "bug_id"; + my $join = $join_info->{join} || 'LEFT'; + my @extra = @{$join_info->{extra} || []}; + $name =~ s/\./_/g; + + # If a term contains ORs, we need to put parens around the condition. + # This is a pretty weak test, but it's actually OK to put parens + # around too many things. + @extra = map { $_ =~ /\bOR\b/i ? "($_)" : $_ } @extra; + my $extra_condition = join(' AND ', uniq @extra); + if ($extra_condition) { + $extra_condition = " AND $extra_condition"; + } + + my @join_sql = "$join JOIN $table AS $name" + . " ON $from_table.$from = $name.$to$extra_condition"; + return @join_sql; } ############################# @@ -1336,54 +1277,60 @@ sub _translate_join { # The terms that are always in the WHERE clause. These implement bug # group security. sub _standard_where { - my ($self) = @_; - return ('1=1') if $self->{_no_security_check}; - # If replication lags badly between the shadow db and the main DB, - # it's possible for bugs to show up in searches before their group - # controls are properly set. To prevent this, when initially creating - # bugs we set their creation_ts to NULL, and don't give them a creation_ts - # until their group controls are set. So if a bug has a NULL creation_ts, - # it shouldn't show up in searches at all. - my @where = ('bugs.creation_ts IS NOT NULL'); - - my $user = $self->_user; - my $security_term = ''; - # See also _standard_joins for the other half of the below statement - if (Bugzilla->params->{'or_groups'}) { - $security_term .= " (security_map.group_id IS NULL OR security_map.group_id IN (" . $user->groups_as_string . "))"; - } - else { - $security_term = 'security_map.group_id IS NULL'; - } - - if ($user->id) { - my $userid = $user->id; - # This indentation makes the resulting SQL more readable. - $security_term .= <<END; + my ($self) = @_; + return ('1=1') if $self->{_no_security_check}; + + # If replication lags badly between the shadow db and the main DB, + # it's possible for bugs to show up in searches before their group + # controls are properly set. To prevent this, when initially creating + # bugs we set their creation_ts to NULL, and don't give them a creation_ts + # until their group controls are set. So if a bug has a NULL creation_ts, + # it shouldn't show up in searches at all. + my @where = ('bugs.creation_ts IS NOT NULL'); + + my $user = $self->_user; + my $security_term = ''; + + # See also _standard_joins for the other half of the below statement + if (Bugzilla->params->{'or_groups'}) { + $security_term + .= " (security_map.group_id IS NULL OR security_map.group_id IN (" + . $user->groups_as_string . "))"; + } + else { + $security_term = 'security_map.group_id IS NULL'; + } + + if ($user->id) { + my $userid = $user->id; + + # This indentation makes the resulting SQL more readable. + $security_term .= <<END; OR (bugs.reporter_accessible = 1 AND bugs.reporter = $userid) OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL) OR bugs.assigned_to = $userid END - if (Bugzilla->params->{'useqacontact'}) { - $security_term.= " OR bugs.qa_contact = $userid"; - } - $security_term = "($security_term)"; + if (Bugzilla->params->{'useqacontact'}) { + $security_term .= " OR bugs.qa_contact = $userid"; } + $security_term = "($security_term)"; + } - push(@where, $security_term); + push(@where, $security_term); - return @where; + return @where; } sub _sql_where { - my ($self, $main_clause) = @_; - # The newline and this particular spacing makes the resulting - # SQL a bit more readable for debugging. - my $where = join("\n AND ", $self->_standard_where); - my $clause_sql = $main_clause->as_string; - $where .= "\n AND " . $clause_sql if $clause_sql; - return $where; + my ($self, $main_clause) = @_; + + # The newline and this particular spacing makes the resulting + # SQL a bit more readable for debugging. + my $where = join("\n AND ", $self->_standard_where); + my $clause_sql = $main_clause->as_string; + $where .= "\n AND " . $clause_sql if $clause_sql; + return $where; } ################################ @@ -1393,40 +1340,40 @@ sub _sql_where { # And these are the fields that we have to do GROUP BY for in DBs # that are more strict about putting everything into GROUP BY. sub _sql_group_by { - my ($self) = @_; - - # Strict DBs require every element from the SELECT to be in the GROUP BY, - # unless that element is being used in an aggregate function. - my @extra_group_by; - foreach my $column ($self->_select_columns) { - next if $self->_skip_group_by->{$column}; - my $sql = $self->COLUMNS->{$column}->{name} // $column; - push(@extra_group_by, $sql); - } + my ($self) = @_; - # And all items from ORDER BY must be in the GROUP BY. The above loop - # doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER. - foreach my $column ($self->_valid_order_columns) { - my $special_order = $self->_special_order->{$column}->{order}; - next if !$special_order; - push(@extra_group_by, @$special_order); - } - - @extra_group_by = uniq @extra_group_by; - - # bug_id is the only field we actually group by. - return ('bugs.bug_id', join(',', @extra_group_by)); + # Strict DBs require every element from the SELECT to be in the GROUP BY, + # unless that element is being used in an aggregate function. + my @extra_group_by; + foreach my $column ($self->_select_columns) { + next if $self->_skip_group_by->{$column}; + my $sql = $self->COLUMNS->{$column}->{name} // $column; + push(@extra_group_by, $sql); + } + + # And all items from ORDER BY must be in the GROUP BY. The above loop + # doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER. + foreach my $column ($self->_valid_order_columns) { + my $special_order = $self->_special_order->{$column}->{order}; + next if !$special_order; + push(@extra_group_by, @$special_order); + } + + @extra_group_by = uniq @extra_group_by; + + # bug_id is the only field we actually group by. + return ('bugs.bug_id', join(',', @extra_group_by)); } # A helper for _sql_group_by. sub _skip_group_by { - my ($self) = @_; - return $self->{skip_group_by} if $self->{skip_group_by}; - my @skip_list = GROUP_BY_SKIP; - push(@skip_list, keys %{ $self->_multi_select_fields }); - my %skip_hash = map { $_ => 1 } @skip_list; - $self->{skip_group_by} = \%skip_hash; - return $self->{skip_group_by}; + my ($self) = @_; + return $self->{skip_group_by} if $self->{skip_group_by}; + my @skip_list = GROUP_BY_SKIP; + push(@skip_list, keys %{$self->_multi_select_fields}); + my %skip_hash = map { $_ => 1 } @skip_list; + $self->{skip_group_by} = \%skip_hash; + return $self->{skip_group_by}; } ############################################## @@ -1435,244 +1382,255 @@ sub _skip_group_by { # Backwards compatibility for old field names. sub _convert_old_params { - my ($self) = @_; - my $params = $self->_params; - - # bugidtype has different values in modern Search.pm. - if (defined $params->{'bugidtype'}) { - my $value = $params->{'bugidtype'}; - $params->{'bugidtype'} = $value eq 'exclude' ? 'nowords' : 'anyexact'; - } - - foreach my $old_name (keys %{ FIELD_MAP() }) { - if (defined $params->{$old_name}) { - my $new_name = FIELD_MAP->{$old_name}; - $params->{$new_name} = delete $params->{$old_name}; - } + my ($self) = @_; + my $params = $self->_params; + + # bugidtype has different values in modern Search.pm. + if (defined $params->{'bugidtype'}) { + my $value = $params->{'bugidtype'}; + $params->{'bugidtype'} = $value eq 'exclude' ? 'nowords' : 'anyexact'; + } + + foreach my $old_name (keys %{FIELD_MAP()}) { + if (defined $params->{$old_name}) { + my $new_name = FIELD_MAP->{$old_name}; + $params->{$new_name} = delete $params->{$old_name}; } + } } # This parses all the standard search parameters except for the boolean # charts. sub _special_charts { - my ($self) = @_; - $self->_convert_old_params(); - $self->_special_parse_bug_status(); - $self->_special_parse_resolution(); - my $clause = new Bugzilla::Search::Clause(); - $clause->add( $self->_parse_basic_fields() ); - $clause->add( $self->_special_parse_email() ); - $clause->add( $self->_special_parse_chfield() ); - $clause->add( $self->_special_parse_deadline() ); - return $clause; + my ($self) = @_; + $self->_convert_old_params(); + $self->_special_parse_bug_status(); + $self->_special_parse_resolution(); + my $clause = new Bugzilla::Search::Clause(); + $clause->add($self->_parse_basic_fields()); + $clause->add($self->_special_parse_email()); + $clause->add($self->_special_parse_chfield()); + $clause->add($self->_special_parse_deadline()); + return $clause; } sub _parse_basic_fields { - my ($self) = @_; - my $params = $self->_params; - my $chart_fields = $self->_chart_fields; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $field_name (keys %$chart_fields) { - # CGI params shouldn't have periods in them, so we only accept - # period-separated fields with underscores where the periods go. - my $param_name = $field_name; - $param_name =~ s/\./_/g; - my @values = $self->_param_array($param_name); - next if !@values; - my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact'; - my $operator = $params->{"${param_name}_type"} || $default_op; - # Fields that are displayed as multi-selects are passed as arrays, - # so that they can properly search values that contain commas. - # However, other fields are sent as strings, so that they are properly - # split on commas if required. - my $field = $chart_fields->{$field_name}; - my $pass_value; - if ($field->is_select or $field->name eq 'version' - or $field->name eq 'target_milestone') - { - $pass_value = \@values; - } - else { - $pass_value = join(',', @values); - } - $clause->add($field_name, $operator, $pass_value); + my ($self) = @_; + my $params = $self->_params; + my $chart_fields = $self->_chart_fields; + + my $clause = new Bugzilla::Search::Clause(); + foreach my $field_name (keys %$chart_fields) { + + # CGI params shouldn't have periods in them, so we only accept + # period-separated fields with underscores where the periods go. + my $param_name = $field_name; + $param_name =~ s/\./_/g; + my @values = $self->_param_array($param_name); + next if !@values; + my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact'; + my $operator = $params->{"${param_name}_type"} || $default_op; + + # Fields that are displayed as multi-selects are passed as arrays, + # so that they can properly search values that contain commas. + # However, other fields are sent as strings, so that they are properly + # split on commas if required. + my $field = $chart_fields->{$field_name}; + my $pass_value; + if ( $field->is_select + or $field->name eq 'version' + or $field->name eq 'target_milestone') + { + $pass_value = \@values; + } + else { + $pass_value = join(',', @values); } - return @{$clause->children} ? $clause : undef; + $clause->add($field_name, $operator, $pass_value); + } + return @{$clause->children} ? $clause : undef; } sub _special_parse_bug_status { - my ($self) = @_; - my $params = $self->_params; - return if !defined $params->{'bug_status'}; - # We want to allow the bug_status_type parameter to work normally, - # meaning that this special code should only be activated if we are - # doing the normal "anyexact" search on bug_status. - return if (defined $params->{'bug_status_type'} - and $params->{'bug_status_type'} ne 'anyexact'); - - my @bug_status = $self->_param_array('bug_status'); - # Also include inactive bug statuses, as you can query them. - my $legal_statuses = $self->_chart_fields->{'bug_status'}->legal_values; - - # If the status contains __open__ or __closed__, translate those - # into their equivalent lists of open and closed statuses. - if (grep { $_ eq '__open__' } @bug_status) { - my @open = grep { $_->is_open } @$legal_statuses; - @open = map { $_->name } @open; - push(@bug_status, @open); - } - if (grep { $_ eq '__closed__' } @bug_status) { - my @closed = grep { not $_->is_open } @$legal_statuses; - @closed = map { $_->name } @closed; - push(@bug_status, @closed); - } - - @bug_status = uniq @bug_status; - my $all = grep { $_ eq "__all__" } @bug_status; - # This will also handle removing __open__ and __closed__ for us - # (__all__ too, which is why we check for it above, first). - @bug_status = _valid_values(\@bug_status, $legal_statuses); - - # If the user has selected every status, change to selecting none. - # This is functionally equivalent, but quite a lot faster. - if ($all or scalar(@bug_status) == scalar(@$legal_statuses)) { - delete $params->{'bug_status'}; - } - else { - $params->{'bug_status'} = \@bug_status; - } + my ($self) = @_; + my $params = $self->_params; + return if !defined $params->{'bug_status'}; + + # We want to allow the bug_status_type parameter to work normally, + # meaning that this special code should only be activated if we are + # doing the normal "anyexact" search on bug_status. + return + if (defined $params->{'bug_status_type'} + and $params->{'bug_status_type'} ne 'anyexact'); + + my @bug_status = $self->_param_array('bug_status'); + + # Also include inactive bug statuses, as you can query them. + my $legal_statuses = $self->_chart_fields->{'bug_status'}->legal_values; + + # If the status contains __open__ or __closed__, translate those + # into their equivalent lists of open and closed statuses. + if (grep { $_ eq '__open__' } @bug_status) { + my @open = grep { $_->is_open } @$legal_statuses; + @open = map { $_->name } @open; + push(@bug_status, @open); + } + if (grep { $_ eq '__closed__' } @bug_status) { + my @closed = grep { not $_->is_open } @$legal_statuses; + @closed = map { $_->name } @closed; + push(@bug_status, @closed); + } + + @bug_status = uniq @bug_status; + my $all = grep { $_ eq "__all__" } @bug_status; + + # This will also handle removing __open__ and __closed__ for us + # (__all__ too, which is why we check for it above, first). + @bug_status = _valid_values(\@bug_status, $legal_statuses); + + # If the user has selected every status, change to selecting none. + # This is functionally equivalent, but quite a lot faster. + if ($all or scalar(@bug_status) == scalar(@$legal_statuses)) { + delete $params->{'bug_status'}; + } + else { + $params->{'bug_status'} = \@bug_status; + } } sub _special_parse_chfield { - my ($self) = @_; - my $params = $self->_params; - - my $date_from = trim(lc($params->{'chfieldfrom'} || '')); - my $date_to = trim(lc($params->{'chfieldto'} || '')); - $date_from = '' if $date_from eq 'now'; - $date_to = '' if $date_to eq 'now'; - my @fields = $self->_param_array('chfield'); - my $value_to = $params->{'chfieldvalue'}; - $value_to = '' if !defined $value_to; - - @fields = map { $_ eq '[Bug creation]' ? 'creation_ts' : $_ } @fields; - - return undef unless ($date_from ne '' || $date_to ne '' || $value_to ne ''); - - my $clause = new Bugzilla::Search::Clause(); - - # It is always safe and useful to push delta_ts into the charts - # if there is a "from" date specified. It doesn't conflict with - # searching [Bug creation], because a bug's delta_ts is set to - # its creation_ts when it is created. So this just gives the - # database an additional index to possibly choose, on a table that - # is smaller than bugs_activity. - if ($date_from ne '') { - $clause->add('delta_ts', 'greaterthaneq', $date_from); - } - # It's not normally safe to do it for "to" dates, though--"chfieldto" means - # "a field that changed before this date", and delta_ts could be either - # later or earlier than that, if we're searching for the time that a field - # changed. However, chfieldto all by itself, without any chfieldvalue or - # chfield, means "just search delta_ts", and so we still want that to - # work. - if ($date_to ne '' and !@fields and $value_to eq '') { - $clause->add('delta_ts', 'lessthaneq', $date_to); - } - - # chfieldto is supposed to be a relative date or a date of the form - # YYYY-MM-DD, i.e. without the time appended to it. We append the - # time ourselves so that the end date is correctly taken into account. - $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/; - - my $join_clause = new Bugzilla::Search::Clause('OR'); - - foreach my $field (@fields) { - my $sub_clause = new Bugzilla::Search::ClauseGroup(); - $sub_clause->add(condition($field, 'changedto', $value_to)) if $value_to ne ''; - $sub_clause->add(condition($field, 'changedafter', $date_from)) if $date_from ne ''; - $sub_clause->add(condition($field, 'changedbefore', $date_to)) if $date_to ne ''; - $join_clause->add($sub_clause); - } - $clause->add($join_clause); - - return @{$clause->children} ? $clause : undef; + my ($self) = @_; + my $params = $self->_params; + + my $date_from = trim(lc($params->{'chfieldfrom'} || '')); + my $date_to = trim(lc($params->{'chfieldto'} || '')); + $date_from = '' if $date_from eq 'now'; + $date_to = '' if $date_to eq 'now'; + my @fields = $self->_param_array('chfield'); + my $value_to = $params->{'chfieldvalue'}; + $value_to = '' if !defined $value_to; + + @fields = map { $_ eq '[Bug creation]' ? 'creation_ts' : $_ } @fields; + + return undef unless ($date_from ne '' || $date_to ne '' || $value_to ne ''); + + my $clause = new Bugzilla::Search::Clause(); + + # It is always safe and useful to push delta_ts into the charts + # if there is a "from" date specified. It doesn't conflict with + # searching [Bug creation], because a bug's delta_ts is set to + # its creation_ts when it is created. So this just gives the + # database an additional index to possibly choose, on a table that + # is smaller than bugs_activity. + if ($date_from ne '') { + $clause->add('delta_ts', 'greaterthaneq', $date_from); + } + + # It's not normally safe to do it for "to" dates, though--"chfieldto" means + # "a field that changed before this date", and delta_ts could be either + # later or earlier than that, if we're searching for the time that a field + # changed. However, chfieldto all by itself, without any chfieldvalue or + # chfield, means "just search delta_ts", and so we still want that to + # work. + if ($date_to ne '' and !@fields and $value_to eq '') { + $clause->add('delta_ts', 'lessthaneq', $date_to); + } + + # chfieldto is supposed to be a relative date or a date of the form + # YYYY-MM-DD, i.e. without the time appended to it. We append the + # time ourselves so that the end date is correctly taken into account. + $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/; + + my $join_clause = new Bugzilla::Search::Clause('OR'); + + foreach my $field (@fields) { + my $sub_clause = new Bugzilla::Search::ClauseGroup(); + $sub_clause->add(condition($field, 'changedto', $value_to)) if $value_to ne ''; + $sub_clause->add(condition($field, 'changedafter', $date_from)) + if $date_from ne ''; + $sub_clause->add(condition($field, 'changedbefore', $date_to)) + if $date_to ne ''; + $join_clause->add($sub_clause); + } + $clause->add($join_clause); + + return @{$clause->children} ? $clause : undef; } sub _special_parse_deadline { - my ($self) = @_; - my $params = $self->_params; + my ($self) = @_; + my $params = $self->_params; - my $clause = new Bugzilla::Search::Clause(); - if (my $from = $params->{'deadlinefrom'}) { - $clause->add('deadline', 'greaterthaneq', $from); - } - if (my $to = $params->{'deadlineto'}) { - $clause->add('deadline', 'lessthaneq', $to); - } + my $clause = new Bugzilla::Search::Clause(); + if (my $from = $params->{'deadlinefrom'}) { + $clause->add('deadline', 'greaterthaneq', $from); + } + if (my $to = $params->{'deadlineto'}) { + $clause->add('deadline', 'lessthaneq', $to); + } - return @{$clause->children} ? $clause : undef; + return @{$clause->children} ? $clause : undef; } sub _special_parse_email { - my ($self) = @_; - my $params = $self->_params; - - my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $param (@email_params) { - $param =~ /(\d+)$/; - my $id = $1; - my $email = trim($params->{"email$id"}); - next if !$email; - my $type = $params->{"emailtype$id"} || 'anyexact'; - # for backward compatibility - $type = "equals" if $type eq "exact"; - - my $or_clause = new Bugzilla::Search::Clause('OR'); - foreach my $field (qw(assigned_to reporter cc qa_contact)) { - if ($params->{"email$field$id"}) { - $or_clause->add($field, $type, $email); - } - } - if ($params->{"emaillongdesc$id"}) { - $or_clause->add("commenter", $type, $email); - } - - $clause->add($or_clause); + my ($self) = @_; + my $params = $self->_params; + + my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params; + + my $clause = new Bugzilla::Search::Clause(); + foreach my $param (@email_params) { + $param =~ /(\d+)$/; + my $id = $1; + my $email = trim($params->{"email$id"}); + next if !$email; + my $type = $params->{"emailtype$id"} || 'anyexact'; + + # for backward compatibility + $type = "equals" if $type eq "exact"; + + my $or_clause = new Bugzilla::Search::Clause('OR'); + foreach my $field (qw(assigned_to reporter cc qa_contact)) { + if ($params->{"email$field$id"}) { + $or_clause->add($field, $type, $email); + } + } + if ($params->{"emaillongdesc$id"}) { + $or_clause->add("commenter", $type, $email); } - return @{$clause->children} ? $clause : undef; + $clause->add($or_clause); + } + + return @{$clause->children} ? $clause : undef; } sub _special_parse_resolution { - my ($self) = @_; - my $params = $self->_params; - return if !defined $params->{'resolution'}; - - my @resolution = $self->_param_array('resolution'); - my $legal_resolutions = $self->_chart_fields->{resolution}->legal_values; - @resolution = _valid_values(\@resolution, $legal_resolutions, '---'); - if (scalar(@resolution) == scalar(@$legal_resolutions)) { - delete $params->{'resolution'}; - } + my ($self) = @_; + my $params = $self->_params; + return if !defined $params->{'resolution'}; + + my @resolution = $self->_param_array('resolution'); + my $legal_resolutions = $self->_chart_fields->{resolution}->legal_values; + @resolution = _valid_values(\@resolution, $legal_resolutions, '---'); + if (scalar(@resolution) == scalar(@$legal_resolutions)) { + delete $params->{'resolution'}; + } } sub _valid_values { - my ($input, $valid, $extra_value) = @_; - my @result; - foreach my $item (@$input) { - $item = trim($item); - if (defined $extra_value and $item eq $extra_value) { - push(@result, $item); - } - elsif (grep { $_->name eq $item } @$valid) { - push(@result, $item); - } + my ($input, $valid, $extra_value) = @_; + my @result; + foreach my $item (@$input) { + $item = trim($item); + if (defined $extra_value and $item eq $extra_value) { + push(@result, $item); } - return @result; + elsif (grep { $_->name eq $item } @$valid) { + push(@result, $item); + } + } + return @result; } ###################################### @@ -1680,239 +1638,247 @@ sub _valid_values { ###################################### sub _charts_to_conditions { - my ($self) = @_; - - my $clause = $self->_charts; - my @joins; - $clause->walk_conditions(sub { - my ($clause, $condition) = @_; - return if !$condition->translated; - push(@joins, @{ $condition->translated->{joins} }); - }); - return (\@joins, $clause); + my ($self) = @_; + + my $clause = $self->_charts; + my @joins; + $clause->walk_conditions(sub { + my ($clause, $condition) = @_; + return if !$condition->translated; + push(@joins, @{$condition->translated->{joins}}); + }); + return (\@joins, $clause); } sub _charts { - my ($self) = @_; - - my $clause = $self->_params_to_data_structure(); - my $chart_id = 0; - $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) }); - return $clause; + my ($self) = @_; + + my $clause = $self->_params_to_data_structure(); + my $chart_id = 0; + $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) }); + return $clause; } sub _params_to_data_structure { - my ($self) = @_; - - # First we get the "special" charts, representing all the normal - # fields on the search page. This may modify _params, so it needs to - # happen first. - my $clause = $self->_special_charts; - - # Then we process the old Boolean Charts input format. - $clause->add( $self->_boolean_charts ); - - # And then process the modern "custom search" format. - $clause->add( $self->_custom_search ); - - return $clause; + my ($self) = @_; + + # First we get the "special" charts, representing all the normal + # fields on the search page. This may modify _params, so it needs to + # happen first. + my $clause = $self->_special_charts; + + # Then we process the old Boolean Charts input format. + $clause->add($self->_boolean_charts); + + # And then process the modern "custom search" format. + $clause->add($self->_custom_search); + + return $clause; } sub _boolean_charts { - my ($self) = @_; - - my $params = $self->_params; - my @param_list = keys %$params; - - my @all_field_params = grep { /^field-?\d+/ } @param_list; - my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params; - @chart_ids = sort { $a <=> $b } uniq @chart_ids; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $chart_id (@chart_ids) { - my @all_and = grep { /^field$chart_id-\d+/ } @param_list; - my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and; - @and_ids = sort { $a <=> $b } uniq @and_ids; - - my $and_clause = new Bugzilla::Search::Clause(); - foreach my $and_id (@and_ids) { - my @all_or = grep { /^field$chart_id-$and_id-\d+/ } @param_list; - my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or; - @or_ids = sort { $a <=> $b } uniq @or_ids; - - my $or_clause = new Bugzilla::Search::Clause('OR'); - foreach my $or_id (@or_ids) { - my $identifier = "$chart_id-$and_id-$or_id"; - my $field = $params->{"field$identifier"}; - my $operator = $params->{"type$identifier"}; - my $value = $params->{"value$identifier"}; - # no-value operators ignore the value, however a value needs to be set - $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; - $or_clause->add($field, $operator, $value); - } - $and_clause->add($or_clause); - $and_clause->negate(1) if $params->{"negate$chart_id"}; - } - $clause->add($and_clause); + my ($self) = @_; + + my $params = $self->_params; + my @param_list = keys %$params; + + my @all_field_params = grep {/^field-?\d+/} @param_list; + my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params; + @chart_ids = sort { $a <=> $b } uniq @chart_ids; + + my $clause = new Bugzilla::Search::Clause(); + foreach my $chart_id (@chart_ids) { + my @all_and = grep {/^field$chart_id-\d+/} @param_list; + my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and; + @and_ids = sort { $a <=> $b } uniq @and_ids; + + my $and_clause = new Bugzilla::Search::Clause(); + foreach my $and_id (@and_ids) { + my @all_or = grep {/^field$chart_id-$and_id-\d+/} @param_list; + my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or; + @or_ids = sort { $a <=> $b } uniq @or_ids; + + my $or_clause = new Bugzilla::Search::Clause('OR'); + foreach my $or_id (@or_ids) { + my $identifier = "$chart_id-$and_id-$or_id"; + my $field = $params->{"field$identifier"}; + my $operator = $params->{"type$identifier"}; + my $value = $params->{"value$identifier"}; + + # no-value operators ignore the value, however a value needs to be set + $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; + $or_clause->add($field, $operator, $value); + } + $and_clause->add($or_clause); + $and_clause->negate(1) if $params->{"negate$chart_id"}; } + $clause->add($and_clause); + } - return @{$clause->children} ? $clause : undef; + return @{$clause->children} ? $clause : undef; } sub _custom_search { - my ($self) = @_; - my $params = $self->_params; - - my @field_ids = $self->_field_ids; - return unless scalar @field_ids; - - my $joiner = $params->{j_top} || ''; - my $current_clause = $joiner eq 'AND_G' + my ($self) = @_; + my $params = $self->_params; + + my @field_ids = $self->_field_ids; + return unless scalar @field_ids; + + my $joiner = $params->{j_top} || ''; + my $current_clause + = $joiner eq 'AND_G' + ? new Bugzilla::Search::ClauseGroup() + : new Bugzilla::Search::Clause($joiner); + + my @clause_stack; + foreach my $id (@field_ids) { + my $field = $params->{"f$id"}; + if ($field eq 'OP') { + my $joiner = $params->{"j$id"} || ''; + my $new_clause + = $joiner eq 'AND_G' ? new Bugzilla::Search::ClauseGroup() : new Bugzilla::Search::Clause($joiner); - - my @clause_stack; - foreach my $id (@field_ids) { - my $field = $params->{"f$id"}; - if ($field eq 'OP') { - my $joiner = $params->{"j$id"} || ''; - my $new_clause = $joiner eq 'AND_G' - ? new Bugzilla::Search::ClauseGroup() - : new Bugzilla::Search::Clause($joiner); - $new_clause->negate($params->{"n$id"}); - $current_clause->add($new_clause); - push(@clause_stack, $current_clause); - $current_clause = $new_clause; - next; - } - if ($field eq 'CP') { - $current_clause = pop @clause_stack; - ThrowCodeError('search_cp_without_op', { id => $id }) - if !$current_clause; - next; - } - - my $operator = $params->{"o$id"}; - my $value = $params->{"v$id"}; - # no-value operators ignore the value, however a value needs to be set - $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; - my $condition = condition($field, $operator, $value); - $condition->negate($params->{"n$id"}); - $current_clause->add($condition); + $new_clause->negate($params->{"n$id"}); + $current_clause->add($new_clause); + push(@clause_stack, $current_clause); + $current_clause = $new_clause; + next; } - - # We allow people to specify more OPs than CPs, so at the end of the - # loop our top clause may be still in the stack instead of being - # $current_clause. - return $clause_stack[0] || $current_clause; + if ($field eq 'CP') { + $current_clause = pop @clause_stack; + ThrowCodeError('search_cp_without_op', {id => $id}) if !$current_clause; + next; + } + + my $operator = $params->{"o$id"}; + my $value = $params->{"v$id"}; + + # no-value operators ignore the value, however a value needs to be set + $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; + my $condition = condition($field, $operator, $value); + $condition->negate($params->{"n$id"}); + $current_clause->add($condition); + } + + # We allow people to specify more OPs than CPs, so at the end of the + # loop our top clause may be still in the stack instead of being + # $current_clause. + return $clause_stack[0] || $current_clause; } sub _field_ids { - my ($self) = @_; - my $params = $self->_params; - my @param_list = keys %$params; - - my @field_params = grep { /^f\d+$/ } @param_list; - my @field_ids = map { /(\d+)/; $1 } @field_params; - @field_ids = sort { $a <=> $b } @field_ids; - return @field_ids; + my ($self) = @_; + my $params = $self->_params; + my @param_list = keys %$params; + + my @field_params = grep {/^f\d+$/} @param_list; + my @field_ids = map { /(\d+)/; $1 } @field_params; + @field_ids = sort { $a <=> $b } @field_ids; + return @field_ids; } sub _handle_chart { - my ($self, $chart_id, $clause, $condition) = @_; - my $dbh = Bugzilla->dbh; - my $params = $self->_params; - my ($field, $operator, $value) = $condition->fov; - return if (!defined $field or !defined $operator or !defined $value); - $field = FIELD_MAP->{$field} || $field; - - my ($string_value, $orig_value); - state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; - - if (ref $value eq 'ARRAY') { - # Trim input and ignore blank values. - @$value = map { trim($_) } @$value; - @$value = grep { defined $_ and $_ ne '' } @$value; - return if !@$value; - $orig_value = join(',', @$value); - if ($field eq 'longdesc' && $is_mysql) { - @$value = map { _convert_unicode_characters($_) } @$value; - } - $string_value = join(',', @$value); - } - else { - return if $value eq ''; - $orig_value = $value; - if ($field eq 'longdesc' && $is_mysql) { - $value = _convert_unicode_characters($value); - } - $string_value = $value; - } - - $self->_chart_fields->{$field} - or ThrowCodeError("invalid_field_name", { field => $field }); - trick_taint($field); - - # This is the field as you'd reference it in a SQL statement. - my $full_field = $field =~ /\./ ? $field : "bugs.$field"; - - # "value" and "quoted" are for search functions that always operate - # on a scalar string and never care if they were passed multiple - # parameters. If the user does pass multiple parameters, they will - # become a space-separated string for those search functions. - # - # all_values is for search functions that do operate - # on multiple values, like anyexact. - - my %search_args = ( - chart_id => $chart_id, - sequence => $chart_id, - field => $field, - full_field => $full_field, - operator => $operator, - value => $string_value, - all_values => $value, - joins => [], - bugs_table => 'bugs', - table_suffix => '', - condition => $condition, - ); - $clause->update_search_args(\%search_args); - - $search_args{quoted} = $self->_quote_unless_numeric(\%search_args); - # This should add a "term" selement to %search_args. - $self->do_search_function(\%search_args); - - # If term is left empty, then this means the criteria - # has no effect and can be ignored. - return unless $search_args{term}; - - # All the things here that don't get pulled out of - # %search_args are their original values before - # do_search_function modified them. - $self->search_description({ - field => $field, type => $operator, - value => $orig_value, term => $search_args{term}, - }); - - foreach my $join (@{ $search_args{joins} }) { - $join->{bugs_table} = $search_args{bugs_table}; - $join->{table_suffix} = $search_args{table_suffix}; - } - - $condition->translated(\%search_args); + my ($self, $chart_id, $clause, $condition) = @_; + my $dbh = Bugzilla->dbh; + my $params = $self->_params; + my ($field, $operator, $value) = $condition->fov; + return if (!defined $field or !defined $operator or !defined $value); + $field = FIELD_MAP->{$field} || $field; + + my ($string_value, $orig_value); + state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + + if (ref $value eq 'ARRAY') { + + # Trim input and ignore blank values. + @$value = map { trim($_) } @$value; + @$value = grep { defined $_ and $_ ne '' } @$value; + return if !@$value; + $orig_value = join(',', @$value); + if ($field eq 'longdesc' && $is_mysql) { + @$value = map { _convert_unicode_characters($_) } @$value; + } + $string_value = join(',', @$value); + } + else { + return if $value eq ''; + $orig_value = $value; + if ($field eq 'longdesc' && $is_mysql) { + $value = _convert_unicode_characters($value); + } + $string_value = $value; + } + + $self->_chart_fields->{$field} + or ThrowCodeError("invalid_field_name", {field => $field}); + trick_taint($field); + + # This is the field as you'd reference it in a SQL statement. + my $full_field = $field =~ /\./ ? $field : "bugs.$field"; + + # "value" and "quoted" are for search functions that always operate + # on a scalar string and never care if they were passed multiple + # parameters. If the user does pass multiple parameters, they will + # become a space-separated string for those search functions. + # + # all_values is for search functions that do operate + # on multiple values, like anyexact. + + my %search_args = ( + chart_id => $chart_id, + sequence => $chart_id, + field => $field, + full_field => $full_field, + operator => $operator, + value => $string_value, + all_values => $value, + joins => [], + bugs_table => 'bugs', + table_suffix => '', + condition => $condition, + ); + $clause->update_search_args(\%search_args); + + $search_args{quoted} = $self->_quote_unless_numeric(\%search_args); + + # This should add a "term" selement to %search_args. + $self->do_search_function(\%search_args); + + # If term is left empty, then this means the criteria + # has no effect and can be ignored. + return unless $search_args{term}; + + # All the things here that don't get pulled out of + # %search_args are their original values before + # do_search_function modified them. + $self->search_description({ + field => $field, + type => $operator, + value => $orig_value, + term => $search_args{term}, + }); + + foreach my $join (@{$search_args{joins}}) { + $join->{bugs_table} = $search_args{bugs_table}; + $join->{table_suffix} = $search_args{table_suffix}; + } + + $condition->translated(\%search_args); } # XXX - This is a hack for MySQL which doesn't understand Unicode characters # above U+FFFF, see Bugzilla::Comment::_check_thetext(). This hack can go away # once we require MySQL 5.5.3 and use utf8mb4. sub _convert_unicode_characters { - my $string = shift; + my $string = shift; - # Perl 5.13.8 and older complain about non-characters. - no warnings 'utf8'; - $string =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; - return $string; + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $string + =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; + return $string; } ################################## @@ -1922,121 +1888,126 @@ sub _convert_unicode_characters { # This takes information about the current boolean chart and translates # it into SQL, using the constants at the top of this file. sub do_search_function { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; - - if (my $parse_func = SPECIAL_PARSING->{$field}) { - $self->$parse_func($args); - # Some parsing functions set $term, though most do not. - # For the ones that set $term, we don't need to do any further - # parsing. - return if $args->{term}; - } - - my $operator_field_override = $self->_get_operator_field_override(); - my $override = $operator_field_override->{$field}; - # Attachment fields get special handling, if they don't have a specific - # individual override. - if (!$override and $field =~ /^attachments\./) { - $override = $operator_field_override->{attachments}; - } - # If there's still no override, check for an override on the field's type. - if (!$override) { - my $field_obj = $self->_chart_fields->{$field}; - $override = $operator_field_override->{$field_obj->type}; - } - - if ($override) { - my $search_func = $self->_pick_override_function($override, $operator); - $self->$search_func($args) if $search_func; - } + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; - # Some search functions set $term, and some don't. For the ones that - # don't (or for fields that don't have overrides) we now call the - # direct operator function from OPERATORS. - if (!defined $args->{term}) { - $self->_do_operator_function($args); - } - - if (!defined $args->{term}) { - # This field and this type don't work together. Generally, - # this should never be reached, because it should be handled - # explicitly by OPERATOR_FIELD_OVERRIDE. - ThrowUserError("search_field_operator_invalid", - { field => $field, operator => $operator }); - } + if (my $parse_func = SPECIAL_PARSING->{$field}) { + $self->$parse_func($args); + + # Some parsing functions set $term, though most do not. + # For the ones that set $term, we don't need to do any further + # parsing. + return if $args->{term}; + } + + my $operator_field_override = $self->_get_operator_field_override(); + my $override = $operator_field_override->{$field}; + + # Attachment fields get special handling, if they don't have a specific + # individual override. + if (!$override and $field =~ /^attachments\./) { + $override = $operator_field_override->{attachments}; + } + + # If there's still no override, check for an override on the field's type. + if (!$override) { + my $field_obj = $self->_chart_fields->{$field}; + $override = $operator_field_override->{$field_obj->type}; + } + + if ($override) { + my $search_func = $self->_pick_override_function($override, $operator); + $self->$search_func($args) if $search_func; + } + + # Some search functions set $term, and some don't. For the ones that + # don't (or for fields that don't have overrides) we now call the + # direct operator function from OPERATORS. + if (!defined $args->{term}) { + $self->_do_operator_function($args); + } + + if (!defined $args->{term}) { + + # This field and this type don't work together. Generally, + # this should never be reached, because it should be handled + # explicitly by OPERATOR_FIELD_OVERRIDE. + ThrowUserError("search_field_operator_invalid", + {field => $field, operator => $operator}); + } } # A helper for various search functions that need to run operator # functions directly. sub _do_operator_function { - my ($self, $func_args) = @_; - my $operator = $func_args->{operator}; - my $operator_func = OPERATORS->{$operator} - || ThrowCodeError("search_field_operator_unsupported", - { operator => $operator }); - $self->$operator_func($func_args); + my ($self, $func_args) = @_; + my $operator = $func_args->{operator}; + my $operator_func + = OPERATORS->{$operator} + || ThrowCodeError("search_field_operator_unsupported", + {operator => $operator}); + $self->$operator_func($func_args); } sub _reverse_operator { - my ($self, $operator) = @_; - my $reverse = OPERATOR_REVERSE->{$operator}; - return $reverse if $reverse; - if ($operator =~ s/^not//) { - return $operator; - } - return "not$operator"; + my ($self, $operator) = @_; + my $reverse = OPERATOR_REVERSE->{$operator}; + return $reverse if $reverse; + if ($operator =~ s/^not//) { + return $operator; + } + return "not$operator"; } sub _pick_override_function { - my ($self, $override, $operator) = @_; - my $search_func = $override->{$operator}; - - if (!$search_func) { - # If we don't find an override for one specific operator, - # then there are some special override types: - # _non_changed: For any operator that doesn't have the word - # "changed" in it - # _default: Overrides all operators that aren't explicitly specified. - if ($override->{_non_changed} and $operator !~ /changed/) { - $search_func = $override->{_non_changed}; - } - elsif ($override->{_default}) { - $search_func = $override->{_default}; - } + my ($self, $override, $operator) = @_; + my $search_func = $override->{$operator}; + + if (!$search_func) { + + # If we don't find an override for one specific operator, + # then there are some special override types: + # _non_changed: For any operator that doesn't have the word + # "changed" in it + # _default: Overrides all operators that aren't explicitly specified. + if ($override->{_non_changed} and $operator !~ /changed/) { + $search_func = $override->{_non_changed}; } + elsif ($override->{_default}) { + $search_func = $override->{_default}; + } + } - return $search_func; + return $search_func; } sub _get_operator_field_override { - my $self = shift; - my $cache = Bugzilla->request_cache; + my $self = shift; + my $cache = Bugzilla->request_cache; - return $cache->{operator_field_override} - if defined $cache->{operator_field_override}; + return $cache->{operator_field_override} + if defined $cache->{operator_field_override}; - my %operator_field_override = %{ OPERATOR_FIELD_OVERRIDE() }; - Bugzilla::Hook::process('search_operator_field_override', - { search => $self, - operators => \%operator_field_override }); + my %operator_field_override = %{OPERATOR_FIELD_OVERRIDE()}; + Bugzilla::Hook::process('search_operator_field_override', + {search => $self, operators => \%operator_field_override}); - $cache->{operator_field_override} = \%operator_field_override; - return $cache->{operator_field_override}; + $cache->{operator_field_override} = \%operator_field_override; + return $cache->{operator_field_override}; } sub _get_column_joins { - my $self = shift; - my $cache = Bugzilla->request_cache; + my $self = shift; + my $cache = Bugzilla->request_cache; - return $cache->{column_joins} if defined $cache->{column_joins}; + return $cache->{column_joins} if defined $cache->{column_joins}; - my %column_joins = %{ $self->COLUMN_JOINS() }; - Bugzilla::Hook::process('buglist_column_joins', - { column_joins => \%column_joins }); + my %column_joins = %{$self->COLUMN_JOINS()}; + Bugzilla::Hook::process('buglist_column_joins', + {column_joins => \%column_joins}); - $cache->{column_joins} = \%column_joins; - return $cache->{column_joins}; + $cache->{column_joins} = \%column_joins; + return $cache->{column_joins}; } ########################### @@ -2048,47 +2019,49 @@ sub _get_column_joins { # is just a performance optimization, but on SQLite it actually changes # the behavior of some searches. sub _quote_unless_numeric { - my ($self, $args, $value) = @_; - if (!defined $value) { - $value = $args->{value}; - } - my ($field, $operator) = @$args{qw(field operator)}; - - my $numeric_operator = !grep { $_ eq $operator } NON_NUMERIC_OPERATORS; - my $numeric_field = $self->_chart_fields->{$field}->is_numeric; - my $numeric_value = ($value =~ NUMBER_REGEX) ? 1 : 0; - my $is_numeric = $numeric_operator && $numeric_field && $numeric_value; - - # These operators are really numeric operators with numeric fields. - $numeric_operator = grep { $_ eq $operator } keys %{ SIMPLE_OPERATORS() }; - - if ($is_numeric) { - my $quoted = $value; - trick_taint($quoted); - return $quoted; - } - elsif ($numeric_field && !$numeric_value && $numeric_operator) { - ThrowUserError('number_not_numeric', { field => $field, num => $value }); - } - return Bugzilla->dbh->quote($value); + my ($self, $args, $value) = @_; + if (!defined $value) { + $value = $args->{value}; + } + my ($field, $operator) = @$args{qw(field operator)}; + + my $numeric_operator = !grep { $_ eq $operator } NON_NUMERIC_OPERATORS; + my $numeric_field = $self->_chart_fields->{$field}->is_numeric; + my $numeric_value = ($value =~ NUMBER_REGEX) ? 1 : 0; + my $is_numeric = $numeric_operator && $numeric_field && $numeric_value; + + # These operators are really numeric operators with numeric fields. + $numeric_operator = grep { $_ eq $operator } keys %{SIMPLE_OPERATORS()}; + + if ($is_numeric) { + my $quoted = $value; + trick_taint($quoted); + return $quoted; + } + elsif ($numeric_field && !$numeric_value && $numeric_operator) { + ThrowUserError('number_not_numeric', {field => $field, num => $value}); + } + return Bugzilla->dbh->quote($value); } sub build_subselect { - my ($outer, $inner, $table, $cond, $negate) = @_; - if ($table =~ /\battach_data\b/) { - # It takes a long time to scan the whole attach_data table - # unconditionally, so we return the subselect and let the DB optimizer - # restrict the search based on other search criteria. - my $not = $negate ? "NOT" : ""; - return "$outer $not IN (SELECT DISTINCT $inner FROM $table WHERE $cond)"; - } - # Execute subselects immediately to avoid dependent subqueries, which are - # large performance hits on MySql - my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond"; - my $dbh = Bugzilla->dbh; - my $list = $dbh->selectcol_arrayref($q); - return $negate ? "1=1" : "1=2" unless @$list; - return $dbh->sql_in($outer, $list, $negate); + my ($outer, $inner, $table, $cond, $negate) = @_; + if ($table =~ /\battach_data\b/) { + + # It takes a long time to scan the whole attach_data table + # unconditionally, so we return the subselect and let the DB optimizer + # restrict the search based on other search criteria. + my $not = $negate ? "NOT" : ""; + return "$outer $not IN (SELECT DISTINCT $inner FROM $table WHERE $cond)"; + } + + # Execute subselects immediately to avoid dependent subqueries, which are + # large performance hits on MySql + my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond"; + my $dbh = Bugzilla->dbh; + my $list = $dbh->selectcol_arrayref($q); + return $negate ? "1=1" : "1=2" unless @$list; + return $dbh->sql_in($outer, $list, $negate); } # Used by anyexact to get the list of input values. This allows us to @@ -2096,68 +2069,69 @@ sub build_subselect { # still accept string values for the boolean charts (and split them on # commas). sub _all_values { - my ($self, $args, $split_on) = @_; - $split_on ||= qr/[\s,]+/; - my $dbh = Bugzilla->dbh; - my $all_values = $args->{all_values}; - - my @array; - if (ref $all_values eq 'ARRAY') { - @array = @$all_values; - } - else { - @array = split($split_on, $all_values); - @array = map { trim($_) } @array; - @array = grep { defined $_ and $_ ne '' } @array; - } - - if ($args->{field} eq 'resolution') { - @array = map { $_ eq '---' ? '' : $_ } @array; - } - - return @array; + my ($self, $args, $split_on) = @_; + $split_on ||= qr/[\s,]+/; + my $dbh = Bugzilla->dbh; + my $all_values = $args->{all_values}; + + my @array; + if (ref $all_values eq 'ARRAY') { + @array = @$all_values; + } + else { + @array = split($split_on, $all_values); + @array = map { trim($_) } @array; + @array = grep { defined $_ and $_ ne '' } @array; + } + + if ($args->{field} eq 'resolution') { + @array = map { $_ eq '---' ? '' : $_ } @array; + } + + return @array; } # Support for "any/all/nowordssubstr" comparison type ("words as substrings") sub _substring_terms { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; - # We don't have to (or want to) use _all_values, because we'd just - # split each term on spaces and commas anyway. - my @words = split(/[\s,]+/, $args->{value}); - @words = grep { defined $_ and $_ ne '' } @words; - my @terms = map { $dbh->sql_ilike($_, $args->{full_field}) } @words; - return @terms; + # We don't have to (or want to) use _all_values, because we'd just + # split each term on spaces and commas anyway. + my @words = split(/[\s,]+/, $args->{value}); + @words = grep { defined $_ and $_ ne '' } @words; + my @terms = map { $dbh->sql_ilike($_, $args->{full_field}) } @words; + return @terms; } sub _word_terms { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - my @values = split(/[\s,]+/, $args->{value}); - @values = grep { defined $_ and $_ ne '' } @values; - my @substring_terms = $self->_substring_terms($args); - - my @terms; - my $start = $dbh->WORD_START; - my $end = $dbh->WORD_END; - foreach my $word (@values) { - my $regex = $start . quotemeta($word) . $end; - my $quoted = $dbh->quote($regex); - # We don't have to check the regexp, because we escaped it, so we're - # sure it's valid. - my $regex_term = $dbh->sql_regexp($args->{full_field}, $quoted, - 'no check'); - # Regular expressions are slow--substring searches are faster. - # If we're searching for a word, we're also certain that the - # substring will appear in the value. So we limit first by - # substring and then by a regex that will match just words. - my $substring_term = shift @substring_terms; - push(@terms, "$substring_term AND $regex_term"); - } - - return @terms; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + my @values = split(/[\s,]+/, $args->{value}); + @values = grep { defined $_ and $_ ne '' } @values; + my @substring_terms = $self->_substring_terms($args); + + my @terms; + my $start = $dbh->WORD_START; + my $end = $dbh->WORD_END; + foreach my $word (@values) { + my $regex = $start . quotemeta($word) . $end; + my $quoted = $dbh->quote($regex); + + # We don't have to check the regexp, because we escaped it, so we're + # sure it's valid. + my $regex_term = $dbh->sql_regexp($args->{full_field}, $quoted, 'no check'); + + # Regular expressions are slow--substring searches are faster. + # If we're searching for a word, we're also certain that the + # substring will appear in the value. So we limit first by + # substring and then by a regex that will match just words. + my $substring_term = shift @substring_terms; + push(@terms, "$substring_term AND $regex_term"); + } + + return @terms; } ##################################### @@ -2165,109 +2139,118 @@ sub _word_terms { ##################################### sub _timestamp_translate { - my ($self, $ignore_time, $args) = @_; - my $value = $args->{value}; - my $dbh = Bugzilla->dbh; - - # Force parsing of all dates & times, so that we filter weird values out - # from users. - #return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i; - - $value = SqlifyDate($value); - # By default, the time is appended to the date, which we don't always want. - if ($ignore_time) { - ($value) = split(/\s/, $value); - } - $args->{value} = $value; - $args->{quoted} = $dbh->quote($value); + my ($self, $ignore_time, $args) = @_; + my $value = $args->{value}; + my $dbh = Bugzilla->dbh; + + # Force parsing of all dates & times, so that we filter weird values out + # from users. + #return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i; + + $value = SqlifyDate($value); + + # By default, the time is appended to the date, which we don't always want. + if ($ignore_time) { + ($value) = split(/\s/, $value); + } + $args->{value} = $value; + $args->{quoted} = $dbh->quote($value); } sub _datetime_translate { - return shift->_timestamp_translate(0, @_); + return shift->_timestamp_translate(0, @_); } sub _last_visit_datetime { - my ($self, $args) = @_; - my $value = $args->{value}; - - $self->_datetime_translate($args); - if ($value eq $args->{value}) { - # Failed to translate a datetime. let's try the pronoun expando. - if ($value eq '%last_changed%') { - $self->_add_extra_column('changeddate'); - $args->{value} = $args->{quoted} = 'bugs.delta_ts'; - } + my ($self, $args) = @_; + my $value = $args->{value}; + + $self->_datetime_translate($args); + if ($value eq $args->{value}) { + + # Failed to translate a datetime. let's try the pronoun expando. + if ($value eq '%last_changed%') { + $self->_add_extra_column('changeddate'); + $args->{value} = $args->{quoted} = 'bugs.delta_ts'; } + } } sub _date_translate { - return shift->_timestamp_translate(1, @_); + return shift->_timestamp_translate(1, @_); } sub SqlifyDate { - my ($str) = @_; - my $fmt = "%Y-%m-%d %H:%M:%S"; - $str = "" if (!defined $str || lc($str) eq 'now'); - if ($str eq "") { - my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time()); - return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday); - } - - if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date - my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time); - my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); - if ($sign && $sign eq '+') { $amount = -$amount; } - $startof = 1 if $amount == 0; - if ($unit eq 'w') { # convert weeks to days - $amount = 7*$amount; - $amount += $wday if $startof; - $unit = 'd'; - } - if ($unit eq 'd') { - if ($startof) { - $fmt = "%Y-%m-%d 00:00:00"; - $date -= $sec + 60*$min + 3600*$hour; - } - $date -= 24*3600*$amount; - return time2str($fmt, $date); - } - elsif ($unit eq 'y') { - if ($startof) { - return sprintf("%4d-01-01 00:00:00", $year+1900-$amount); - } - else { - return sprintf("%4d-%02d-%02d %02d:%02d:%02d", - $year+1900-$amount, $month+1, $mday, $hour, $min, $sec); - } - } - elsif ($unit eq 'm') { - $month -= $amount; - $year += floor($month/12); - $month %= 12; - if ($startof) { - return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1); - } - else { - return sprintf("%4d-%02d-%02d %02d:%02d:%02d", - $year+1900, $month+1, $mday, $hour, $min, $sec); - } - } - elsif ($unit eq 'h') { - # Special case for 'beginning of an hour' - if ($startof) { - $fmt = "%Y-%m-%d %H:00:00"; - } - $date -= 3600*$amount; - return time2str($fmt, $date); - } - return undef; # should not happen due to regexp at top - } - my $date = str2time($str); - if (!defined($date)) { - ThrowUserError("illegal_date", { date => $str }); - } - return time2str($fmt, $date); + my ($str) = @_; + my $fmt = "%Y-%m-%d %H:%M:%S"; + $str = "" if (!defined $str || lc($str) eq 'now'); + if ($str eq "") { + my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time()); + return sprintf("%4d-%02d-%02d 00:00:00", $year + 1900, $month + 1, $mday); + } + + if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date + my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time); + my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); + if ($sign && $sign eq '+') { $amount = -$amount; } + $startof = 1 if $amount == 0; + if ($unit eq 'w') { # convert weeks to days + $amount = 7 * $amount; + $amount += $wday if $startof; + $unit = 'd'; + } + if ($unit eq 'd') { + if ($startof) { + $fmt = "%Y-%m-%d 00:00:00"; + $date -= $sec + 60 * $min + 3600 * $hour; + } + $date -= 24 * 3600 * $amount; + return time2str($fmt, $date); + } + elsif ($unit eq 'y') { + if ($startof) { + return sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount); + } + else { + return sprintf( + "%4d-%02d-%02d %02d:%02d:%02d", + $year + 1900 - $amount, + $month + 1, $mday, $hour, $min, $sec + ); + } + } + elsif ($unit eq 'm') { + $month -= $amount; + $year += floor($month / 12); + $month %= 12; + if ($startof) { + return sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1); + } + else { + return sprintf( + "%4d-%02d-%02d %02d:%02d:%02d", + $year + 1900, + $month + 1, $mday, $hour, $min, $sec + ); + } + } + elsif ($unit eq 'h') { + + # Special case for 'beginning of an hour' + if ($startof) { + $fmt = "%Y-%m-%d %H:00:00"; + } + $date -= 3600 * $amount; + return time2str($fmt, $date); + } + return undef; # should not happen due to regexp at top + } + my $date = str2time($str); + if (!defined($date)) { + ThrowUserError("illegal_date", {date => $str}); + } + return time2str($fmt, $date); } ###################################### @@ -2275,104 +2258,109 @@ sub SqlifyDate { ###################################### sub pronoun { - my ($noun, $user) = (@_); - if ($noun eq "%user%") { - if ($user->id) { - return $user->id; - } else { - ThrowUserError('login_required_for_pronoun'); - } - } - if ($noun eq "%reporter%") { - return "bugs.reporter"; - } - if ($noun eq "%assignee%") { - return "bugs.assigned_to"; + my ($noun, $user) = (@_); + if ($noun eq "%user%") { + if ($user->id) { + return $user->id; } - if ($noun eq "%qacontact%") { - return "COALESCE(bugs.qa_contact,0)"; + else { + ThrowUserError('login_required_for_pronoun'); } + } + if ($noun eq "%reporter%") { + return "bugs.reporter"; + } + if ($noun eq "%assignee%") { + return "bugs.assigned_to"; + } + if ($noun eq "%qacontact%") { + return "COALESCE(bugs.qa_contact,0)"; + } - ThrowUserError('illegal_pronoun', { pronoun => $noun }); + ThrowUserError('illegal_pronoun', {pronoun => $noun}); } sub _contact_pronoun { - my ($self, $args) = @_; - my $value = $args->{value}; - my $user = $self->_user; + my ($self, $args) = @_; + my $value = $args->{value}; + my $user = $self->_user; - if ($value =~ /^\%group\.[^%]+%$/) { - $self->_contact_exact_group($args); - } - elsif ($value =~ /^(%\w+%)$/) { - $args->{value} = pronoun($1, $user); - $args->{quoted} = $args->{value}; - $args->{value_is_id} = 1; - } + if ($value =~ /^\%group\.[^%]+%$/) { + $self->_contact_exact_group($args); + } + elsif ($value =~ /^(%\w+%)$/) { + $args->{value} = pronoun($1, $user); + $args->{quoted} = $args->{value}; + $args->{value_is_id} = 1; + } } sub _contact_exact_group { - my ($self, $args) = @_; - my ($value, $operator, $field, $chart_id, $joins, $sequence) = - @$args{qw(value operator field chart_id joins sequence)}; - my $dbh = Bugzilla->dbh; - my $user = $self->_user; - - # We already know $value will match this regexp, else we wouldn't be here. - $value =~ /\%group\.([^%]+)%/; - my $group_name = $1; - my $group = Bugzilla::Group->check({ name => $group_name, _error => 'invalid_group_name' }); - # Pass $group_name instead of $group->name to the error message - # to not leak the existence of the group. - $user->in_group($group) - || ThrowUserError('invalid_group_name', { name => $group_name }); - # Now that we know the user belongs to this group, it's safe - # to disclose more information. - $group->check_members_are_visible(); - - my $group_ids = Bugzilla::Group->flatten_group_membership($group->id); - - if ($field eq 'cc' && $chart_id eq '') { - # This is for the email1, email2, email3 fields from query.cgi. - $chart_id = "CC$$sequence"; - $args->{sequence}++; - } - - my $from = $field; - # These fields need an additional table. - if ($field =~ /^(commenter|cc)$/) { - my $join_table = $field; - $join_table = 'longdescs' if $field eq 'commenter'; - my $join_table_alias = "${field}_$chart_id"; - push(@$joins, { table => $join_table, as => $join_table_alias }); - $from = "$join_table_alias.who"; - } - - my $table = "user_group_map_$chart_id"; - my $join = { - table => 'user_group_map', - as => $table, - from => $from, - to => 'user_id', - extra => [$dbh->sql_in("$table.group_id", $group_ids), - "$table.isbless = 0"], - }; - push(@$joins, $join); - if ($operator =~ /^not/) { - $args->{term} = "$table.group_id IS NULL"; - } - else { - $args->{term} = "$table.group_id IS NOT NULL"; - } + my ($self, $args) = @_; + my ($value, $operator, $field, $chart_id, $joins, $sequence) + = @$args{qw(value operator field chart_id joins sequence)}; + my $dbh = Bugzilla->dbh; + my $user = $self->_user; + + # We already know $value will match this regexp, else we wouldn't be here. + $value =~ /\%group\.([^%]+)%/; + my $group_name = $1; + my $group = Bugzilla::Group->check( + {name => $group_name, _error => 'invalid_group_name'}); + + # Pass $group_name instead of $group->name to the error message + # to not leak the existence of the group. + $user->in_group($group) + || ThrowUserError('invalid_group_name', {name => $group_name}); + + # Now that we know the user belongs to this group, it's safe + # to disclose more information. + $group->check_members_are_visible(); + + my $group_ids = Bugzilla::Group->flatten_group_membership($group->id); + + if ($field eq 'cc' && $chart_id eq '') { + + # This is for the email1, email2, email3 fields from query.cgi. + $chart_id = "CC$$sequence"; + $args->{sequence}++; + } + + my $from = $field; + + # These fields need an additional table. + if ($field =~ /^(commenter|cc)$/) { + my $join_table = $field; + $join_table = 'longdescs' if $field eq 'commenter'; + my $join_table_alias = "${field}_$chart_id"; + push(@$joins, {table => $join_table, as => $join_table_alias}); + $from = "$join_table_alias.who"; + } + + my $table = "user_group_map_$chart_id"; + my $join = { + table => 'user_group_map', + as => $table, + from => $from, + to => 'user_id', + extra => [$dbh->sql_in("$table.group_id", $group_ids), "$table.isbless = 0"], + }; + push(@$joins, $join); + if ($operator =~ /^not/) { + $args->{term} = "$table.group_id IS NULL"; + } + else { + $args->{term} = "$table.group_id IS NOT NULL"; + } } sub _get_user_id { - my ($self, $value) = @_; + my ($self, $value) = @_; - if ($value =~ /^%\w+%$/) { - return pronoun($value, $self->_user); - } - return login_to_id($value, THROW_ERROR); + if ($value =~ /^%\w+%$/) { + return pronoun($value, $self->_user); + } + return login_to_id($value, THROW_ERROR); } ##################################################################### @@ -2380,546 +2368,556 @@ sub _get_user_id { ##################################################################### sub _invalid_combination { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; - ThrowUserError('search_field_operator_invalid', - { field => $field, operator => $operator }); + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); } # For all the "user" fields--assigned_to, reporter, qa_contact, # cc, commenter, requestee, etc. sub _user_nonchanged { - my ($self, $args) = @_; - my ($field, $operator, $chart_id, $sequence, $joins) = - @$args{qw(field operator chart_id sequence joins)}; - - my $is_in_other_table; - if (my $join = USER_FIELDS->{$field}->{join}) { - $is_in_other_table = 1; - my $as = "${field}_$chart_id"; - # Needed for setters.login_name and requestees.login_name. - # Otherwise when we try to join "profiles" below, we'd get - # something like "setters.login_name.login_name" in the "from". - $as =~ s/\./_/g; - # This helps implement the email1, email2, etc. parameters. - if ($chart_id =~ /default/) { - $as .= "_$sequence"; - } - my $isprivate = USER_FIELDS->{$field}->{isprivate}; - my $extra = ($isprivate and !$self->_user->is_insider) - ? ["$as.isprivate = 0"] : []; - # We want to copy $join so as not to modify USER_FIELDS. - push(@$joins, { %$join, as => $as, extra => $extra }); - my $search_field = USER_FIELDS->{$field}->{field}; - $args->{full_field} = "$as.$search_field"; - } + my ($self, $args) = @_; + my ($field, $operator, $chart_id, $sequence, $joins) + = @$args{qw(field operator chart_id sequence joins)}; + + my $is_in_other_table; + if (my $join = USER_FIELDS->{$field}->{join}) { + $is_in_other_table = 1; + my $as = "${field}_$chart_id"; + + # Needed for setters.login_name and requestees.login_name. + # Otherwise when we try to join "profiles" below, we'd get + # something like "setters.login_name.login_name" in the "from". + $as =~ s/\./_/g; + + # This helps implement the email1, email2, etc. parameters. + if ($chart_id =~ /default/) { + $as .= "_$sequence"; + } + my $isprivate = USER_FIELDS->{$field}->{isprivate}; + my $extra + = ($isprivate and !$self->_user->is_insider) ? ["$as.isprivate = 0"] : []; + + # We want to copy $join so as not to modify USER_FIELDS. + push(@$joins, {%$join, as => $as, extra => $extra}); + my $search_field = USER_FIELDS->{$field}->{field}; + $args->{full_field} = "$as.$search_field"; + } + + my $is_nullable = USER_FIELDS->{$field}->{nullable}; + my $null_alternate = "''"; + + # When using a pronoun, we use the userid, and we don't have to + # join the profiles table. + if ($args->{value_is_id}) { + $null_alternate = 0; + } + elsif (substr($field, -9) eq '_realname') { + my $as = "name_${field}_$chart_id"; + + # For fields with periods in their name. + $as =~ s/\./_/; + my $join = { + table => 'profiles', + as => $as, + from => substr($args->{full_field}, 0, -9), + to => 'userid', + join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef, + }; + push(@$joins, $join); + $args->{full_field} = "$as.realname"; + } + else { + my $as = "name_${field}_$chart_id"; - my $is_nullable = USER_FIELDS->{$field}->{nullable}; - my $null_alternate = "''"; - # When using a pronoun, we use the userid, and we don't have to - # join the profiles table. - if ($args->{value_is_id}) { - $null_alternate = 0; + # For fields with periods in their name. + $as =~ s/\./_/; + my $join = { + table => 'profiles', + as => $as, + from => $args->{full_field}, + to => 'userid', + join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef, + }; + push(@$joins, $join); + $args->{full_field} = "$as.login_name"; + } + + # We COALESCE fields that can be NULL, to make "not"-style operators + # continue to work properly. For example, "qa_contact is not equal to bob" + # should also show bugs where the qa_contact is NULL. With COALESCE, + # it does. + if ($is_nullable) { + $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)"; + } + + # For fields whose values are stored in other tables, negation (NOT) + # only works properly if we put the condition into the JOIN instead + # of the WHERE. + if ($is_in_other_table) { + + # Using the last join works properly whether we're searching based + # on userid or login_name. + my $last_join = $joins->[-1]; + + # For negative operators, the system we're using here + # only works properly if we reverse the operator and check IS NULL + # in the WHERE. + my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0; + if ($is_negative) { + $args->{operator} = $self->_reverse_operator($operator); } - elsif (substr($field, -9) eq '_realname') { - my $as = "name_${field}_$chart_id"; - # For fields with periods in their name. - $as =~ s/\./_/; - my $join = { - table => 'profiles', - as => $as, - from => substr($args->{full_field}, 0, -9), - to => 'userid', - join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef, - }; - push(@$joins, $join); - $args->{full_field} = "$as.realname"; + $self->_do_operator_function($args); + push(@{$last_join->{extra}}, $args->{term}); + + # For login_name searches, we only want a single join. + # So we create a subselect table out of our two joins. This makes + # negation (NOT) work properly for values that are in other + # tables. + if ($last_join->{table} eq 'profiles') { + pop @$joins; + $last_join->{join} = 'INNER'; + my ($join_sql) = $self->_translate_join($last_join); + my $first_join = $joins->[-1]; + my $as = $first_join->{as}; + my $table = $first_join->{table}; + my $columns = "bug_id"; + $columns .= ",isprivate" if @{$first_join->{extra}}; + my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql"; + $first_join->{table} = "($new_table)"; + + # We always want to LEFT JOIN the generated table. + delete $first_join->{join}; + + # To support OR charts, we need multiple tables. + my $new_as = $first_join->{as} . "_$sequence"; + $_ =~ s/\Q$as\E/$new_as/ foreach @{$first_join->{extra}}; + $first_join->{as} = $new_as; + $last_join = $first_join; + } + + # If we're joining the first table (we're using a pronoun and + # searching by user id) then we need to check $other_table->{field}. + my $check_field = $last_join->{as} . '.bug_id'; + if ($is_negative) { + $args->{term} = "$check_field IS NULL"; } else { - my $as = "name_${field}_$chart_id"; - # For fields with periods in their name. - $as =~ s/\./_/; - my $join = { - table => 'profiles', - as => $as, - from => $args->{full_field}, - to => 'userid', - join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef, - }; - push(@$joins, $join); - $args->{full_field} = "$as.login_name"; - } - - # We COALESCE fields that can be NULL, to make "not"-style operators - # continue to work properly. For example, "qa_contact is not equal to bob" - # should also show bugs where the qa_contact is NULL. With COALESCE, - # it does. - if ($is_nullable) { - $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)"; - } - - # For fields whose values are stored in other tables, negation (NOT) - # only works properly if we put the condition into the JOIN instead - # of the WHERE. - if ($is_in_other_table) { - # Using the last join works properly whether we're searching based - # on userid or login_name. - my $last_join = $joins->[-1]; - - # For negative operators, the system we're using here - # only works properly if we reverse the operator and check IS NULL - # in the WHERE. - my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0; - if ($is_negative) { - $args->{operator} = $self->_reverse_operator($operator); - } - $self->_do_operator_function($args); - push(@{ $last_join->{extra} }, $args->{term}); - - # For login_name searches, we only want a single join. - # So we create a subselect table out of our two joins. This makes - # negation (NOT) work properly for values that are in other - # tables. - if ($last_join->{table} eq 'profiles') { - pop @$joins; - $last_join->{join} = 'INNER'; - my ($join_sql) = $self->_translate_join($last_join); - my $first_join = $joins->[-1]; - my $as = $first_join->{as}; - my $table = $first_join->{table}; - my $columns = "bug_id"; - $columns .= ",isprivate" if @{ $first_join->{extra} }; - my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql"; - $first_join->{table} = "($new_table)"; - # We always want to LEFT JOIN the generated table. - delete $first_join->{join}; - # To support OR charts, we need multiple tables. - my $new_as = $first_join->{as} . "_$sequence"; - $_ =~ s/\Q$as\E/$new_as/ foreach @{ $first_join->{extra} }; - $first_join->{as} = $new_as; - $last_join = $first_join; - } - - # If we're joining the first table (we're using a pronoun and - # searching by user id) then we need to check $other_table->{field}. - my $check_field = $last_join->{as} . '.bug_id'; - if ($is_negative) { - $args->{term} = "$check_field IS NULL"; - } - else { - $args->{term} = "$check_field IS NOT NULL"; - } + $args->{term} = "$check_field IS NOT NULL"; } + } } # XXX This duplicates having Commenter as a search field. sub _long_desc_changedby { - my ($self, $args) = @_; - my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)}; - - my $table = "longdescs_$chart_id"; - push(@$joins, { table => 'longdescs', as => $table }); - my $user_id = $self->_get_user_id($value); - $args->{term} = "$table.who = $user_id"; - - # If the user is not part of the insiders group, they cannot see - # private comments - if (!$self->_user->is_insider) { - $args->{term} .= " AND $table.isprivate = 0"; - } + my ($self, $args) = @_; + my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)}; + + my $table = "longdescs_$chart_id"; + push(@$joins, {table => 'longdescs', as => $table}); + my $user_id = $self->_get_user_id($value); + $args->{term} = "$table.who = $user_id"; + + # If the user is not part of the insiders group, they cannot see + # private comments + if (!$self->_user->is_insider) { + $args->{term} .= " AND $table.isprivate = 0"; + } } sub _long_desc_changedbefore_after { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins) = - @$args{qw(chart_id operator value joins)}; - my $dbh = Bugzilla->dbh; - - my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; - my $table = "longdescs_$chart_id"; - my $sql_date = $dbh->quote(SqlifyDate($value)); - my $join = { - table => 'longdescs', - as => $table, - extra => ["$table.bug_when $sql_operator $sql_date"], - }; - push(@$joins, $join); - $args->{term} = "$table.bug_when IS NOT NULL"; - - # If the user is not part of the insiders group, they cannot see - # private comments - if (!$self->_user->is_insider) { - $args->{term} .= " AND $table.isprivate = 0"; - } + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins) + = @$args{qw(chart_id operator value joins)}; + my $dbh = Bugzilla->dbh; + + my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; + my $table = "longdescs_$chart_id"; + my $sql_date = $dbh->quote(SqlifyDate($value)); + my $join = { + table => 'longdescs', + as => $table, + extra => ["$table.bug_when $sql_operator $sql_date"], + }; + push(@$joins, $join); + $args->{term} = "$table.bug_when IS NOT NULL"; + + # If the user is not part of the insiders group, they cannot see + # private comments + if (!$self->_user->is_insider) { + $args->{term} .= " AND $table.isprivate = 0"; + } } sub _long_desc_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins, $bugs_table) = - @$args{qw(chart_id operator value joins bugs_table)}; - - if ($operator =~ /^is(not)?empty$/) { - $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); - return; - } - my $dbh = Bugzilla->dbh; - - my $table = "longdescs_$chart_id"; - my $join_args = { - chart_id => $chart_id, - sequence => $chart_id, - field => 'longdesc', - full_field => "$table.thetext", - operator => $operator, - value => $value, - all_values => $value, - quoted => $dbh->quote($value), - joins => [], - bugs_table => $bugs_table, - }; - $self->_do_operator_function($join_args); - - # If the user is not part of the insiders group, they cannot see - # private comments - if (!$self->_user->is_insider) { - $join_args->{term} .= " AND $table.isprivate = 0"; - } - - my $join = { - table => 'longdescs', - as => $table, - extra => [ $join_args->{term} ], - }; - push(@$joins, $join); - - $args->{term} = "$table.comment_id IS NOT NULL"; + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins, $bugs_table) + = @$args{qw(chart_id operator value joins bugs_table)}; + + if ($operator =~ /^is(not)?empty$/) { + $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); + return; + } + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$chart_id"; + my $join_args = { + chart_id => $chart_id, + sequence => $chart_id, + field => 'longdesc', + full_field => "$table.thetext", + operator => $operator, + value => $value, + all_values => $value, + quoted => $dbh->quote($value), + joins => [], + bugs_table => $bugs_table, + }; + $self->_do_operator_function($join_args); + + # If the user is not part of the insiders group, they cannot see + # private comments + if (!$self->_user->is_insider) { + $join_args->{term} .= " AND $table.isprivate = 0"; + } + + my $join = {table => 'longdescs', as => $table, extra => [$join_args->{term}],}; + push(@$joins, $join); + + $args->{term} = "$table.comment_id IS NOT NULL"; } sub _content_matches { - my ($self, $args) = @_; - my ($chart_id, $joins, $fields, $operator, $value) = - @$args{qw(chart_id joins fields operator value)}; - my $dbh = Bugzilla->dbh; - - # "content" is an alias for columns containing text for which we - # can search a full-text index and retrieve results by relevance, - # currently just bug comments (and summaries to some degree). - # There's only one way to search a full-text index, so we only - # accept the "matches" operator, which is specific to full-text - # index searches. - - # Add the fulltext table to the query so we can search on it. - my $table = "bugs_fulltext_$chart_id"; - my $comments_col = "comments"; - $comments_col = "comments_noprivate" unless $self->_user->is_insider; - push(@$joins, { table => 'bugs_fulltext', as => $table }); - - # Create search terms to add to the SELECT and WHERE clauses. - my ($term1, $rterm1) = - $dbh->sql_fulltext_search("$table.$comments_col", $value); - my ($term2, $rterm2) = - $dbh->sql_fulltext_search("$table.short_desc", $value); - $rterm1 = $term1 if !$rterm1; - $rterm2 = $term2 if !$rterm2; - - # The term to use in the WHERE clause. - my $term = "$term1 OR $term2"; - if ($operator =~ /not/i) { - $term = "NOT($term)"; - } - $args->{term} = $term; - - # In order to sort by relevance (in case the user requests it), - # we SELECT the relevance value so we can add it to the ORDER BY - # clause. Every time a new fulltext chart isadded, this adds more - # terms to the relevance sql. - # - # We build the relevance SQL by modifying the COLUMNS list directly, - # which is kind of a hack but works. - my $current = $self->COLUMNS->{'relevance'}->{name}; - $current = $current ? "$current + " : ''; - # For NOT searches, we just add 0 to the relevance. - my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)"; - $self->COLUMNS->{'relevance'}->{name} = $select_term; + my ($self, $args) = @_; + my ($chart_id, $joins, $fields, $operator, $value) + = @$args{qw(chart_id joins fields operator value)}; + my $dbh = Bugzilla->dbh; + + # "content" is an alias for columns containing text for which we + # can search a full-text index and retrieve results by relevance, + # currently just bug comments (and summaries to some degree). + # There's only one way to search a full-text index, so we only + # accept the "matches" operator, which is specific to full-text + # index searches. + + # Add the fulltext table to the query so we can search on it. + my $table = "bugs_fulltext_$chart_id"; + my $comments_col = "comments"; + $comments_col = "comments_noprivate" unless $self->_user->is_insider; + push(@$joins, {table => 'bugs_fulltext', as => $table}); + + # Create search terms to add to the SELECT and WHERE clauses. + my ($term1, $rterm1) + = $dbh->sql_fulltext_search("$table.$comments_col", $value); + my ($term2, $rterm2) = $dbh->sql_fulltext_search("$table.short_desc", $value); + $rterm1 = $term1 if !$rterm1; + $rterm2 = $term2 if !$rterm2; + + # The term to use in the WHERE clause. + my $term = "$term1 OR $term2"; + if ($operator =~ /not/i) { + $term = "NOT($term)"; + } + $args->{term} = $term; + + # In order to sort by relevance (in case the user requests it), + # we SELECT the relevance value so we can add it to the ORDER BY + # clause. Every time a new fulltext chart isadded, this adds more + # terms to the relevance sql. + # + # We build the relevance SQL by modifying the COLUMNS list directly, + # which is kind of a hack but works. + my $current = $self->COLUMNS->{'relevance'}->{name}; + $current = $current ? "$current + " : ''; + + # For NOT searches, we just add 0 to the relevance. + my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)"; + $self->COLUMNS->{'relevance'}->{name} = $select_term; } sub _long_descs_count { - my ($self, $args) = @_; - my ($chart_id, $joins) = @$args{qw(chart_id joins)}; - my $table = "longdescs_count_$chart_id"; - my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0"; - my $join = { - table => "(SELECT bug_id, COUNT(*) AS num" - . " FROM longdescs $extra GROUP BY bug_id)", - as => $table, - }; - push(@$joins, $join); - $args->{full_field} = "${table}.num"; + my ($self, $args) = @_; + my ($chart_id, $joins) = @$args{qw(chart_id joins)}; + my $table = "longdescs_count_$chart_id"; + my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0"; + my $join = { + table => "(SELECT bug_id, COUNT(*) AS num" + . " FROM longdescs $extra GROUP BY bug_id)", + as => $table, + }; + push(@$joins, $join); + $args->{full_field} = "${table}.num"; } sub _work_time_changedby { - my ($self, $args) = @_; - my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)}; - - my $table = "longdescs_$chart_id"; - push(@$joins, { table => 'longdescs', as => $table }); - my $user_id = $self->_get_user_id($value); - $args->{term} = "$table.who = $user_id AND $table.work_time != 0"; + my ($self, $args) = @_; + my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)}; + + my $table = "longdescs_$chart_id"; + push(@$joins, {table => 'longdescs', as => $table}); + my $user_id = $self->_get_user_id($value); + $args->{term} = "$table.who = $user_id AND $table.work_time != 0"; } sub _work_time_changedbefore_after { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins) = - @$args{qw(chart_id operator value joins)}; - my $dbh = Bugzilla->dbh; - - my $table = "longdescs_$chart_id"; - my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; - my $sql_date = $dbh->quote(SqlifyDate($value)); - my $join = { - table => 'longdescs', - as => $table, - extra => ["$table.work_time != 0", - "$table.bug_when $sql_operator $sql_date"], - }; - push(@$joins, $join); - - $args->{term} = "$table.bug_when IS NOT NULL"; + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins) + = @$args{qw(chart_id operator value joins)}; + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$chart_id"; + my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; + my $sql_date = $dbh->quote(SqlifyDate($value)); + my $join = { + table => 'longdescs', + as => $table, + extra => ["$table.work_time != 0", "$table.bug_when $sql_operator $sql_date"], + }; + push(@$joins, $join); + + $args->{term} = "$table.bug_when IS NOT NULL"; } sub _work_time { - my ($self, $args) = @_; - $self->_add_extra_column('actual_time'); - $args->{full_field} = $self->COLUMNS->{actual_time}->{name}; + my ($self, $args) = @_; + $self->_add_extra_column('actual_time'); + $args->{full_field} = $self->COLUMNS->{actual_time}->{name}; } sub _percentage_complete { - my ($self, $args) = @_; - - $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name}; + my ($self, $args) = @_; - # We need actual_time in _select_columns, otherwise we can't use - # it in the expression for searching percentage_complete. - $self->_add_extra_column('actual_time'); + $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name}; + + # We need actual_time in _select_columns, otherwise we can't use + # it in the expression for searching percentage_complete. + $self->_add_extra_column('actual_time'); } sub _last_visit_ts { - my ($self, $args) = @_; + my ($self, $args) = @_; - $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name}; - $self->_add_extra_column('last_visit_ts'); + $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name}; + $self->_add_extra_column('last_visit_ts'); } sub _last_visit_ts_invalid_operator { - my ($self, $args) = @_; + my ($self, $args) = @_; - ThrowUserError('search_field_operator_invalid', - { field => $args->{field}, - operator => $args->{operator} }); + ThrowUserError('search_field_operator_invalid', + {field => $args->{field}, operator => $args->{operator}}); } sub _days_elapsed { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - $args->{full_field} = "(" . $dbh->sql_to_days('NOW()') . " - " . - $dbh->sql_to_days('bugs.delta_ts') . ")"; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + $args->{full_field} + = "(" + . $dbh->sql_to_days('NOW()') . " - " + . $dbh->sql_to_days('bugs.delta_ts') . ")"; } sub _component_nonchanged { - my ($self, $args) = @_; - - $args->{full_field} = "components.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("bugs.component_id", - "components.id", "components", $args->{term}); + my ($self, $args) = @_; + + $args->{full_field} = "components.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} + = build_subselect("bugs.component_id", "components.id", "components", + $args->{term}); } sub _product_nonchanged { - my ($self, $args) = @_; - - # Generate the restriction condition - $args->{full_field} = "products.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("bugs.product_id", - "products.id", "products", $term); + my ($self, $args) = @_; + + # Generate the restriction condition + $args->{full_field} = "products.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} + = build_subselect("bugs.product_id", "products.id", "products", $term); } sub _alias_nonchanged { - my ($self, $args) = @_; + my ($self, $args) = @_; - $args->{full_field} = "bugs_aliases.alias"; - $self->_do_operator_function($args); - $args->{term} = build_subselect("bugs.bug_id", - "bugs_aliases.bug_id", "bugs_aliases", $args->{term}); + $args->{full_field} = "bugs_aliases.alias"; + $self->_do_operator_function($args); + $args->{term} + = build_subselect("bugs.bug_id", "bugs_aliases.bug_id", "bugs_aliases", + $args->{term}); } sub _classification_nonchanged { - my ($self, $args) = @_; - my $joins = $args->{joins}; - - # This joins the right tables for us. - $self->_add_extra_column('product'); - - # Generate the restriction condition - $args->{full_field} = "classifications.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("map_product.classification_id", - "classifications.id", "classifications", $term); + my ($self, $args) = @_; + my $joins = $args->{joins}; + + # This joins the right tables for us. + $self->_add_extra_column('product'); + + # Generate the restriction condition + $args->{full_field} = "classifications.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} = build_subselect("map_product.classification_id", + "classifications.id", "classifications", $term); } sub _nullable { - my ($self, $args) = @_; - my $field = $args->{full_field}; - $args->{full_field} = "COALESCE($field, '')"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + $args->{full_field} = "COALESCE($field, '')"; } sub _nullable_int { - my ($self, $args) = @_; - my $field = $args->{full_field}; - $args->{full_field} = "COALESCE($field, 0)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + $args->{full_field} = "COALESCE($field, 0)"; } sub _nullable_datetime { - my ($self, $args) = @_; - my $field = $args->{full_field}; - my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME); - $args->{full_field} = "COALESCE($field, $empty)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME); + $args->{full_field} = "COALESCE($field, $empty)"; } sub _nullable_date { - my ($self, $args) = @_; - my $field = $args->{full_field}; - my $empty = Bugzilla->dbh->quote(EMPTY_DATE); - $args->{full_field} = "COALESCE($field, $empty)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + my $empty = Bugzilla->dbh->quote(EMPTY_DATE); + $args->{full_field} = "COALESCE($field, $empty)"; } sub _deadline { - my ($self, $args) = @_; - my $field = $args->{full_field}; - # This makes "equals" searches work on all DBs (even on MySQL, which - # has a bug: http://bugs.mysql.com/bug.php?id=60324). - $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d'); - $self->_nullable_datetime($args); + my ($self, $args) = @_; + my $field = $args->{full_field}; + + # This makes "equals" searches work on all DBs (even on MySQL, which + # has a bug: http://bugs.mysql.com/bug.php?id=60324). + $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d'); + $self->_nullable_datetime($args); } sub _owner_idle_time_greater_less { - my ($self, $args) = @_; - my ($chart_id, $joins, $value, $operator) = - @$args{qw(chart_id joins value operator)}; - my $dbh = Bugzilla->dbh; - - my $table = "idle_$chart_id"; - my $quoted = $dbh->quote(SqlifyDate($value)); - - my $ld_table = "comment_$table"; - my $act_table = "activity_$table"; - my $comments_join = { - table => 'longdescs', - as => $ld_table, - from => 'assigned_to', - to => 'who', - extra => ["$ld_table.bug_when > $quoted"], - }; - my $activity_join = { - table => 'bugs_activity', - as => $act_table, - from => 'assigned_to', - to => 'who', - extra => ["$act_table.bug_when > $quoted"] - }; - - push(@$joins, $comments_join, $activity_join); - - if ($operator =~ /greater/) { - $args->{term} = - "$ld_table.who IS NULL AND $act_table.who IS NULL"; - } else { - $args->{term} = - "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)"; - } + my ($self, $args) = @_; + my ($chart_id, $joins, $value, $operator) + = @$args{qw(chart_id joins value operator)}; + my $dbh = Bugzilla->dbh; + + my $table = "idle_$chart_id"; + my $quoted = $dbh->quote(SqlifyDate($value)); + + my $ld_table = "comment_$table"; + my $act_table = "activity_$table"; + my $comments_join = { + table => 'longdescs', + as => $ld_table, + from => 'assigned_to', + to => 'who', + extra => ["$ld_table.bug_when > $quoted"], + }; + my $activity_join = { + table => 'bugs_activity', + as => $act_table, + from => 'assigned_to', + to => 'who', + extra => ["$act_table.bug_when > $quoted"] + }; + + push(@$joins, $comments_join, $activity_join); + + if ($operator =~ /greater/) { + $args->{term} = "$ld_table.who IS NULL AND $act_table.who IS NULL"; + } + else { + $args->{term} = "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)"; + } } sub _multiselect_negative { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; - $args->{operator} = $self->_reverse_operator($operator); - $args->{term} = $self->_multiselect_term($args, 1); + $args->{operator} = $self->_reverse_operator($operator); + $args->{term} = $self->_multiselect_term($args, 1); } sub _multiselect_multiple { - my ($self, $args) = @_; - my ($chart_id, $field, $operator, $value) - = @$args{qw(chart_id field operator value)}; - my $dbh = Bugzilla->dbh; - - # We want things like "cf_multi_select=two+words" to still be - # considered a search for two separate words, unless we're using - # anyexact. (_all_values would consider that to be one "word" with a - # space in it, because it's not in the Boolean Charts). - my @words = $operator eq 'anyexact' ? $self->_all_values($args) - : split(/[\s,]+/, $value); - - my @terms; - foreach my $word (@words) { - next if $word eq ''; - $args->{value} = $word; - $args->{quoted} = $dbh->quote($word); - push(@terms, $self->_multiselect_term($args)); - } - - # The spacing in the joins helps make the resulting SQL more readable. - if ($operator =~ /^any/) { - $args->{term} = join("\n OR ", @terms); - } - else { - $args->{term} = join("\n AND ", @terms); - } + my ($self, $args) = @_; + my ($chart_id, $field, $operator, $value) + = @$args{qw(chart_id field operator value)}; + my $dbh = Bugzilla->dbh; + + # We want things like "cf_multi_select=two+words" to still be + # considered a search for two separate words, unless we're using + # anyexact. (_all_values would consider that to be one "word" with a + # space in it, because it's not in the Boolean Charts). + my @words + = $operator eq 'anyexact' + ? $self->_all_values($args) + : split(/[\s,]+/, $value); + + my @terms; + foreach my $word (@words) { + next if $word eq ''; + $args->{value} = $word; + $args->{quoted} = $dbh->quote($word); + push(@terms, $self->_multiselect_term($args)); + } + + # The spacing in the joins helps make the resulting SQL more readable. + if ($operator =~ /^any/) { + $args->{term} = join("\n OR ", @terms); + } + else { + $args->{term} = join("\n AND ", @terms); + } } sub _flagtypes_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) = - @$args{qw(chart_id operator value joins bugs_table condition)}; - - if ($operator =~ /^is(not)?empty$/) { - $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); - return; - } - - my $dbh = Bugzilla->dbh; - - # For 'not' operators, we need to negate the whole term. - # If you search for "Flags" (does not contain) "approval+" we actually want - # to return *bugs* that don't contain an approval+ flag. Without rewriting - # the negation we'll search for *flags* which don't contain approval+. - if ($operator =~ s/^not//) { - $args->{operator} = $operator; - $condition->operator($operator); - $condition->negate(1); - } - - my $subselect_args = { - chart_id => $chart_id, - sequence => $chart_id, - field => 'flagtypes.name', - full_field => $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"), - operator => $operator, - value => $value, - all_values => $value, - quoted => $dbh->quote($value), - joins => [], - bugs_table => "bugs_$chart_id", - }; - $self->_do_operator_function($subselect_args); - my $subselect_term = $subselect_args->{term}; - - # don't call build_subselect as this must run as a true sub-select - $args->{term} = "EXISTS ( + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) + = @$args{qw(chart_id operator value joins bugs_table condition)}; + + if ($operator =~ /^is(not)?empty$/) { + $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); + return; + } + + my $dbh = Bugzilla->dbh; + + # For 'not' operators, we need to negate the whole term. + # If you search for "Flags" (does not contain) "approval+" we actually want + # to return *bugs* that don't contain an approval+ flag. Without rewriting + # the negation we'll search for *flags* which don't contain approval+. + if ($operator =~ s/^not//) { + $args->{operator} = $operator; + $condition->operator($operator); + $condition->negate(1); + } + + my $subselect_args = { + chart_id => $chart_id, + sequence => $chart_id, + field => 'flagtypes.name', + full_field => + $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"), + operator => $operator, + value => $value, + all_values => $value, + quoted => $dbh->quote($value), + joins => [], + bugs_table => "bugs_$chart_id", + }; + $self->_do_operator_function($subselect_args); + my $subselect_term = $subselect_args->{term}; + + # don't call build_subselect as this must run as a true sub-select + $args->{term} = "EXISTS ( SELECT 1 FROM $bugs_table bugs_$chart_id LEFT JOIN attachments AS attachments_$chart_id @@ -2936,209 +2934,224 @@ sub _flagtypes_nonchanged { } sub _multiselect_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator) = - @$args{qw(chart_id joins field operator)}; - $args->{term} = $self->_multiselect_term($args) + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator) + = @$args{qw(chart_id joins field operator)}; + $args->{term} = $self->_multiselect_term($args); } sub _multiselect_table { - my ($self, $args) = @_; - my ($field, $chart_id) = @$args{qw(field chart_id)}; - my $dbh = Bugzilla->dbh; - - if ($field eq 'keywords') { - $args->{full_field} = 'keyworddefs.name'; - return "keywords INNER JOIN keyworddefs". - " ON keywords.keywordid = keyworddefs.id"; - } - elsif ($field eq 'tag') { - $args->{full_field} = 'tag.name'; - return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = " - . ($self->_sharer_id || $self->_user->id); - } - elsif ($field eq 'bug_group') { - $args->{full_field} = 'groups.name'; - return "bug_group_map INNER JOIN groups + my ($self, $args) = @_; + my ($field, $chart_id) = @$args{qw(field chart_id)}; + my $dbh = Bugzilla->dbh; + + if ($field eq 'keywords') { + $args->{full_field} = 'keyworddefs.name'; + return "keywords INNER JOIN keyworddefs" + . " ON keywords.keywordid = keyworddefs.id"; + } + elsif ($field eq 'tag') { + $args->{full_field} = 'tag.name'; + return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = " + . ($self->_sharer_id || $self->_user->id); + } + elsif ($field eq 'bug_group') { + $args->{full_field} = 'groups.name'; + return "bug_group_map INNER JOIN groups ON bug_group_map.group_id = groups.id"; - } - elsif ($field eq 'blocked' or $field eq 'dependson') { - my $select = $field eq 'blocked' ? 'dependson' : 'blocked'; - $args->{_select_field} = $select; - $args->{full_field} = $field; - return "dependencies"; - } - elsif ($field eq 'longdesc') { - $args->{_extra_where} = " AND isprivate = 0" - if !$self->_user->is_insider; - $args->{full_field} = 'thetext'; - return "longdescs"; - } - elsif ($field eq 'longdescs.isprivate') { - ThrowUserError('auth_failure', { action => 'search', - object => 'bug_fields', - field => 'longdescs.isprivate' }) - if !$self->_user->is_insider; - $args->{full_field} = 'isprivate'; - return "longdescs"; - } - elsif ($field =~ /^attachments/) { - $args->{_extra_where} = " AND isprivate = 0" - if !$self->_user->is_insider; - $field =~ /^attachments\.(.+)$/; - $args->{full_field} = $1; - return "attachments"; - } - elsif ($field eq 'attach_data.thedata') { - $args->{_extra_where} = " AND attachments.isprivate = 0" - if !$self->_user->is_insider; - return "attachments INNER JOIN attach_data " - . " ON attachments.attach_id = attach_data.id" - } - elsif ($field eq 'comment_tag') { - $args->{_extra_where} = " AND longdescs.isprivate = 0" - if !$self->_user->is_insider; - $args->{full_field} = 'longdescs_tags.tag'; - return "longdescs INNER JOIN longdescs_tags". - " ON longdescs.comment_id = longdescs_tags.comment_id"; - } - my $table = "bug_$field"; - $args->{full_field} = "bug_$field.value"; - return $table; + } + elsif ($field eq 'blocked' or $field eq 'dependson') { + my $select = $field eq 'blocked' ? 'dependson' : 'blocked'; + $args->{_select_field} = $select; + $args->{full_field} = $field; + return "dependencies"; + } + elsif ($field eq 'longdesc') { + $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider; + $args->{full_field} = 'thetext'; + return "longdescs"; + } + elsif ($field eq 'longdescs.isprivate') { + ThrowUserError('auth_failure', + {action => 'search', object => 'bug_fields', field => 'longdescs.isprivate'}) + if !$self->_user->is_insider; + $args->{full_field} = 'isprivate'; + return "longdescs"; + } + elsif ($field =~ /^attachments/) { + $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider; + $field =~ /^attachments\.(.+)$/; + $args->{full_field} = $1; + return "attachments"; + } + elsif ($field eq 'attach_data.thedata') { + $args->{_extra_where} = " AND attachments.isprivate = 0" + if !$self->_user->is_insider; + return "attachments INNER JOIN attach_data " + . " ON attachments.attach_id = attach_data.id"; + } + elsif ($field eq 'comment_tag') { + $args->{_extra_where} = " AND longdescs.isprivate = 0" + if !$self->_user->is_insider; + $args->{full_field} = 'longdescs_tags.tag'; + return "longdescs INNER JOIN longdescs_tags" + . " ON longdescs.comment_id = longdescs_tags.comment_id"; + } + my $table = "bug_$field"; + $args->{full_field} = "bug_$field.value"; + return $table; } sub _multiselect_term { - my ($self, $args, $not) = @_; - my ($operator) = $args->{operator}; - my $value = $args->{value} || ''; - # 'empty' operators require special handling - return $self->_multiselect_isempty($args, $not) - if ($operator =~ /^is(not)?empty$/ || $value eq '---'); - my $table = $self->_multiselect_table($args); - $self->_do_operator_function($args); - my $term = $args->{term}; - $term .= $args->{_extra_where} || ''; - my $select = $args->{_select_field} || 'bug_id'; - return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, $not); + my ($self, $args, $not) = @_; + my ($operator) = $args->{operator}; + my $value = $args->{value} || ''; + + # 'empty' operators require special handling + return $self->_multiselect_isempty($args, $not) + if ($operator =~ /^is(not)?empty$/ || $value eq '---'); + my $table = $self->_multiselect_table($args); + $self->_do_operator_function($args); + my $term = $args->{term}; + $term .= $args->{_extra_where} || ''; + my $select = $args->{_select_field} || 'bug_id'; + return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, + $not); } # We can't use the normal operator_functions to build isempty queries which # join to different tables. sub _multiselect_isempty { - my ($self, $args, $not) = @_; - my ($field, $operator, $joins, $chart_id) = @$args{qw(field operator joins chart_id)}; - my $dbh = Bugzilla->dbh; - $operator = $self->_reverse_operator($operator) if $not; - $not = $operator eq 'isnotempty' ? 'NOT' : ''; - - if ($field eq 'keywords') { - push @$joins, { - table => 'keywords', - as => "keywords_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "keywords_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'bug_group') { - push @$joins, { - table => 'bug_group_map', - as => "bug_group_map_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "bug_group_map_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'flagtypes.name') { - push @$joins, { - table => 'flags', - as => "flags_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "flags_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'blocked' or $field eq 'dependson') { - my $to = $field eq 'blocked' ? 'dependson' : 'blocked'; - push @$joins, { - table => 'dependencies', - as => "dependencies_$chart_id", - from => 'bug_id', - to => $to, - }; - return "dependencies_$chart_id.$to IS $not NULL"; - } - elsif ($field eq 'longdesc') { - my @extra = ( "longdescs_$chart_id.type != " . CMT_HAS_DUPE ); - push @extra, "longdescs_$chart_id.isprivate = 0" - unless $self->_user->is_insider; - push @$joins, { - table => 'longdescs', - as => "longdescs_$chart_id", - from => 'bug_id', - to => 'bug_id', - extra => \@extra, - }; - return $not - ? "longdescs_$chart_id.thetext != ''" - : "longdescs_$chart_id.thetext = ''"; - } - elsif ($field eq 'longdescs.isprivate') { - ThrowUserError('search_field_operator_invalid', { field => $field, - operator => $operator }); - } - elsif ($field =~ /^attachments\.(.+)/) { - my $sub_field = $1; - if ($sub_field eq 'description' || $sub_field eq 'filename' || $sub_field eq 'mimetype') { - # can't be null/empty - return $not ? '1=1' : '1=2'; - } else { - # all other fields which get here are boolean - ThrowUserError('search_field_operator_invalid', { field => $field, - operator => $operator }); - } - } - elsif ($field eq 'attach_data.thedata') { - push @$joins, { - table => 'attachments', - as => "attachments_$chart_id", - from => 'bug_id', - to => 'bug_id', - extra => [ $self->_user->is_insider ? '' : "attachments_$chart_id.isprivate = 0" ], - }; - push @$joins, { - table => 'attach_data', - as => "attach_data_$chart_id", - from => "attachments_$chart_id.attach_id", - to => 'id', - }; - return "attach_data_$chart_id.thedata IS $not NULL"; - } - elsif ($field eq 'tag') { - push @$joins, { - table => 'bug_tag', - as => "bug_tag_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - push @$joins, { - table => 'tag', - as => "tag_$chart_id", - from => "bug_tag_$chart_id.tag_id", - to => 'id', - extra => [ "tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id) ], - }; - return "tag_$chart_id.id IS $not NULL"; - } - elsif ($self->_multi_select_fields->{$field}) { - push @$joins, { - table => "bug_$field", - as => "bug_${field}_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "bug_${field}_$chart_id.bug_id IS $not NULL"; + my ($self, $args, $not) = @_; + my ($field, $operator, $joins, $chart_id) + = @$args{qw(field operator joins chart_id)}; + my $dbh = Bugzilla->dbh; + $operator = $self->_reverse_operator($operator) if $not; + $not = $operator eq 'isnotempty' ? 'NOT' : ''; + + if ($field eq 'keywords') { + push @$joins, + { + table => 'keywords', + as => "keywords_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "keywords_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'bug_group') { + push @$joins, + { + table => 'bug_group_map', + as => "bug_group_map_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "bug_group_map_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'flagtypes.name') { + push @$joins, + { + table => 'flags', + as => "flags_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "flags_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'blocked' or $field eq 'dependson') { + my $to = $field eq 'blocked' ? 'dependson' : 'blocked'; + push @$joins, + { + table => 'dependencies', + as => "dependencies_$chart_id", + from => 'bug_id', + to => $to, + }; + return "dependencies_$chart_id.$to IS $not NULL"; + } + elsif ($field eq 'longdesc') { + my @extra = ("longdescs_$chart_id.type != " . CMT_HAS_DUPE); + push @extra, "longdescs_$chart_id.isprivate = 0" + unless $self->_user->is_insider; + push @$joins, + { + table => 'longdescs', + as => "longdescs_$chart_id", + from => 'bug_id', + to => 'bug_id', + extra => \@extra, + }; + return $not + ? "longdescs_$chart_id.thetext != ''" + : "longdescs_$chart_id.thetext = ''"; + } + elsif ($field eq 'longdescs.isprivate') { + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); + } + elsif ($field =~ /^attachments\.(.+)/) { + my $sub_field = $1; + if ( $sub_field eq 'description' + || $sub_field eq 'filename' + || $sub_field eq 'mimetype') + { + # can't be null/empty + return $not ? '1=1' : '1=2'; } + else { + # all other fields which get here are boolean + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); + } + } + elsif ($field eq 'attach_data.thedata') { + push @$joins, + { + table => 'attachments', + as => "attachments_$chart_id", + from => 'bug_id', + to => 'bug_id', + extra => + [$self->_user->is_insider ? '' : "attachments_$chart_id.isprivate = 0"], + }; + push @$joins, + { + table => 'attach_data', + as => "attach_data_$chart_id", + from => "attachments_$chart_id.attach_id", + to => 'id', + }; + return "attach_data_$chart_id.thedata IS $not NULL"; + } + elsif ($field eq 'tag') { + push @$joins, + { + table => 'bug_tag', + as => "bug_tag_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + push @$joins, + { + table => 'tag', + as => "tag_$chart_id", + from => "bug_tag_$chart_id.tag_id", + to => 'id', + extra => ["tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id)], + }; + return "tag_$chart_id.id IS $not NULL"; + } + elsif ($self->_multi_select_fields->{$field}) { + push @$joins, + { + table => "bug_$field", + as => "bug_${field}_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "bug_${field}_$chart_id.bug_id IS $not NULL"; + } } ############################### @@ -3146,234 +3159,236 @@ sub _multiselect_isempty { ############################### sub _simple_operator { - my ($self, $args) = @_; - my ($full_field, $quoted, $operator) = - @$args{qw(full_field quoted operator)}; - my $sql_operator = SIMPLE_OPERATORS->{$operator}; - $args->{term} = "$full_field $sql_operator $quoted"; + my ($self, $args) = @_; + my ($full_field, $quoted, $operator) = @$args{qw(full_field quoted operator)}; + my $sql_operator = SIMPLE_OPERATORS->{$operator}; + $args->{term} = "$full_field $sql_operator $quoted"; } sub _casesubstring { - my ($self, $args) = @_; - my ($full_field, $value) = @$args{qw(full_field value)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $value) = @$args{qw(full_field value)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_like($value, $full_field); + $args->{term} = $dbh->sql_like($value, $full_field); } sub _substring { - my ($self, $args) = @_; - my ($full_field, $value) = @$args{qw(full_field value)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $value) = @$args{qw(full_field value)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_ilike($value, $full_field); + $args->{term} = $dbh->sql_ilike($value, $full_field); } sub _notsubstring { - my ($self, $args) = @_; - my ($full_field, $value) = @$args{qw(full_field value)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $value) = @$args{qw(full_field value)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_not_ilike($value, $full_field); + $args->{term} = $dbh->sql_not_ilike($value, $full_field); } sub _regexp { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; - - $args->{term} = $dbh->sql_regexp($full_field, $quoted); + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; + + $args->{term} = $dbh->sql_regexp($full_field, $quoted); } sub _notregexp { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; - - $args->{term} = $dbh->sql_not_regexp($full_field, $quoted); + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; + + $args->{term} = $dbh->sql_not_regexp($full_field, $quoted); } sub _anyexact { - my ($self, $args) = @_; - my ($field, $full_field) = @$args{qw(field full_field)}; - my $dbh = Bugzilla->dbh; - - my @list = $self->_all_values($args, ','); - @list = map { $self->_quote_unless_numeric($args, $_) } @list; - - if (@list) { - $args->{term} = $dbh->sql_in($full_field, \@list); - } - else { - $args->{term} = ''; - } + my ($self, $args) = @_; + my ($field, $full_field) = @$args{qw(field full_field)}; + my $dbh = Bugzilla->dbh; + + my @list = $self->_all_values($args, ','); + @list = map { $self->_quote_unless_numeric($args, $_) } @list; + + if (@list) { + $args->{term} = $dbh->sql_in($full_field, \@list); + } + else { + $args->{term} = ''; + } } sub _anywordsubstr { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_substring_terms($args); - $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; + my @terms = $self->_substring_terms($args); + $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; } sub _allwordssubstr { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_substring_terms($args); - $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; + my @terms = $self->_substring_terms($args); + $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; } sub _nowordssubstr { - my ($self, $args) = @_; - $self->_anywordsubstr($args); - my $term = $args->{term}; - $args->{term} = "NOT($term)"; + my ($self, $args) = @_; + $self->_anywordsubstr($args); + my $term = $args->{term}; + $args->{term} = "NOT($term)"; } sub _anywords { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my @terms = $self->_word_terms($args); - my @terms = $self->_word_terms($args); - # Because _word_terms uses AND, we need to parenthesize its terms - # if there are more than one. - @terms = map("($_)", @terms) if scalar(@terms) > 1; - $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; + # Because _word_terms uses AND, we need to parenthesize its terms + # if there are more than one. + @terms = map("($_)", @terms) if scalar(@terms) > 1; + $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; } sub _allwords { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_word_terms($args); - $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; + my @terms = $self->_word_terms($args); + $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; } sub _nowords { - my ($self, $args) = @_; - $self->_anywords($args); - my $term = $args->{term}; - $args->{term} = "NOT($term)"; + my ($self, $args) = @_; + $self->_anywords($args); + my $term = $args->{term}; + $args->{term} = "NOT($term)"; } sub _changedbefore_changedafter { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $value) = - @$args{qw(chart_id joins field operator value)}; - my $dbh = Bugzilla->dbh; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - - # Asking when creation_ts changed is just asking when the bug was created. - if ($field_object->name eq 'creation_ts') { - $args->{operator} = - $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq'; - return $self->_do_operator_function($args); - } - - my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; - my $field_id = $field_object->id; - # Charts on changed* fields need to be field-specific. Otherwise, - # OR chart rows make no sense if they contain multiple fields. - my $table = "act_${field_id}_$chart_id"; - - my $sql_date = $dbh->quote(SqlifyDate($value)); - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.bug_when $sql_operator $sql_date"], - }; + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $value) + = @$args{qw(chart_id joins field operator value)}; + my $dbh = Bugzilla->dbh; - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + + # Asking when creation_ts changed is just asking when the bug was created. + if ($field_object->name eq 'creation_ts') { + $args->{operator} + = $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq'; + return $self->_do_operator_function($args); + } + + my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; + my $field_id = $field_object->id; + + # Charts on changed* fields need to be field-specific. Otherwise, + # OR chart rows make no sense if they contain multiple fields. + my $table = "act_${field_id}_$chart_id"; + + my $sql_date = $dbh->quote(SqlifyDate($value)); + my $join = { + table => 'bugs_activity', + as => $table, + extra => + ["$table.fieldid = $field_id", "$table.bug_when $sql_operator $sql_date"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changedfrom_changedto { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $quoted) = - @$args{qw(chart_id joins field operator quoted)}; - - my $column = ($operator =~ /from/) ? 'removed' : 'added'; - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - my $table = "act_${field_id}_$chart_id"; - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.$column = $quoted"], - }; - - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $quoted) + = @$args{qw(chart_id joins field operator quoted)}; + + my $column = ($operator =~ /from/) ? 'removed' : 'added'; + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + my $table = "act_${field_id}_$chart_id"; + my $join = { + table => 'bugs_activity', + as => $table, + extra => ["$table.fieldid = $field_id", "$table.$column = $quoted"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changedby { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $value) = - @$args{qw(chart_id joins field operator value)}; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - my $table = "act_${field_id}_$chart_id"; - my $user_id = $self->_get_user_id($value); - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.who = $user_id"], - }; - - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $value) + = @$args{qw(chart_id joins field operator value)}; + + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + my $table = "act_${field_id}_$chart_id"; + my $user_id = $self->_get_user_id($value); + my $join = { + table => 'bugs_activity', + as => $table, + extra => ["$table.fieldid = $field_id", "$table.who = $user_id"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changed_security_check { - my ($self, $args, $join) = @_; - my ($chart_id, $field) = @$args{qw(chart_id field)}; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - - # If the user is not part of the insiders group, they cannot see - # changes to attachments (including attachment flags) that are private - if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) { - $join->{then_to} = { - as => "attach_${field_id}_$chart_id", - table => 'attachments', - from => "act_${field_id}_$chart_id.attach_id", - to => 'attach_id', - }; - - $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0"; - } + my ($self, $args, $join) = @_; + my ($chart_id, $field) = @$args{qw(chart_id field)}; + + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + + # If the user is not part of the insiders group, they cannot see + # changes to attachments (including attachment flags) that are private + if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) { + $join->{then_to} = { + as => "attach_${field_id}_$chart_id", + table => 'attachments', + from => "act_${field_id}_$chart_id.attach_id", + to => 'attach_id', + }; + + $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0"; + } } sub _isempty { - my ($self, $args) = @_; - my $full_field = $args->{full_field}; - $args->{term} = "$full_field IS NULL OR $full_field = " . $self->_empty_value($args->{field}); + my ($self, $args) = @_; + my $full_field = $args->{full_field}; + $args->{term} = "$full_field IS NULL OR $full_field = " + . $self->_empty_value($args->{field}); } sub _isnotempty { - my ($self, $args) = @_; - my $full_field = $args->{full_field}; - $args->{term} = "$full_field IS NOT NULL AND $full_field != " . $self->_empty_value($args->{field}); + my ($self, $args) = @_; + my $full_field = $args->{full_field}; + $args->{term} = "$full_field IS NOT NULL AND $full_field != " + . $self->_empty_value($args->{field}); } sub _empty_value { - my ($self, $field) = @_; - my $field_obj = $self->_chart_fields->{$field}; - return "0" if $field_obj->type == FIELD_TYPE_BUG_ID; - return Bugzilla->dbh->quote(EMPTY_DATETIME) if $field_obj->type == FIELD_TYPE_DATETIME; - return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE; - return "''"; + my ($self, $field) = @_; + my $field_obj = $self->_chart_fields->{$field}; + return "0" if $field_obj->type == FIELD_TYPE_BUG_ID; + return Bugzilla->dbh->quote(EMPTY_DATETIME) + if $field_obj->type == FIELD_TYPE_DATETIME; + return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE; + return "''"; } ###################### @@ -3381,46 +3396,47 @@ sub _empty_value { ###################### # Validate that the query type is one we can deal with -sub IsValidQueryType -{ - my ($queryType) = @_; - if (grep { $_ eq $queryType } qw(specific advanced)) { - return 1; - } - return 0; +sub IsValidQueryType { + my ($queryType) = @_; + if (grep { $_ eq $queryType } qw(specific advanced)) { + return 1; + } + return 0; } # Splits out "asc|desc" from a sort order item. sub split_order_term { - my $fragment = shift; - $fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i; - my ($column_name, $direction) = (lc($1), uc($2 || '')); - return wantarray ? ($column_name, $direction) : $column_name; + my $fragment = shift; + $fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i; + my ($column_name, $direction) = (lc($1), uc($2 || '')); + return wantarray ? ($column_name, $direction) : $column_name; } # Used to translate old SQL fragments from buglist.cgi's "order" argument # into our modern field IDs. sub _translate_old_column { - my ($self, $column) = @_; - # All old SQL fragments have a period in them somewhere. - return $column if $column !~ /\./; + my ($self, $column) = @_; - if ($column =~ /\bAS\s+(\w+)$/i) { - return $1; - } - # product, component, classification, assigned_to, qa_contact, reporter - elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) { - return $1; - } - - # If it doesn't match the regexps above, check to see if the old - # SQL fragment matches the SQL of an existing column - foreach my $key (%{ $self->COLUMNS }) { - next unless exists $self->COLUMNS->{$key}->{name}; - return $key if $self->COLUMNS->{$key}->{name} eq $column; - } + # All old SQL fragments have a period in them somewhere. + return $column if $column !~ /\./; + + if ($column =~ /\bAS\s+(\w+)$/i) { + return $1; + } + + # product, component, classification, assigned_to, qa_contact, reporter + elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) { + return $1; + } + + # If it doesn't match the regexps above, check to see if the old + # SQL fragment matches the SQL of an existing column + foreach my $key (%{$self->COLUMNS}) { + next unless exists $self->COLUMNS->{$key}->{name}; + return $key if $self->COLUMNS->{$key}->{name} eq $column; + } - return $column; + return $column; } 1; diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm index 1d7872c78..940f88ff3 100644 --- a/Bugzilla/Search/Clause.pm +++ b/Bugzilla/Search/Clause.pm @@ -16,121 +16,123 @@ use Bugzilla::Search::Condition qw(condition); use Bugzilla::Util qw(trick_taint); sub new { - my ($class, $joiner) = @_; - if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') { - ThrowCodeError('search_invalid_joiner', { joiner => $joiner }); - } - # This will go into SQL directly so needs to be untainted. - trick_taint($joiner) if $joiner; - bless { joiner => $joiner || 'AND' }, $class; + my ($class, $joiner) = @_; + if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') { + ThrowCodeError('search_invalid_joiner', {joiner => $joiner}); + } + + # This will go into SQL directly so needs to be untainted. + trick_taint($joiner) if $joiner; + bless {joiner => $joiner || 'AND'}, $class; } sub children { - my ($self) = @_; - $self->{children} ||= []; - return $self->{children}; + my ($self) = @_; + $self->{children} ||= []; + return $self->{children}; } sub update_search_args { - my ($self, $search_args) = @_; - # abstract + my ($self, $search_args) = @_; + + # abstract } sub joiner { return $_[0]->{joiner} } sub has_translated_conditions { - my ($self) = @_; - my $children = $self->children; - return 1 if grep { $_->isa('Bugzilla::Search::Condition') - && $_->translated } @$children; - foreach my $child (@$children) { - next if $child->isa('Bugzilla::Search::Condition'); - return 1 if $child->has_translated_conditions; - } - return 0; + my ($self) = @_; + my $children = $self->children; + return 1 + if grep { $_->isa('Bugzilla::Search::Condition') && $_->translated } + @$children; + foreach my $child (@$children) { + next if $child->isa('Bugzilla::Search::Condition'); + return 1 if $child->has_translated_conditions; + } + return 0; } sub add { - my $self = shift; - my $children = $self->children; - if (@_ == 3) { - push(@$children, condition(@_)); - return; - } - - my ($child) = @_; - return if !defined $child; - $child->isa(__PACKAGE__) || $child->isa('Bugzilla::Search::Condition') - || die 'child not the right type: ' . $child; - push(@{ $self->children }, $child); + my $self = shift; + my $children = $self->children; + if (@_ == 3) { + push(@$children, condition(@_)); + return; + } + + my ($child) = @_; + return if !defined $child; + $child->isa(__PACKAGE__) + || $child->isa('Bugzilla::Search::Condition') + || die 'child not the right type: ' . $child; + push(@{$self->children}, $child); } sub negate { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{negate} = $value ? 1 : 0; - } - return $self->{negate}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{negate} = $value ? 1 : 0; + } + return $self->{negate}; } sub walk_conditions { - my ($self, $callback) = @_; - foreach my $child (@{ $self->children }) { - if ($child->isa('Bugzilla::Search::Condition')) { - $callback->($self, $child); - } - else { - $child->walk_conditions($callback); - } + my ($self, $callback) = @_; + foreach my $child (@{$self->children}) { + if ($child->isa('Bugzilla::Search::Condition')) { + $callback->($self, $child); + } + else { + $child->walk_conditions($callback); } + } } sub as_string { - my ($self) = @_; - if (!$self->{sql}) { - my @strings; - foreach my $child (@{ $self->children }) { - next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions; - next if $child->isa('Bugzilla::Search::Condition') - && !$child->translated; - - my $string = $child->as_string; - next unless $string; - if ($self->joiner eq 'AND') { - $string = "( $string )" if $string =~ /OR/; - } - else { - $string = "( $string )" if $string =~ /AND/; - } - push(@strings, $string); - } - - my $sql = join(' ' . $self->joiner . ' ', @strings); - $sql = "NOT( $sql )" if $sql && $self->negate; - $self->{sql} = $sql; + my ($self) = @_; + if (!$self->{sql}) { + my @strings; + foreach my $child (@{$self->children}) { + next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions; + next if $child->isa('Bugzilla::Search::Condition') && !$child->translated; + + my $string = $child->as_string; + next unless $string; + if ($self->joiner eq 'AND') { + $string = "( $string )" if $string =~ /OR/; + } + else { + $string = "( $string )" if $string =~ /AND/; + } + push(@strings, $string); } - return $self->{sql}; + + my $sql = join(' ' . $self->joiner . ' ', @strings); + $sql = "NOT( $sql )" if $sql && $self->negate; + $self->{sql} = $sql; + } + return $self->{sql}; } # Search.pm converts URL parameters to Clause objects. This helps do the # reverse. sub as_params { - my ($self) = @_; - my @params; - foreach my $child (@{ $self->children }) { - if ($child->isa(__PACKAGE__)) { - my %open_paren = (f => 'OP', n => scalar $child->negate, - j => $child->joiner); - push(@params, \%open_paren); - push(@params, $child->as_params); - my %close_paren = (f => 'CP'); - push(@params, \%close_paren); - } - else { - push(@params, $child->as_params); - } + my ($self) = @_; + my @params; + foreach my $child (@{$self->children}) { + if ($child->isa(__PACKAGE__)) { + my %open_paren = (f => 'OP', n => scalar $child->negate, j => $child->joiner); + push(@params, \%open_paren); + push(@params, $child->as_params); + my %close_paren = (f => 'CP'); + push(@params, \%close_paren); + } + else { + push(@params, $child->as_params); } - return @params; + } + return @params; } 1; diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm index 590c737fa..5c7791734 100644 --- a/Bugzilla/Search/ClauseGroup.pm +++ b/Bugzilla/Search/ClauseGroup.pm @@ -19,83 +19,88 @@ use Bugzilla::Util qw(trick_taint); use List::MoreUtils qw(uniq); use constant UNSUPPORTED_FIELDS => qw( - attach_data.thedata - classification - commenter - component - longdescs.count - product - owner_idle_time + attach_data.thedata + classification + commenter + component + longdescs.count + product + owner_idle_time ); sub new { - my ($class) = @_; - my $self = bless({ joiner => 'AND' }, $class); - # Add a join back to the bugs table which will be used to group conditions - # for this clause - my $condition = Bugzilla::Search::Condition->new({}); - $condition->translated({ - joins => [{ - table => 'bugs', - as => 'bugs_g0', - from => 'bug_id', - to => 'bug_id', - extra => [], - }], - term => '1 = 1', - }); - $self->SUPER::add($condition); - $self->{group_condition} = $condition; - return $self; + my ($class) = @_; + my $self = bless({joiner => 'AND'}, $class); + + # Add a join back to the bugs table which will be used to group conditions + # for this clause + my $condition = Bugzilla::Search::Condition->new({}); + $condition->translated({ + joins => [{ + table => 'bugs', + as => 'bugs_g0', + from => 'bug_id', + to => 'bug_id', + extra => [], + }], + term => '1 = 1', + }); + $self->SUPER::add($condition); + $self->{group_condition} = $condition; + return $self; } sub add { - my ($self, @args) = @_; - my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field}; - - # We don't support nesting of conditions under this clause - if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) { - ThrowUserError('search_grouped_invalid_nesting'); - } - - # Ensure all conditions use the same field - if (!$self->{_field}) { - $self->{_field} = $field; - } elsif ($field ne $self->{_field}) { - ThrowUserError('search_grouped_field_mismatch'); - } - - # Unsupported fields - if (grep { $_ eq $field } UNSUPPORTED_FIELDS ) { - # XXX - Hack till bug 916882 is fixed. - my $operator = scalar(@args) == 3 ? $args[1] : $args[0]->{operator}; - ThrowUserError('search_grouped_field_invalid', { field => $field }) - unless (($field eq 'product' || $field eq 'component') && $operator =~ /^changed/); - } - - $self->SUPER::add(@args); + my ($self, @args) = @_; + my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field}; + + # We don't support nesting of conditions under this clause + if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) { + ThrowUserError('search_grouped_invalid_nesting'); + } + + # Ensure all conditions use the same field + if (!$self->{_field}) { + $self->{_field} = $field; + } + elsif ($field ne $self->{_field}) { + ThrowUserError('search_grouped_field_mismatch'); + } + + # Unsupported fields + if (grep { $_ eq $field } UNSUPPORTED_FIELDS) { + + # XXX - Hack till bug 916882 is fixed. + my $operator = scalar(@args) == 3 ? $args[1] : $args[0]->{operator}; + ThrowUserError('search_grouped_field_invalid', {field => $field}) + unless (($field eq 'product' || $field eq 'component') + && $operator =~ /^changed/); + } + + $self->SUPER::add(@args); } sub update_search_args { - my ($self, $search_args) = @_; + my ($self, $search_args) = @_; - # No need to change things if there's only one child condition - return unless scalar(@{ $self->children }) > 1; + # No need to change things if there's only one child condition + return unless scalar(@{$self->children}) > 1; - # we want all the terms to use the same join table - if (!exists $self->{_first_chart_id}) { - $self->{_first_chart_id} = $search_args->{chart_id}; - } else { - $search_args->{chart_id} = $self->{_first_chart_id}; - } + # we want all the terms to use the same join table + if (!exists $self->{_first_chart_id}) { + $self->{_first_chart_id} = $search_args->{chart_id}; + } + else { + $search_args->{chart_id} = $self->{_first_chart_id}; + } - my $suffix = '_g' . $self->{_first_chart_id}; - $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix"; + my $suffix = '_g' . $self->{_first_chart_id}; + $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix"; - $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./; + $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./; - $search_args->{table_suffix} = $suffix; - $search_args->{bugs_table} = "bugs$suffix"; + $search_args->{table_suffix} = $suffix; + $search_args->{bugs_table} = "bugs$suffix"; } 1; diff --git a/Bugzilla/Search/Condition.pm b/Bugzilla/Search/Condition.pm index 306a63eed..9ddb6a898 100644 --- a/Bugzilla/Search/Condition.pm +++ b/Bugzilla/Search/Condition.pm @@ -15,55 +15,59 @@ use parent qw(Exporter); our @EXPORT_OK = qw(condition); sub new { - my ($class, $params) = @_; - my %self = %$params; - bless \%self, $class; - return \%self; + my ($class, $params) = @_; + my %self = %$params; + bless \%self, $class; + return \%self; } -sub field { return $_[0]->{field} } -sub value { return $_[0]->{value} } +sub field { return $_[0]->{field} } +sub value { return $_[0]->{value} } sub operator { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{operator} = $value; - } - return $self->{operator}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{operator} = $value; + } + return $self->{operator}; } sub fov { - my ($self) = @_; - return ($self->field, $self->operator, $self->value); + my ($self) = @_; + return ($self->field, $self->operator, $self->value); } sub translated { - my ($self, $params) = @_; - if (@_ == 2) { - $self->{translated} = $params; - } - return $self->{translated}; + my ($self, $params) = @_; + if (@_ == 2) { + $self->{translated} = $params; + } + return $self->{translated}; } sub as_string { - my ($self) = @_; - my $term = $self->translated->{term}; - $term = "NOT( $term )" if $term && $self->negate; - return $term; + my ($self) = @_; + my $term = $self->translated->{term}; + $term = "NOT( $term )" if $term && $self->negate; + return $term; } sub as_params { - my ($self) = @_; - return { f => $self->field, o => $self->operator, v => $self->value, - n => scalar $self->negate }; + my ($self) = @_; + return { + f => $self->field, + o => $self->operator, + v => $self->value, + n => scalar $self->negate + }; } sub negate { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{negate} = $value ? 1 : 0; - } - return $self->{negate}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{negate} = $value ? 1 : 0; + } + return $self->{negate}; } ########################### @@ -71,9 +75,9 @@ sub negate { ########################### sub condition { - my ($field, $operator, $value) = @_; - return __PACKAGE__->new({ field => $field, operator => $operator, - value => $value }); + my ($field, $operator, $value) = @_; + return __PACKAGE__->new( + {field => $field, operator => $operator, value => $value}); } 1; diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm index 4f57b4ebc..278172e01 100644 --- a/Bugzilla/Search/Quicksearch.pm +++ b/Bugzilla/Search/Quicksearch.pm @@ -27,246 +27,251 @@ use parent qw(Exporter); # Custom mappings for some fields. use constant MAPPINGS => { - # Status, Resolution, Platform, OS, Priority, Severity - "status" => "bug_status", - "platform" => "rep_platform", - "os" => "op_sys", - "severity" => "bug_severity", - - # People: AssignedTo, Reporter, QA Contact, CC, etc. - "assignee" => "assigned_to", - "owner" => "assigned_to", - - # Product, Version, Component, Target Milestone - "milestone" => "target_milestone", - - # Summary, Description, URL, Status whiteboard, Keywords - "summary" => "short_desc", - "description" => "longdesc", - "comment" => "longdesc", - "url" => "bug_file_loc", - "whiteboard" => "status_whiteboard", - "sw" => "status_whiteboard", - "kw" => "keywords", - "group" => "bug_group", - - # Flags - "flag" => "flagtypes.name", - "requestee" => "requestees.login_name", - "setter" => "setters.login_name", - - # Attachments - "attachment" => "attachments.description", - "attachmentdesc" => "attachments.description", - "attachdesc" => "attachments.description", - "attachmentdata" => "attach_data.thedata", - "attachdata" => "attach_data.thedata", - "attachmentmimetype" => "attachments.mimetype", - "attachmimetype" => "attachments.mimetype" + + # Status, Resolution, Platform, OS, Priority, Severity + "status" => "bug_status", + "platform" => "rep_platform", + "os" => "op_sys", + "severity" => "bug_severity", + + # People: AssignedTo, Reporter, QA Contact, CC, etc. + "assignee" => "assigned_to", + "owner" => "assigned_to", + + # Product, Version, Component, Target Milestone + "milestone" => "target_milestone", + + # Summary, Description, URL, Status whiteboard, Keywords + "summary" => "short_desc", + "description" => "longdesc", + "comment" => "longdesc", + "url" => "bug_file_loc", + "whiteboard" => "status_whiteboard", + "sw" => "status_whiteboard", + "kw" => "keywords", + "group" => "bug_group", + + # Flags + "flag" => "flagtypes.name", + "requestee" => "requestees.login_name", + "setter" => "setters.login_name", + + # Attachments + "attachment" => "attachments.description", + "attachmentdesc" => "attachments.description", + "attachdesc" => "attachments.description", + "attachmentdata" => "attach_data.thedata", + "attachdata" => "attach_data.thedata", + "attachmentmimetype" => "attachments.mimetype", + "attachmimetype" => "attachments.mimetype" }; sub FIELD_MAP { - my $cache = Bugzilla->request_cache; - return $cache->{quicksearch_fields} if $cache->{quicksearch_fields}; - - # Get all the fields whose names don't contain periods. (Fields that - # contain periods are always handled in MAPPINGS.) - my @db_fields = grep { $_->name !~ /\./ } - @{ Bugzilla->fields({ obsolete => 0 }) }; - my %full_map = (%{ MAPPINGS() }, map { $_->name => $_->name } @db_fields); - - # Eliminate the fields that start with bug_ or rep_, because those are - # handled by the MAPPINGS instead, and we don't want too many names - # for them. (Also, otherwise "rep" doesn't match "reporter".) - # - # Remove "status_whiteboard" because we have "whiteboard" for it in - # the mappings, and otherwise "stat" can't match "status". - # - # Also, don't allow searching the _accessible stuff via quicksearch - # (both because it's unnecessary and because otherwise - # "reporter_accessible" and "reporter" both match "rep". - delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group - bug_severity bug_status - status_whiteboard - cclist_accessible reporter_accessible)}; - - Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map} ); - - $cache->{quicksearch_fields} = \%full_map; - - return $cache->{quicksearch_fields}; + my $cache = Bugzilla->request_cache; + return $cache->{quicksearch_fields} if $cache->{quicksearch_fields}; + + # Get all the fields whose names don't contain periods. (Fields that + # contain periods are always handled in MAPPINGS.) + my @db_fields = grep { $_->name !~ /\./ } @{Bugzilla->fields({obsolete => 0})}; + my %full_map = (%{MAPPINGS()}, map { $_->name => $_->name } @db_fields); + + # Eliminate the fields that start with bug_ or rep_, because those are + # handled by the MAPPINGS instead, and we don't want too many names + # for them. (Also, otherwise "rep" doesn't match "reporter".) + # + # Remove "status_whiteboard" because we have "whiteboard" for it in + # the mappings, and otherwise "stat" can't match "status". + # + # Also, don't allow searching the _accessible stuff via quicksearch + # (both because it's unnecessary and because otherwise + # "reporter_accessible" and "reporter" both match "rep". + delete @full_map{ + qw(rep_platform bug_status bug_file_loc bug_group + bug_severity bug_status + status_whiteboard + cclist_accessible reporter_accessible) + }; + + Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map}); + + $cache->{quicksearch_fields} = \%full_map; + + return $cache->{quicksearch_fields}; } # Certain fields, when specified like "field:value" get an operator other # than "substring" -use constant FIELD_OPERATOR => { - content => 'matches', - owner_idle_time => 'greaterthan', -}; +use constant FIELD_OPERATOR => + {content => 'matches', owner_idle_time => 'greaterthan',}; # Mappings for operators symbols to support operators other than "substring" use constant OPERATOR_SYMBOLS => { - ':' => 'substring', - '=' => 'equals', - '!=' => 'notequals', - '>=' => 'greaterthaneq', - '<=' => 'lessthaneq', - '>' => 'greaterthan', - '<' => 'lessthan', + ':' => 'substring', + '=' => 'equals', + '!=' => 'notequals', + '>=' => 'greaterthaneq', + '<=' => 'lessthaneq', + '>' => 'greaterthan', + '<' => 'lessthan', }; # We might want to put this into localconfig or somewhere use constant PRODUCT_EXCEPTIONS => ( - 'row', # [Browser] - # ^^^ - 'new', # [MailNews] - # ^^^ + 'row', # [Browser] + # ^^^ + 'new', # [MailNews] + # ^^^ ); use constant COMPONENT_EXCEPTIONS => ( - 'hang' # [Bugzilla: Component/Keyword Changes] - # ^^^^ + 'hang' # [Bugzilla: Component/Keyword Changes] + # ^^^^ ); # Quicksearch-wide globals for boolean charts. our ($chart, $and, $or, $fulltext, $bug_status_set); sub quicksearch { - my ($searchstring) = (@_); - my $cgi = Bugzilla->cgi; - - $chart = 0; - $and = 0; - $or = 0; + my ($searchstring) = (@_); + my $cgi = Bugzilla->cgi; + + $chart = 0; + $and = 0; + $or = 0; + + # Remove leading and trailing commas and whitespace. + $searchstring =~ s/(^[\s,]+|[\s,]+$)//g; + ThrowUserError('buglist_parameters_required') unless ($searchstring); + + if ($searchstring =~ m/^#?[0-9,\s]*$/) { + _bug_numbers_only($searchstring); + } + else { + _handle_alias($searchstring); + + # Retain backslashes and quotes, to know which strings are quoted, + # and which ones are not. + my @words = _parse_line('\s+', 1, $searchstring); + + # If parse_line() returns no data, this means strings are badly quoted. + # Rather than trying to guess what the user wanted to do, we throw an error. + scalar(@words) + || ThrowUserError('quicksearch_unbalanced_quotes', {string => $searchstring}); + + # A query cannot start with AND or OR, nor can it end with AND, OR or NOT. + ThrowUserError('quicksearch_invalid_query') + if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/); + + my (@qswords, @or_group); + while (scalar @words) { + my $word = shift @words; + + # AND is the default word separator, similar to a whitespace, + # but |a AND OR b| is not a valid combination. + if ($word eq 'AND') { + ThrowUserError('quicksearch_invalid_query', {operators => ['AND', 'OR']}) + if $words[0] eq 'OR'; + } + + # |a OR AND b| is not a valid combination. + # |a OR OR b| is equivalent to |a OR b| and so is harmless. + elsif ($word eq 'OR') { + ThrowUserError('quicksearch_invalid_query', {operators => ['OR', 'AND']}) + if $words[0] eq 'AND'; + } + + # NOT negates the following word. + # |NOT AND| and |NOT OR| are not valid combinations. + # |NOT NOT| is fine but has no effect as they cancel themselves. + elsif ($word eq 'NOT') { + $word = shift @words; + next if $word eq 'NOT'; + if ($word eq 'AND' || $word eq 'OR') { + ThrowUserError('quicksearch_invalid_query', {operators => ['NOT', $word]}); + } + unshift(@words, "-$word"); + } + else { + # OR groups words together, as OR has higher precedence than AND. + push(@or_group, $word); + + # If the next word is not OR, then we are not in a OR group, + # or we are leaving it. + if (!defined $words[0] || $words[0] ne 'OR') { + push(@qswords, join('|', @or_group)); + @or_group = (); + } + } + } - # Remove leading and trailing commas and whitespace. - $searchstring =~ s/(^[\s,]+|[\s,]+$)//g; - ThrowUserError('buglist_parameters_required') unless ($searchstring); + _handle_status_and_resolution($qswords[0]); + shift(@qswords) if $bug_status_set; - if ($searchstring =~ m/^#?[0-9,\s]*$/) { - _bug_numbers_only($searchstring); - } - else { - _handle_alias($searchstring); - - # Retain backslashes and quotes, to know which strings are quoted, - # and which ones are not. - my @words = _parse_line('\s+', 1, $searchstring); - # If parse_line() returns no data, this means strings are badly quoted. - # Rather than trying to guess what the user wanted to do, we throw an error. - scalar(@words) - || ThrowUserError('quicksearch_unbalanced_quotes', {string => $searchstring}); - - # A query cannot start with AND or OR, nor can it end with AND, OR or NOT. - ThrowUserError('quicksearch_invalid_query') - if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/); - - my (@qswords, @or_group); - while (scalar @words) { - my $word = shift @words; - # AND is the default word separator, similar to a whitespace, - # but |a AND OR b| is not a valid combination. - if ($word eq 'AND') { - ThrowUserError('quicksearch_invalid_query', {operators => ['AND', 'OR']}) - if $words[0] eq 'OR'; - } - # |a OR AND b| is not a valid combination. - # |a OR OR b| is equivalent to |a OR b| and so is harmless. - elsif ($word eq 'OR') { - ThrowUserError('quicksearch_invalid_query', {operators => ['OR', 'AND']}) - if $words[0] eq 'AND'; - } - # NOT negates the following word. - # |NOT AND| and |NOT OR| are not valid combinations. - # |NOT NOT| is fine but has no effect as they cancel themselves. - elsif ($word eq 'NOT') { - $word = shift @words; - next if $word eq 'NOT'; - if ($word eq 'AND' || $word eq 'OR') { - ThrowUserError('quicksearch_invalid_query', {operators => ['NOT', $word]}); - } - unshift(@words, "-$word"); - } - else { - # OR groups words together, as OR has higher precedence than AND. - push(@or_group, $word); - # If the next word is not OR, then we are not in a OR group, - # or we are leaving it. - if (!defined $words[0] || $words[0] ne 'OR') { - push(@qswords, join('|', @or_group)); - @or_group = (); - } - } - } + my (@unknownFields, %ambiguous_fields); + $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0; - _handle_status_and_resolution($qswords[0]); - shift(@qswords) if $bug_status_set; - - my (@unknownFields, %ambiguous_fields); - $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0; - - # Loop over all main-level QuickSearch words. - foreach my $qsword (@qswords) { - my @or_operand = _parse_line('\|', 1, $qsword); - foreach my $term (@or_operand) { - next unless defined $term; - my $negate = substr($term, 0, 1) eq '-'; - if ($negate) { - $term = substr($term, 1); - } - - next if _handle_special_first_chars($term, $negate); - next if _handle_field_names($term, $negate, \@unknownFields, - \%ambiguous_fields); - - # Having ruled out the special cases, we may now split - # by comma, which is another legal boolean OR indicator. - # Remove quotes from quoted words, if any. - @words = _parse_line(',', 0, $term); - foreach my $word (@words) { - if (!_special_field_syntax($word, $negate)) { - _default_quicksearch_word($word, $negate); - } - _handle_urls($word, $negate); - } - } - $chart++; - $and = 0; - $or = 0; + # Loop over all main-level QuickSearch words. + foreach my $qsword (@qswords) { + my @or_operand = _parse_line('\|', 1, $qsword); + foreach my $term (@or_operand) { + next unless defined $term; + my $negate = substr($term, 0, 1) eq '-'; + if ($negate) { + $term = substr($term, 1); } - # If there is no mention of a bug status, we restrict the query - # to open bugs by default. - unless ($bug_status_set) { - $cgi->param('bug_status', BUG_STATE_OPEN); + next if _handle_special_first_chars($term, $negate); + next + if _handle_field_names($term, $negate, \@unknownFields, \%ambiguous_fields); + + # Having ruled out the special cases, we may now split + # by comma, which is another legal boolean OR indicator. + # Remove quotes from quoted words, if any. + @words = _parse_line(',', 0, $term); + foreach my $word (@words) { + if (!_special_field_syntax($word, $negate)) { + _default_quicksearch_word($word, $negate); + } + _handle_urls($word, $negate); } + } + $chart++; + $and = 0; + $or = 0; + } - # Inform user about any unknown fields - if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) { - ThrowUserError("quicksearch_unknown_field", - { unknown => \@unknownFields, - ambiguous => \%ambiguous_fields }); - } + # If there is no mention of a bug status, we restrict the query + # to open bugs by default. + unless ($bug_status_set) { + $cgi->param('bug_status', BUG_STATE_OPEN); + } - # Make sure we have some query terms left - scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required"); + # Inform user about any unknown fields + if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) { + ThrowUserError("quicksearch_unknown_field", + {unknown => \@unknownFields, ambiguous => \%ambiguous_fields}); } - # List of quicksearch-specific CGI parameters to get rid of. - my @params_to_strip = ('quicksearch', 'load', 'run'); - my $modified_query_string = $cgi->canonicalise_query(@params_to_strip); + # Make sure we have some query terms left + scalar($cgi->param()) > 0 || ThrowUserError("buglist_parameters_required"); + } - if ($cgi->param('load')) { - my $urlbase = correct_urlbase(); - # Param 'load' asks us to display the query in the advanced search form. - print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&" - . $modified_query_string); - } + # List of quicksearch-specific CGI parameters to get rid of. + my @params_to_strip = ('quicksearch', 'load', 'run'); + my $modified_query_string = $cgi->canonicalise_query(@params_to_strip); - # Otherwise, pass the modified query string to the caller. - # We modified $cgi->params, so the caller can choose to look at that, too, - # and disregard the return value. - $cgi->delete(@params_to_strip); - return $modified_query_string; + if ($cgi->param('load')) { + my $urlbase = correct_urlbase(); + + # Param 'load' asks us to display the query in the advanced search form. + print $cgi->redirect( + -uri => "${urlbase}query.cgi?format=advanced&" . $modified_query_string); + } + + # Otherwise, pass the modified query string to the caller. + # We modified $cgi->params, so the caller can choose to look at that, too, + # and disregard the return value. + $cgi->delete(@params_to_strip); + return $modified_query_string; } ########################## @@ -274,337 +279,355 @@ sub quicksearch { ########################## sub _parse_line { - my ($delim, $keep, $line) = @_; - return () unless defined $line; - - # parse_line always treats ' as a quote character, making it impossible - # to sanely search for contractions. As this behavour isn't - # configurable, we replace ' with a placeholder to hide it from the - # parser. - - # only treat ' at the start or end of words as quotes - # it's easier to do this in reverse with regexes - $line =~ s/(^|\s|:)'/$1\001/g; - $line =~ s/'($|\s)/\001$1/g; - $line =~ s/\\?'/\000/g; - $line =~ tr/\001/'/; - - my @words = parse_line($delim, $keep, $line); - foreach my $word (@words) { - $word =~ tr/\000/'/ if defined $word; - } - return @words; + my ($delim, $keep, $line) = @_; + return () unless defined $line; + + # parse_line always treats ' as a quote character, making it impossible + # to sanely search for contractions. As this behavour isn't + # configurable, we replace ' with a placeholder to hide it from the + # parser. + + # only treat ' at the start or end of words as quotes + # it's easier to do this in reverse with regexes + $line =~ s/(^|\s|:)'/$1\001/g; + $line =~ s/'($|\s)/\001$1/g; + $line =~ s/\\?'/\000/g; + $line =~ tr/\001/'/; + + my @words = parse_line($delim, $keep, $line); + foreach my $word (@words) { + $word =~ tr/\000/'/ if defined $word; + } + return @words; } sub _bug_numbers_only { - my $searchstring = shift; - my $cgi = Bugzilla->cgi; - # Allow separation by comma or whitespace. - $searchstring =~ s/[,\s]+/,/g; - # Trim the leading # if used. - $searchstring =~ s/^#//; - - if ($searchstring !~ /,/ && !i_am_webservice()) { - # Single bug number; shortcut to show_bug.cgi. - print $cgi->redirect( - -uri => correct_urlbase() . "show_bug.cgi?id=$searchstring"); - exit; - } - else { - # List of bug numbers. - $cgi->param('bug_id', $searchstring); - $cgi->param('order', 'bugs.bug_id'); - $cgi->param('bug_id_type', 'anyexact'); - } + my $searchstring = shift; + my $cgi = Bugzilla->cgi; + + # Allow separation by comma or whitespace. + $searchstring =~ s/[,\s]+/,/g; + + # Trim the leading # if used. + $searchstring =~ s/^#//; + + if ($searchstring !~ /,/ && !i_am_webservice()) { + + # Single bug number; shortcut to show_bug.cgi. + print $cgi->redirect( + -uri => correct_urlbase() . "show_bug.cgi?id=$searchstring"); + exit; + } + else { + # List of bug numbers. + $cgi->param('bug_id', $searchstring); + $cgi->param('order', 'bugs.bug_id'); + $cgi->param('bug_id_type', 'anyexact'); + } } sub _handle_alias { - my $searchstring = shift; - if ($searchstring =~ /^([^,\s]+)$/) { - my $alias = $1; - # We use this direct SQL because we want quicksearch to be VERY fast. - my $bug_id = Bugzilla->dbh->selectrow_array( - q{SELECT bug_id FROM bugs_aliases WHERE alias = ?}, undef, $alias); - # If the user cannot see the bug or if we are using a webservice, - # do not resolve its alias. - if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) { - $alias = url_quote($alias); - print Bugzilla->cgi->redirect( - -uri => correct_urlbase() . "show_bug.cgi?id=$alias"); - exit; - } + my $searchstring = shift; + if ($searchstring =~ /^([^,\s]+)$/) { + my $alias = $1; + + # We use this direct SQL because we want quicksearch to be VERY fast. + my $bug_id + = Bugzilla->dbh->selectrow_array( + q{SELECT bug_id FROM bugs_aliases WHERE alias = ?}, + undef, $alias); + + # If the user cannot see the bug or if we are using a webservice, + # do not resolve its alias. + if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) { + $alias = url_quote($alias); + print Bugzilla->cgi->redirect( + -uri => correct_urlbase() . "show_bug.cgi?id=$alias"); + exit; } + } } sub _handle_status_and_resolution { - my $word = shift; - my $legal_statuses = get_legal_field_values('bug_status'); - my (%states, %resolutions); - $bug_status_set = 1; - - if ($word eq 'OPEN') { - $states{$_} = 1 foreach BUG_STATE_OPEN; - } - # If we want all bugs, then there is nothing to do. - elsif ($word ne 'ALL' - && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses)) - { - $bug_status_set = 0; - } - - # If we have wanted resolutions, allow closed states - if (keys(%resolutions)) { - foreach my $status (@$legal_statuses) { - $states{$status} = 1 unless is_open_state($status); - } + my $word = shift; + my $legal_statuses = get_legal_field_values('bug_status'); + my (%states, %resolutions); + $bug_status_set = 1; + + if ($word eq 'OPEN') { + $states{$_} = 1 foreach BUG_STATE_OPEN; + } + + # If we want all bugs, then there is nothing to do. + elsif ($word ne 'ALL' + && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses)) + { + $bug_status_set = 0; + } + + # If we have wanted resolutions, allow closed states + if (keys(%resolutions)) { + foreach my $status (@$legal_statuses) { + $states{$status} = 1 unless is_open_state($status); } + } - Bugzilla->cgi->param('bug_status', keys(%states)); - Bugzilla->cgi->param('resolution', keys(%resolutions)); + Bugzilla->cgi->param('bug_status', keys(%states)); + Bugzilla->cgi->param('resolution', keys(%resolutions)); } sub _handle_special_first_chars { - my ($qsword, $negate) = @_; - return 0 if !defined $qsword || length($qsword) <= 1; - - my $firstChar = substr($qsword, 0, 1); - my $baseWord = substr($qsword, 1); - my @subWords = split(/,/, $baseWord); - - if ($firstChar eq '#') { - addChart('short_desc', 'substring', $baseWord, $negate); - addChart('content', 'matches', _matches_phrase($baseWord), $negate) if $fulltext; - return 1; - } - if ($firstChar eq ':') { - foreach (@subWords) { - addChart('product', 'substring', $_, $negate); - addChart('component', 'substring', $_, $negate); - } - return 1; - } - if ($firstChar eq '@') { - addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords); - return 1; - } - if ($firstChar eq '[') { - addChart('short_desc', 'substring', $baseWord, $negate); - addChart('status_whiteboard', 'substring', $baseWord, $negate); - return 1; - } - if ($firstChar eq '!') { - addChart('keywords', 'anywords', $baseWord, $negate); - return 1; + my ($qsword, $negate) = @_; + return 0 if !defined $qsword || length($qsword) <= 1; + + my $firstChar = substr($qsword, 0, 1); + my $baseWord = substr($qsword, 1); + my @subWords = split(/,/, $baseWord); + + if ($firstChar eq '#') { + addChart('short_desc', 'substring', $baseWord, $negate); + addChart('content', 'matches', _matches_phrase($baseWord), $negate) + if $fulltext; + return 1; + } + if ($firstChar eq ':') { + foreach (@subWords) { + addChart('product', 'substring', $_, $negate); + addChart('component', 'substring', $_, $negate); } - return 0; + return 1; + } + if ($firstChar eq '@') { + addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords); + return 1; + } + if ($firstChar eq '[') { + addChart('short_desc', 'substring', $baseWord, $negate); + addChart('status_whiteboard', 'substring', $baseWord, $negate); + return 1; + } + if ($firstChar eq '!') { + addChart('keywords', 'anywords', $baseWord, $negate); + return 1; + } + return 0; } sub _handle_field_names { - my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_; - - # Generic field1,field2,field3:value1,value2 notation. - # We have to correctly ignore commas and colons in quotes. - # Longer operators must be tested first as we don't want single character - # operators such as <, > and = to be tested before <=, >= and !=. - my @operators = sort { length($b) <=> length($a) } keys %{ OPERATOR_SYMBOLS() }; - - foreach my $symbol (@operators) { - my @field_values = _parse_line($symbol, 1, $or_operand); - next unless scalar @field_values == 2; - my @fields = _parse_line(',', 1, $field_values[0]); - my @values = _parse_line(',', 1, $field_values[1]); - foreach my $field (@fields) { - my $translated = _translate_field_name($field); - # Skip and record any unknown fields - if (!defined $translated) { - push(@$unknownFields, $field); - } - # If we got back an array, that means the substring is - # ambiguous and could match more than field name - elsif (ref $translated) { - $ambiguous_fields->{$field} = $translated; - } - else { - if ($translated eq 'bug_status' || $translated eq 'resolution') { - $bug_status_set = 1; - } - foreach my $value (@values) { - my $operator = FIELD_OPERATOR->{$translated} - || OPERATOR_SYMBOLS->{$symbol} - || 'substring'; - # If the string was quoted to protect some special - # characters such as commas and colons, we need - # to remove quotes. - if ($value =~ /^(["'])(.+)\1$/) { - $value = $2; - $value =~ s/\\(["'])/$1/g; - } - # If a requestee is set, we need to handle it separately. - if ($translated eq 'flagtypes.name' && $value =~ /^([^\?]+\?)([^\?]+)$/) { - _handle_flags($1, $2, $negate); - next; - } - addChart($translated, $operator, $value, $negate); - } - } + my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_; + + # Generic field1,field2,field3:value1,value2 notation. + # We have to correctly ignore commas and colons in quotes. + # Longer operators must be tested first as we don't want single character + # operators such as <, > and = to be tested before <=, >= and !=. + my @operators = sort { length($b) <=> length($a) } keys %{OPERATOR_SYMBOLS()}; + + foreach my $symbol (@operators) { + my @field_values = _parse_line($symbol, 1, $or_operand); + next unless scalar @field_values == 2; + my @fields = _parse_line(',', 1, $field_values[0]); + my @values = _parse_line(',', 1, $field_values[1]); + foreach my $field (@fields) { + my $translated = _translate_field_name($field); + + # Skip and record any unknown fields + if (!defined $translated) { + push(@$unknownFields, $field); + } + + # If we got back an array, that means the substring is + # ambiguous and could match more than field name + elsif (ref $translated) { + $ambiguous_fields->{$field} = $translated; + } + else { + if ($translated eq 'bug_status' || $translated eq 'resolution') { + $bug_status_set = 1; } - return 1; + foreach my $value (@values) { + my $operator + = FIELD_OPERATOR->{$translated} || OPERATOR_SYMBOLS->{$symbol} || 'substring'; + + # If the string was quoted to protect some special + # characters such as commas and colons, we need + # to remove quotes. + if ($value =~ /^(["'])(.+)\1$/) { + $value = $2; + $value =~ s/\\(["'])/$1/g; + } + + # If a requestee is set, we need to handle it separately. + if ($translated eq 'flagtypes.name' && $value =~ /^([^\?]+\?)([^\?]+)$/) { + _handle_flags($1, $2, $negate); + next; + } + addChart($translated, $operator, $value, $negate); + } + } } + return 1; + } - # Do not look inside quoted strings. - return 0 if ($or_operand =~ /^(["']).*\1$/); + # Do not look inside quoted strings. + return 0 if ($or_operand =~ /^(["']).*\1$/); - # Flag and requestee shortcut. - if ($or_operand =~ /^([^\?]+\?)([^\?]*)$/) { - _handle_flags($1, $2, $negate); - return 1; - } + # Flag and requestee shortcut. + if ($or_operand =~ /^([^\?]+\?)([^\?]*)$/) { + _handle_flags($1, $2, $negate); + return 1; + } - return 0; + return 0; } sub _handle_flags { - my ($flag, $requestee, $negate) = @_; - - addChart('flagtypes.name', 'substring', $flag, $negate); - if ($requestee) { - # FIXME - Every time a requestee is involved and you use OR somewhere - # in your quick search, the logic will be wrong because boolean charts - # are unable to run queries of the form (a AND b) OR c. In our case: - # (flag name is foo AND requestee is bar) OR (any other criteria). - # But this has never been possible, so this is not a regression. If one - # needs to run such queries, they must use the Custom Search section of - # the Advanced Search page. - $chart++; - $and = $or = 0; - addChart('requestees.login_name', 'substring', $requestee, $negate); - } + my ($flag, $requestee, $negate) = @_; + + addChart('flagtypes.name', 'substring', $flag, $negate); + if ($requestee) { + + # FIXME - Every time a requestee is involved and you use OR somewhere + # in your quick search, the logic will be wrong because boolean charts + # are unable to run queries of the form (a AND b) OR c. In our case: + # (flag name is foo AND requestee is bar) OR (any other criteria). + # But this has never been possible, so this is not a regression. If one + # needs to run such queries, they must use the Custom Search section of + # the Advanced Search page. + $chart++; + $and = $or = 0; + addChart('requestees.login_name', 'substring', $requestee, $negate); + } } sub _translate_field_name { - my $field = shift; - $field = lc($field); - my $field_map = FIELD_MAP; - - # If the field exactly matches a mapping, just return right now. - return $field_map->{$field} if exists $field_map->{$field}; - - # Check if we match, as a starting substring, exactly one field. - my @field_names = keys %$field_map; - my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names; - # Eliminate duplicates that are actually the same field - # (otherwise "assi" matches both "assignee" and "assigned_to", and - # the lines below fail when they shouldn't.) - my %match_unique = map { $field_map->{$_} => $_ } @matches; - @matches = values %match_unique; - - if (scalar(@matches) == 1) { - return $field_map->{$matches[0]}; - } - elsif (scalar(@matches) > 1) { - return \@matches; + my $field = shift; + $field = lc($field); + my $field_map = FIELD_MAP; + + # If the field exactly matches a mapping, just return right now. + return $field_map->{$field} if exists $field_map->{$field}; + + # Check if we match, as a starting substring, exactly one field. + my @field_names = keys %$field_map; + my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names; + + # Eliminate duplicates that are actually the same field + # (otherwise "assi" matches both "assignee" and "assigned_to", and + # the lines below fail when they shouldn't.) + my %match_unique = map { $field_map->{$_} => $_ } @matches; + @matches = values %match_unique; + + if (scalar(@matches) == 1) { + return $field_map->{$matches[0]}; + } + elsif (scalar(@matches) > 1) { + return \@matches; + } + + # Check if we match exactly one custom field, ignoring the cf_ on the + # custom fields (to allow people to type things like "build" for + # "cf_build"). + my %cfless; + foreach my $name (@field_names) { + my $no_cf = $name; + if ($no_cf =~ s/^cf_//) { + if ($field eq $no_cf) { + return $field_map->{$name}; + } + $cfless{$no_cf} = $name; } + } - # Check if we match exactly one custom field, ignoring the cf_ on the - # custom fields (to allow people to type things like "build" for - # "cf_build"). - my %cfless; - foreach my $name (@field_names) { - my $no_cf = $name; - if ($no_cf =~ s/^cf_//) { - if ($field eq $no_cf) { - return $field_map->{$name}; - } - $cfless{$no_cf} = $name; - } - } + # See if we match exactly one substring of any of the cf_-less fields. + my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless); - # See if we match exactly one substring of any of the cf_-less fields. - my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless); + if (scalar(@cfless_matches) == 1) { + my $match = $cfless_matches[0]; + my $actual_field = $cfless{$match}; + return $field_map->{$actual_field}; + } + elsif (scalar(@matches) > 1) { + return \@matches; + } - if (scalar(@cfless_matches) == 1) { - my $match = $cfless_matches[0]; - my $actual_field = $cfless{$match}; - return $field_map->{$actual_field}; - } - elsif (scalar(@matches) > 1) { - return \@matches; - } - - return undef; + return undef; } sub _special_field_syntax { - my ($word, $negate) = @_; - - # P1-5 Syntax - if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { - my ($p_start, $p_end) = ($1, $2); - my $legal_priorities = get_legal_field_values('priority'); - - # If Pn exists explicitly, use it. - my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities; - my $end; - $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end; - - # If Pn doesn't exist explicitly, then we mean the nth priority. - if ($start == -1) { - $start = max(0, $p_start - 1); - } - my $prios = $legal_priorities->[$start]; - - if (defined $end) { - # If Pn doesn't exist explicitly, then we mean the nth priority. - if ($end == -1) { - $end = min(scalar(@$legal_priorities), $p_end) - 1; - $end = max(0, $end); # Just in case the user typed P0. - } - ($start, $end) = ($end, $start) if $end < $start; - $prios = join(',', @$legal_priorities[$start..$end]) - } + my ($word, $negate) = @_; + + # P1-5 Syntax + if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { + my ($p_start, $p_end) = ($1, $2); + my $legal_priorities = get_legal_field_values('priority'); + + # If Pn exists explicitly, use it. + my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities; + my $end; + $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end; + + # If Pn doesn't exist explicitly, then we mean the nth priority. + if ($start == -1) { + $start = max(0, $p_start - 1); + } + my $prios = $legal_priorities->[$start]; - addChart('priority', 'anyexact', $prios, $negate); - return 1; + if (defined $end) { + + # If Pn doesn't exist explicitly, then we mean the nth priority. + if ($end == -1) { + $end = min(scalar(@$legal_priorities), $p_end) - 1; + $end = max(0, $end); # Just in case the user typed P0. + } + ($start, $end) = ($end, $start) if $end < $start; + $prios = join(',', @$legal_priorities[$start .. $end]); } - return 0; + + addChart('priority', 'anyexact', $prios, $negate); + return 1; + } + return 0; } sub _default_quicksearch_word { - my ($word, $negate) = @_; - + my ($word, $negate) = @_; + # if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) { # addChart('product', 'substring', $word, $negate); # } - + # if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) { # addChart('component', 'substring', $word, $negate); # } - + # my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all); # if (grep { lc($word) eq lc($_) } @legal_keywords) { # addChart('keywords', 'substring', $word, $negate); # } - - addChart('alias', 'substring', $word, $negate); - addChart('short_desc', 'substring', $word, $negate); + + addChart('alias', 'substring', $word, $negate); + addChart('short_desc', 'substring', $word, $negate); + # addChart('status_whiteboard', 'substring', $word, $negate); # addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext; } sub _handle_urls { - my ($word, $negate) = @_; - # URL field (for IP addrs, host.names, - # scheme://urls) - if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ - || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ - || $word =~ /:[\\\/][\\\/]/ - || $word =~ /localhost/ - || $word =~ /mailto[:]?/) - # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port - { - addChart('bug_file_loc', 'substring', $word, $negate); - } + my ($word, $negate) = @_; + + # URL field (for IP addrs, host.names, + # scheme://urls) + if ( $word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ + || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ + || $word =~ /:[\\\/][\\\/]/ + || $word =~ /localhost/ + || $word =~ /mailto[:]?/) + + # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port + { + addChart('bug_file_loc', 'substring', $word, $negate); + } } ########################################################################### @@ -613,70 +636,70 @@ sub _handle_urls { # Quote and escape a phrase appropriately for a "content matches" search. sub _matches_phrase { - my ($phrase) = @_; - $phrase =~ s/"/\\"/g; - return "\"$phrase\""; + my ($phrase) = @_; + $phrase =~ s/"/\\"/g; + return "\"$phrase\""; } # Expand found prefixes to states or resolutions sub matchPrefixes { - my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_; - return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*$/; - - my @ar_prefixes = split(/,/, $word); - my $ar_check_resolutions = get_legal_field_values('resolution'); - my $foundMatch = 0; - - foreach my $prefix (@ar_prefixes) { - foreach (@$ar_check_states) { - if (/^$prefix/) { - $$hr_states{$_} = 1; - $foundMatch = 1; - } - } - foreach (@$ar_check_resolutions) { - if (/^$prefix/) { - $$hr_resolutions{$_} = 1; - $foundMatch = 1; - } - } + my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_; + return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*$/; + + my @ar_prefixes = split(/,/, $word); + my $ar_check_resolutions = get_legal_field_values('resolution'); + my $foundMatch = 0; + + foreach my $prefix (@ar_prefixes) { + foreach (@$ar_check_states) { + if (/^$prefix/) { + $$hr_states{$_} = 1; + $foundMatch = 1; + } } - return $foundMatch; + foreach (@$ar_check_resolutions) { + if (/^$prefix/) { + $$hr_resolutions{$_} = 1; + $foundMatch = 1; + } + } + } + return $foundMatch; } # Negate comparison type sub negateComparisonType { - my $comparisonType = shift; + my $comparisonType = shift; - if ($comparisonType eq 'anywords') { - return 'nowords'; - } - return "not$comparisonType"; + if ($comparisonType eq 'anywords') { + return 'nowords'; + } + return "not$comparisonType"; } # Add a boolean chart sub addChart { - my ($field, $comparisonType, $value, $negate) = @_; - - $negate && ($comparisonType = negateComparisonType($comparisonType)); - makeChart("$chart-$and-$or", $field, $comparisonType, $value); - if ($negate) { - $and++; - $or = 0; - } - else { - $or++; - } + my ($field, $comparisonType, $value, $negate) = @_; + + $negate && ($comparisonType = negateComparisonType($comparisonType)); + makeChart("$chart-$and-$or", $field, $comparisonType, $value); + if ($negate) { + $and++; + $or = 0; + } + else { + $or++; + } } # Create the CGI parameters for a boolean chart sub makeChart { - my ($expr, $field, $type, $value) = @_; + my ($expr, $field, $type, $value) = @_; - my $cgi = Bugzilla->cgi; - $cgi->param("field$expr", $field); - $cgi->param("type$expr", $type); - $cgi->param("value$expr", $value); + my $cgi = Bugzilla->cgi; + $cgi->param("field$expr", $field); + $cgi->param("type$expr", $type); + $cgi->param("value$expr", $value); } 1; diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm index e774c7fe0..5c12db156 100644 --- a/Bugzilla/Search/Recent.pm +++ b/Bugzilla/Search/Recent.pm @@ -21,24 +21,25 @@ use Bugzilla::Util; # Constants # ############# -use constant DB_TABLE => 'profile_search'; +use constant DB_TABLE => 'profile_search'; use constant LIST_ORDER => 'id DESC'; + # Do not track buglists viewed by users. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - user_id - bug_list - list_order + id + user_id + bug_list + list_order ); use constant VALIDATORS => { - user_id => \&_check_user_id, - bug_list => \&_check_bug_list, - list_order => \&_check_list_order, + user_id => \&_check_user_id, + bug_list => \&_check_bug_list, + list_order => \&_check_list_order, }; use constant UPDATE_COLUMNS => qw(bug_list list_order); @@ -51,29 +52,30 @@ use constant USE_MEMCACHED => 0; ################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $search = $class->SUPER::create(@_); - my $user_id = $search->user_id; - - # Enforce there only being SAVE_NUM_SEARCHES per user. - my @ids = @{ $dbh->selectcol_arrayref( - "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", - undef, $user_id) }; - if (scalar(@ids) > SAVE_NUM_SEARCHES) { - splice(@ids, - SAVE_NUM_SEARCHES); - $dbh->do( - "DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")"); - } - $dbh->bz_commit_transaction(); - return $search; + my $class = shift; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $search = $class->SUPER::create(@_); + my $user_id = $search->user_id; + + # Enforce there only being SAVE_NUM_SEARCHES per user. + my @ids = @{ + $dbh->selectcol_arrayref( + "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", undef, + $user_id + ) + }; + if (scalar(@ids) > SAVE_NUM_SEARCHES) { + splice(@ids, - SAVE_NUM_SEARCHES); + $dbh->do("DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")"); + } + $dbh->bz_commit_transaction(); + return $search; } sub create_placeholder { - my $class = shift; - return $class->create({ user_id => Bugzilla->user->id, - bug_list => '' }); + my $class = shift; + return $class->create({user_id => Bugzilla->user->id, bug_list => ''}); } ############### @@ -81,41 +83,43 @@ sub create_placeholder { ############### sub check { - my $class = shift; - my $search = $class->SUPER::check(@_); - my $user = Bugzilla->user; - if ($search->user_id != $user->id) { - ThrowUserError('object_does_not_exist', { id => $search->id }); - } - return $search; + my $class = shift; + my $search = $class->SUPER::check(@_); + my $user = Bugzilla->user; + if ($search->user_id != $user->id) { + ThrowUserError('object_does_not_exist', {id => $search->id}); + } + return $search; } sub check_quietly { - my $class = shift; - my $error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $search = eval { $class->check(@_) }; - Bugzilla->error_mode($error_mode); - return $search; + my $class = shift; + my $error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $search = eval { $class->check(@_) }; + Bugzilla->error_mode($error_mode); + return $search; } sub new_from_cookie { - my ($invocant, $bug_ids) = @_; - my $class = ref($invocant) || $invocant; + my ($invocant, $bug_ids) = @_; + my $class = ref($invocant) || $invocant; - my $search = { id => 'cookie', - user_id => Bugzilla->user->id, - bug_list => join(',', @$bug_ids) }; + my $search = { + id => 'cookie', + user_id => Bugzilla->user->id, + bug_list => join(',', @$bug_ids) + }; - bless $search, $class; - return $search; + bless $search, $class; + return $search; } #################### # Simple Accessors # #################### -sub bug_list { return [split(',', $_[0]->{'bug_list'})]; } +sub bug_list { return [split(',', $_[0]->{'bug_list'})]; } sub list_order { return $_[0]->{'list_order'}; } sub user_id { return $_[0]->{'user_id'}; } @@ -131,17 +135,17 @@ sub set_list_order { $_[0]->set('list_order', $_[1]); } ############## sub _check_user_id { - my ($invocant, $id) = @_; - require Bugzilla::User; - return Bugzilla::User->check({ id => $id })->id; + my ($invocant, $id) = @_; + require Bugzilla::User; + return Bugzilla::User->check({id => $id})->id; } sub _check_bug_list { - my ($invocant, $list) = @_; + my ($invocant, $list) = @_; - my @bug_ids = ref($list) ? @$list : split(',', $list || ''); - detaint_natural($_) foreach @bug_ids; - return join(',', @bug_ids); + my @bug_ids = ref($list) ? @$list : split(',', $list || ''); + detaint_natural($_) foreach @bug_ids; + return join(',', @bug_ids); } sub _check_list_order { defined $_[1] ? trim($_[1]) : '' } diff --git a/Bugzilla/Search/Saved.pm b/Bugzilla/Search/Saved.pm index 50a9cdd67..1611cea56 100644 --- a/Bugzilla/Search/Saved.pm +++ b/Bugzilla/Search/Saved.pm @@ -28,22 +28,23 @@ use Scalar::Util qw(blessed); ############# use constant DB_TABLE => 'namedqueries'; + # Do not track buglists saved by users. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - userid - name - query + id + userid + name + query ); use constant VALIDATORS => { - name => \&_check_name, - query => \&_check_query, - link_in_footer => \&_check_link_in_footer, + name => \&_check_name, + query => \&_check_query, + link_in_footer => \&_check_link_in_footer, }; use constant UPDATE_COLUMNS => qw(name query); @@ -53,56 +54,53 @@ use constant UPDATE_COLUMNS => qw(name query); ############### sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $user; - if (ref $param) { - $user = $param->{user} || Bugzilla->user; - my $name = $param->{name}; - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - my $condition = 'userid = ? AND name = ?'; - my $user_id = blessed $user ? $user->id : $user; - detaint_natural($user_id) - || ThrowCodeError('param_must_be_numeric', - {function => $class . '::_init', param => 'user'}); - my @values = ($user_id, $name); - $param = { condition => $condition, values => \@values }; - } - - unshift @_, $param; - my $self = $class->SUPER::new(@_); - if ($self) { - $self->{user} = $user if blessed $user; - - # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8 - # when it's coming out of the database, even though it has no UTF-8 - # characters in it, which prevents Bugzilla::CGI from later reading - # it correctly. - utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query}); + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $user; + if (ref $param) { + $user = $param->{user} || Bugzilla->user; + my $name = $param->{name}; + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - return $self; + my $condition = 'userid = ? AND name = ?'; + my $user_id = blessed $user ? $user->id : $user; + detaint_natural($user_id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::_init', param => 'user'}); + my @values = ($user_id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + my $self = $class->SUPER::new(@_); + if ($self) { + $self->{user} = $user if blessed $user; + + # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8 + # when it's coming out of the database, even though it has no UTF-8 + # characters in it, which prevents Bugzilla::CGI from later reading + # it correctly. + utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query}); + } + return $self; } sub check { - my $class = shift; - my $search = $class->SUPER::check(@_); - my $user = Bugzilla->user; - return $search if $search->user->id == $user->id; - - if (!$search->shared_with_group - or !$user->in_group($search->shared_with_group)) - { - ThrowUserError('missing_query', { name => $search->name, - sharer_id => $search->user->id }); - } - - return $search; + my $class = shift; + my $search = $class->SUPER::check(@_); + my $user = Bugzilla->user; + return $search if $search->user->id == $user->id; + + if (!$search->shared_with_group or !$user->in_group($search->shared_with_group)) + { + ThrowUserError('missing_query', + {name => $search->name, sharer_id => $search->user->id}); + } + + return $search; } ############## @@ -112,24 +110,25 @@ sub check { sub _check_link_in_footer { return $_[1] ? 1 : 0; } sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError("query_name_missing"); - $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); - if (length($name) > MAX_LEN_QUERY_NAME) { - ThrowUserError("query_name_too_long"); - } - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError("query_name_missing"); + $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); + if (length($name) > MAX_LEN_QUERY_NAME) { + ThrowUserError("query_name_too_long"); + } + return $name; } sub _check_query { - my ($invocant, $query) = @_; - $query || ThrowUserError("buglist_parameters_required"); - my $cgi = new Bugzilla::CGI($query); - $cgi->clean_search_url; - # Don't store the query name as a parameter. - $cgi->delete('known_name'); - return $cgi->query_string; + my ($invocant, $query) = @_; + $query || ThrowUserError("buglist_parameters_required"); + my $cgi = new Bugzilla::CGI($query); + $cgi->clean_search_url; + + # Don't store the query name as a parameter. + $cgi->delete('known_name'); + return $cgi->query_string; } ######################### @@ -137,170 +136,180 @@ sub _check_query { ######################### sub create { - my $class = shift; - Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; - $class->check_required_create_fields(@_); - $dbh->bz_start_transaction(); - my $params = $class->run_create_validators(@_); - - # Right now you can only create a Saved Search for the current user. - $params->{userid} = Bugzilla->user->id; - - my $lif = delete $params->{link_in_footer}; - my $obj = $class->insert_create_data($params); - if ($lif) { - $dbh->do('INSERT INTO namedqueries_link_in_footer - (user_id, namedquery_id) VALUES (?,?)', - undef, $params->{userid}, $obj->id); - } - $dbh->bz_commit_transaction(); - - return $obj; + my $class = shift; + Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + $class->check_required_create_fields(@_); + $dbh->bz_start_transaction(); + my $params = $class->run_create_validators(@_); + + # Right now you can only create a Saved Search for the current user. + $params->{userid} = Bugzilla->user->id; + + my $lif = delete $params->{link_in_footer}; + my $obj = $class->insert_create_data($params); + if ($lif) { + $dbh->do( + 'INSERT INTO namedqueries_link_in_footer + (user_id, namedquery_id) VALUES (?,?)', undef, $params->{userid}, + $obj->id + ); + } + $dbh->bz_commit_transaction(); + + return $obj; } sub rename_field_value { - my ($class, $field, $old_value, $new_value) = @_; - - my $old = url_quote($old_value); - my $new = url_quote($new_value); - my $old_sql = $old; - $old_sql =~ s/([_\%])/\\$1/g; - - my $table = $class->DB_TABLE; - my $id_field = $class->ID_FIELD; - - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - - my %queries = @{ $dbh->selectcol_arrayref( - "SELECT $id_field, query FROM $table WHERE query LIKE ?", - {Columns=>[1,2]}, "\%$old_sql\%") }; - foreach my $id (keys %queries) { - my $query = $queries{$id}; - $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi; - # Fix boolean charts. - while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) { - my $chart_id = $1; - # Note that this won't handle lists or substrings inside of - # boolean charts. Users will have to fix those themselves. - $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i; - } - $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", - undef, $query, $id); - Bugzilla->memcached->clear({ table => $table, id => $id }); + my ($class, $field, $old_value, $new_value) = @_; + + my $old = url_quote($old_value); + my $new = url_quote($new_value); + my $old_sql = $old; + $old_sql =~ s/([_\%])/\\$1/g; + + my $table = $class->DB_TABLE; + my $id_field = $class->ID_FIELD; + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + + my %queries = @{ + $dbh->selectcol_arrayref( + "SELECT $id_field, query FROM $table WHERE query LIKE ?", + {Columns => [1, 2]}, + "\%$old_sql\%" + ) + }; + foreach my $id (keys %queries) { + my $query = $queries{$id}; + $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi; + + # Fix boolean charts. + while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) { + my $chart_id = $1; + + # Note that this won't handle lists or substrings inside of + # boolean charts. Users will have to fix those themselves. + $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i; } + $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", undef, $query, $id); + Bugzilla->memcached->clear({table => $table, id => $id}); + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub preload { - my ($searches) = @_; - my $dbh = Bugzilla->dbh; + my ($searches) = @_; + my $dbh = Bugzilla->dbh; - return unless scalar @$searches; + return unless scalar @$searches; - my @query_ids = map { $_->id } @$searches; - my $queries_in_footer = $dbh->selectcol_arrayref( - 'SELECT namedquery_id + my @query_ids = map { $_->id } @$searches; + my $queries_in_footer = $dbh->selectcol_arrayref( + 'SELECT namedquery_id FROM namedqueries_link_in_footer WHERE ' . $dbh->sql_in('namedquery_id', \@query_ids) . ' AND user_id = ?', - undef, Bugzilla->user->id); + undef, Bugzilla->user->id + ); - my %links_in_footer = map { $_ => 1 } @$queries_in_footer; - foreach my $query (@$searches) { - $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0; - } + my %links_in_footer = map { $_ => 1 } @$queries_in_footer; + foreach my $query (@$searches) { + $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0; + } } ##################### # Complex Accessors # ##################### sub edit_link { - my ($self) = @_; - return $self->{edit_link} if defined $self->{edit_link}; - my $cgi = new Bugzilla::CGI($self->url); - if (!$cgi->param('query_type') - || !IsValidQueryType($cgi->param('query_type'))) - { - $cgi->param('query_type', 'advanced'); - } - $self->{edit_link} = $cgi->canonicalise_query; - return $self->{edit_link}; + my ($self) = @_; + return $self->{edit_link} if defined $self->{edit_link}; + my $cgi = new Bugzilla::CGI($self->url); + if (!$cgi->param('query_type') || !IsValidQueryType($cgi->param('query_type'))) + { + $cgi->param('query_type', 'advanced'); + } + $self->{edit_link} = $cgi->canonicalise_query; + return $self->{edit_link}; } sub used_in_whine { - my ($self) = @_; - return $self->{used_in_whine} if exists $self->{used_in_whine}; - ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array( - 'SELECT 1 FROM whine_events INNER JOIN whine_queries + my ($self) = @_; + return $self->{used_in_whine} if exists $self->{used_in_whine}; + ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array( + 'SELECT 1 FROM whine_events INNER JOIN whine_queries ON whine_events.id = whine_queries.eventid - WHERE whine_events.owner_userid = ? AND query_name = ?', undef, - $self->{userid}, $self->name) || 0; - return $self->{used_in_whine}; + WHERE whine_events.owner_userid = ? AND query_name = ?', undef, + $self->{userid}, $self->name + ) || 0; + return $self->{used_in_whine}; } sub link_in_footer { - my ($self, $user) = @_; - # We only cache link_in_footer for the current Bugzilla->user. - return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user; - my $user_id = $user ? $user->id : Bugzilla->user->id; - my $link_in_footer = Bugzilla->dbh->selectrow_array( - 'SELECT 1 FROM namedqueries_link_in_footer - WHERE namedquery_id = ? AND user_id = ?', - undef, $self->id, $user_id) || 0; - $self->{link_in_footer} = $link_in_footer if !$user; - return $link_in_footer; + my ($self, $user) = @_; + + # We only cache link_in_footer for the current Bugzilla->user. + return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user; + my $user_id = $user ? $user->id : Bugzilla->user->id; + my $link_in_footer = Bugzilla->dbh->selectrow_array( + 'SELECT 1 FROM namedqueries_link_in_footer + WHERE namedquery_id = ? AND user_id = ?', undef, $self->id, $user_id + ) || 0; + $self->{link_in_footer} = $link_in_footer if !$user; + return $link_in_footer; } sub shared_with_group { - my ($self) = @_; - return $self->{shared_with_group} if exists $self->{shared_with_group}; - # Bugzilla only currently supports sharing with one group, even - # though the database backend allows for an infinite number. - my ($group_id) = Bugzilla->dbh->selectrow_array( - 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?', - undef, $self->id); - $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) - : undef; - return $self->{shared_with_group}; + my ($self) = @_; + return $self->{shared_with_group} if exists $self->{shared_with_group}; + + # Bugzilla only currently supports sharing with one group, even + # though the database backend allows for an infinite number. + my ($group_id) + = Bugzilla->dbh->selectrow_array( + 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?', + undef, $self->id); + $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) : undef; + return $self->{shared_with_group}; } sub shared_with_users { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!exists $self->{shared_with_users}) { - $self->{shared_with_users} = - $dbh->selectrow_array('SELECT COUNT(*) + if (!exists $self->{shared_with_users}) { + $self->{shared_with_users} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM namedqueries_link_in_footer INNER JOIN namedqueries ON namedquery_id = id WHERE namedquery_id = ? - AND user_id != userid', - undef, $self->id); - } - return $self->{shared_with_users}; + AND user_id != userid', undef, $self->id + ); + } + return $self->{shared_with_users}; } #################### # Simple Accessors # #################### -sub url { return $_[0]->{'query'}; } +sub url { return $_[0]->{'query'}; } sub user { - my ($self) = @_; - return $self->{user} ||= - Bugzilla::User->new({ id => $self->{userid}, cache => 1 }); + my ($self) = @_; + return $self->{user} + ||= Bugzilla::User->new({id => $self->{userid}, cache => 1}); } ############ # Mutators # ############ -sub set_name { $_[0]->set('name', $_[1]); } -sub set_url { $_[0]->set('query', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_url { $_[0]->set('query', $_[1]); } 1; diff --git a/Bugzilla/Sender/Transport/Sendmail.pm b/Bugzilla/Sender/Transport/Sendmail.pm index 49f00777f..d2be2cded 100644 --- a/Bugzilla/Sender/Transport/Sendmail.pm +++ b/Bugzilla/Sender/Transport/Sendmail.pm @@ -16,72 +16,93 @@ use parent qw(Email::Sender::Transport::Sendmail); use Email::Sender::Failure; sub send_email { - my ($self, $email, $envelope) = @_; - - my $pipe = $self->_sendmail_pipe($envelope); - - my $string = $email->as_string; - $string =~ s/\x0D\x0A/\x0A/g unless $^O eq 'MSWin32'; - - print $pipe $string - or Email::Sender::Failure->throw("couldn't send message to sendmail: $!"); - - unless (close $pipe) { - Email::Sender::Failure->throw("error when closing pipe to sendmail: $!") if $!; - my ($error_message, $is_transient) = _map_exitcode($? >> 8); - if (Bugzilla->params->{'use_mailer_queue'}) { - # Return success for errors which are fatal so Bugzilla knows to - # remove them from the queue. - if ($is_transient) { - Email::Sender::Failure->throw("error when closing pipe to sendmail: $error_message"); - } else { - warn "error when closing pipe to sendmail: $error_message\n"; - return $self->success; - } - } else { - Email::Sender::Failure->throw("error when closing pipe to sendmail: $error_message"); - } + my ($self, $email, $envelope) = @_; + + my $pipe = $self->_sendmail_pipe($envelope); + + my $string = $email->as_string; + $string =~ s/\x0D\x0A/\x0A/g unless $^O eq 'MSWin32'; + + print $pipe $string + or Email::Sender::Failure->throw("couldn't send message to sendmail: $!"); + + unless (close $pipe) { + Email::Sender::Failure->throw("error when closing pipe to sendmail: $!") if $!; + my ($error_message, $is_transient) = _map_exitcode($? >> 8); + if (Bugzilla->params->{'use_mailer_queue'}) { + + # Return success for errors which are fatal so Bugzilla knows to + # remove them from the queue. + if ($is_transient) { + Email::Sender::Failure->throw( + "error when closing pipe to sendmail: $error_message"); + } + else { + warn "error when closing pipe to sendmail: $error_message\n"; + return $self->success; + } + } + else { + Email::Sender::Failure->throw( + "error when closing pipe to sendmail: $error_message"); } - return $self->success; + } + return $self->success; } sub _map_exitcode { - # Returns (error message, is_transient) - # from the sendmail source (sendmail/sysexits.h) - my $code = shift; - if ($code == 64) { - return ("Command line usage error (EX_USAGE)", 1); - } elsif ($code == 65) { - return ("Data format error (EX_DATAERR)", 1); - } elsif ($code == 66) { - return ("Cannot open input (EX_NOINPUT)", 1); - } elsif ($code == 67) { - return ("Addressee unknown (EX_NOUSER)", 0); - } elsif ($code == 68) { - return ("Host name unknown (EX_NOHOST)", 0); - } elsif ($code == 69) { - return ("Service unavailable (EX_UNAVAILABLE)", 1); - } elsif ($code == 70) { - return ("Internal software error (EX_SOFTWARE)", 1); - } elsif ($code == 71) { - return ("System error (EX_OSERR)", 1); - } elsif ($code == 72) { - return ("Critical OS file missing (EX_OSFILE)", 1); - } elsif ($code == 73) { - return ("Can't create output file (EX_CANTCREAT)", 1); - } elsif ($code == 74) { - return ("Input/output error (EX_IOERR)", 1); - } elsif ($code == 75) { - return ("Temp failure (EX_TEMPFAIL)", 1); - } elsif ($code == 76) { - return ("Remote error in protocol (EX_PROTOCOL)", 1); - } elsif ($code == 77) { - return ("Permission denied (EX_NOPERM)", 1); - } elsif ($code == 78) { - return ("Configuration error (EX_CONFIG)", 1); - } else { - return ("Unknown Error ($code)", 1); - } + + # Returns (error message, is_transient) + # from the sendmail source (sendmail/sysexits.h) + my $code = shift; + if ($code == 64) { + return ("Command line usage error (EX_USAGE)", 1); + } + elsif ($code == 65) { + return ("Data format error (EX_DATAERR)", 1); + } + elsif ($code == 66) { + return ("Cannot open input (EX_NOINPUT)", 1); + } + elsif ($code == 67) { + return ("Addressee unknown (EX_NOUSER)", 0); + } + elsif ($code == 68) { + return ("Host name unknown (EX_NOHOST)", 0); + } + elsif ($code == 69) { + return ("Service unavailable (EX_UNAVAILABLE)", 1); + } + elsif ($code == 70) { + return ("Internal software error (EX_SOFTWARE)", 1); + } + elsif ($code == 71) { + return ("System error (EX_OSERR)", 1); + } + elsif ($code == 72) { + return ("Critical OS file missing (EX_OSFILE)", 1); + } + elsif ($code == 73) { + return ("Can't create output file (EX_CANTCREAT)", 1); + } + elsif ($code == 74) { + return ("Input/output error (EX_IOERR)", 1); + } + elsif ($code == 75) { + return ("Temp failure (EX_TEMPFAIL)", 1); + } + elsif ($code == 76) { + return ("Remote error in protocol (EX_PROTOCOL)", 1); + } + elsif ($code == 77) { + return ("Permission denied (EX_NOPERM)", 1); + } + elsif ($code == 78) { + return ("Configuration error (EX_CONFIG)", 1); + } + else { + return ("Unknown Error ($code)", 1); + } } 1; diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm index 22202c6f1..36b6d13ad 100644 --- a/Bugzilla/Series.pm +++ b/Bugzilla/Series.pm @@ -7,9 +7,9 @@ # This module implements a series - a set of data to be plotted on a chart. # -# This Series is in the database if and only if self->{'series_id'} is defined. -# Note that the series being in the database does not mean that the fields of -# this object are the same as the DB entries, as the object may have been +# This Series is in the database if and only if self->{'series_id'} is defined. +# Note that the series being in the database does not mean that the fields of +# this object are the same as the DB entries, as the object may have been # altered. package Bugzilla::Series; @@ -27,224 +27,255 @@ use constant DB_TABLE => 'series'; use constant ID_FIELD => 'series_id'; sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - - # Create a ref to an empty hash and bless it - my $self = {}; - bless($self, $class); - - my $arg_count = scalar(@_); - - # new() can return undef if you pass in a series_id and the user doesn't - # have sufficient permissions. If you create a new series in this way, - # you need to check for an undef return, and act appropriately. - my $retval = $self; - - # There are three ways of creating Series objects. Two (CGI and Parameters) - # are for use when creating a new series. One (Database) is for retrieving - # information on existing series. - if ($arg_count == 1) { - if (ref($_[0])) { - # We've been given a CGI object to create a new Series from. - # This series may already exist - external code needs to check - # before it calls writeToDatabase(). - $self->initFromCGI($_[0]); - } - else { - # We've been given a series_id, which should represent an existing - # Series. - $retval = $self->initFromDatabase($_[0]); - } - } - elsif ($arg_count >= 6 && $arg_count <= 8) { - # We've been given a load of parameters to create a new Series from. - # Currently, undef is always passed as the first parameter; this allows - # you to call writeToDatabase() unconditionally. - # XXX - You cannot set category_id and subcategory_id from here. - $self->initFromParameters(@_); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + # Create a ref to an empty hash and bless it + my $self = {}; + bless($self, $class); + + my $arg_count = scalar(@_); + + # new() can return undef if you pass in a series_id and the user doesn't + # have sufficient permissions. If you create a new series in this way, + # you need to check for an undef return, and act appropriately. + my $retval = $self; + + # There are three ways of creating Series objects. Two (CGI and Parameters) + # are for use when creating a new series. One (Database) is for retrieving + # information on existing series. + if ($arg_count == 1) { + if (ref($_[0])) { + + # We've been given a CGI object to create a new Series from. + # This series may already exist - external code needs to check + # before it calls writeToDatabase(). + $self->initFromCGI($_[0]); } else { - die("Bad parameters passed in - invalid number of args: $arg_count"); + # We've been given a series_id, which should represent an existing + # Series. + $retval = $self->initFromDatabase($_[0]); } - - return $retval; + } + elsif ($arg_count >= 6 && $arg_count <= 8) { + + # We've been given a load of parameters to create a new Series from. + # Currently, undef is always passed as the first parameter; this allows + # you to call writeToDatabase() unconditionally. + # XXX - You cannot set category_id and subcategory_id from here. + $self->initFromParameters(@_); + } + else { + die("Bad parameters passed in - invalid number of args: $arg_count"); + } + + return $retval; } sub initFromDatabase { - my ($self, $series_id) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - detaint_natural($series_id) - || ThrowCodeError("invalid_series_id", { 'series_id' => $series_id }); - - my $grouplist = $user->groups_as_string; - - my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " . - "cc2.name, series.name, series.creator, series.frequency, " . - "series.query, series.is_public, series.category, series.subcategory " . - "FROM series " . - "INNER JOIN series_categories AS cc1 " . - " ON series.category = cc1.id " . - "INNER JOIN series_categories AS cc2 " . - " ON series.subcategory = cc2.id " . - "LEFT JOIN category_group_map AS cgm " . - " ON series.category = cgm.category_id " . - " AND cgm.group_id NOT IN($grouplist) " . - "WHERE series.series_id = ? " . - " AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))", - undef, ($series_id, $user->id)); - - if (@series) { - $self->initFromParameters(@series); - return $self; - } - else { - return undef; - } + my ($self, $series_id) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + detaint_natural($series_id) + || ThrowCodeError("invalid_series_id", {'series_id' => $series_id}); + + my $grouplist = $user->groups_as_string; + + my @series = $dbh->selectrow_array( + "SELECT series.series_id, cc1.name, " + . "cc2.name, series.name, series.creator, series.frequency, " + . "series.query, series.is_public, series.category, series.subcategory " + . "FROM series " + . "INNER JOIN series_categories AS cc1 " + . " ON series.category = cc1.id " + . "INNER JOIN series_categories AS cc2 " + . " ON series.subcategory = cc2.id " + . "LEFT JOIN category_group_map AS cgm " + . " ON series.category = cgm.category_id " + . " AND cgm.group_id NOT IN($grouplist) " + . "WHERE series.series_id = ? " + . " AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))", + undef, + ($series_id, $user->id) + ); + + if (@series) { + $self->initFromParameters(@series); + return $self; + } + else { + return undef; + } } sub initFromParameters { - # Pass undef as the first parameter if you are creating a new series. - my $self = shift; - ($self->{'series_id'}, $self->{'category'}, $self->{'subcategory'}, - $self->{'name'}, $self->{'creator_id'}, $self->{'frequency'}, - $self->{'query'}, $self->{'public'}, $self->{'category_id'}, - $self->{'subcategory_id'}) = @_; + # Pass undef as the first parameter if you are creating a new series. + my $self = shift; + + ( + $self->{'series_id'}, $self->{'category'}, $self->{'subcategory'}, + $self->{'name'}, $self->{'creator_id'}, $self->{'frequency'}, + $self->{'query'}, $self->{'public'}, $self->{'category_id'}, + $self->{'subcategory_id'} + ) = @_; - # If the first parameter is undefined, check if this series already - # exists and update it series_id accordingly - $self->{'series_id'} ||= $self->existsInDatabase(); + # If the first parameter is undefined, check if this series already + # exists and update it series_id accordingly + $self->{'series_id'} ||= $self->existsInDatabase(); } sub initFromCGI { - my $self = shift; - my $cgi = shift; - - $self->{'series_id'} = $cgi->param('series_id') || undef; - if (defined($self->{'series_id'})) { - detaint_natural($self->{'series_id'}) - || ThrowCodeError("invalid_series_id", - { 'series_id' => $self->{'series_id'} }); - } - - $self->{'category'} = $cgi->param('category') - || $cgi->param('newcategory') - || ThrowUserError("missing_category"); - - $self->{'subcategory'} = $cgi->param('subcategory') - || $cgi->param('newsubcategory') - || ThrowUserError("missing_subcategory"); - - $self->{'name'} = $cgi->param('name') - || ThrowUserError("missing_name"); - - $self->{'creator_id'} = Bugzilla->user->id; - - $self->{'frequency'} = $cgi->param('frequency'); - detaint_natural($self->{'frequency'}) - || ThrowUserError("missing_frequency"); - - $self->{'query'} = $cgi->canonicalise_query("format", "ctype", "action", - "category", "subcategory", "name", - "frequency", "public", "query_format"); - trick_taint($self->{'query'}); - - $self->{'public'} = $cgi->param('public') ? 1 : 0; - - # Change 'admin' here and in series.html.tmpl, or remove the check - # completely, if you want to change who can make series public. - $self->{'public'} = 0 unless Bugzilla->user->in_group('admin'); + my $self = shift; + my $cgi = shift; + + $self->{'series_id'} = $cgi->param('series_id') || undef; + if (defined($self->{'series_id'})) { + detaint_natural($self->{'series_id'}) + || ThrowCodeError("invalid_series_id", {'series_id' => $self->{'series_id'}}); + } + + $self->{'category'} + = $cgi->param('category') + || $cgi->param('newcategory') + || ThrowUserError("missing_category"); + + $self->{'subcategory'} + = $cgi->param('subcategory') + || $cgi->param('newsubcategory') + || ThrowUserError("missing_subcategory"); + + $self->{'name'} = $cgi->param('name') || ThrowUserError("missing_name"); + + $self->{'creator_id'} = Bugzilla->user->id; + + $self->{'frequency'} = $cgi->param('frequency'); + detaint_natural($self->{'frequency'}) || ThrowUserError("missing_frequency"); + + $self->{'query'} = $cgi->canonicalise_query( + "format", "ctype", "action", "category", + "subcategory", "name", "frequency", "public", + "query_format" + ); + trick_taint($self->{'query'}); + + $self->{'public'} = $cgi->param('public') ? 1 : 0; + + # Change 'admin' here and in series.html.tmpl, or remove the check + # completely, if you want to change who can make series public. + $self->{'public'} = 0 unless Bugzilla->user->in_group('admin'); } sub writeToDatabase { - my $self = shift; + my $self = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); - my $category_id = getCategoryID($self->{'category'}); - my $subcategory_id = getCategoryID($self->{'subcategory'}); + my $category_id = getCategoryID($self->{'category'}); + my $subcategory_id = getCategoryID($self->{'subcategory'}); - my $exists; - if ($self->{'series_id'}) { - $exists = - $dbh->selectrow_array("SELECT series_id FROM series - WHERE series_id = $self->{'series_id'}"); - } - - # Is this already in the database? - if ($exists) { - # Update existing series - my $dbh = Bugzilla->dbh; - $dbh->do("UPDATE series SET " . - "category = ?, subcategory = ?," . - "name = ?, frequency = ?, is_public = ? " . - "WHERE series_id = ?", undef, - $category_id, $subcategory_id, $self->{'name'}, - $self->{'frequency'}, $self->{'public'}, - $self->{'series_id'}); - } - else { - # Insert the new series into the series table - $dbh->do("INSERT INTO series (creator, category, subcategory, " . - "name, frequency, query, is_public) VALUES " . - "(?, ?, ?, ?, ?, ?, ?)", undef, - $self->{'creator_id'}, $category_id, $subcategory_id, $self->{'name'}, - $self->{'frequency'}, $self->{'query'}, $self->{'public'}); - - # Retrieve series_id - $self->{'series_id'} = $dbh->selectrow_array("SELECT MAX(series_id) " . - "FROM series"); - $self->{'series_id'} - || ThrowCodeError("missing_series_id", { 'series' => $self }); - } - - $dbh->bz_commit_transaction(); + my $exists; + if ($self->{'series_id'}) { + $exists = $dbh->selectrow_array( + "SELECT series_id FROM series + WHERE series_id = $self->{'series_id'}" + ); + } + + # Is this already in the database? + if ($exists) { + + # Update existing series + my $dbh = Bugzilla->dbh; + $dbh->do( + "UPDATE series SET " + . "category = ?, subcategory = ?," + . "name = ?, frequency = ?, is_public = ? " + . "WHERE series_id = ?", + undef, + $category_id, + $subcategory_id, + $self->{'name'}, + $self->{'frequency'}, + $self->{'public'}, + $self->{'series_id'} + ); + } + else { + # Insert the new series into the series table + $dbh->do( + "INSERT INTO series (creator, category, subcategory, " + . "name, frequency, query, is_public) VALUES " + . "(?, ?, ?, ?, ?, ?, ?)", + undef, + $self->{'creator_id'}, + $category_id, + $subcategory_id, + $self->{'name'}, + $self->{'frequency'}, + $self->{'query'}, + $self->{'public'} + ); + + # Retrieve series_id + $self->{'series_id'} + = $dbh->selectrow_array("SELECT MAX(series_id) " . "FROM series"); + $self->{'series_id'} + || ThrowCodeError("missing_series_id", {'series' => $self}); + } + + $dbh->bz_commit_transaction(); } # Check whether a series with this name, category and subcategory exists in # the DB and, if so, returns its series_id. sub existsInDatabase { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; + + my $category_id = getCategoryID($self->{'category'}); + my $subcategory_id = getCategoryID($self->{'subcategory'}); - my $category_id = getCategoryID($self->{'category'}); - my $subcategory_id = getCategoryID($self->{'subcategory'}); - - trick_taint($self->{'name'}); - my $series_id = $dbh->selectrow_array("SELECT series_id " . - "FROM series WHERE category = $category_id " . - "AND subcategory = $subcategory_id AND name = " . - $dbh->quote($self->{'name'})); - - return($series_id); + trick_taint($self->{'name'}); + my $series_id + = $dbh->selectrow_array("SELECT series_id " + . "FROM series WHERE category = $category_id " + . "AND subcategory = $subcategory_id AND name = " + . $dbh->quote($self->{'name'})); + + return ($series_id); } # Get a category or subcategory IDs, creating the category if it doesn't exist. sub getCategoryID { - my ($category) = @_; - my $category_id; - my $dbh = Bugzilla->dbh; + my ($category) = @_; + my $category_id; + my $dbh = Bugzilla->dbh; - # This seems for the best idiom for "Do A. Then maybe do B and A again." - while (1) { - # We are quoting this to put it in the DB, so we can remove taint - trick_taint($category); + # This seems for the best idiom for "Do A. Then maybe do B and A again." + while (1) { - $category_id = $dbh->selectrow_array("SELECT id " . - "from series_categories " . - "WHERE name =" . $dbh->quote($category)); + # We are quoting this to put it in the DB, so we can remove taint + trick_taint($category); - last if defined($category_id); + $category_id + = $dbh->selectrow_array("SELECT id " + . "from series_categories " + . "WHERE name =" + . $dbh->quote($category)); - $dbh->do("INSERT INTO series_categories (name) " . - "VALUES (" . $dbh->quote($category) . ")"); - } + last if defined($category_id); + + $dbh->do("INSERT INTO series_categories (name) " + . "VALUES (" + . $dbh->quote($category) + . ")"); + } - return $category_id; + return $category_id; } ########## @@ -254,20 +285,20 @@ sub id { return $_[0]->{'series_id'}; } sub name { return $_[0]->{'name'}; } sub creator { - my $self = shift; + my $self = shift; - if (!$self->{creator} && $self->{creator_id}) { - require Bugzilla::User; - $self->{creator} = new Bugzilla::User($self->{creator_id}); - } - return $self->{creator}; + if (!$self->{creator} && $self->{creator_id}) { + require Bugzilla::User; + $self->{creator} = new Bugzilla::User($self->{creator_id}); + } + return $self->{creator}; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id); + $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id); } 1; diff --git a/Bugzilla/Status.pm b/Bugzilla/Status.pm index 275510216..615c7b250 100644 --- a/Bugzilla/Status.pm +++ b/Bugzilla/Status.pm @@ -11,17 +11,17 @@ use 5.10.1; use strict; use warnings; -# This subclasses Bugzilla::Field::Choice instead of implementing +# This subclasses Bugzilla::Field::Choice instead of implementing # ChoiceInterface, because a bug status literally is a special type # of Field::Choice, not just an object that happens to have the same # methods. use parent qw(Bugzilla::Field::Choice Exporter); @Bugzilla::Status::EXPORT = qw( - BUG_STATE_OPEN - SPECIAL_STATUS_WORKFLOW_ACTIONS + BUG_STATE_OPEN + SPECIAL_STATUS_WORKFLOW_ACTIONS - is_open_state - closed_bug_statuses + is_open_state + closed_bug_statuses ); use Bugzilla::Error; @@ -31,25 +31,25 @@ use Bugzilla::Error; ################################ use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw( - none - duplicate - change_resolution - clearresolution + none + duplicate + change_resolution + clearresolution ); use constant DB_TABLE => 'bug_status'; # This has all the standard Bugzilla::Field::Choice columns plus "is_open" sub DB_COLUMNS { - return ($_[0]->SUPER::DB_COLUMNS, 'is_open'); + return ($_[0]->SUPER::DB_COLUMNS, 'is_open'); } sub VALIDATORS { - my $invocant = shift; - my $validators = $invocant->SUPER::VALIDATORS; - $validators->{is_open} = \&Bugzilla::Object::check_boolean; - $validators->{value} = \&_check_value; - return $validators; + my $invocant = shift; + my $validators = $invocant->SUPER::VALIDATORS; + $validators->{is_open} = \&Bugzilla::Object::check_boolean; + $validators->{value} = \&_check_value; + return $validators; } ######################### @@ -57,17 +57,17 @@ sub VALIDATORS { ######################### sub create { - my $class = shift; - my $self = $class->SUPER::create(@_); - delete Bugzilla->request_cache->{status_bug_state_open}; - add_missing_bug_status_transitions(); - return $self; + my $class = shift; + my $self = $class->SUPER::create(@_); + delete Bugzilla->request_cache->{status_bug_state_open}; + add_missing_bug_status_transitions(); + return $self; } sub remove_from_db { - my $self = shift; - $self->SUPER::remove_from_db(); - delete Bugzilla->request_cache->{status_bug_state_open}; + my $self = shift; + $self->SUPER::remove_from_db(); + delete Bugzilla->request_cache->{status_bug_state_open}; } ############################### @@ -75,16 +75,16 @@ sub remove_from_db { ############################### sub is_active { return $_[0]->{'isactive'}; } -sub is_open { return $_[0]->{'is_open'}; } +sub is_open { return $_[0]->{'is_open'}; } sub is_static { - my $self = shift; - if ($self->name eq 'UNCONFIRMED' - || $self->name eq Bugzilla->params->{'duplicate_or_move_bug_status'}) - { - return 1; - } - return 0; + my $self = shift; + if ( $self->name eq 'UNCONFIRMED' + || $self->name eq Bugzilla->params->{'duplicate_or_move_bug_status'}) + { + return 1; + } + return 0; } ############## @@ -92,14 +92,14 @@ sub is_static { ############## sub _check_value { - my $invocant = shift; - my $value = $invocant->SUPER::_check_value(@_); - - if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) { - ThrowUserError('fieldvalue_reserved_word', - { field => $invocant->field, value => $value }); - } - return $value; + my $invocant = shift; + my $value = $invocant->SUPER::_check_value(@_); + + if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) { + ThrowUserError('fieldvalue_reserved_word', + {field => $invocant->field, value => $value}); + } + return $value; } @@ -108,118 +108,125 @@ sub _check_value { ############################### sub BUG_STATE_OPEN { - my $dbh = Bugzilla->dbh; - my $request_cache = Bugzilla->request_cache; - my $cache_key = 'status_bug_state_open'; - return @{ $request_cache->{$cache_key} } - if exists $request_cache->{$cache_key}; - - my $rows = Bugzilla->memcached->get_config({ key => $cache_key }); - if (!$rows) { - $rows = $dbh->selectcol_arrayref( - 'SELECT value FROM bug_status WHERE is_open = 1' - ); - Bugzilla->memcached->set_config({ key => $cache_key, data => $rows }); - } - - $request_cache->{$cache_key} = $rows; - return @$rows; + my $dbh = Bugzilla->dbh; + my $request_cache = Bugzilla->request_cache; + my $cache_key = 'status_bug_state_open'; + return @{$request_cache->{$cache_key}} if exists $request_cache->{$cache_key}; + + my $rows = Bugzilla->memcached->get_config({key => $cache_key}); + if (!$rows) { + $rows + = $dbh->selectcol_arrayref('SELECT value FROM bug_status WHERE is_open = 1'); + Bugzilla->memcached->set_config({key => $cache_key, data => $rows}); + } + + $request_cache->{$cache_key} = $rows; + return @$rows; } # Tells you whether or not the argument is a valid "open" state. sub is_open_state { - my ($state) = @_; - return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0); + my ($state) = @_; + return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0); } sub closed_bug_statuses { - my @bug_statuses = Bugzilla::Status->get_all; - @bug_statuses = grep { !$_->is_open } @bug_statuses; - return @bug_statuses; + my @bug_statuses = Bugzilla::Status->get_all; + @bug_statuses = grep { !$_->is_open } @bug_statuses; + return @bug_statuses; } sub can_change_to { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!ref($self) || !defined $self->{'can_change_to'}) { - my ($cond, @args, $self_exists); - if (ref($self)) { - $cond = '= ?'; - push(@args, $self->id); - $self_exists = 1; - } - else { - $cond = 'IS NULL'; - # Let's do it so that the code below works in all cases. - $self = {}; - } - - my $new_status_ids = $dbh->selectcol_arrayref("SELECT new_status + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!ref($self) || !defined $self->{'can_change_to'}) { + my ($cond, @args, $self_exists); + if (ref($self)) { + $cond = '= ?'; + push(@args, $self->id); + $self_exists = 1; + } + else { + $cond = 'IS NULL'; + + # Let's do it so that the code below works in all cases. + $self = {}; + } + + my $new_status_ids = $dbh->selectcol_arrayref( + "SELECT new_status FROM status_workflow INNER JOIN bug_status ON id = new_status WHERE isactive = 1 AND old_status $cond - ORDER BY sortkey", - undef, @args); + ORDER BY sortkey", undef, @args + ); - # Allow the bug status to remain unchanged. - push(@$new_status_ids, $self->id) if $self_exists; - $self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids); - } + # Allow the bug status to remain unchanged. + push(@$new_status_ids, $self->id) if $self_exists; + $self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids); + } - return $self->{'can_change_to'}; + return $self->{'can_change_to'}; } sub comment_required_on_change_from { - my ($self, $old_status) = @_; - my ($cond, $values) = $self->_status_condition($old_status); - - my ($require_comment) = Bugzilla->dbh->selectrow_array( - "SELECT require_comment FROM status_workflow - WHERE $cond", undef, @$values); - return $require_comment; + my ($self, $old_status) = @_; + my ($cond, $values) = $self->_status_condition($old_status); + + my ($require_comment) = Bugzilla->dbh->selectrow_array( + "SELECT require_comment FROM status_workflow + WHERE $cond", undef, @$values + ); + return $require_comment; } # Used as a helper for various functions that have to deal with old_status # sometimes being NULL and sometimes having a value. sub _status_condition { - my ($self, $old_status) = @_; - my @values; - my $cond = 'old_status IS NULL'; - # We may pass a fake status object to represent the initial unset state. - if ($old_status && $old_status->id) { - $cond = 'old_status = ?'; - push(@values, $old_status->id); - } - $cond .= " AND new_status = ?"; - push(@values, $self->id); - return ($cond, \@values); + my ($self, $old_status) = @_; + my @values; + my $cond = 'old_status IS NULL'; + + # We may pass a fake status object to represent the initial unset state. + if ($old_status && $old_status->id) { + $cond = 'old_status = ?'; + push(@values, $old_status->id); + } + $cond .= " AND new_status = ?"; + push(@values, $self->id); + return ($cond, \@values); } sub add_missing_bug_status_transitions { - my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'}; - my $dbh = Bugzilla->dbh; - my $new_status = new Bugzilla::Status({name => $bug_status}); - # Silently discard invalid bug statuses. - $new_status || return; + my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'}; + my $dbh = Bugzilla->dbh; + my $new_status = new Bugzilla::Status({name => $bug_status}); + + # Silently discard invalid bug statuses. + $new_status || return; - my $missing_statuses = $dbh->selectcol_arrayref('SELECT id + my $missing_statuses = $dbh->selectcol_arrayref( + 'SELECT id FROM bug_status LEFT JOIN status_workflow ON old_status = id AND new_status = ? WHERE old_status IS NULL', - undef, $new_status->id); - - my $sth = $dbh->prepare('INSERT INTO status_workflow - (old_status, new_status) VALUES (?, ?)'); - - foreach my $old_status_id (@$missing_statuses) { - next if ($old_status_id == $new_status->id); - $sth->execute($old_status_id, $new_status->id); - } + undef, $new_status->id + ); + + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow + (old_status, new_status) VALUES (?, ?)' + ); + + foreach my $old_status_id (@$missing_statuses) { + next if ($old_status_id == $new_status->id); + $sth->execute($old_status_id, $new_status->id); + } } 1; diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 48270b129..cee7d9363 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -16,8 +16,8 @@ use Bugzilla::Constants; use Bugzilla::WebService::Constants; use Bugzilla::Hook; use Bugzilla::Install::Requirements; -use Bugzilla::Install::Util qw(install_string template_include_path - include_languages); +use Bugzilla::Install::Util qw(install_string template_include_path + include_languages); use Bugzilla::Classification; use Bugzilla::Keyword; use Bugzilla::Util; @@ -40,47 +40,49 @@ use Scalar::Util qw(blessed); use parent qw(Template); use constant FORMAT_TRIPLE => '%19s|%-28s|%-28s'; -use constant FORMAT_3_SIZE => [19,28,28]; +use constant FORMAT_3_SIZE => [19, 28, 28]; use constant FORMAT_DOUBLE => '%19s %-55s'; -use constant FORMAT_2_SIZE => [19,55]; +use constant FORMAT_2_SIZE => [19, 55]; # Pseudo-constant. sub SAFE_URL_REGEXP { - my $safe_protocols = join('|', SAFE_PROTOCOLS); - return qr/($safe_protocols):[^:\s<>\"][^\s<>\"]+[\w\/]/i; + my $safe_protocols = join('|', SAFE_PROTOCOLS); + return qr/($safe_protocols):[^:\s<>\"][^\s<>\"]+[\w\/]/i; } # Convert the constants in the Bugzilla::Constants and Bugzilla::WebService::Constants -# modules into a hash we can pass to the template object for reflection into its "constants" +# modules into a hash we can pass to the template object for reflection into its "constants" # namespace (which is like its "variables" namespace, but for constants). To do so, we # traverse the arrays of exported and exportable symbols and ignoring the rest # (which, if Constants.pm exports only constants, as it should, will be nothing else). sub _load_constants { - my %constants; - foreach my $constant (@Bugzilla::Constants::EXPORT, - @Bugzilla::Constants::EXPORT_OK) - { - if (ref Bugzilla::Constants->$constant) { - $constants{$constant} = Bugzilla::Constants->$constant; - } - else { - my @list = (Bugzilla::Constants->$constant); - $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; - } + my %constants; + foreach + my $constant (@Bugzilla::Constants::EXPORT, @Bugzilla::Constants::EXPORT_OK) + { + if (ref Bugzilla::Constants->$constant) { + $constants{$constant} = Bugzilla::Constants->$constant; } - - foreach my $constant (@Bugzilla::WebService::Constants::EXPORT, - @Bugzilla::WebService::Constants::EXPORT_OK) - { - if (ref Bugzilla::WebService::Constants->$constant) { - $constants{$constant} = Bugzilla::WebService::Constants->$constant; - } - else { - my @list = (Bugzilla::WebService::Constants->$constant); - $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; - } + else { + my @list = (Bugzilla::Constants->$constant); + $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; + } + } + + foreach my $constant ( + @Bugzilla::WebService::Constants::EXPORT, + @Bugzilla::WebService::Constants::EXPORT_OK + ) + { + if (ref Bugzilla::WebService::Constants->$constant) { + $constants{$constant} = Bugzilla::WebService::Constants->$constant; } - return \%constants; + else { + my @list = (Bugzilla::WebService::Constants->$constant); + $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; + } + } + return \%constants; } # Returns the path to the templates based on the Accept-Language @@ -88,55 +90,51 @@ sub _load_constants { # If no Accept-Language is present it uses the defined default # Templates may also be found in the extensions/ tree sub _include_path { - my $lang = shift || ''; - my $cache = Bugzilla->request_cache; - $cache->{"template_include_path_$lang"} ||= - template_include_path({ language => $lang }); - return $cache->{"template_include_path_$lang"}; + my $lang = shift || ''; + my $cache = Bugzilla->request_cache; + $cache->{"template_include_path_$lang"} + ||= template_include_path({language => $lang}); + return $cache->{"template_include_path_$lang"}; } sub get_format { - my $self = shift; - my ($template, $format, $ctype) = @_; - - $ctype //= 'html'; - $format //= ''; - - # ctype and format can have letters and a hyphen only. - if ($ctype =~ /[^a-zA-Z\-]/ || $format =~ /[^a-zA-Z\-]/) { - ThrowUserError('format_not_found', {'format' => $format, - 'ctype' => $ctype, - 'invalid' => 1}); - } - trick_taint($ctype); - trick_taint($format); - - $template .= ($format ? "-$format" : ""); - $template .= ".$ctype.tmpl"; - - # Now check that the template actually exists. We only want to check - # if the template exists; any other errors (eg parse errors) will - # end up being detected later. - eval { - $self->context->template($template); - }; - # This parsing may seem fragile, but it's OK: - # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html - # Even if it is wrong, any sort of error is going to cause a failure - # eventually, so the only issue would be an incorrect error message - if ($@ && $@->info =~ /: not found$/) { - ThrowUserError('format_not_found', {'format' => $format, - 'ctype' => $ctype}); - } - - # Else, just return the info - return - { - 'template' => $template, - 'format' => $format, - 'extension' => $ctype, - 'ctype' => Bugzilla::Constants::contenttypes->{$ctype} - }; + my $self = shift; + my ($template, $format, $ctype) = @_; + + $ctype //= 'html'; + $format //= ''; + + # ctype and format can have letters and a hyphen only. + if ($ctype =~ /[^a-zA-Z\-]/ || $format =~ /[^a-zA-Z\-]/) { + ThrowUserError('format_not_found', + {'format' => $format, 'ctype' => $ctype, 'invalid' => 1}); + } + trick_taint($ctype); + trick_taint($format); + + $template .= ($format ? "-$format" : ""); + $template .= ".$ctype.tmpl"; + + # Now check that the template actually exists. We only want to check + # if the template exists; any other errors (eg parse errors) will + # end up being detected later. + eval { $self->context->template($template); }; + + # This parsing may seem fragile, but it's OK: + # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html + # Even if it is wrong, any sort of error is going to cause a failure + # eventually, so the only issue would be an incorrect error message + if ($@ && $@->info =~ /: not found$/) { + ThrowUserError('format_not_found', {'format' => $format, 'ctype' => $ctype}); + } + + # Else, just return the info + return { + 'template' => $template, + 'format' => $format, + 'extension' => $ctype, + 'ctype' => Bugzilla::Constants::contenttypes->{$ctype} + }; } # This routine quoteUrls contains inspirations from the HTML::FromText CPAN @@ -147,203 +145,216 @@ sub get_format { # If you want to modify this routine, read the comments carefully sub quoteUrls { - my ($text, $bug, $comment, $user) = @_; - return $text unless $text; - $user ||= Bugzilla->user; - - # We use /g for speed, but uris can have other things inside them - # (http://foo/bug#3 for example). Filtering that out filters valid - # bug refs out, so we have to do replacements. - # mailto can't contain space or #, so we don't have to bother for that - # Do this by replacing matches with \x{FDD2}$count\x{FDD3} - # \x{FDDx} is used because it's unlikely to occur in the text - # and are reserved unicode characters. We disable warnings for now - # until we require Perl 5.13.9 or newer. - no warnings 'utf8'; - - # If the comment is already wrapped, we should ignore newlines when - # looking for matching regexps. Else we should take them into account. - my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/; - - # However, note that adding the title (for buglinks) can affect things - # In particular, attachment matches go before bug titles, so that titles - # with 'attachment 1' don't double match. - # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur - # if it was substituted as a bug title (since that always involve leading - # and trailing text) - - # Because of entities, it's easier (and quicker) to do this before escaping - - my @things; - my $count = 0; - my $tmp; - - my @hook_regexes; - Bugzilla::Hook::process('bug_format_comment', - { text => \$text, bug => $bug, regexes => \@hook_regexes, - comment => $comment, user => $user }); - - foreach my $re (@hook_regexes) { - my ($match, $replace) = @$re{qw(match replace)}; - if (ref($replace) eq 'CODE') { - $text =~ s/$match/($things[$count++] = $replace->({matches => [ + my ($text, $bug, $comment, $user) = @_; + return $text unless $text; + $user ||= Bugzilla->user; + + # We use /g for speed, but uris can have other things inside them + # (http://foo/bug#3 for example). Filtering that out filters valid + # bug refs out, so we have to do replacements. + # mailto can't contain space or #, so we don't have to bother for that + # Do this by replacing matches with \x{FDD2}$count\x{FDD3} + # \x{FDDx} is used because it's unlikely to occur in the text + # and are reserved unicode characters. We disable warnings for now + # until we require Perl 5.13.9 or newer. + no warnings 'utf8'; + + # If the comment is already wrapped, we should ignore newlines when + # looking for matching regexps. Else we should take them into account. + my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/; + + # However, note that adding the title (for buglinks) can affect things + # In particular, attachment matches go before bug titles, so that titles + # with 'attachment 1' don't double match. + # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur + # if it was substituted as a bug title (since that always involve leading + # and trailing text) + + # Because of entities, it's easier (and quicker) to do this before escaping + + my @things; + my $count = 0; + my $tmp; + + my @hook_regexes; + Bugzilla::Hook::process( + 'bug_format_comment', + { + text => \$text, + bug => $bug, + regexes => \@hook_regexes, + comment => $comment, + user => $user + } + ); + + foreach my $re (@hook_regexes) { + my ($match, $replace) = @$re{qw(match replace)}; + if (ref($replace) eq 'CODE') { + $text =~ s/$match/($things[$count++] = $replace->({matches => [ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10]})) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx; - } - else { - $text =~ s/$match/($things[$count++] = $replace) + } + else { + $text =~ s/$match/($things[$count++] = $replace) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx; - } } - - # Provide tooltips for full bug links (Bug 74355) - my $urlbase_re = '(' . join('|', - map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'}, - Bugzilla->params->{'sslbase'})) . ')'; - $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b + } + + # Provide tooltips for full bug links (Bug 74355) + my $urlbase_re = '(' + . join('|', + map {qr/$_/} + grep($_, Bugzilla->params->{'urlbase'}, Bugzilla->params->{'sslbase'})) + . ')'; + $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b ~($things[$count++] = get_bug_link($3, $1, { comment_num => $5, user => $user })) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; - # non-mailto protocols - my $safe_protocols = SAFE_URL_REGEXP(); - $text =~ s~\b($safe_protocols) + # non-mailto protocols + my $safe_protocols = SAFE_URL_REGEXP(); + $text =~ s~\b($safe_protocols) ~($tmp = html_quote($1)) && ($things[$count++] = "<a href=\"$tmp\">$tmp</a>") && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; - # We have to quote now, otherwise the html itself is escaped - # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH + # We have to quote now, otherwise the html itself is escaped + # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH - $text = html_quote($text); + $text = html_quote($text); - # Color quoted text - $text =~ s~^(>.+)$~<span class="quote">$1</span >~mg; - $text =~ s~</span >\n<span class="quote">~\n~g; + # Color quoted text + $text =~ s~^(>.+)$~<span class="quote">$1</span >~mg; + $text =~ s~</span >\n<span class="quote">~\n~g; - # mailto: - # Use |<nothing> so that $1 is defined regardless - # @ is the encoded '@' character. - $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b + # mailto: + # Use |<nothing> so that $1 is defined regardless + # @ is the encoded '@' character. + $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b ~<a href=\"mailto:$2\">$1$2</a>~igx; - # attachment links - $text =~ s~\b(attachment$s*\#?$s*([0-9]+)(?:$s+\[details\])?) + # attachment links + $text =~ s~\b(attachment$s*\#?$s*([0-9]+)(?:$s+\[details\])?) ~($things[$count++] = get_attachment_link($2, $1, $user)) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egmxi; - # Current bug ID this comment belongs to - my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : ""; - - # This handles bug a, comment b type stuff. Because we're using /g - # we have to do this in one pattern, and so this is semi-messy. - # Also, we can't use $bug_re?$comment_re? because that will match the - # empty string - my $bug_word = template_var('terms')->{bug}; - my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i; - my $comment_word = template_var('terms')->{comment}; - my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*([0-9]+)/i; - $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re) + # Current bug ID this comment belongs to + my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : ""; + + # This handles bug a, comment b type stuff. Because we're using /g + # we have to do this in one pattern, and so this is semi-messy. + # Also, we can't use $bug_re?$comment_re? because that will match the + # empty string + my $bug_word = template_var('terms')->{bug}; + my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i; + my $comment_word = template_var('terms')->{comment}; + my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*([0-9]+)/i; + $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re) ~ # We have several choices. $1 here is the link, and $2-4 are set # depending on which part matched (defined($2) ? get_bug_link($2, $1, { comment_num => $3, user => $user }) : "<a href=\"$current_bugurl#c$4\">$1</a>") ~egx; - # Handle a list of bug ids: bugs 1, #2, 3, 4 - # Currently, the only delimiter supported is comma. - # Concluding "and" and "or" are not supported. - my $bugs_word = template_var('terms')->{bugs}; + # Handle a list of bug ids: bugs 1, #2, 3, 4 + # Currently, the only delimiter supported is comma. + # Concluding "and" and "or" are not supported. + my $bugs_word = template_var('terms')->{bugs}; - my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s* + my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s* [0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix; - $text =~ s{($bugs_re)}{ + $text =~ s{($bugs_re)}{ my $match = $1; $match =~ s/((?:#$s*)?([0-9]+))/get_bug_link($2, $1);/eg; $match; }eg; - my $comments_word = template_var('terms')->{comments}; + my $comments_word = template_var('terms')->{comments}; - my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s* + my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s* [0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix; - $text =~ s{($comments_re)}{ + $text =~ s{($comments_re)}{ my $match = $1; $match =~ s|((?:#$s*)?([0-9]+))|<a href="$current_bugurl#c$2">$1</a>|g; $match; }eg; - # Old duplicate markers. These don't use $bug_word because they are old - # and were never customizable. - $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ ) + # Old duplicate markers. These don't use $bug_word because they are old + # and were never customizable. + $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ ) ([0-9]+) (?=\ \*\*\*\Z) ~get_bug_link($1, $1, { user => $user }) ~egmx; - # Now remove the encoding hacks in reverse order - for (my $i = $#things; $i >= 0; $i--) { - $text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg; - } + # Now remove the encoding hacks in reverse order + for (my $i = $#things; $i >= 0; $i--) { + $text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg; + } - return $text; + return $text; } # Creates a link to an attachment, including its title. sub get_attachment_link { - my ($attachid, $link_text, $user) = @_; - $user ||= Bugzilla->user; - - my $attachment = new Bugzilla::Attachment({ id => $attachid, cache => 1 }); - - if ($attachment) { - my $title = ""; - my $className = ""; - if ($user->can_see_bug($attachment->bug_id) - && (!$attachment->isprivate || $user->is_insider)) - { - $title = $attachment->description; - } - if ($attachment->isobsolete) { - $className = "bz_obsolete"; - } - # Prevent code injection in the title. - $title = html_quote(clean_text($title)); + my ($attachid, $link_text, $user) = @_; + $user ||= Bugzilla->user; - $link_text =~ s/ \[details(?:, diff)?\]$//; - my $linkval = "attachment.cgi?id=$attachid"; + my $attachment = new Bugzilla::Attachment({id => $attachid, cache => 1}); - # If the attachment is a patch, try to link to the diff rather - # than the text, by default. - my $patchlink = ""; - if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) { - $patchlink = '&action=diff'; - } + if ($attachment) { + my $title = ""; + my $className = ""; + if ($user->can_see_bug($attachment->bug_id) + && (!$attachment->isprivate || $user->is_insider)) + { + $title = $attachment->description; + } + if ($attachment->isobsolete) { + $className = "bz_obsolete"; + } - if ($patchlink) { - # Whitespace matters here because these links are in <pre> tags. - return qq|<span class="$className">| - . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>| - . qq| [<a href="${linkval}&action=edit" title="$title">details</a>, <a href="${linkval}${patchlink}" title="$title">diff</a>]| - . qq|</span>|; - } - else { - # Whitespace matters here because these links are in <pre> tags. - return qq|<span class="$className">| - . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>| - . qq| [<a href="${linkval}&action=edit" title="$title">details</a>]| - . qq|</span>|; - } + # Prevent code injection in the title. + $title = html_quote(clean_text($title)); + + $link_text =~ s/ \[details(?:, diff)?\]$//; + my $linkval = "attachment.cgi?id=$attachid"; + + # If the attachment is a patch, try to link to the diff rather + # than the text, by default. + my $patchlink = ""; + if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) { + $patchlink = '&action=diff'; + } + + if ($patchlink) { + + # Whitespace matters here because these links are in <pre> tags. + return + qq|<span class="$className">| + . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>| + . qq| [<a href="${linkval}&action=edit" title="$title">details</a>, <a href="${linkval}${patchlink}" title="$title">diff</a>]| + . qq|</span>|; } else { - return qq{$link_text}; + # Whitespace matters here because these links are in <pre> tags. + return + qq|<span class="$className">| + . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>| + . qq| [<a href="${linkval}&action=edit" title="$title">details</a>]| + . qq|</span>|; } + } + else { + return qq{$link_text}; + } } # Creates a link to a bug, including its title. @@ -354,53 +365,59 @@ sub get_attachment_link { # comment in the bug sub get_bug_link { - my ($bug, $link_text, $options) = @_; - $options ||= {}; - $options->{user} ||= Bugzilla->user; - - if (defined $bug && $bug ne '') { - if (!blessed($bug)) { - require Bugzilla::Bug; - $bug = new Bugzilla::Bug({ id => $bug, cache => 1 }); - } - return $link_text if $bug->{error}; + my ($bug, $link_text, $options) = @_; + $options ||= {}; + $options->{user} ||= Bugzilla->user; + + if (defined $bug && $bug ne '') { + if (!blessed($bug)) { + require Bugzilla::Bug; + $bug = new Bugzilla::Bug({id => $bug, cache => 1}); } - - my $template = Bugzilla->template_inner; - my $linkified; - $template->process('bug/link.html.tmpl', - { bug => $bug, link_text => $link_text, %$options }, \$linkified); - return $linkified; + return $link_text if $bug->{error}; + } + + my $template = Bugzilla->template_inner; + my $linkified; + $template->process('bug/link.html.tmpl', + {bug => $bug, link_text => $link_text, %$options}, + \$linkified); + return $linkified; } # We use this instead of format because format doesn't deal well with # multi-byte languages. sub multiline_sprintf { - my ($format, $args, $sizes) = @_; - my @parts; - my @my_sizes = @$sizes; # Copy this so we don't modify the input array. - foreach my $string (@$args) { - my $size = shift @my_sizes; - my @pieces = split("\n", wrap_hard($string, $size)); - push(@parts, \@pieces); - } - - my $formatted; - while (1) { - # Get the first item of each part. - my @line = map { shift @$_ } @parts; - # If they're all undef, we're done. - last if !grep { defined $_ } @line; - # Make any single undef item into '' - @line = map { defined $_ ? $_ : '' } @line; - # And append a formatted line - $formatted .= sprintf($format, @line); - # Remove trailing spaces, or they become lots of =20's in - # quoted-printable emails. - $formatted =~ s/\s+$//; - $formatted .= "\n"; - } - return $formatted; + my ($format, $args, $sizes) = @_; + my @parts; + my @my_sizes = @$sizes; # Copy this so we don't modify the input array. + foreach my $string (@$args) { + my $size = shift @my_sizes; + my @pieces = split("\n", wrap_hard($string, $size)); + push(@parts, \@pieces); + } + + my $formatted; + while (1) { + + # Get the first item of each part. + my @line = map { shift @$_ } @parts; + + # If they're all undef, we're done. + last if !grep { defined $_ } @line; + + # Make any single undef item into '' + @line = map { defined $_ ? $_ : '' } @line; + + # And append a formatted line + $formatted .= sprintf($format, @line); + + # Remove trailing spaces, or they become lots of =20's in + # quoted-printable emails. + $formatted =~ s/\s+$//; + $formatted .= "\n"; + } + return $formatted; } ##################### @@ -412,17 +429,18 @@ sub multiline_sprintf { sub _mtime { return (stat($_[0]))[9] } sub mtime_filter { - my ($file_url, $mtime) = @_; - # This environment var is set in the .htaccess if we have mod_headers - # and mod_expires installed, to make sure that JS and CSS with "?" - # after them will still be cached by clients. - return $file_url if !$ENV{BZ_CACHE_CONTROL}; - if (!$mtime) { - my $cgi_path = bz_locations()->{'cgi_path'}; - my $file_path = "$cgi_path/$file_url"; - $mtime = _mtime($file_path); - } - return "$file_url?$mtime"; + my ($file_url, $mtime) = @_; + + # This environment var is set in the .htaccess if we have mod_headers + # and mod_expires installed, to make sure that JS and CSS with "?" + # after them will still be cached by clients. + return $file_url if !$ENV{BZ_CACHE_CONTROL}; + if (!$mtime) { + my $cgi_path = bz_locations()->{'cgi_path'}; + my $file_path = "$cgi_path/$file_url"; + $mtime = _mtime($file_path); + } + return "$file_url?$mtime"; } # Set up the skin CSS cascade: @@ -435,183 +453,186 @@ sub mtime_filter { # 6. Custom Bugzilla stylesheet set sub css_files { - my ($style_urls, $yui, $yui_css) = @_; + my ($style_urls, $yui, $yui_css) = @_; - # global.css goes on every page. - my @requested_css = ('skins/standard/global.css', @$style_urls); + # global.css goes on every page. + my @requested_css = ('skins/standard/global.css', @$style_urls); - my @yui_required_css; - foreach my $yui_name (@$yui) { - next if !$yui_css->{$yui_name}; - push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css"); - } - unshift(@requested_css, @yui_required_css); - - my @css_sets = map { _css_link_set($_) } @requested_css; - - my %by_type = (standard => [], skin => [], custom => []); - foreach my $set (@css_sets) { - foreach my $key (keys %$set) { - push(@{ $by_type{$key} }, $set->{$key}); - } + my @yui_required_css; + foreach my $yui_name (@$yui) { + next if !$yui_css->{$yui_name}; + push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css"); + } + unshift(@requested_css, @yui_required_css); + + my @css_sets = map { _css_link_set($_) } @requested_css; + + my %by_type = (standard => [], skin => [], custom => []); + foreach my $set (@css_sets) { + foreach my $key (keys %$set) { + push(@{$by_type{$key}}, $set->{$key}); } + } - # build unified - $by_type{unified_standard_skin} = _concatenate_css($by_type{standard}, - $by_type{skin}); - $by_type{unified_custom} = _concatenate_css($by_type{custom}); + # build unified + $by_type{unified_standard_skin} + = _concatenate_css($by_type{standard}, $by_type{skin}); + $by_type{unified_custom} = _concatenate_css($by_type{custom}); - return \%by_type; + return \%by_type; } sub _css_link_set { - my ($file_name) = @_; - - my %set = (standard => mtime_filter($file_name)); - - # We use (?:^|/) to allow Extensions to use the skins system if they want. - if ($file_name !~ m{(?:^|/)skins/standard/}) { - return \%set; - } - - my $skin = Bugzilla->user->settings->{skin}->{value}; - my $cgi_path = bz_locations()->{'cgi_path'}; - my $skin_file_name = $file_name; - $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/}; - if (my $mtime = _mtime("$cgi_path/$skin_file_name")) { - $set{skin} = mtime_filter($skin_file_name, $mtime); - } + my ($file_name) = @_; - my $custom_file_name = $file_name; - $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/}; - if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) { - $set{custom} = mtime_filter($custom_file_name, $custom_mtime); - } + my %set = (standard => mtime_filter($file_name)); + # We use (?:^|/) to allow Extensions to use the skins system if they want. + if ($file_name !~ m{(?:^|/)skins/standard/}) { return \%set; + } + + my $skin = Bugzilla->user->settings->{skin}->{value}; + my $cgi_path = bz_locations()->{'cgi_path'}; + my $skin_file_name = $file_name; + $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/}; + if (my $mtime = _mtime("$cgi_path/$skin_file_name")) { + $set{skin} = mtime_filter($skin_file_name, $mtime); + } + + my $custom_file_name = $file_name; + $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/}; + if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) { + $set{custom} = mtime_filter($custom_file_name, $custom_mtime); + } + + return \%set; } sub _concatenate_css { - my @sources = map { @$_ } @_; - return unless @sources; - - my %files = - map { - (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; - $_ => $file; - } @sources; - - my $cgi_path = bz_locations()->{cgi_path}; - my $skins_path = bz_locations()->{assetsdir}; - - # build minified files - my @minified; - foreach my $source (@sources) { - next unless -e "$cgi_path/$files{$source}"; - my $file = $skins_path . '/' . md5_hex($source) . '.css'; - if (!-e $file) { - my $content = read_text("$cgi_path/$files{$source}"); - - # minify - $content =~ s{/\*.*?\*/}{}sg; # comments - $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace - $content =~ s{\n}{}g; # single line - - # rewrite urls - $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig; - - write_text($file, "/* $files{$source} */\n" . $content . "\n"); - } - push @minified, $file; - } - - # concat files - my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css'; + my @sources = map {@$_} @_; + return unless @sources; + + my %files = map { + (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; + $_ => $file; + } @sources; + + my $cgi_path = bz_locations()->{cgi_path}; + my $skins_path = bz_locations()->{assetsdir}; + + # build minified files + my @minified; + foreach my $source (@sources) { + next unless -e "$cgi_path/$files{$source}"; + my $file = $skins_path . '/' . md5_hex($source) . '.css'; if (!-e $file) { - my $content = ''; - foreach my $source (@minified) { - $content .= read_text($source); - } - write_text($file, $content); + my $content = read_text("$cgi_path/$files{$source}"); + + # minify + $content =~ s{/\*.*?\*/}{}sg; # comments + $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace + $content =~ s{\n}{}g; # single line + + # rewrite urls + $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig; + + write_text($file, "/* $files{$source} */\n" . $content . "\n"); + } + push @minified, $file; + } + + # concat files + my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css'; + if (!-e $file) { + my $content = ''; + foreach my $source (@minified) { + $content .= read_text($source); } + write_text($file, $content); + } - $file =~ s/^\Q$cgi_path\E\///o; - return mtime_filter($file); + $file =~ s/^\Q$cgi_path\E\///o; + return mtime_filter($file); } sub _css_url_rewrite { - my ($source, $url) = @_; - # rewrite relative urls as the unified stylesheet lives in a different - # directory from the source - $url =~ s/(^['"]|['"]$)//g; - if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') { - return 'url(' . $url . ')'; - } - return 'url(../../' . ($ENV{'PROJECT'} ? '../' : '') . dirname($source) . '/' . $url . ')'; + my ($source, $url) = @_; + + # rewrite relative urls as the unified stylesheet lives in a different + # directory from the source + $url =~ s/(^['"]|['"]$)//g; + if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') { + return 'url(' . $url . ')'; + } + return + 'url(../../' + . ($ENV{'PROJECT'} ? '../' : '') + . dirname($source) . '/' + . $url . ')'; } sub _concatenate_js { - return @_ unless CONCATENATE_ASSETS; - my ($sources) = @_; - return [] unless $sources; - $sources = ref($sources) ? $sources : [ $sources ]; - - my %files = - map { - (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; - $_ => $file; - } @$sources; - - my $cgi_path = bz_locations()->{cgi_path}; - my $skins_path = bz_locations()->{assetsdir}; - - # build minified files - my @minified; - foreach my $source (@$sources) { - next unless -e "$cgi_path/$files{$source}"; - my $file = $skins_path . '/' . md5_hex($source) . '.js'; - if (!-e $file) { - my $content = read_text("$cgi_path/$files{$source}"); - - # minimal minification - $content =~ s#/\*.*?\*/##sg; # block comments - $content =~ s#(^ +| +$)##gm; # leading/trailing spaces - $content =~ s#^//.+$##gm; # single line comments - $content =~ s#\n{2,}#\n#g; # blank lines - $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file - - write_text($file, ";/* $files{$source} */\n" . $content . "\n"); - } - push @minified, $file; - } - - # concat files - my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js'; + return @_ unless CONCATENATE_ASSETS; + my ($sources) = @_; + return [] unless $sources; + $sources = ref($sources) ? $sources : [$sources]; + + my %files = map { + (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/; + $_ => $file; + } @$sources; + + my $cgi_path = bz_locations()->{cgi_path}; + my $skins_path = bz_locations()->{assetsdir}; + + # build minified files + my @minified; + foreach my $source (@$sources) { + next unless -e "$cgi_path/$files{$source}"; + my $file = $skins_path . '/' . md5_hex($source) . '.js'; if (!-e $file) { - my $content = ''; - foreach my $source (@minified) { - $content .= read_text($source); - } - write_text($file, $content); + my $content = read_text("$cgi_path/$files{$source}"); + + # minimal minification + $content =~ s#/\*.*?\*/##sg; # block comments + $content =~ s#(^ +| +$)##gm; # leading/trailing spaces + $content =~ s#^//.+$##gm; # single line comments + $content =~ s#\n{2,}#\n#g; # blank lines + $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file + + write_text($file, ";/* $files{$source} */\n" . $content . "\n"); + } + push @minified, $file; + } + + # concat files + my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js'; + if (!-e $file) { + my $content = ''; + foreach my $source (@minified) { + $content .= read_text($source); } + write_text($file, $content); + } - $file =~ s/^\Q$cgi_path\E\///o; - return [ $file ]; + $file =~ s/^\Q$cgi_path\E\///o; + return [$file]; } # YUI dependency resolution sub yui_resolve_deps { - my ($yui, $yui_deps) = @_; - - my @yui_resolved; - foreach my $yui_name (@$yui) { - my $deps = $yui_deps->{$yui_name} || []; - foreach my $dep (reverse @$deps) { - push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved; - } - push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved; + my ($yui, $yui_deps) = @_; + + my @yui_resolved; + foreach my $yui_name (@$yui) { + my $deps = $yui_deps->{$yui_name} || []; + foreach my $dep (reverse @$deps) { + push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved; } - return \@yui_resolved; + push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved; + } + return \@yui_resolved; } ############################################################################### @@ -630,73 +651,75 @@ use Template::Stash; # Allow keys to start with an underscore or a dot. $Template::Stash::PRIVATE = undef; -# Add "contains***" methods to list variables that search for one or more -# items in a list and return boolean values representing whether or not +# Add "contains***" methods to list variables that search for one or more +# items in a list and return boolean values representing whether or not # one/all/any item(s) were found. -$Template::Stash::LIST_OPS->{ contains } = - sub { - my ($list, $item) = @_; - if (ref $item && $item->isa('Bugzilla::Object')) { - return grep($_->id == $item->id, @$list); - } else { - return grep($_ eq $item, @$list); - } - }; - -$Template::Stash::LIST_OPS->{ containsany } = - sub { - my ($list, $items) = @_; - foreach my $item (@$items) { - if (ref $item && $item->isa('Bugzilla::Object')) { - return 1 if grep($_->id == $item->id, @$list); - } else { - return 1 if grep($_ eq $item, @$list); - } - } - return 0; - }; +$Template::Stash::LIST_OPS->{contains} = sub { + my ($list, $item) = @_; + if (ref $item && $item->isa('Bugzilla::Object')) { + return grep($_->id == $item->id, @$list); + } + else { + return grep($_ eq $item, @$list); + } +}; + +$Template::Stash::LIST_OPS->{containsany} = sub { + my ($list, $items) = @_; + foreach my $item (@$items) { + if (ref $item && $item->isa('Bugzilla::Object')) { + return 1 if grep($_->id == $item->id, @$list); + } + else { + return 1 if grep($_ eq $item, @$list); + } + } + return 0; +}; # Clone the array reference to leave the original one unaltered. -$Template::Stash::LIST_OPS->{ clone } = - sub { - my $list = shift; - return [@$list]; - }; +$Template::Stash::LIST_OPS->{clone} = sub { + my $list = shift; + return [@$list]; +}; # Allow us to sort the list of fields correctly -$Template::Stash::LIST_OPS->{ sort_by_field_name } = - sub { - sub field_name { - if ($_[0] eq 'noop') { - # Sort --- first - return ''; - } - # Otherwise sort by field_desc or description - return $_[1]{$_[0]} || $_[0]; - } - my ($list, $field_desc) = @_; - return [ sort { lc field_name($a, $field_desc) cmp lc field_name($b, $field_desc) } @$list ]; - }; +$Template::Stash::LIST_OPS->{sort_by_field_name} = sub { + + sub field_name { + if ($_[0] eq 'noop') { + + # Sort --- first + return ''; + } + + # Otherwise sort by field_desc or description + return $_[1]{$_[0]} || $_[0]; + } + my ($list, $field_desc) = @_; + return [ + sort { lc field_name($a, $field_desc) cmp lc field_name($b, $field_desc) } + @$list + ]; +}; # Allow us to still get the scalar if we use the list operation ".0" on it, # as we often do for defaults in query.cgi and other places. -$Template::Stash::SCALAR_OPS->{ 0 } = - sub { - return $_[0]; - }; +$Template::Stash::SCALAR_OPS->{0} = sub { + return $_[0]; +}; # Add a "truncate" method to the Template Toolkit's "scalar" object # that truncates a string to a certain length. -$Template::Stash::SCALAR_OPS->{ truncate } = - sub { - my ($string, $length, $ellipsis) = @_; - return $string if !$length || length($string) <= $length; - - $ellipsis ||= ''; - my $strlen = $length - length($ellipsis); - my $newstr = substr($string, 0, $strlen) . $ellipsis; - return $newstr; - }; +$Template::Stash::SCALAR_OPS->{truncate} = sub { + my ($string, $length, $ellipsis) = @_; + return $string if !$length || length($string) <= $length; + + $ellipsis ||= ''; + my $strlen = $length - length($ellipsis); + my $newstr = substr($string, 0, $strlen) . $ellipsis; + return $newstr; +}; # Create the template object that processes templates and specify # configuration parameters that apply to all templates. @@ -704,14 +727,15 @@ $Template::Stash::SCALAR_OPS->{ truncate } = ############################################################################### sub process { - my $self = shift; - # All of this current_langs stuff allows template_inner to correctly - # determine what-language Template object it should instantiate. - my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= []; - unshift(@$current_langs, $self->context->{bz_language}); - my $retval = $self->SUPER::process(@_); - shift @$current_langs; - return $retval; + my $self = shift; + + # All of this current_langs stuff allows template_inner to correctly + # determine what-language Template object it should instantiate. + my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= []; + unshift(@$current_langs, $self->context->{bz_language}); + my $retval = $self->SUPER::process(@_); + shift @$current_langs; + return $retval; } # Construct the Template object @@ -720,604 +744,625 @@ sub process { # since we won't have a template to use... sub create { - my $class = shift; - my %opts = @_; - - # IMPORTANT - If you make any FILTER changes here, make sure to - # make them in t/004.template.t also, if required. - - my $config = { - # Colon-separated list of directories containing templates. - INCLUDE_PATH => $opts{'include_path'} - || _include_path($opts{'language'}), - - # Remove white-space before template directives (PRE_CHOMP) and at the - # beginning and end of templates and template blocks (TRIM) for better - # looking, more compact content. Use the plus sign at the beginning - # of directives to maintain white space (i.e. [%+ DIRECTIVE %]). - PRE_CHOMP => 1, - TRIM => 1, - - # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl) - # or relative (in mod_cgi) paths of hook files to explicitly compile - # a specific file. Also, these paths may be absolute at any time - # if a packager has modified bz_locations() to contain absolute - # paths. - ABSOLUTE => 1, - RELATIVE => $ENV{MOD_PERL} ? 0 : 1, - - COMPILE_DIR => bz_locations()->{'template_cache'}, - - # Don't check for a template update until 1 hour has passed since the - # last check. - STAT_TTL => 60 * 60, - - # Initialize templates (f.e. by loading plugins like Hook). - PRE_PROCESS => ["global/variables.none.tmpl"], - - ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef, - - # Functions for processing text within templates in various ways. - # IMPORTANT! When adding a filter here that does not override a - # built-in filter, please also add a stub filter to t/004template.t. - FILTERS => { - - # Render text in required style. - - inactive => [ - sub { - my($context, $isinactive) = @_; - return sub { - return $isinactive ? '<span class="bz_inactive">'.$_[0].'</span>' : $_[0]; - } - }, 1 - ], - - closed => [ - sub { - my($context, $isclosed) = @_; - return sub { - return $isclosed ? '<span class="bz_closed">'.$_[0].'</span>' : $_[0]; - } - }, 1 - ], - - obsolete => [ - sub { - my($context, $isobsolete) = @_; - return sub { - return $isobsolete ? '<span class="bz_obsolete">'.$_[0].'</span>' : $_[0]; - } - }, 1 - ], - - # Returns the text with backslashes, single/double quotes, - # and newlines/carriage returns escaped for use in JS strings. - js => sub { - my ($var) = @_; - $var =~ s/([\\\'\"\/])/\\$1/g; - $var =~ s/\n/\\n/g; - $var =~ s/\r/\\r/g; - $var =~ s/\x{2028}/\\u2028/g; # unicode line separator - $var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator - $var =~ s/\@/\\x40/g; # anti-spam for email addresses - $var =~ s/</\\x3c/g; - $var =~ s/>/\\x3e/g; - return $var; - }, - - # Converts data to base64 - base64 => sub { - my ($data) = @_; - return encode_base64($data); - }, - - # Strips out control characters excepting whitespace - strip_control_chars => sub { - my ($data) = @_; - state $use_utf8 = Bugzilla->params->{'utf8'}; - # Only run for utf8 to avoid issues with other multibyte encodings - # that may be reassigning meaning to ascii characters. - if ($use_utf8) { - $data =~ s/(?![\t\r\n])[[:cntrl:]]//g; - } - return $data; - }, - - # HTML collapses newlines in element attributes to a single space, - # so form elements which may have whitespace (ie comments) need - # to be encoded using 
 - # See bugs 4928, 22983 and 32000 for more details - html_linebreak => sub { - my ($var) = @_; - $var = html_quote($var); - $var =~ s/\r\n/\
/g; - $var =~ s/\n\r/\
/g; - $var =~ s/\r/\
/g; - $var =~ s/\n/\
/g; - return $var; - }, - - xml => \&Bugzilla::Util::xml_quote , - - # This filter is similar to url_quote but used a \ instead of a % - # as prefix. In addition it replaces a ' ' by a '_'. - css_class_quote => \&Bugzilla::Util::css_class_quote , - - # Removes control characters and trims extra whitespace. - clean_text => \&Bugzilla::Util::clean_text , - - quoteUrls => [ sub { - my ($context, $bug, $comment, $user) = @_; - return sub { - my $text = shift; - return quoteUrls($text, $bug, $comment, $user); - }; - }, - 1 - ], - - bug_link => [ sub { - my ($context, $bug, $options) = @_; - return sub { - my $text = shift; - return get_bug_link($bug, $text, $options); - }; - }, - 1 - ], - - bug_list_link => sub { - my ($buglist, $options) = @_; - return join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist))); - }, - - # In CSV, quotes are doubled, and any value containing a quote or a - # comma is enclosed in quotes. - # If a field starts with either "=", "+", "-" or "@", it is preceded - # by a space to prevent stupid formula execution from Excel & co. - csv => sub - { - my ($var) = @_; - $var = ' ' . $var if $var =~ /^[+=@-]/; - # backslash is not special to CSV, but it can be used to confuse some browsers... - # so we do not allow it to happen. We only do this for logged-in users. - $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id; - $var =~ s/\"/\"\"/g; - if ($var !~ /^-?(\d+\.)?\d*$/) { - $var = "\"$var\""; - } - return $var; - } , - - # Format a filesize in bytes to a human readable value - unitconvert => sub - { - my ($data) = @_; - my $retval = ""; - my %units = ( - 'KB' => 1024, - 'MB' => 1024 * 1024, - 'GB' => 1024 * 1024 * 1024, - ); - - if ($data < 1024) { - return "$data bytes"; - } - else { - my $u; - foreach $u ('GB', 'MB', 'KB') { - if ($data >= $units{$u}) { - return sprintf("%.2f %s", $data/$units{$u}, $u); - } - } - } - }, - - # Format a time for display (more info in Bugzilla::Util) - time => [ sub { - my ($context, $format, $timezone) = @_; - return sub { - my $time = shift; - return format_time($time, $format, $timezone); - }; - }, - 1 - ], - - html => \&Bugzilla::Util::html_quote, - - html_light => \&Bugzilla::Util::html_light_quote, - - email => \&Bugzilla::Util::email_filter, - - mtime => \&mtime_filter, - - # iCalendar contentline filter - ics => [ sub { - my ($context, @args) = @_; - return sub { - my ($var) = shift; - my ($par) = shift @args; - my ($output) = ""; - - $var =~ s/[\r\n]/ /g; - $var =~ s/([;\\\",])/\\$1/g; - - if ($par) { - $output = sprintf("%s:%s", $par, $var); - } else { - $output = $var; - } - - $output =~ s/(.{75,75})/$1\n /g; - - return $output; - }; - }, - 1 - ], - - # Note that using this filter is even more dangerous than - # using "none," and you should only use it when you're SURE - # the output won't be displayed directly to a web browser. - txt => sub { - my ($var) = @_; - # Trivial HTML tag remover - $var =~ s/<[^>]*>//g; - # And this basically reverses the html filter. - $var =~ s/\@/@/g; - $var =~ s/\</</g; - $var =~ s/\>/>/g; - $var =~ s/\"/\"/g; - $var =~ s/\&/\&/g; - # Now remove extra whitespace... - my $collapse_filter = $Template::Filters::FILTERS->{collapse}; - $var = $collapse_filter->($var); - # And if we're not in the WebService, wrap the message. - # (Wrapping the message in the WebService is unnecessary - # and causes awkward things like \n's appearing in error - # messages in JSON-RPC.) - unless (i_am_webservice()) { - $var = wrap_comment($var, 72); - } - $var =~ s/\ / /g; - - return $var; - }, - - # Wrap a displayed comment to the appropriate length - wrap_comment => [ - sub { - my ($context, $cols) = @_; - return sub { wrap_comment($_[0], $cols) } - }, 1], - - # We force filtering of every variable in key security-critical - # places; we have a none filter for people to use when they - # really, really don't want a variable to be changed. - none => sub { return $_[0]; } , + my $class = shift; + my %opts = @_; + + # IMPORTANT - If you make any FILTER changes here, make sure to + # make them in t/004.template.t also, if required. + + my $config = { + + # Colon-separated list of directories containing templates. + INCLUDE_PATH => $opts{'include_path'} || _include_path($opts{'language'}), + + # Remove white-space before template directives (PRE_CHOMP) and at the + # beginning and end of templates and template blocks (TRIM) for better + # looking, more compact content. Use the plus sign at the beginning + # of directives to maintain white space (i.e. [%+ DIRECTIVE %]). + PRE_CHOMP => 1, + TRIM => 1, + + # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl) + # or relative (in mod_cgi) paths of hook files to explicitly compile + # a specific file. Also, these paths may be absolute at any time + # if a packager has modified bz_locations() to contain absolute + # paths. + ABSOLUTE => 1, + RELATIVE => $ENV{MOD_PERL} ? 0 : 1, + + COMPILE_DIR => bz_locations()->{'template_cache'}, + + # Don't check for a template update until 1 hour has passed since the + # last check. + STAT_TTL => 60 * 60, + + # Initialize templates (f.e. by loading plugins like Hook). + PRE_PROCESS => ["global/variables.none.tmpl"], + + ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef, + + # Functions for processing text within templates in various ways. + # IMPORTANT! When adding a filter here that does not override a + # built-in filter, please also add a stub filter to t/004template.t. + FILTERS => { + + # Render text in required style. + + inactive => [ + sub { + my ($context, $isinactive) = @_; + return sub { + return $isinactive ? '<span class="bz_inactive">' . $_[0] . '</span>' : $_[0]; + } + }, + 1 + ], + + closed => [ + sub { + my ($context, $isclosed) = @_; + return sub { + return $isclosed ? '<span class="bz_closed">' . $_[0] . '</span>' : $_[0]; + } + }, + 1 + ], + + obsolete => [ + sub { + my ($context, $isobsolete) = @_; + return sub { + return $isobsolete ? '<span class="bz_obsolete">' . $_[0] . '</span>' : $_[0]; + } + }, + 1 + ], + + # Returns the text with backslashes, single/double quotes, + # and newlines/carriage returns escaped for use in JS strings. + js => sub { + my ($var) = @_; + $var =~ s/([\\\'\"\/])/\\$1/g; + $var =~ s/\n/\\n/g; + $var =~ s/\r/\\r/g; + $var =~ s/\x{2028}/\\u2028/g; # unicode line separator + $var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator + $var =~ s/\@/\\x40/g; # anti-spam for email addresses + $var =~ s/</\\x3c/g; + $var =~ s/>/\\x3e/g; + return $var; + }, + + # Converts data to base64 + base64 => sub { + my ($data) = @_; + return encode_base64($data); + }, + + # Strips out control characters excepting whitespace + strip_control_chars => sub { + my ($data) = @_; + state $use_utf8 = Bugzilla->params->{'utf8'}; + + # Only run for utf8 to avoid issues with other multibyte encodings + # that may be reassigning meaning to ascii characters. + if ($use_utf8) { + $data =~ s/(?![\t\r\n])[[:cntrl:]]//g; + } + return $data; + }, + + # HTML collapses newlines in element attributes to a single space, + # so form elements which may have whitespace (ie comments) need + # to be encoded using 
 + # See bugs 4928, 22983 and 32000 for more details + html_linebreak => sub { + my ($var) = @_; + $var = html_quote($var); + $var =~ s/\r\n/\
/g; + $var =~ s/\n\r/\
/g; + $var =~ s/\r/\
/g; + $var =~ s/\n/\
/g; + return $var; + }, + + xml => \&Bugzilla::Util::xml_quote, + + # This filter is similar to url_quote but used a \ instead of a % + # as prefix. In addition it replaces a ' ' by a '_'. + css_class_quote => \&Bugzilla::Util::css_class_quote, + + # Removes control characters and trims extra whitespace. + clean_text => \&Bugzilla::Util::clean_text, + + quoteUrls => [ + sub { + my ($context, $bug, $comment, $user) = @_; + return sub { + my $text = shift; + return quoteUrls($text, $bug, $comment, $user); + }; + }, + 1 + ], + + bug_link => [ + sub { + my ($context, $bug, $options) = @_; + return sub { + my $text = shift; + return get_bug_link($bug, $text, $options); + }; + }, + 1 + ], + + bug_list_link => sub { + my ($buglist, $options) = @_; + return + join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist))); + }, + + # In CSV, quotes are doubled, and any value containing a quote or a + # comma is enclosed in quotes. + # If a field starts with either "=", "+", "-" or "@", it is preceded + # by a space to prevent stupid formula execution from Excel & co. + csv => sub { + my ($var) = @_; + $var = ' ' . $var if $var =~ /^[+=@-]/; + + # backslash is not special to CSV, but it can be used to confuse some browsers... + # so we do not allow it to happen. We only do this for logged-in users. + $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id; + $var =~ s/\"/\"\"/g; + if ($var !~ /^-?(\d+\.)?\d*$/) { + $var = "\"$var\""; + } + return $var; + }, + + # Format a filesize in bytes to a human readable value + unitconvert => sub { + my ($data) = @_; + my $retval = ""; + my %units = ('KB' => 1024, 'MB' => 1024 * 1024, 'GB' => 1024 * 1024 * 1024,); + + if ($data < 1024) { + return "$data bytes"; + } + else { + my $u; + foreach $u ('GB', 'MB', 'KB') { + if ($data >= $units{$u}) { + return sprintf("%.2f %s", $data / $units{$u}, $u); + } + } + } + }, + + # Format a time for display (more info in Bugzilla::Util) + time => [ + sub { + my ($context, $format, $timezone) = @_; + return sub { + my $time = shift; + return format_time($time, $format, $timezone); + }; + }, + 1 + ], + + html => \&Bugzilla::Util::html_quote, + + html_light => \&Bugzilla::Util::html_light_quote, + + email => \&Bugzilla::Util::email_filter, + + mtime => \&mtime_filter, + + # iCalendar contentline filter + ics => [ + sub { + my ($context, @args) = @_; + return sub { + my ($var) = shift; + my ($par) = shift @args; + my ($output) = ""; + + $var =~ s/[\r\n]/ /g; + $var =~ s/([;\\\",])/\\$1/g; + + if ($par) { + $output = sprintf("%s:%s", $par, $var); + } + else { + $output = $var; + } + + $output =~ s/(.{75,75})/$1\n /g; + + return $output; + }; }, + 1 + ], + + # Note that using this filter is even more dangerous than + # using "none," and you should only use it when you're SURE + # the output won't be displayed directly to a web browser. + txt => sub { + my ($var) = @_; + + # Trivial HTML tag remover + $var =~ s/<[^>]*>//g; + + # And this basically reverses the html filter. + $var =~ s/\@/@/g; + $var =~ s/\</</g; + $var =~ s/\>/>/g; + $var =~ s/\"/\"/g; + $var =~ s/\&/\&/g; + + # Now remove extra whitespace... + my $collapse_filter = $Template::Filters::FILTERS->{collapse}; + $var = $collapse_filter->($var); + + # And if we're not in the WebService, wrap the message. + # (Wrapping the message in the WebService is unnecessary + # and causes awkward things like \n's appearing in error + # messages in JSON-RPC.) + unless (i_am_webservice()) { + $var = wrap_comment($var, 72); + } + $var =~ s/\ / /g; - PLUGIN_BASE => 'Bugzilla::Template::Plugin', - - CONSTANTS => _load_constants(), - - # Default variables for all templates - VARIABLES => { - # Function for retrieving global parameters. - 'Param' => sub { return Bugzilla->params->{$_[0]}; }, - - # Function to create date strings - 'time2str' => \&Date::Format::time2str, - - # Fixed size column formatting for bugmail. - 'format_columns' => sub { - my $cols = shift; - my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE; - my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE; - return multiline_sprintf($format, \@_, $col_size); - }, - - # Generic linear search function - 'lsearch' => sub { - my ($array, $item) = @_; - return firstidx { $_ eq $item } @$array; - }, - - # Currently logged in user, if any - # If an sudo session is in progress, this is the user we're faking - 'user' => sub { return Bugzilla->user; }, - - # Currenly active language - 'current_language' => sub { return Bugzilla->current_language; }, - - # If an sudo session is in progress, this is the user who - # started the session. - 'sudoer' => sub { return Bugzilla->sudoer; }, - - # Allow templates to access the "correct" URLBase value - 'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); }, - 'httpbase' => sub { return Bugzilla->params->{'urlbase'}; }, - 'sslbase' => sub { return Bugzilla->params->{'sslbase'}; }, - 'ssl_redirect' => sub { return Bugzilla->params->{'ssl_redirect'}; }, - - # Allow templates to access docs url with users' preferred language - # We fall back to English if documentation in the preferred - # language is not available - 'docs_urlbase' => sub { - my $docs_urlbase; - my $lang = Bugzilla->current_language; - # Translations currently available on readthedocs.org - my @rtd_translations = ('en', 'fr'); - - if ($lang ne 'en' && -f "docs/$lang/html/index.html") { - $docs_urlbase = "docs/$lang/html/"; - } - elsif (-f "docs/en/html/index.html") { - $docs_urlbase = "docs/en/html/"; - } - else { - if (!grep { $_ eq $lang } @rtd_translations) { - $lang = "en"; - } - - my $version = BUGZILLA_VERSION; - $version =~ /^(\d+)\.(\d+)/; - if ($2 % 2 == 1) { - # second number is odd; development version - $version = 'latest'; - } - else { - $version = "$1.$2"; - } - - $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/"; - } - - return $docs_urlbase; - }, - - # Check whether the URL is safe. - 'is_safe_url' => sub { - my $url = shift; - return 0 unless $url; - - my $safe_url_regexp = SAFE_URL_REGEXP(); - return 1 if $url =~ /^$safe_url_regexp$/; - # Pointing to a local file with no colon in its name is fine. - return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i; - # If we come here, then we cannot guarantee it's safe. - return 0; - }, - - # Allow templates to generate a token themselves. - 'issue_hash_token' => \&Bugzilla::Token::issue_hash_token, - - 'get_login_request_token' => sub { - my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie'); - return $cookie ? issue_hash_token(['login_request', $cookie]) : ''; - }, - - 'get_api_token' => sub { - return '' unless Bugzilla->user->id; - my $cache = Bugzilla->request_cache; - return $cache->{api_token} //= issue_api_token(); - }, - - # A way for all templates to get at Field data, cached. - 'bug_fields' => sub { - my $cache = Bugzilla->request_cache; - $cache->{template_bug_fields} ||= - Bugzilla->fields({ by_name => 1 }); - return $cache->{template_bug_fields}; - }, - - # A general purpose cache to store rendered templates for reuse. - # Make sure to not mix language-specific data. - 'template_cache' => sub { - my $cache = Bugzilla->request_cache->{template_cache} ||= {}; - $cache->{users} ||= {}; - return $cache; - }, - - 'css_files' => \&css_files, - yui_resolve_deps => \&yui_resolve_deps, - concatenate_js => \&_concatenate_js, - - # All classifications (sorted by sortkey, name) - 'all_classifications' => sub { - return [map { $_->name } Bugzilla::Classification->get_all()]; - }, - - # Whether or not keywords are enabled, in this Bugzilla. - 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; }, - - # All the keywords. - 'all_keywords' => sub { - return [map { $_->name } Bugzilla::Keyword->get_all()]; - }, - - 'feature_enabled' => sub { return Bugzilla->feature(@_); }, - - # field_descs can be somewhat slow to generate, so we generate - # it only once per-language no matter how many times - # $template->process() is called. - 'field_descs' => sub { return template_var('field_descs') }, - - # Calling bug/field-help.none.tmpl once per label is very - # expensive, so we generate it once per-language. - 'help_html' => sub { return template_var('help_html') }, - - # This way we don't have to load field-descs.none.tmpl in - # many templates. - 'display_value' => \&Bugzilla::Util::display_value, - - 'install_string' => \&Bugzilla::Install::Util::install_string, - - 'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS, - - # These don't work as normal constants. - DB_MODULE => \&Bugzilla::Constants::DB_MODULE, - REQUIRED_MODULES => - \&Bugzilla::Install::Requirements::REQUIRED_MODULES, - OPTIONAL_MODULES => sub { - my @optional = @{OPTIONAL_MODULES()}; - foreach my $item (@optional) { - my @features; - foreach my $feat_id (@{ $item->{feature} }) { - push(@features, install_string("feature_$feat_id")); - } - $item->{feature} = \@features; - } - return \@optional; - }, - 'default_authorizer' => sub { return Bugzilla::Auth->new() }, - - 'login_not_email' => sub { - my $params = Bugzilla->params; - my $cache = Bugzilla->request_cache; - - return $cache->{login_not_email} //= - ($params->{emailsuffix} - || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute}) - || ($params->{user_verify_class} =~ /RADIUS/ && $params->{RADIUS_email_suffix})) - ? 1 : 0; - }, + return $var; + }, + + # Wrap a displayed comment to the appropriate length + wrap_comment => [ + sub { + my ($context, $cols) = @_; + return sub { wrap_comment($_[0], $cols) } }, - }; - # Use a per-process provider to cache compiled templates in memory across - # requests. - my $provider_key = join(':', @{ $config->{INCLUDE_PATH} }); - my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {}; - $shared_providers->{$provider_key} ||= Template::Provider->new($config); - $config->{LOAD_TEMPLATES} = [ $shared_providers->{$provider_key} ]; - - local $Template::Config::CONTEXT = 'Bugzilla::Template::Context'; - - Bugzilla::Hook::process('template_before_create', { config => $config }); - my $template = $class->new($config) - || die("Template creation failed: " . $class->error()); - Bugzilla::Hook::process('template_after_create', { template => $template }); - - # Pass on our current language to any template hooks or inner templates - # called by this Template object. - $template->context->{bz_language} = $opts{language} || ''; - - return $template; + 1 + ], + + # We force filtering of every variable in key security-critical + # places; we have a none filter for people to use when they + # really, really don't want a variable to be changed. + none => sub { return $_[0]; }, + }, + + PLUGIN_BASE => 'Bugzilla::Template::Plugin', + + CONSTANTS => _load_constants(), + + # Default variables for all templates + VARIABLES => { + + # Function for retrieving global parameters. + 'Param' => sub { return Bugzilla->params->{$_[0]}; }, + + # Function to create date strings + 'time2str' => \&Date::Format::time2str, + + # Fixed size column formatting for bugmail. + 'format_columns' => sub { + my $cols = shift; + my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE; + my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE; + return multiline_sprintf($format, \@_, $col_size); + }, + + # Generic linear search function + 'lsearch' => sub { + my ($array, $item) = @_; + return firstidx { $_ eq $item } @$array; + }, + + # Currently logged in user, if any + # If an sudo session is in progress, this is the user we're faking + 'user' => sub { return Bugzilla->user; }, + + # Currenly active language + 'current_language' => sub { return Bugzilla->current_language; }, + + # If an sudo session is in progress, this is the user who + # started the session. + 'sudoer' => sub { return Bugzilla->sudoer; }, + + # Allow templates to access the "correct" URLBase value + 'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); }, + 'httpbase' => sub { return Bugzilla->params->{'urlbase'}; }, + 'sslbase' => sub { return Bugzilla->params->{'sslbase'}; }, + 'ssl_redirect' => sub { return Bugzilla->params->{'ssl_redirect'}; }, + + # Allow templates to access docs url with users' preferred language + # We fall back to English if documentation in the preferred + # language is not available + 'docs_urlbase' => sub { + my $docs_urlbase; + my $lang = Bugzilla->current_language; + + # Translations currently available on readthedocs.org + my @rtd_translations = ('en', 'fr'); + + if ($lang ne 'en' && -f "docs/$lang/html/index.html") { + $docs_urlbase = "docs/$lang/html/"; + } + elsif (-f "docs/en/html/index.html") { + $docs_urlbase = "docs/en/html/"; + } + else { + if (!grep { $_ eq $lang } @rtd_translations) { + $lang = "en"; + } + + my $version = BUGZILLA_VERSION; + $version =~ /^(\d+)\.(\d+)/; + if ($2 % 2 == 1) { + + # second number is odd; development version + $version = 'latest'; + } + else { + $version = "$1.$2"; + } + + $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/"; + } + + return $docs_urlbase; + }, + + # Check whether the URL is safe. + 'is_safe_url' => sub { + my $url = shift; + return 0 unless $url; + + my $safe_url_regexp = SAFE_URL_REGEXP(); + return 1 if $url =~ /^$safe_url_regexp$/; + + # Pointing to a local file with no colon in its name is fine. + return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i; + + # If we come here, then we cannot guarantee it's safe. + return 0; + }, + + # Allow templates to generate a token themselves. + 'issue_hash_token' => \&Bugzilla::Token::issue_hash_token, + + 'get_login_request_token' => sub { + my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie'); + return $cookie ? issue_hash_token(['login_request', $cookie]) : ''; + }, + + 'get_api_token' => sub { + return '' unless Bugzilla->user->id; + my $cache = Bugzilla->request_cache; + return $cache->{api_token} //= issue_api_token(); + }, + + # A way for all templates to get at Field data, cached. + 'bug_fields' => sub { + my $cache = Bugzilla->request_cache; + $cache->{template_bug_fields} ||= Bugzilla->fields({by_name => 1}); + return $cache->{template_bug_fields}; + }, + + # A general purpose cache to store rendered templates for reuse. + # Make sure to not mix language-specific data. + 'template_cache' => sub { + my $cache = Bugzilla->request_cache->{template_cache} ||= {}; + $cache->{users} ||= {}; + return $cache; + }, + + 'css_files' => \&css_files, + yui_resolve_deps => \&yui_resolve_deps, + concatenate_js => \&_concatenate_js, + + # All classifications (sorted by sortkey, name) + 'all_classifications' => sub { + return [map { $_->name } Bugzilla::Classification->get_all()]; + }, + + # Whether or not keywords are enabled, in this Bugzilla. + 'use_keywords' => sub { return Bugzilla::Keyword->any_exist; }, + + # All the keywords. + 'all_keywords' => sub { + return [map { $_->name } Bugzilla::Keyword->get_all()]; + }, + + 'feature_enabled' => sub { return Bugzilla->feature(@_); }, + + # field_descs can be somewhat slow to generate, so we generate + # it only once per-language no matter how many times + # $template->process() is called. + 'field_descs' => sub { return template_var('field_descs') }, + + # Calling bug/field-help.none.tmpl once per label is very + # expensive, so we generate it once per-language. + 'help_html' => sub { return template_var('help_html') }, + + # This way we don't have to load field-descs.none.tmpl in + # many templates. + 'display_value' => \&Bugzilla::Util::display_value, + + 'install_string' => \&Bugzilla::Install::Util::install_string, + + 'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS, + + # These don't work as normal constants. + DB_MODULE => \&Bugzilla::Constants::DB_MODULE, + REQUIRED_MODULES => \&Bugzilla::Install::Requirements::REQUIRED_MODULES, + OPTIONAL_MODULES => sub { + my @optional = @{OPTIONAL_MODULES()}; + foreach my $item (@optional) { + my @features; + foreach my $feat_id (@{$item->{feature}}) { + push(@features, install_string("feature_$feat_id")); + } + $item->{feature} = \@features; + } + return \@optional; + }, + 'default_authorizer' => sub { return Bugzilla::Auth->new() }, + + 'login_not_email' => sub { + my $params = Bugzilla->params; + my $cache = Bugzilla->request_cache; + + return $cache->{login_not_email} + //= ($params->{emailsuffix} + || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute}) + || ($params->{user_verify_class} =~ /RADIUS/ + && $params->{RADIUS_email_suffix})) ? 1 : 0; + }, + }, + }; + + # Use a per-process provider to cache compiled templates in memory across + # requests. + my $provider_key = join(':', @{$config->{INCLUDE_PATH}}); + my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {}; + $shared_providers->{$provider_key} ||= Template::Provider->new($config); + $config->{LOAD_TEMPLATES} = [$shared_providers->{$provider_key}]; + + local $Template::Config::CONTEXT = 'Bugzilla::Template::Context'; + + Bugzilla::Hook::process('template_before_create', {config => $config}); + my $template = $class->new($config) + || die("Template creation failed: " . $class->error()); + Bugzilla::Hook::process('template_after_create', {template => $template}); + + # Pass on our current language to any template hooks or inner templates + # called by this Template object. + $template->context->{bz_language} = $opts{language} || ''; + + return $template; } # Used as part of the two subroutines below. our %_templates_to_precompile; + sub precompile_templates { - my ($output) = @_; + my ($output) = @_; - # Remove the compiled templates. - my $cache_dir = bz_locations()->{'template_cache'}; - my $datadir = bz_locations()->{'datadir'}; + # Remove the compiled templates. + my $cache_dir = bz_locations()->{'template_cache'}; + my $datadir = bz_locations()->{'datadir'}; + if (-e $cache_dir) { + print install_string('template_removing_dir') . "\n" if $output; + + # This frequently fails if the webserver made the files, because + # then the webserver owns the directories. + rmtree($cache_dir); + + # Check that the directory was really removed, and if not, move it + # into data/deleteme/. if (-e $cache_dir) { - print install_string('template_removing_dir') . "\n" if $output; - - # This frequently fails if the webserver made the files, because - # then the webserver owns the directories. - rmtree($cache_dir); - - # Check that the directory was really removed, and if not, move it - # into data/deleteme/. - if (-e $cache_dir) { - my $deleteme = "$datadir/deleteme"; - - print STDERR "\n\n", - install_string('template_removal_failed', - { deleteme => $deleteme, - template_cache => $cache_dir }), "\n\n"; - mkpath($deleteme); - my $random = generate_random_password(); - rename($cache_dir, "$deleteme/$random") - or die "move failed: $!"; - } + my $deleteme = "$datadir/deleteme"; + + print STDERR "\n\n", + install_string('template_removal_failed', + {deleteme => $deleteme, template_cache => $cache_dir}), + "\n\n"; + mkpath($deleteme); + my $random = generate_random_password(); + rename($cache_dir, "$deleteme/$random") or die "move failed: $!"; } + } - print install_string('template_precompile') if $output; + print install_string('template_precompile') if $output; - # Pre-compile all available languages. - my $paths = template_include_path({ language => Bugzilla->languages }); + # Pre-compile all available languages. + my $paths = template_include_path({language => Bugzilla->languages}); - foreach my $dir (@$paths) { - my $template = Bugzilla::Template->create(include_path => [$dir]); + foreach my $dir (@$paths) { + my $template = Bugzilla::Template->create(include_path => [$dir]); - %_templates_to_precompile = (); - # Traverse the template hierarchy. - find({ wanted => \&_precompile_push, no_chdir => 1 }, $dir); - # The sort isn't totally necessary, but it makes debugging easier - # by making the templates always be compiled in the same order. - foreach my $file (sort keys %_templates_to_precompile) { - $file =~ s{^\Q$dir\E/}{}; - # Compile the template but throw away the result. This has the side- - # effect of writing the compiled version to disk. - $template->context->template($file); - } + %_templates_to_precompile = (); - # Clear out the cached Provider object - Bugzilla->process_cache->{shared_providers} = undef; - } + # Traverse the template hierarchy. + find({wanted => \&_precompile_push, no_chdir => 1}, $dir); - # Under mod_perl, we look for templates using the absolute path of the - # template directory, which causes Template Toolkit to look for their - # *compiled* versions using the full absolute path under the data/template - # directory. (Like data/template/var/www/html/bugzilla/.) To avoid - # re-compiling templates under mod_perl, we symlink to the - # already-compiled templates. This doesn't work on Windows. - if (!ON_WINDOWS) { - # We do these separately in case they're in different locations. - _do_template_symlink(bz_locations()->{'templatedir'}); - _do_template_symlink(bz_locations()->{'extensionsdir'}); + # The sort isn't totally necessary, but it makes debugging easier + # by making the templates always be compiled in the same order. + foreach my $file (sort keys %_templates_to_precompile) { + $file =~ s{^\Q$dir\E/}{}; + + # Compile the template but throw away the result. This has the side- + # effect of writing the compiled version to disk. + $template->context->template($file); } - # If anything created a Template object before now, clear it out. - delete Bugzilla->request_cache->{template}; + # Clear out the cached Provider object + Bugzilla->process_cache->{shared_providers} = undef; + } + + # Under mod_perl, we look for templates using the absolute path of the + # template directory, which causes Template Toolkit to look for their + # *compiled* versions using the full absolute path under the data/template + # directory. (Like data/template/var/www/html/bugzilla/.) To avoid + # re-compiling templates under mod_perl, we symlink to the + # already-compiled templates. This doesn't work on Windows. + if (!ON_WINDOWS) { + + # We do these separately in case they're in different locations. + _do_template_symlink(bz_locations()->{'templatedir'}); + _do_template_symlink(bz_locations()->{'extensionsdir'}); + } - print install_string('done') . "\n" if $output; + # If anything created a Template object before now, clear it out. + delete Bugzilla->request_cache->{template}; + + print install_string('done') . "\n" if $output; } # Helper for precompile_templates sub _precompile_push { - my $name = $File::Find::name; - return if (-d $name); - return if ($name =~ /\/CVS\//); - return if ($name !~ /\.tmpl$/); - $_templates_to_precompile{$name} = 1; + my $name = $File::Find::name; + return if (-d $name); + return if ($name =~ /\/CVS\//); + return if ($name !~ /\.tmpl$/); + $_templates_to_precompile{$name} = 1; } # Helper for precompile_templates sub _do_template_symlink { - my $dir_to_symlink = shift; - - my $abs_path = abs_path($dir_to_symlink); - - # If $dir_to_symlink is already an absolute path (as might happen - # with packagers who set $libpath to an absolute path), then we don't - # need to do this symlink. - return if ($abs_path eq $dir_to_symlink); - - my $abs_root = dirname($abs_path); - my $dir_name = basename($abs_path); - my $cache_dir = bz_locations()->{'template_cache'}; - my $container = "$cache_dir$abs_root"; - mkpath($container); - my $target = "$cache_dir/$dir_name"; - # Check if the directory exists, because if there are no extensions, - # there won't be an "data/template/extensions" directory to link to. - if (-d $target) { - # We use abs2rel so that the symlink will look like - # "../../../../template" which works, while just - # "data/template/template/" doesn't work. - my $relative_target = File::Spec->abs2rel($target, $container); - - my $link_name = "$container/$dir_name"; - symlink($relative_target, $link_name) - or warn "Could not make $link_name a symlink to $relative_target: $!"; - } + my $dir_to_symlink = shift; + + my $abs_path = abs_path($dir_to_symlink); + + # If $dir_to_symlink is already an absolute path (as might happen + # with packagers who set $libpath to an absolute path), then we don't + # need to do this symlink. + return if ($abs_path eq $dir_to_symlink); + + my $abs_root = dirname($abs_path); + my $dir_name = basename($abs_path); + my $cache_dir = bz_locations()->{'template_cache'}; + my $container = "$cache_dir$abs_root"; + mkpath($container); + my $target = "$cache_dir/$dir_name"; + + # Check if the directory exists, because if there are no extensions, + # there won't be an "data/template/extensions" directory to link to. + if (-d $target) { + + # We use abs2rel so that the symlink will look like + # "../../../../template" which works, while just + # "data/template/template/" doesn't work. + my $relative_target = File::Spec->abs2rel($target, $container); + + my $link_name = "$container/$dir_name"; + symlink($relative_target, $link_name) + or warn "Could not make $link_name a symlink to $relative_target: $!"; + } } 1; diff --git a/Bugzilla/Template/Context.pm b/Bugzilla/Template/Context.pm index 470e6a9ee..01c8c5981 100644 --- a/Bugzilla/Template/Context.pm +++ b/Bugzilla/Template/Context.pm @@ -18,23 +18,24 @@ use Bugzilla::Hook; use Scalar::Util qw(blessed); sub process { - my $self = shift; - # We don't want to run the template_before_process hook for - # template hooks (but we do want it to run if a hook calls - # PROCESS inside itself). The problem is that the {component}->{name} of - # hooks is unreliable--sometimes it starts with ./ and it's the - # full path to the hook template, and sometimes it's just the relative - # name (like hook/global/field-descs-end.none.tmpl). Also, calling - # template_before_process for hook templates doesn't seem too useful, - # because that's already part of the extension and they should be able - # to modify their hook if they want (or just modify the variables in the - # calling template). - if (not delete $self->{bz_in_hook}) { - $self->{bz_in_process} = 1; - } - my $result = $self->SUPER::process(@_); - delete $self->{bz_in_process}; - return $result; + my $self = shift; + + # We don't want to run the template_before_process hook for + # template hooks (but we do want it to run if a hook calls + # PROCESS inside itself). The problem is that the {component}->{name} of + # hooks is unreliable--sometimes it starts with ./ and it's the + # full path to the hook template, and sometimes it's just the relative + # name (like hook/global/field-descs-end.none.tmpl). Also, calling + # template_before_process for hook templates doesn't seem too useful, + # because that's already part of the extension and they should be able + # to modify their hook if they want (or just modify the variables in the + # calling template). + if (not delete $self->{bz_in_hook}) { + $self->{bz_in_process} = 1; + } + my $result = $self->SUPER::process(@_); + delete $self->{bz_in_process}; + return $result; } # This method is called by Template-Toolkit exactly once per template or @@ -46,58 +47,59 @@ sub process { # in the PROCESS or INCLUDE directive haven't been set, and if we're # in an INCLUDE, the stash is not yet localized during process(). sub stash { - my $self = shift; - my $stash = $self->SUPER::stash(@_); - - my $name = $stash->{component}->{name}; - my $pre_process = $self->config->{PRE_PROCESS}; - - # Checking bz_in_process tells us that we were indeed called as part of a - # Context::process, and not at some other point. - # - # Checking $name makes sure that we're processing a file, and not just a - # block, by checking that the name has a period in it. We don't allow - # blocks because their names are too unreliable--an extension could have - # a block with the same name, or multiple files could have a same-named - # block, and then your extension would malfunction. - # - # We also make sure that we don't run, ever, during the PRE_PROCESS - # templates, because if somebody calls Throw*Error globally inside of - # template_before_process, that causes an infinite recursion into - # the PRE_PROCESS templates (because Bugzilla, while inside - # global/intialize.none.tmpl, loads the template again to create the - # template object for Throw*Error). - # - # Checking Bugzilla::Hook::in prevents infinite recursion on this hook. - if ($self->{bz_in_process} and $name =~ /\./ - and !grep($_ eq $name, @$pre_process) - and !Bugzilla::Hook::in('template_before_process')) - { - Bugzilla::Hook::process("template_before_process", - { vars => $stash, context => $self, - file => $name }); - } - - # This prevents other calls to stash() that might somehow happen - # later in the file from also triggering the hook. - delete $self->{bz_in_process}; - - return $stash; + my $self = shift; + my $stash = $self->SUPER::stash(@_); + + my $name = $stash->{component}->{name}; + my $pre_process = $self->config->{PRE_PROCESS}; + + # Checking bz_in_process tells us that we were indeed called as part of a + # Context::process, and not at some other point. + # + # Checking $name makes sure that we're processing a file, and not just a + # block, by checking that the name has a period in it. We don't allow + # blocks because their names are too unreliable--an extension could have + # a block with the same name, or multiple files could have a same-named + # block, and then your extension would malfunction. + # + # We also make sure that we don't run, ever, during the PRE_PROCESS + # templates, because if somebody calls Throw*Error globally inside of + # template_before_process, that causes an infinite recursion into + # the PRE_PROCESS templates (because Bugzilla, while inside + # global/intialize.none.tmpl, loads the template again to create the + # template object for Throw*Error). + # + # Checking Bugzilla::Hook::in prevents infinite recursion on this hook. + if ( $self->{bz_in_process} + and $name =~ /\./ + and !grep($_ eq $name, @$pre_process) + and !Bugzilla::Hook::in('template_before_process')) + { + Bugzilla::Hook::process("template_before_process", + {vars => $stash, context => $self, file => $name}); + } + + # This prevents other calls to stash() that might somehow happen + # later in the file from also triggering the hook. + delete $self->{bz_in_process}; + + return $stash; } sub filter { - my ($self, $name, $args) = @_; - # If we pass an alias for the filter name, the filter code is cached - # instead of looking for it at each call. - # If the filter has arguments, then we can't cache it. - $self->SUPER::filter($name, $args, $args ? undef : $name); + my ($self, $name, $args) = @_; + + # If we pass an alias for the filter name, the filter code is cached + # instead of looking for it at each call. + # If the filter has arguments, then we can't cache it. + $self->SUPER::filter($name, $args, $args ? undef : $name); } # We need a DESTROY sub for the same reason that Bugzilla::CGI does. sub DESTROY { - my $self = shift; - $self->SUPER::DESTROY(@_); -}; + my $self = shift; + $self->SUPER::DESTROY(@_); +} 1; diff --git a/Bugzilla/Template/Plugin/Bugzilla.pm b/Bugzilla/Template/Plugin/Bugzilla.pm index 806dd903b..0734fb942 100644 --- a/Bugzilla/Template/Plugin/Bugzilla.pm +++ b/Bugzilla/Template/Plugin/Bugzilla.pm @@ -16,20 +16,20 @@ use parent qw(Template::Plugin); use Bugzilla; sub new { - my ($class, $context) = @_; + my ($class, $context) = @_; - return bless {}, $class; + return bless {}, $class; } sub AUTOLOAD { - my $class = shift; - our $AUTOLOAD; + my $class = shift; + our $AUTOLOAD; - $AUTOLOAD =~ s/^.*:://; + $AUTOLOAD =~ s/^.*:://; - return if $AUTOLOAD eq 'DESTROY'; + return if $AUTOLOAD eq 'DESTROY'; - return Bugzilla->$AUTOLOAD(@_); + return Bugzilla->$AUTOLOAD(@_); } 1; diff --git a/Bugzilla/Template/Plugin/Hook.pm b/Bugzilla/Template/Plugin/Hook.pm index 669c77614..c57db4223 100644 --- a/Bugzilla/Template/Plugin/Hook.pm +++ b/Bugzilla/Template/Plugin/Hook.pm @@ -14,81 +14,80 @@ use warnings; use parent qw(Template::Plugin); use Bugzilla::Constants; -use Bugzilla::Install::Util qw(template_include_path); +use Bugzilla::Install::Util qw(template_include_path); use Bugzilla::Util; use Bugzilla::Error; use File::Spec; sub new { - my ($class, $context) = @_; - return bless { _CONTEXT => $context }, $class; + my ($class, $context) = @_; + return bless {_CONTEXT => $context}, $class; } sub _context { return $_[0]->{_CONTEXT} } sub process { - my ($self, $hook_name, $template) = @_; - my $context = $self->_context(); - $template ||= $context->stash->{component}->{name}; - - # sanity check: - if (!$template =~ /[\w\.\/\-_\\]+/) { - ThrowCodeError('template_invalid', { name => $template }); - } - - my (undef, $path, $filename) = File::Spec->splitpath($template); - $path ||= ''; - $filename =~ m/(.+)\.(.+)\.tmpl$/; - my $template_name = $1; - my $type = $2; - - # Hooks are named like this: - my $extension_template = "$path$template_name-$hook_name.$type.tmpl"; - - # Get the hooks out of the cache if they exist. Otherwise, read them - # from the disk. - my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {}; - my $lang = $context->{bz_language} || ''; - $cache->{"${lang}__$extension_template"} - ||= $self->_get_hooks($extension_template); - - # process() accepts an arrayref of templates, so we just pass the whole - # arrayref. - $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context - return $context->process($cache->{"${lang}__$extension_template"}); + my ($self, $hook_name, $template) = @_; + my $context = $self->_context(); + $template ||= $context->stash->{component}->{name}; + + # sanity check: + if (!$template =~ /[\w\.\/\-_\\]+/) { + ThrowCodeError('template_invalid', {name => $template}); + } + + my (undef, $path, $filename) = File::Spec->splitpath($template); + $path ||= ''; + $filename =~ m/(.+)\.(.+)\.tmpl$/; + my $template_name = $1; + my $type = $2; + + # Hooks are named like this: + my $extension_template = "$path$template_name-$hook_name.$type.tmpl"; + + # Get the hooks out of the cache if they exist. Otherwise, read them + # from the disk. + my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {}; + my $lang = $context->{bz_language} || ''; + $cache->{"${lang}__$extension_template"} + ||= $self->_get_hooks($extension_template); + + # process() accepts an arrayref of templates, so we just pass the whole + # arrayref. + $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context + return $context->process($cache->{"${lang}__$extension_template"}); } sub _get_hooks { - my ($self, $extension_template) = @_; - - my $template_sets = $self->_template_hook_include_path(); - my @hooks; - foreach my $dir_set (@$template_sets) { - foreach my $template_dir (@$dir_set) { - my $file = "$template_dir/hook/$extension_template"; - if (-e $file) { - my $template = $self->_context->template($file); - push(@hooks, $template); - # Don't run the hook for more than one language. - last; - } - } + my ($self, $extension_template) = @_; + + my $template_sets = $self->_template_hook_include_path(); + my @hooks; + foreach my $dir_set (@$template_sets) { + foreach my $template_dir (@$dir_set) { + my $file = "$template_dir/hook/$extension_template"; + if (-e $file) { + my $template = $self->_context->template($file); + push(@hooks, $template); + + # Don't run the hook for more than one language. + last; + } } + } - return \@hooks; + return \@hooks; } sub _template_hook_include_path { - my $self = shift; - my $cache = Bugzilla->request_cache; - my $language = $self->_context->{bz_language} || ''; - my $cache_key = "template_plugin_hook_include_path_$language"; - $cache->{$cache_key} ||= template_include_path({ - language => $language, - hook => 1, - }); - return $cache->{$cache_key}; + my $self = shift; + my $cache = Bugzilla->request_cache; + my $language = $self->_context->{bz_language} || ''; + my $cache_key = "template_plugin_hook_include_path_$language"; + $cache->{$cache_key} + ||= template_include_path({language => $language, hook => 1,}); + return $cache->{$cache_key}; } 1; diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm index 28122e818..62106c5e5 100644 --- a/Bugzilla/Token.pm +++ b/Bugzilla/Token.pm @@ -25,8 +25,8 @@ use Digest::SHA qw(hmac_sha256_base64); use parent qw(Exporter); @Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token - check_token_data delete_token - issue_hash_token check_hash_token); + check_token_data delete_token + issue_hash_token check_hash_token); use constant SEND_NOW => 1; @@ -36,394 +36,420 @@ use constant SEND_NOW => 1; # Create a token used for internal API authentication sub issue_api_token { - # Generates a random token, adds it to the tokens table if one does not - # already exist, and returns the token to the caller. - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my ($token) = $dbh->selectrow_array(" + + # Generates a random token, adds it to the tokens table if one does not + # already exist, and returns the token to the caller. + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my ($token) = $dbh->selectrow_array(" SELECT token FROM tokens WHERE userid = ? AND tokentype = 'api_token' - AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()", - undef, $user->id); - return $token // _create_token($user->id, 'api_token', ''); + AND (" + . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') + . ") > NOW()", undef, $user->id); + return $token // _create_token($user->id, 'api_token', ''); } # Creates and sends a token to create a new user account. # It assumes that the login has the correct format and is not already in use. sub issue_new_user_account_token { - my $login_name = shift; - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - my $vars = {}; - - # Is there already a pending request for this login name? If yes, do not throw - # an error because the user may have lost their email with the token inside. - # But to prevent using this way to mailbomb an email address, make sure - # the last request is old enough before sending a new email (default: 10 minutes). - - my $pending_requests = $dbh->selectrow_array( - 'SELECT COUNT(*) + my $login_name = shift; + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + my $vars = {}; + +# Is there already a pending request for this login name? If yes, do not throw +# an error because the user may have lost their email with the token inside. +# But to prevent using this way to mailbomb an email address, make sure +# the last request is old enough before sending a new email (default: 10 minutes). + + my $pending_requests = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM tokens WHERE tokentype = ? AND ' . $dbh->sql_istrcmp('eventdata', '?') . ' AND issuedate > ' - . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), - undef, ('account', $login_name)); + . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), + undef, ('account', $login_name) + ); - ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) if $pending_requests; + ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) + if $pending_requests; - my ($token, $token_ts) = _create_token(undef, 'account', $login_name); + my ($token, $token_ts) = _create_token(undef, 'account', $login_name); - $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'}; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - $vars->{'token'} = $token; + $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'}; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + $vars->{'token'} = $token; - my $message; - $template->process('account/email/request-new.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + my $message; + $template->process('account/email/request-new.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - # In 99% of cases, the user getting the confirmation email is the same one - # who made the request, and so it is reasonable to send the email in the same - # language used to view the "Create a New Account" page (we cannot use their - # user prefs as the user has no account yet!). - MessageToMTA($message, SEND_NOW); + # In 99% of cases, the user getting the confirmation email is the same one + # who made the request, and so it is reasonable to send the email in the same + # language used to view the "Create a New Account" page (we cannot use their + # user prefs as the user has no account yet!). + MessageToMTA($message, SEND_NOW); } sub IssueEmailChangeToken { - my $new_email = shift; - my $user = Bugzilla->user; + my $new_email = shift; + my $user = Bugzilla->user; - my ($token, $token_ts) = _create_token($user->id, 'emailold', $user->login . ":$new_email"); - my $newtoken = _create_token($user->id, 'emailnew', $user->login . ":$new_email"); + my ($token, $token_ts) + = _create_token($user->id, 'emailold', $user->login . ":$new_email"); + my $newtoken + = _create_token($user->id, 'emailnew', $user->login . ":$new_email"); - # Mail the user the token along with instructions for using it. + # Mail the user the token along with instructions for using it. - my $template = Bugzilla->template_inner($user->setting('lang')); - my $vars = {}; + my $template = Bugzilla->template_inner($user->setting('lang')); + my $vars = {}; - $vars->{'newemailaddress'} = $new_email . Bugzilla->params->{'emailsuffix'}; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + $vars->{'newemailaddress'} = $new_email . Bugzilla->params->{'emailsuffix'}; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - # First send an email to the new address. If this one doesn't exist, - # then the whole process must stop immediately. This means the email must - # be sent immediately and must not be stored in the queue. - $vars->{'token'} = $newtoken; + # First send an email to the new address. If this one doesn't exist, + # then the whole process must stop immediately. This means the email must + # be sent immediately and must not be stored in the queue. + $vars->{'token'} = $newtoken; - my $message; - $template->process('account/email/change-new.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + my $message; + $template->process('account/email/change-new.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - MessageToMTA($message, SEND_NOW); + MessageToMTA($message, SEND_NOW); - # If we come here, then the new address exists. We now email the current - # address, but we don't want to stop the process if it no longer exists, - # to give a chance to the user to confirm the email address change. - $vars->{'token'} = $token; + # If we come here, then the new address exists. We now email the current + # address, but we don't want to stop the process if it no longer exists, + # to give a chance to the user to confirm the email address change. + $vars->{'token'} = $token; - $message = ''; - $template->process('account/email/change-old.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + $message = ''; + $template->process('account/email/change-old.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - eval { MessageToMTA($message, SEND_NOW); }; + eval { MessageToMTA($message, SEND_NOW); }; - # Give the user a chance to cancel the process even if he never got - # the email above. The token is required. - return $token; + # Give the user a chance to cancel the process even if he never got + # the email above. The token is required. + return $token; } # Generates a random token, adds it to the tokens table, and sends it # to the user with instructions for using it to change their password. sub IssuePasswordToken { - my $user = shift; - my $dbh = Bugzilla->dbh; + my $user = shift; + my $dbh = Bugzilla->dbh; - my $too_soon = $dbh->selectrow_array( - 'SELECT 1 FROM tokens + my $too_soon = $dbh->selectrow_array( + 'SELECT 1 FROM tokens WHERE userid = ? AND tokentype = ? - AND issuedate > ' - . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), - undef, ($user->id, 'password')); + AND issuedate > ' + . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'), + undef, ($user->id, 'password') + ); - ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon; + ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon; - my $ip_addr = remote_ip(); - my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr); + my $ip_addr = remote_ip(); + my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr); - # Mail the user the token along with instructions for using it. - my $template = Bugzilla->template_inner($user->setting('lang')); - my $vars = {}; + # Mail the user the token along with instructions for using it. + my $template = Bugzilla->template_inner($user->setting('lang')); + my $vars = {}; - $vars->{'token'} = $token; - $vars->{'ip_addr'} = $ip_addr; - $vars->{'emailaddress'} = $user->email; - $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); - # The user is not logged in (else they wouldn't request a new password). - # So we have to pass this information to the template. - $vars->{'timezone'} = $user->timezone; - - my $message = ""; - $template->process("account/password/forgotten-password.txt.tmpl", - $vars, \$message) - || ThrowTemplateError($template->error()); + $vars->{'token'} = $token; + $vars->{'ip_addr'} = $ip_addr; + $vars->{'emailaddress'} = $user->email; + $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400); + + # The user is not logged in (else they wouldn't request a new password). + # So we have to pass this information to the template. + $vars->{'timezone'} = $user->timezone; - MessageToMTA($message); + my $message = ""; + $template->process("account/password/forgotten-password.txt.tmpl", + $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); } sub issue_session_token { - # Generates a random token, adds it to the tokens table, and returns - # the token to the caller. - my $data = shift; - return _create_token(Bugzilla->user->id, 'session', $data); + # Generates a random token, adds it to the tokens table, and returns + # the token to the caller. + + my $data = shift; + return _create_token(Bugzilla->user->id, 'session', $data); } sub issue_hash_token { - my ($data, $time) = @_; - $data ||= []; - $time ||= time(); - - # For the user ID, use the actual ID if the user is logged in. - # Otherwise, use the remote IP, in case this is for something - # such as creating an account or logging in. - my $user_id = Bugzilla->user->id || remote_ip(); - - # The concatenated string is of the form - # token creation time + user ID (either ID or remote IP) + data - my @args = ($time, $user_id, @$data); - - my $token = join('*', @args); - # Wide characters cause Digest::SHA to die. - if (Bugzilla->params->{'utf8'}) { - utf8::encode($token) if utf8::is_utf8($token); - } - $token = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'}); - $token =~ s/\+/-/g; - $token =~ s/\//_/g; - - # Prepend the token creation time, unencrypted, so that the token - # lifetime can be validated. - return $time . '-' . $token; + my ($data, $time) = @_; + $data ||= []; + $time ||= time(); + + # For the user ID, use the actual ID if the user is logged in. + # Otherwise, use the remote IP, in case this is for something + # such as creating an account or logging in. + my $user_id = Bugzilla->user->id || remote_ip(); + + # The concatenated string is of the form + # token creation time + user ID (either ID or remote IP) + data + my @args = ($time, $user_id, @$data); + + my $token = join('*', @args); + + # Wide characters cause Digest::SHA to die. + if (Bugzilla->params->{'utf8'}) { + utf8::encode($token) if utf8::is_utf8($token); + } + $token + = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'}); + $token =~ s/\+/-/g; + $token =~ s/\//_/g; + + # Prepend the token creation time, unencrypted, so that the token + # lifetime can be validated. + return $time . '-' . $token; } sub check_hash_token { - my ($token, $data) = @_; - $data ||= []; - my ($time, $expected_token); - - if ($token) { - ($time, undef) = split(/-/, $token); - # Regenerate the token based on the information we have. - $expected_token = issue_hash_token($data, $time); - } + my ($token, $data) = @_; + $data ||= []; + my ($time, $expected_token); - if (!$token - || $expected_token ne $token - || time() - $time > MAX_TOKEN_AGE * 86400) - { - my $template = Bugzilla->template; - my $vars = {}; - $vars->{'script_name'} = basename($0); - $vars->{'token'} = issue_hash_token($data); - $vars->{'reason'} = (!$token) ? 'missing_token' : - ($expected_token ne $token) ? 'invalid_token' : - 'expired_token'; - print Bugzilla->cgi->header(); - $template->process('global/confirm-action.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - } + if ($token) { + ($time, undef) = split(/-/, $token); + + # Regenerate the token based on the information we have. + $expected_token = issue_hash_token($data, $time); + } + + if (!$token + || $expected_token ne $token + || time() - $time > MAX_TOKEN_AGE * 86400) + { + my $template = Bugzilla->template; + my $vars = {}; + $vars->{'script_name'} = basename($0); + $vars->{'token'} = issue_hash_token($data); + $vars->{'reason'} + = (!$token) ? 'missing_token' + : ($expected_token ne $token) ? 'invalid_token' + : 'expired_token'; + print Bugzilla->cgi->header(); + $template->process('global/confirm-action.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } - # If we come here, then the token is valid and not too old. - return 1; + # If we come here, then the token is valid and not too old. + return 1; } sub CleanTokenTable { - my $dbh = Bugzilla->dbh; - $dbh->do('DELETE FROM tokens - WHERE ' . $dbh->sql_to_days('NOW()') . ' - ' . - $dbh->sql_to_days('issuedate') . ' >= ?', - undef, MAX_TOKEN_AGE); + my $dbh = Bugzilla->dbh; + $dbh->do( + 'DELETE FROM tokens + WHERE ' + . $dbh->sql_to_days('NOW()') . ' - ' + . $dbh->sql_to_days('issuedate') + . ' >= ?', undef, MAX_TOKEN_AGE + ); } sub GenerateUniqueToken { - # Generates a unique random token. Uses generate_random_password - # for the tokens themselves and checks uniqueness by searching for - # the token in the "tokens" table. Gives up if it can't come up - # with a token after about one hundred tries. - my ($table, $column) = @_; - - my $token; - my $duplicate = 1; - my $tries = 0; - $table ||= "tokens"; - $column ||= "token"; - - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?"); - - while ($duplicate) { - ++$tries; - if ($tries > 100) { - ThrowCodeError("token_generation_error"); - } - $token = generate_random_password(); - $sth->execute($token); - $duplicate = $sth->fetchrow_array; + + # Generates a unique random token. Uses generate_random_password + # for the tokens themselves and checks uniqueness by searching for + # the token in the "tokens" table. Gives up if it can't come up + # with a token after about one hundred tries. + my ($table, $column) = @_; + + my $token; + my $duplicate = 1; + my $tries = 0; + $table ||= "tokens"; + $column ||= "token"; + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?"); + + while ($duplicate) { + ++$tries; + if ($tries > 100) { + ThrowCodeError("token_generation_error"); } - return $token; + $token = generate_random_password(); + $sth->execute($token); + $duplicate = $sth->fetchrow_array; + } + return $token; } # Cancels a previously issued token and notifies the user. # This should only happen when the user accidentally makes a token request # or when a malicious hacker makes a token request on behalf of a user. sub Cancel { - my ($token, $cancelaction, $vars) = @_; - my $dbh = Bugzilla->dbh; - $vars ||= {}; - - # Get information about the token being canceled. - trick_taint($token); - my ($db_token, $issuedate, $tokentype, $eventdata, $userid) = - $dbh->selectrow_array('SELECT token, ' . $dbh->sql_date_format('issuedate') . ', + my ($token, $cancelaction, $vars) = @_; + my $dbh = Bugzilla->dbh; + $vars ||= {}; + + # Get information about the token being canceled. + trick_taint($token); + my ($db_token, $issuedate, $tokentype, $eventdata, $userid) + = $dbh->selectrow_array( + 'SELECT token, ' + . $dbh->sql_date_format('issuedate') . ', tokentype, eventdata, userid FROM tokens - WHERE token = ?', - undef, $token); - - # Some DBs such as MySQL are case-insensitive by default so we do - # a quick comparison to make sure the tokens are indeed the same. - (defined $db_token && $db_token eq $token) - || ThrowCodeError("cancel_token_does_not_exist"); - - # If we are canceling the creation of a new user account, then there - # is no entry in the 'profiles' table. - my $user = new Bugzilla::User($userid); - - $vars->{'emailaddress'} = $userid ? $user->email : $eventdata; - $vars->{'remoteaddress'} = remote_ip(); - $vars->{'token'} = $token; - $vars->{'tokentype'} = $tokentype; - $vars->{'issuedate'} = $issuedate; - # The user is probably not logged in. - # So we have to pass this information to the template. - $vars->{'timezone'} = $user->timezone; - $vars->{'eventdata'} = $eventdata; - $vars->{'cancelaction'} = $cancelaction; - - # Notify the user via email about the cancellation. - my $template = Bugzilla->template_inner($user->setting('lang')); - - my $message; - $template->process("account/cancel-token.txt.tmpl", $vars, \$message) - || ThrowTemplateError($template->error()); + WHERE token = ?', undef, $token + ); - MessageToMTA($message); + # Some DBs such as MySQL are case-insensitive by default so we do + # a quick comparison to make sure the tokens are indeed the same. + (defined $db_token && $db_token eq $token) + || ThrowCodeError("cancel_token_does_not_exist"); - # Delete the token from the database. - delete_token($token); + # If we are canceling the creation of a new user account, then there + # is no entry in the 'profiles' table. + my $user = new Bugzilla::User($userid); + + $vars->{'emailaddress'} = $userid ? $user->email : $eventdata; + $vars->{'remoteaddress'} = remote_ip(); + $vars->{'token'} = $token; + $vars->{'tokentype'} = $tokentype; + $vars->{'issuedate'} = $issuedate; + + # The user is probably not logged in. + # So we have to pass this information to the template. + $vars->{'timezone'} = $user->timezone; + $vars->{'eventdata'} = $eventdata; + $vars->{'cancelaction'} = $cancelaction; + + # Notify the user via email about the cancellation. + my $template = Bugzilla->template_inner($user->setting('lang')); + + my $message; + $template->process("account/cancel-token.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + + # Delete the token from the database. + delete_token($token); } sub DeletePasswordTokens { - my ($userid, $reason) = @_; - my $dbh = Bugzilla->dbh; + my ($userid, $reason) = @_; + my $dbh = Bugzilla->dbh; - detaint_natural($userid); - my $tokens = $dbh->selectcol_arrayref('SELECT token FROM tokens + detaint_natural($userid); + my $tokens = $dbh->selectcol_arrayref( + 'SELECT token FROM tokens WHERE userid = ? AND tokentype = ?', - undef, ($userid, 'password')); + undef, ($userid, 'password') + ); - foreach my $token (@$tokens) { - Bugzilla::Token::Cancel($token, $reason); - } + foreach my $token (@$tokens) { + Bugzilla::Token::Cancel($token, $reason); + } } -# Returns an email change token if the user has one. +# Returns an email change token if the user has one. sub HasEmailChangeToken { - my $userid = shift; - my $dbh = Bugzilla->dbh; + my $userid = shift; + my $dbh = Bugzilla->dbh; - my $token = $dbh->selectrow_array('SELECT token FROM tokens + my $token = $dbh->selectrow_array( + 'SELECT token FROM tokens WHERE userid = ? - AND (tokentype = ? OR tokentype = ?) ' . - $dbh->sql_limit(1), - undef, ($userid, 'emailnew', 'emailold')); - return $token; + AND (tokentype = ? OR tokentype = ?) ' + . $dbh->sql_limit(1), undef, ($userid, 'emailnew', 'emailold') + ); + return $token; } # Returns the userid, issuedate and eventdata for the specified token sub GetTokenData { - my ($token) = @_; - my $dbh = Bugzilla->dbh; + my ($token) = @_; + my $dbh = Bugzilla->dbh; - return unless defined $token; - $token = clean_text($token); - trick_taint($token); + return unless defined $token; + $token = clean_text($token); + trick_taint($token); - my @token_data = $dbh->selectrow_array( - "SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata, tokentype + my @token_data = $dbh->selectrow_array( + "SELECT token, userid, " + . $dbh->sql_date_format('issuedate') + . ", eventdata, tokentype FROM tokens - WHERE token = ?", undef, $token); + WHERE token = ?", undef, $token + ); - # Some DBs such as MySQL are case-insensitive by default so we do - # a quick comparison to make sure the tokens are indeed the same. - my $db_token = shift @token_data; - return undef if (!defined $db_token || $db_token ne $token); + # Some DBs such as MySQL are case-insensitive by default so we do + # a quick comparison to make sure the tokens are indeed the same. + my $db_token = shift @token_data; + return undef if (!defined $db_token || $db_token ne $token); - return @token_data; + return @token_data; } # Deletes specified token sub delete_token { - my ($token) = @_; - my $dbh = Bugzilla->dbh; + my ($token) = @_; + my $dbh = Bugzilla->dbh; - return unless defined $token; - trick_taint($token); + return unless defined $token; + trick_taint($token); - $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token); + $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token); } # Given a token, makes sure it comes from the currently logged in user # and match the expected event. Returns 1 on success, else displays a warning. sub check_token_data { - my ($token, $expected_action, $alternate_script) = @_; - my $user = Bugzilla->user; - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - - my ($creator_id, $date, $token_action) = GetTokenData($token); - unless ($creator_id - && $creator_id == $user->id - && $token_action eq $expected_action) - { - # Something is going wrong. Ask confirmation before processing. - # It is possible that someone tried to trick an administrator. - # In this case, we want to know their name! - require Bugzilla::User; - - my $vars = {}; - $vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity; - $vars->{'token_action'} = $token_action; - $vars->{'expected_action'} = $expected_action; - $vars->{'script_name'} = basename($0); - $vars->{'alternate_script'} = $alternate_script || basename($0); - - # Now is a good time to remove old tokens from the DB. - CleanTokenTable(); - - # If no token was found, create a valid token for the given action. - unless ($creator_id) { - $token = issue_session_token($expected_action); - $cgi->param('token', $token); - } - - print $cgi->header(); - - $template->process('admin/confirm-action.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; + my ($token, $expected_action, $alternate_script) = @_; + my $user = Bugzilla->user; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + + my ($creator_id, $date, $token_action) = GetTokenData($token); + unless ($creator_id + && $creator_id == $user->id + && $token_action eq $expected_action) + { + # Something is going wrong. Ask confirmation before processing. + # It is possible that someone tried to trick an administrator. + # In this case, we want to know their name! + require Bugzilla::User; + + my $vars = {}; + $vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity; + $vars->{'token_action'} = $token_action; + $vars->{'expected_action'} = $expected_action; + $vars->{'script_name'} = basename($0); + $vars->{'alternate_script'} = $alternate_script || basename($0); + + # Now is a good time to remove old tokens from the DB. + CleanTokenTable(); + + # If no token was found, create a valid token for the given action. + unless ($creator_id) { + $token = issue_session_token($expected_action); + $cgi->param('token', $token); } - return 1; + + print $cgi->header(); + + $template->process('admin/confirm-action.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } + return 1; } ################################################################################ @@ -433,34 +459,38 @@ sub check_token_data { # Generates a unique token and inserts it into the database # Returns the token and the token timestamp sub _create_token { - my ($userid, $tokentype, $eventdata) = @_; - my $dbh = Bugzilla->dbh; + my ($userid, $tokentype, $eventdata) = @_; + my $dbh = Bugzilla->dbh; - detaint_natural($userid) if defined $userid; - trick_taint($tokentype); - trick_taint($eventdata); + detaint_natural($userid) if defined $userid; + trick_taint($tokentype); + trick_taint($eventdata); - my $is_shadow = Bugzilla->is_shadow_db; - $dbh = Bugzilla->switch_to_main_db() if $is_shadow; + my $is_shadow = Bugzilla->is_shadow_db; + $dbh = Bugzilla->switch_to_main_db() if $is_shadow; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my $token = GenerateUniqueToken(); + my $token = GenerateUniqueToken(); - $dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata) - VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata)); + $dbh->do( + "INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata) + VALUES (?, NOW(), ?, ?, ?)", undef, + ($userid, $token, $tokentype, $eventdata) + ); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - if (wantarray) { - my (undef, $token_ts, undef) = GetTokenData($token); - $token_ts = str2time($token_ts); - Bugzilla->switch_to_shadow_db() if $is_shadow; - return ($token, $token_ts); - } else { - Bugzilla->switch_to_shadow_db() if $is_shadow; - return $token; - } + if (wantarray) { + my (undef, $token_ts, undef) = GetTokenData($token); + $token_ts = str2time($token_ts); + Bugzilla->switch_to_shadow_db() if $is_shadow; + return ($token, $token_ts); + } + else { + Bugzilla->switch_to_shadow_db() if $is_shadow; + return $token; + } } 1; diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm index 72a7108a8..9f9288162 100644 --- a/Bugzilla/Update.pm +++ b/Bugzilla/Update.pm @@ -13,149 +13,159 @@ use warnings; use Bugzilla::Constants; -use constant TIME_INTERVAL => 86400; # Default is one day, in seconds. -use constant TIMEOUT => 5; # Number of seconds before timeout. +use constant TIME_INTERVAL => 86400; # Default is one day, in seconds. +use constant TIMEOUT => 5; # Number of seconds before timeout. # Look for new releases and notify logged in administrators about them. sub get_notifications { - return if !Bugzilla->feature('updates'); - return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled'); - - my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE; - # Update the local XML file if this one doesn't exist or if - # the last modification time (stat[9]) is older than TIME_INTERVAL. - if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) { - unlink $local_file; # Make sure the old copy is away. - return { 'error' => 'no_update' } if (-e $local_file); - - my $error = _synchronize_data(); - # If an error is returned, leave now. - return $error if $error; + return if !Bugzilla->feature('updates'); + return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled'); + + my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE; + + # Update the local XML file if this one doesn't exist or if + # the last modification time (stat[9]) is older than TIME_INTERVAL. + if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) { + unlink $local_file; # Make sure the old copy is away. + return {'error' => 'no_update'} if (-e $local_file); + + my $error = _synchronize_data(); + + # If an error is returned, leave now. + return $error if $error; + } + + # If we cannot access the local XML file, ignore it. + return {'error' => 'no_access'} unless (-r $local_file); + + my $twig = XML::Twig->new(); + $twig->safe_parsefile($local_file); + + # If the XML file is invalid, return. + return {'error' => 'corrupted'} if $@; + my $root = $twig->root; + + my @releases; + foreach my $branch ($root->children('branch')) { + my $release = { + 'branch_ver' => $branch->{'att'}->{'id'}, + 'latest_ver' => $branch->{'att'}->{'vid'}, + 'status' => $branch->{'att'}->{'status'}, + 'url' => $branch->{'att'}->{'url'}, + 'date' => $branch->{'att'}->{'date'} + }; + push(@releases, $release); + } + + # On which branch is the current installation running? + my @current_version + = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + + my @release; + if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') { + @release = grep { $_->{'status'} eq 'development' } @releases; + + # If there is no development snapshot available, then we are in the + # process of releasing a release candidate. That's the release we want. + unless (scalar(@release)) { + @release = grep { $_->{'status'} eq 'release-candidate' } @releases; } - - # If we cannot access the local XML file, ignore it. - return { 'error' => 'no_access' } unless (-r $local_file); - - my $twig = XML::Twig->new(); - $twig->safe_parsefile($local_file); - # If the XML file is invalid, return. - return { 'error' => 'corrupted' } if $@; - my $root = $twig->root; - - my @releases; - foreach my $branch ($root->children('branch')) { - my $release = { - 'branch_ver' => $branch->{'att'}->{'id'}, - 'latest_ver' => $branch->{'att'}->{'vid'}, - 'status' => $branch->{'att'}->{'status'}, - 'url' => $branch->{'att'}->{'url'}, - 'date' => $branch->{'att'}->{'date'} - }; - push(@releases, $release); - } - - # On which branch is the current installation running? - my @current_version = - (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); - - my @release; - if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') { - @release = grep {$_->{'status'} eq 'development'} @releases; - # If there is no development snapshot available, then we are in the - # process of releasing a release candidate. That's the release we want. - unless (scalar(@release)) { - @release = grep {$_->{'status'} eq 'release-candidate'} @releases; - } - } - elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') { - @release = grep {$_->{'status'} eq 'stable'} @releases; - } - elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') { - # We want the latest stable version for the current branch. - # If we are running a development snapshot, we won't match anything. - my $branch_version = $current_version[0] . '.' . $current_version[1]; - - # We do a string comparison instead of a numerical one, because - # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older). - @release = grep {$_->{'branch_ver'} eq $branch_version} @releases; - - # If the branch is now closed, we should strongly suggest - # to upgrade to the latest stable release available. - if (scalar(@release) && $release[0]->{'status'} eq 'closed') { - @release = grep {$_->{'status'} eq 'stable'} @releases; - return {'data' => $release[0], 'deprecated' => $branch_version}; - } + } + elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') { + @release = grep { $_->{'status'} eq 'stable' } @releases; + } + elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') { + + # We want the latest stable version for the current branch. + # If we are running a development snapshot, we won't match anything. + my $branch_version = $current_version[0] . '.' . $current_version[1]; + + # We do a string comparison instead of a numerical one, because + # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older). + @release = grep { $_->{'branch_ver'} eq $branch_version } @releases; + + # If the branch is now closed, we should strongly suggest + # to upgrade to the latest stable release available. + if (scalar(@release) && $release[0]->{'status'} eq 'closed') { + @release = grep { $_->{'status'} eq 'stable' } @releases; + return {'data' => $release[0], 'deprecated' => $branch_version}; } - else { - # Unknown parameter. - return {'error' => 'unknown_parameter'}; - } - - # Return if no new release is available. - return unless scalar(@release); - - # Only notify the administrator if the latest version available - # is newer than the current one. - my @new_version = - ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); - - # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order - # to compare versions easily. - $current_version[2] = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1; - $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1; - - my $is_newer = _compare_versions(\@current_version, \@new_version); - return ($is_newer == 1) ? {'data' => $release[0]} : undef; + } + else { + # Unknown parameter. + return {'error' => 'unknown_parameter'}; + } + + # Return if no new release is available. + return unless scalar(@release); + + # Only notify the administrator if the latest version available + # is newer than the current one. + my @new_version + = ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + + # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order + # to compare versions easily. + $current_version[2] + = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1; + $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1; + + my $is_newer = _compare_versions(\@current_version, \@new_version); + return ($is_newer == 1) ? {'data' => $release[0]} : undef; } sub _synchronize_data { - my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE; - - my $ua = LWP::UserAgent->new(); - $ua->timeout(TIMEOUT); - $ua->protocols_allowed(['http', 'https']); - # If the URL of the proxy is given, use it, else get this information - # from the environment variable. - my $proxy_url = Bugzilla->params->{'proxy_url'}; - if ($proxy_url) { - $ua->proxy(['http', 'https'], $proxy_url); - } - else { - $ua->env_proxy; - } - my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) }; - - # $ua->mirror() forces the modification time of the local XML file - # to match the modification time of the remote one. - # So we have to update it manually to reflect that a newer version - # of the file has effectively been requested. This will avoid - # any new download for the next TIME_INTERVAL. - if (-e $local_file) { - # Try to alter its last modification time. - my $can_alter = utime(undef, undef, $local_file); - # This error should never happen. - $can_alter || return { 'error' => 'no_update' }; - } - elsif ($response && $response->is_error) { - # We have been unable to download the file. - return { 'error' => 'cannot_download', 'reason' => $response->status_line }; - } - else { - return { 'error' => 'no_write', 'reason' => $@ }; - } - - # Everything went well. - return 0; + my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE; + + my $ua = LWP::UserAgent->new(); + $ua->timeout(TIMEOUT); + $ua->protocols_allowed(['http', 'https']); + + # If the URL of the proxy is given, use it, else get this information + # from the environment variable. + my $proxy_url = Bugzilla->params->{'proxy_url'}; + if ($proxy_url) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy; + } + my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) }; + + # $ua->mirror() forces the modification time of the local XML file + # to match the modification time of the remote one. + # So we have to update it manually to reflect that a newer version + # of the file has effectively been requested. This will avoid + # any new download for the next TIME_INTERVAL. + if (-e $local_file) { + + # Try to alter its last modification time. + my $can_alter = utime(undef, undef, $local_file); + + # This error should never happen. + $can_alter || return {'error' => 'no_update'}; + } + elsif ($response && $response->is_error) { + + # We have been unable to download the file. + return {'error' => 'cannot_download', 'reason' => $response->status_line}; + } + else { + return {'error' => 'no_write', 'reason' => $@}; + } + + # Everything went well. + return 0; } sub _compare_versions { - my ($old_ver, $new_ver) = @_; - while (scalar(@$old_ver) && scalar(@$new_ver)) { - my $old = shift(@$old_ver) || 0; - my $new = shift(@$new_ver) || 0; - return $new <=> $old if ($new <=> $old); - } - return scalar(@$new_ver) <=> scalar(@$old_ver); + my ($old_ver, $new_ver) = @_; + while (scalar(@$old_ver) && scalar(@$new_ver)) { + my $old = shift(@$old_ver) || 0; + my $new = shift(@$new_ver) || 0; + return $new <=> $old if ($new <=> $old); + } + return scalar(@$new_ver) <=> scalar(@$old_ver); } diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 77e6cebb0..857b56801 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -33,9 +33,9 @@ use URI::QueryParam; use parent qw(Bugzilla::Object Exporter); @Bugzilla::User::EXPORT = qw(is_available_username - login_to_id validate_password validate_password_check - USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS - MATCH_SKIP_CONFIRM + login_to_id validate_password validate_password_check + USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS + MATCH_SKIP_CONFIRM ); ##################################################################### @@ -46,16 +46,16 @@ use constant USER_MATCH_MULTIPLE => -1; use constant USER_MATCH_FAILED => 0; use constant USER_MATCH_SUCCESS => 1; -use constant MATCH_SKIP_CONFIRM => 1; +use constant MATCH_SKIP_CONFIRM => 1; use constant DEFAULT_USER => { - 'userid' => 0, - 'realname' => '', - 'login_name' => '', - 'showmybugslink' => 0, - 'disabledtext' => '', - 'disable_mail' => 0, - 'is_enabled' => 1, + 'userid' => 0, + 'realname' => '', + 'login_name' => '', + 'showmybugslink' => 0, + 'disabledtext' => '', + 'disable_mail' => 0, + 'is_enabled' => 1, }; use constant DB_TABLE => 'profiles'; @@ -65,18 +65,19 @@ use constant DB_TABLE => 'profiles'; # Bugzilla::User used "name" for the realname field. This should be # fixed one day. sub DB_COLUMNS { - my $dbh = Bugzilla->dbh; - return ( - 'profiles.userid', - 'profiles.login_name', - 'profiles.realname', - 'profiles.mybugslink AS showmybugslink', - 'profiles.disabledtext', - 'profiles.disable_mail', - 'profiles.extern_id', - 'profiles.is_enabled', - $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date', + my $dbh = Bugzilla->dbh; + return ( + 'profiles.userid', + 'profiles.login_name', + 'profiles.realname', + 'profiles.mybugslink AS showmybugslink', + 'profiles.disabledtext', + 'profiles.disable_mail', + 'profiles.extern_id', + 'profiles.is_enabled', + $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date', ), + ; } use constant NAME_FIELD => 'login_name'; @@ -84,32 +85,30 @@ use constant ID_FIELD => 'userid'; use constant LIST_ORDER => NAME_FIELD; use constant VALIDATORS => { - cryptpassword => \&_check_password, - disable_mail => \&_check_disable_mail, - disabledtext => \&_check_disabledtext, - login_name => \&check_login_name, - realname => \&_check_realname, - extern_id => \&_check_extern_id, - is_enabled => \&_check_is_enabled, + cryptpassword => \&_check_password, + disable_mail => \&_check_disable_mail, + disabledtext => \&_check_disabledtext, + login_name => \&check_login_name, + realname => \&_check_realname, + extern_id => \&_check_extern_id, + is_enabled => \&_check_is_enabled, }; sub UPDATE_COLUMNS { - my $self = shift; - my @cols = qw( - disable_mail - disabledtext - login_name - realname - extern_id - is_enabled - ); - push(@cols, 'cryptpassword') if exists $self->{cryptpassword}; - return @cols; -}; + my $self = shift; + my @cols = qw( + disable_mail + disabledtext + login_name + realname + extern_id + is_enabled + ); + push(@cols, 'cryptpassword') if exists $self->{cryptpassword}; + return @cols; +} -use constant VALIDATOR_DEPENDENCIES => { - is_enabled => ['disabledtext'], -}; +use constant VALIDATOR_DEPENDENCIES => {is_enabled => ['disabledtext'],}; use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); @@ -118,129 +117,127 @@ use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); ################################################################################ sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; - - my $user = { %{ DEFAULT_USER() } }; - bless ($user, $class); - return $user unless $param; - - if (ref($param) eq 'HASH') { - if (defined $param->{extern_id}) { - $param = { condition => 'extern_id = ?' , values => [$param->{extern_id}] }; - $_[0] = $param; - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; + + my $user = {%{DEFAULT_USER()}}; + bless($user, $class); + return $user unless $param; + + if (ref($param) eq 'HASH') { + if (defined $param->{extern_id}) { + $param = {condition => 'extern_id = ?', values => [$param->{extern_id}]}; + $_[0] = $param; } - return $class->SUPER::new(@_); + } + return $class->SUPER::new(@_); } sub super_user { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; - my $user = { %{ DEFAULT_USER() } }; - $user->{groups} = [Bugzilla::Group->get_all]; - $user->{bless_groups} = [Bugzilla::Group->get_all]; - bless $user, $class; - return $user; + my $user = {%{DEFAULT_USER()}}; + $user->{groups} = [Bugzilla::Group->get_all]; + $user->{bless_groups} = [Bugzilla::Group->get_all]; + bless $user, $class; + return $user; } sub _update_groups { - my $self = shift; - my $group_changes = shift; - my $changes = shift; - my $dbh = Bugzilla->dbh; - - # Update group settings. - my $sth_add_mapping = $dbh->prepare( - qq{INSERT INTO user_group_map ( + my $self = shift; + my $group_changes = shift; + my $changes = shift; + my $dbh = Bugzilla->dbh; + + # Update group settings. + my $sth_add_mapping = $dbh->prepare( + qq{INSERT INTO user_group_map ( user_id, group_id, isbless, grant_type ) VALUES ( ?, ?, ?, ? ) - }); - my $sth_remove_mapping = $dbh->prepare( - qq{DELETE FROM user_group_map + } + ); + my $sth_remove_mapping = $dbh->prepare( + qq{DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = ? AND grant_type = ? - }); + } + ); - foreach my $is_bless (keys %$group_changes) { - my ($removed, $added) = @{$group_changes->{$is_bless}}; + foreach my $is_bless (keys %$group_changes) { + my ($removed, $added) = @{$group_changes->{$is_bless}}; - foreach my $group (@$removed) { - $sth_remove_mapping->execute( - $self->id, $group->id, $is_bless, GRANT_DIRECT - ); - } - foreach my $group (@$added) { - $sth_add_mapping->execute( - $self->id, $group->id, $is_bless, GRANT_DIRECT - ); - } + foreach my $group (@$removed) { + $sth_remove_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT); + } + foreach my $group (@$added) { + $sth_add_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT); + } - if (! $is_bless) { - my $query = qq{ + if (!$is_bless) { + my $query = qq{ INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue) VALUES ( ?, ?, now(), ?, ?, ?) }; - $dbh->do( - $query, undef, - $self->id, Bugzilla->user->id, - get_field_id('bug_group'), - join(', ', map { $_->name } @$removed), - join(', ', map { $_->name } @$added) - ); - } - else { - # XXX: should create profiles_activity entries for blesser changes. - } + $dbh->do( + $query, undef, $self->id, Bugzilla->user->id, + get_field_id('bug_group'), + join(', ', map { $_->name } @$removed), + join(', ', map { $_->name } @$added) + ); + } + else { + # XXX: should create profiles_activity entries for blesser changes. + } - Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id }); + Bugzilla->memcached->clear_config({key => 'user_groups.' . $self->id}); - my $type = $is_bless ? 'bless_groups' : 'groups'; - $changes->{$type} = [ - [ map { $_->name } @$removed ], - [ map { $_->name } @$added ], - ]; - } + my $type = $is_bless ? 'bless_groups' : 'groups'; + $changes->{$type} = [[map { $_->name } @$removed], [map { $_->name } @$added],]; + } } sub update { - my $self = shift; - my $options = shift; + my $self = shift; + my $options = shift; - my $group_changes = delete $self->{_group_changes}; + my $group_changes = delete $self->{_group_changes}; - my $changes = $self->SUPER::update(@_); - my $dbh = Bugzilla->dbh; - $self->_update_groups($group_changes, $changes); + my $changes = $self->SUPER::update(@_); + my $dbh = Bugzilla->dbh; + $self->_update_groups($group_changes, $changes); - if (exists $changes->{login_name}) { - # Delete all the tokens related to the userid - $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) - unless $options->{keep_tokens}; - # And rederive regex groups - $self->derive_regexp_groups(); - } + if (exists $changes->{login_name}) { + + # Delete all the tokens related to the userid + $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) + unless $options->{keep_tokens}; - # Logout the user if necessary. - Bugzilla->logout_user($self) - if (!$options->{keep_session} - && (exists $changes->{login_name} - || exists $changes->{disabledtext} - || exists $changes->{cryptpassword})); + # And rederive regex groups + $self->derive_regexp_groups(); + } + + # Logout the user if necessary. + Bugzilla->logout_user($self) + if ( + !$options->{keep_session} + && ( exists $changes->{login_name} + || exists $changes->{disabledtext} + || exists $changes->{cryptpassword}) + ); - # XXX Can update profiles_activity here as soon as it understands - # field names like login_name. - - return $changes; + # XXX Can update profiles_activity here as soon as it understands + # field names like login_name. + + return $changes; } ################################################################################ @@ -252,62 +249,63 @@ sub _check_disabledtext { return trim($_[1]) || ''; } # Check whether the extern_id is unique. sub _check_extern_id { - my ($invocant, $extern_id) = @_; - $extern_id = trim($extern_id); - return undef unless defined($extern_id) && $extern_id ne ""; - if (!ref($invocant) || $invocant->extern_id ne $extern_id) { - my $existing_login = $invocant->new({ extern_id => $extern_id }); - if ($existing_login) { - ThrowUserError( 'extern_id_exists', - { extern_id => $extern_id, - existing_login_name => $existing_login->login }); - } + my ($invocant, $extern_id) = @_; + $extern_id = trim($extern_id); + return undef unless defined($extern_id) && $extern_id ne ""; + if (!ref($invocant) || $invocant->extern_id ne $extern_id) { + my $existing_login = $invocant->new({extern_id => $extern_id}); + if ($existing_login) { + ThrowUserError('extern_id_exists', + {extern_id => $extern_id, existing_login_name => $existing_login->login}); } - return $extern_id; + } + return $extern_id; } # This is public since createaccount.cgi needs to use it before issuing # a token for account creation. sub check_login_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError('user_login_required'); - check_email_syntax($name); - - # Check the name if it's a new user, or if we're changing the name. - if (!ref($invocant) || lc($invocant->login) ne lc($name)) { - my @params = ($name); - push(@params, $invocant->login) if ref($invocant); - is_available_username(@params) - || ThrowUserError('account_exists', { email => $name }); - } + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError('user_login_required'); + check_email_syntax($name); - return $name; + # Check the name if it's a new user, or if we're changing the name. + if (!ref($invocant) || lc($invocant->login) ne lc($name)) { + my @params = ($name); + push(@params, $invocant->login) if ref($invocant); + is_available_username(@params) + || ThrowUserError('account_exists', {email => $name}); + } + + return $name; } sub _check_password { - my ($self, $pass) = @_; + my ($self, $pass) = @_; - # If the password is '*', do not encrypt it or validate it further--we - # are creating a user who should not be able to log in using DB - # authentication. - return $pass if $pass eq '*'; + # If the password is '*', do not encrypt it or validate it further--we + # are creating a user who should not be able to log in using DB + # authentication. + return $pass if $pass eq '*'; - validate_password($pass); - my $cryptpassword = bz_crypt($pass); - return $cryptpassword; + validate_password($pass); + my $cryptpassword = bz_crypt($pass); + return $cryptpassword; } sub _check_realname { return trim($_[1]) || ''; } sub _check_is_enabled { - my ($invocant, $is_enabled, undef, $params) = @_; - # is_enabled is set automatically on creation depending on whether - # disabledtext is empty (enabled) or not empty (disabled). - # When updating the user, is_enabled is set by calling set_disabledtext(). - # Any value passed into this validator is ignored. - my $disabledtext = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext}; - return $disabledtext ? 0 : 1; + my ($invocant, $is_enabled, undef, $params) = @_; + + # is_enabled is set automatically on creation depending on whether + # disabledtext is empty (enabled) or not empty (disabled). + # When updating the user, is_enabled is set by calling set_disabledtext(). + # Any value passed into this validator is ignored. + my $disabledtext + = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext}; + return $disabledtext ? 0 : 1; } ################################################################################ @@ -316,150 +314,151 @@ sub _check_is_enabled { sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); } sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); } -sub set_extern_id { $_[0]->set('extern_id', $_[1]); } +sub set_extern_id { $_[0]->set('extern_id', $_[1]); } sub set_login { - my ($self, $login) = @_; - $self->set('login_name', $login); - delete $self->{identity}; - delete $self->{nick}; + my ($self, $login) = @_; + $self->set('login_name', $login); + delete $self->{identity}; + delete $self->{nick}; } sub set_name { - my ($self, $name) = @_; - $self->set('realname', $name); - delete $self->{identity}; + my ($self, $name) = @_; + $self->set('realname', $name); + delete $self->{identity}; } sub set_password { $_[0]->set('cryptpassword', $_[1]); } sub set_disabledtext { - $_[0]->set('disabledtext', $_[1]); - $_[0]->set('is_enabled', $_[1] ? 0 : 1); + $_[0]->set('disabledtext', $_[1]); + $_[0]->set('is_enabled', $_[1] ? 0 : 1); } sub set_groups { - my $self = shift; - $self->_set_groups(GROUP_MEMBERSHIP, @_); + my $self = shift; + $self->_set_groups(GROUP_MEMBERSHIP, @_); } sub set_bless_groups { - my $self = shift; + my $self = shift; - # The person making the change needs to be in the editusers group - Bugzilla->user->in_group('editusers') - || ThrowUserError("auth_failure", {group => "editusers", - reason => "cant_bless", - action => "edit", - object => "users"}); + # The person making the change needs to be in the editusers group + Bugzilla->user->in_group('editusers') || ThrowUserError( + "auth_failure", + { + group => "editusers", + reason => "cant_bless", + action => "edit", + object => "users" + } + ); - $self->_set_groups(GROUP_BLESS, @_); + $self->_set_groups(GROUP_BLESS, @_); } sub _set_groups { - my $self = shift; - my $is_bless = shift; - my $changes = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $is_bless = shift; + my $changes = shift; + my $dbh = Bugzilla->dbh; - # The person making the change is $user, $self is the person being changed - my $user = Bugzilla->user; + # The person making the change is $user, $self is the person being changed + my $user = Bugzilla->user; - # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array - # is a list of group ids and/or names. + # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array + # is a list of group ids and/or names. - # First turn the arrays into group objects. - $changes = $self->_set_groups_to_object($changes); + # First turn the arrays into group objects. + $changes = $self->_set_groups_to_object($changes); - # Get a list of the groups the user currently is a member of - my $ids = $dbh->selectcol_arrayref( - q{SELECT DISTINCT group_id + # Get a list of the groups the user currently is a member of + my $ids = $dbh->selectcol_arrayref( + q{SELECT DISTINCT group_id FROM user_group_map - WHERE user_id = ? AND isbless = ? AND grant_type = ?}, - undef, $self->id, $is_bless, GRANT_DIRECT); - - my $current_groups = Bugzilla::Group->new_from_list($ids); - my $new_groups = dclone($current_groups); - - # Record the changes - if (exists $changes->{set}) { - $new_groups = $changes->{set}; - - # We need to check the user has bless rights on the existing groups - # If they don't, then we need to add them back to new_groups - foreach my $group (@$current_groups) { - if (! $user->can_bless($group->id)) { - push @$new_groups, $group - unless grep { $_->id eq $group->id } @$new_groups; - } - } + WHERE user_id = ? AND isbless = ? AND grant_type = ?}, undef, $self->id, + $is_bless, GRANT_DIRECT + ); + + my $current_groups = Bugzilla::Group->new_from_list($ids); + my $new_groups = dclone($current_groups); + + # Record the changes + if (exists $changes->{set}) { + $new_groups = $changes->{set}; + + # We need to check the user has bless rights on the existing groups + # If they don't, then we need to add them back to new_groups + foreach my $group (@$current_groups) { + if (!$user->can_bless($group->id)) { + push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups; + } } - else { - foreach my $group (@{$changes->{remove} // []}) { - @$new_groups = grep { $_->id ne $group->id } @$new_groups; - } - foreach my $group (@{$changes->{add} // []}) { - push @$new_groups, $group - unless grep { $_->id eq $group->id } @$new_groups; - } + } + else { + foreach my $group (@{$changes->{remove} // []}) { + @$new_groups = grep { $_->id ne $group->id } @$new_groups; } - - # Stash the changes, so self->update can actually make them - my @diffs = diff_arrays($current_groups, $new_groups, 'id'); - if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) { - $self->{_group_changes}{$is_bless} = \@diffs; + foreach my $group (@{$changes->{add} // []}) { + push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups; } + } + + # Stash the changes, so self->update can actually make them + my @diffs = diff_arrays($current_groups, $new_groups, 'id'); + if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) { + $self->{_group_changes}{$is_bless} = \@diffs; + } } sub _set_groups_to_object { - my $self = shift; - my $changes = shift; - my $user = Bugzilla->user; - - foreach my $key (keys %$changes) { - # Check we were given an array - unless (ref($changes->{$key}) eq 'ARRAY') { - ThrowCodeError( - 'param_invalid', - { param => $changes->{$key}, function => $key } - ); - } + my $self = shift; + my $changes = shift; + my $user = Bugzilla->user; - # Go through the array, and turn items into group objects - my @groups = (); - foreach my $value (@{$changes->{$key}}) { - my $type = $value =~ /^\d+$/ ? 'id' : 'name'; - my $group = Bugzilla::Group->new({$type => $value}); - - if (! $group || ! $user->can_bless($group->id)) { - ThrowUserError('auth_failure', - { group => $value, reason => 'cant_bless', - action => 'edit', object => 'users' }); - } - push @groups, $group; - } - $changes->{$key} = \@groups; + foreach my $key (keys %$changes) { + + # Check we were given an array + unless (ref($changes->{$key}) eq 'ARRAY') { + ThrowCodeError('param_invalid', {param => $changes->{$key}, function => $key}); + } + + # Go through the array, and turn items into group objects + my @groups = (); + foreach my $value (@{$changes->{$key}}) { + my $type = $value =~ /^\d+$/ ? 'id' : 'name'; + my $group = Bugzilla::Group->new({$type => $value}); + + if (!$group || !$user->can_bless($group->id)) { + ThrowUserError('auth_failure', + {group => $value, reason => 'cant_bless', action => 'edit', object => 'users'}); + } + push @groups, $group; } + $changes->{$key} = \@groups; + } - return $changes; + return $changes; } sub update_last_seen_date { - my $self = shift; - return unless $self->id; - my $dbh = Bugzilla->dbh; - my $date = $dbh->selectrow_array( - 'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); - - if (!$self->last_seen_date or $date ne $self->last_seen_date) { - $self->{last_seen_date} = $date; - # We don't use the normal update() routine here as we only - # want to update the last_seen_date column, not any other - # pending changes - $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?", - undef, $date, $self->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); - } + my $self = shift; + return unless $self->id; + my $dbh = Bugzilla->dbh; + my $date = $dbh->selectrow_array( + 'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); + + if (!$self->last_seen_date or $date ne $self->last_seen_date) { + $self->{last_seen_date} = $date; + + # We don't use the normal update() routine here as we only + # want to update the last_seen_date column, not any other + # pending changes + $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?", + undef, $date, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); + } } ################################################################################ @@ -467,171 +466,181 @@ sub update_last_seen_date { ################################################################################ # Accessors for user attributes -sub name { $_[0]->{realname}; } -sub login { $_[0]->{login_name}; } -sub extern_id { $_[0]->{extern_id}; } -sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; } -sub disabledtext { $_[0]->{'disabledtext'}; } -sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; } +sub name { $_[0]->{realname}; } +sub login { $_[0]->{login_name}; } +sub extern_id { $_[0]->{extern_id}; } +sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; } +sub disabledtext { $_[0]->{'disabledtext'}; } +sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; } sub showmybugslink { $_[0]->{showmybugslink}; } sub email_disabled { $_[0]->{disable_mail}; } -sub email_enabled { !($_[0]->{disable_mail}); } +sub email_enabled { !($_[0]->{disable_mail}); } sub last_seen_date { $_[0]->{last_seen_date}; } + sub cryptpassword { - my $self = shift; - # We don't store it because we never want it in the object (we - # don't want to accidentally dump even the hash somewhere). - my ($pw) = Bugzilla->dbh->selectrow_array( - 'SELECT cryptpassword FROM profiles WHERE userid = ?', - undef, $self->id); - return $pw; + my $self = shift; + + # We don't store it because we never want it in the object (we + # don't want to accidentally dump even the hash somewhere). + my ($pw) + = Bugzilla->dbh->selectrow_array( + 'SELECT cryptpassword FROM profiles WHERE userid = ?', + undef, $self->id); + return $pw; } sub set_authorizer { - my ($self, $authorizer) = @_; - $self->{authorizer} = $authorizer; + my ($self, $authorizer) = @_; + $self->{authorizer} = $authorizer; } + sub authorizer { - my ($self) = @_; - if (!$self->{authorizer}) { - require Bugzilla::Auth; - $self->{authorizer} = new Bugzilla::Auth(); - } - return $self->{authorizer}; + my ($self) = @_; + if (!$self->{authorizer}) { + require Bugzilla::Auth; + $self->{authorizer} = new Bugzilla::Auth(); + } + return $self->{authorizer}; } # Generate a string to identify the user by name + login if the user # has a name or by login only if they don't. sub identity { - my $self = shift; + my $self = shift; - return "" unless $self->id; + return "" unless $self->id; - if (!defined $self->{identity}) { - $self->{identity} = - $self->name ? $self->name . " <" . $self->login. ">" : $self->login; - } + if (!defined $self->{identity}) { + $self->{identity} + = $self->name ? $self->name . " <" . $self->login . ">" : $self->login; + } - return $self->{identity}; + return $self->{identity}; } sub nick { - my $self = shift; + my $self = shift; - return "" unless $self->id; + return "" unless $self->id; - if (!defined $self->{nick}) { - $self->{nick} = (split(/@/, $self->login, 2))[0]; - } + if (!defined $self->{nick}) { + $self->{nick} = (split(/@/, $self->login, 2))[0]; + } - return $self->{nick}; + return $self->{nick}; } sub queries { - my $self = shift; - return $self->{queries} if defined $self->{queries}; - return [] unless $self->id; + my $self = shift; + return $self->{queries} if defined $self->{queries}; + return [] unless $self->id; - my $dbh = Bugzilla->dbh; - my $query_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id); - require Bugzilla::Search::Saved; - $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids); + my $dbh = Bugzilla->dbh; + my $query_ids + = $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?', + undef, $self->id); + require Bugzilla::Search::Saved; + $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids); - # We preload link_in_footer from here as this information is always requested. - # This only works if the user object represents the current logged in user. - Bugzilla::Search::Saved::preload($self->{queries}) if $self->id == Bugzilla->user->id; + # We preload link_in_footer from here as this information is always requested. + # This only works if the user object represents the current logged in user. + Bugzilla::Search::Saved::preload($self->{queries}) + if $self->id == Bugzilla->user->id; - return $self->{queries}; + return $self->{queries}; } sub queries_subscribed { - my $self = shift; - return $self->{queries_subscribed} if defined $self->{queries_subscribed}; - return [] unless $self->id; - - # Exclude the user's own queries. - my @my_query_ids = map($_->id, @{$self->queries}); - my $query_id_string = join(',', @my_query_ids) || '-1'; - - # Only show subscriptions that we can still actually see. If a - # user changes the shared group of a query, our subscription - # will remain but we won't have access to the query anymore. - my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT lif.namedquery_id + my $self = shift; + return $self->{queries_subscribed} if defined $self->{queries_subscribed}; + return [] unless $self->id; + + # Exclude the user's own queries. + my @my_query_ids = map($_->id, @{$self->queries}); + my $query_id_string = join(',', @my_query_ids) || '-1'; + + # Only show subscriptions that we can still actually see. If a + # user changes the shared group of a query, our subscription + # will remain but we won't have access to the query anymore. + my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT lif.namedquery_id FROM namedqueries_link_in_footer lif INNER JOIN namedquery_group_map ngm ON ngm.namedquery_id = lif.namedquery_id WHERE lif.user_id = ? AND lif.namedquery_id NOT IN ($query_id_string) - AND " . $self->groups_in_sql, - undef, $self->id); - require Bugzilla::Search::Saved; - $self->{queries_subscribed} = - Bugzilla::Search::Saved->new_from_list($subscribed_query_ids); - return $self->{queries_subscribed}; + AND " . $self->groups_in_sql, undef, $self->id + ); + require Bugzilla::Search::Saved; + $self->{queries_subscribed} + = Bugzilla::Search::Saved->new_from_list($subscribed_query_ids); + return $self->{queries_subscribed}; } sub queries_available { - my $self = shift; - return $self->{queries_available} if defined $self->{queries_available}; - return [] unless $self->id; + my $self = shift; + return $self->{queries_available} if defined $self->{queries_available}; + return [] unless $self->id; - # Exclude the user's own queries. - my @my_query_ids = map($_->id, @{$self->queries}); - my $query_id_string = join(',', @my_query_ids) || '-1'; + # Exclude the user's own queries. + my @my_query_ids = map($_->id, @{$self->queries}); + my $query_id_string = join(',', @my_query_ids) || '-1'; - my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT namedquery_id FROM namedquery_group_map - WHERE ' . $self->groups_in_sql . " - AND namedquery_id NOT IN ($query_id_string)"); - require Bugzilla::Search::Saved; - $self->{queries_available} = - Bugzilla::Search::Saved->new_from_list($avail_query_ids); - return $self->{queries_available}; + my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref( + 'SELECT namedquery_id FROM namedquery_group_map + WHERE ' . $self->groups_in_sql . " + AND namedquery_id NOT IN ($query_id_string)" + ); + require Bugzilla::Search::Saved; + $self->{queries_available} + = Bugzilla::Search::Saved->new_from_list($avail_query_ids); + return $self->{queries_available}; } sub tags { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{tags}) { - # We must use LEFT JOIN instead of INNER JOIN as we may be - # in the process of inserting a new tag to some bugs, - # in which case there are no bugs with this tag yet. - $self->{tags} = $dbh->selectall_hashref( - 'SELECT name, id, COUNT(bug_id) AS bug_count + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{tags}) { + + # We must use LEFT JOIN instead of INNER JOIN as we may be + # in the process of inserting a new tag to some bugs, + # in which case there are no bugs with this tag yet. + $self->{tags} = $dbh->selectall_hashref( + 'SELECT name, id, COUNT(bug_id) AS bug_count FROM tag LEFT JOIN bug_tag ON bug_tag.tag_id = tag.id - WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), - 'name', undef, $self->id); - } - return $self->{tags}; + WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), 'name', undef, + $self->id + ); + } + return $self->{tags}; } sub bugs_ignored { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - if (!defined $self->{'bugs_ignored'}) { - $self->{'bugs_ignored'} = $dbh->selectall_arrayref( - 'SELECT bugs.bug_id AS id, + my ($self) = @_; + my $dbh = Bugzilla->dbh; + if (!defined $self->{'bugs_ignored'}) { + $self->{'bugs_ignored'} = $dbh->selectall_arrayref( + 'SELECT bugs.bug_id AS id, bugs.bug_status AS status, bugs.short_desc AS summary FROM bugs INNER JOIN email_bug_ignore ON bugs.bug_id = email_bug_ignore.bug_id - WHERE user_id = ?', - { Slice => {} }, $self->id); - # Go ahead and load these into the visible bugs cache - # to speed up can_see_bug checks later - $self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]); - } - return $self->{'bugs_ignored'}; + WHERE user_id = ?', {Slice => {}}, $self->id + ); + + # Go ahead and load these into the visible bugs cache + # to speed up can_see_bug checks later + $self->visible_bugs([map { $_->{'id'} } @{$self->{'bugs_ignored'}}]); + } + return $self->{'bugs_ignored'}; } sub is_bug_ignored { - my ($self, $bug_id) = @_; - return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0; + my ($self, $bug_id) = @_; + return (grep { $_->{'id'} == $bug_id } @{$self->bugs_ignored}) ? 1 : 0; } ########################## @@ -639,309 +648,312 @@ sub is_bug_ignored { ########################## sub recent_searches { - my $self = shift; - $self->{recent_searches} ||= - Bugzilla::Search::Recent->match({ user_id => $self->id }); - return $self->{recent_searches}; + my $self = shift; + $self->{recent_searches} + ||= Bugzilla::Search::Recent->match({user_id => $self->id}); + return $self->{recent_searches}; } sub recent_search_containing { - my ($self, $bug_id) = @_; - my $searches = $self->recent_searches; + my ($self, $bug_id) = @_; + my $searches = $self->recent_searches; - foreach my $search (@$searches) { - return $search if grep($_ == $bug_id, @{ $search->bug_list }); - } + foreach my $search (@$searches) { + return $search if grep($_ == $bug_id, @{$search->bug_list}); + } - return undef; + return undef; } sub recent_search_for { - my ($self, $bug) = @_; - my $params = Bugzilla->input_params; - my $cgi = Bugzilla->cgi; - - if ($self->id) { - # First see if there's a list_id parameter in the query string. - my $list_id = $params->{list_id}; - if (!$list_id) { - # If not, check for "list_id" in the query string of the referer. - my $referer = $cgi->referer; - if ($referer) { - my $uri = URI->new($referer); - if ($uri->path =~ /buglist\.cgi$/) { - $list_id = $uri->query_param('list_id') - || $uri->query_param('regetlastlist'); - } - } + my ($self, $bug) = @_; + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + + if ($self->id) { + + # First see if there's a list_id parameter in the query string. + my $list_id = $params->{list_id}; + if (!$list_id) { + + # If not, check for "list_id" in the query string of the referer. + my $referer = $cgi->referer; + if ($referer) { + my $uri = URI->new($referer); + if ($uri->path =~ /buglist\.cgi$/) { + $list_id = $uri->query_param('list_id') || $uri->query_param('regetlastlist'); } + } + } - if ($list_id && $list_id ne 'cookie') { - # If we got a bad list_id (either some other user's or an expired - # one) don't crash, just don't return that list. - my $search = Bugzilla::Search::Recent->check_quietly( - { id => $list_id }); - return $search if $search; - } + if ($list_id && $list_id ne 'cookie') { - # If there's no list_id, see if the current bug's id is contained - # in any of the user's saved lists. - my $search = $self->recent_search_containing($bug->id); - return $search if $search; + # If we got a bad list_id (either some other user's or an expired + # one) don't crash, just don't return that list. + my $search = Bugzilla::Search::Recent->check_quietly({id => $list_id}); + return $search if $search; } - # Finally (or always, if we're logged out), if there's a BUGLIST cookie - # and the selected bug is in the list, then return the cookie as a fake - # Search::Recent object. - if (my $list = $cgi->cookie('BUGLIST')) { - # Also split on colons, which was used as a separator in old cookies. - my @bug_ids = split(/[:-]/, $list); - if (grep { $_ == $bug->id } @bug_ids) { - my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids); - return $search; - } + # If there's no list_id, see if the current bug's id is contained + # in any of the user's saved lists. + my $search = $self->recent_search_containing($bug->id); + return $search if $search; + } + + # Finally (or always, if we're logged out), if there's a BUGLIST cookie + # and the selected bug is in the list, then return the cookie as a fake + # Search::Recent object. + if (my $list = $cgi->cookie('BUGLIST')) { + + # Also split on colons, which was used as a separator in old cookies. + my @bug_ids = split(/[:-]/, $list); + if (grep { $_ == $bug->id } @bug_ids) { + my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids); + return $search; } + } - return undef; + return undef; } sub save_last_search { - my ($self, $params) = @_; - my ($bug_ids, $order, $vars, $list_id) = - @$params{qw(bugs order vars list_id)}; - - my $cgi = Bugzilla->cgi; - if ($order) { - $cgi->send_cookie(-name => 'LASTORDER', - -value => $order, - -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'); - } + my ($self, $params) = @_; + my ($bug_ids, $order, $vars, $list_id) = @$params{qw(bugs order vars list_id)}; + + my $cgi = Bugzilla->cgi; + if ($order) { + $cgi->send_cookie( + -name => 'LASTORDER', + -value => $order, + -expires => 'Fri, 01-Jan-2038 00:00:00 GMT' + ); + } - return if !@$bug_ids; - - my $search; - if ($self->id) { - on_main_db { - if ($list_id) { - $search = Bugzilla::Search::Recent->check_quietly({ id => $list_id }); - } - - if ($search) { - if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) { - $search->set_bug_list($bug_ids); - } - if (!$search->list_order || $order ne $search->list_order) { - $search->set_list_order($order); - } - $search->update(); - } - else { - # If we already have an existing search with a totally - # identical bug list, then don't create a new one. This - # prevents people from writing over their whole - # recent-search list by just refreshing a saved search - # (which doesn't have list_id in the header) over and over. - my $list_string = join(',', @$bug_ids); - my $existing_search = Bugzilla::Search::Recent->match({ - user_id => $self->id, bug_list => $list_string }); - - if (!scalar(@$existing_search)) { - $search = Bugzilla::Search::Recent->create({ - user_id => $self->id, - bug_list => $bug_ids, - list_order => $order }); - } - else { - $search = $existing_search->[0]; - } - } - }; - delete $self->{recent_searches}; - } - # Logged-out users use a cookie to store a single last search. We don't - # override that cookie with the logged-in user's latest search, because - # if they did one search while logged out and another while logged in, - # they may still want to navigate through the search they made while - # logged out. - else { - my $bug_list = join('-', @$bug_ids); - if (length($bug_list) < 4000) { - $cgi->send_cookie(-name => 'BUGLIST', - -value => $bug_list, - -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'); + return if !@$bug_ids; + + my $search; + if ($self->id) { + on_main_db { + if ($list_id) { + $search = Bugzilla::Search::Recent->check_quietly({id => $list_id}); + } + + if ($search) { + if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) { + $search->set_bug_list($bug_ids); + } + if (!$search->list_order || $order ne $search->list_order) { + $search->set_list_order($order); + } + $search->update(); + } + else { + # If we already have an existing search with a totally + # identical bug list, then don't create a new one. This + # prevents people from writing over their whole + # recent-search list by just refreshing a saved search + # (which doesn't have list_id in the header) over and over. + my $list_string = join(',', @$bug_ids); + my $existing_search = Bugzilla::Search::Recent->match( + {user_id => $self->id, bug_list => $list_string}); + + if (!scalar(@$existing_search)) { + $search = Bugzilla::Search::Recent->create( + {user_id => $self->id, bug_list => $bug_ids, list_order => $order}); } else { - $cgi->remove_cookie('BUGLIST'); - $vars->{'toolong'} = 1; + $search = $existing_search->[0]; } + } + }; + delete $self->{recent_searches}; + } + + # Logged-out users use a cookie to store a single last search. We don't + # override that cookie with the logged-in user's latest search, because + # if they did one search while logged out and another while logged in, + # they may still want to navigate through the search they made while + # logged out. + else { + my $bug_list = join('-', @$bug_ids); + if (length($bug_list) < 4000) { + $cgi->send_cookie( + -name => 'BUGLIST', + -value => $bug_list, + -expires => 'Fri, 01-Jan-2038 00:00:00 GMT' + ); } - return $search; + else { + $cgi->remove_cookie('BUGLIST'); + $vars->{'toolong'} = 1; + } + } + return $search; } sub reports { - my $self = shift; - return $self->{reports} if defined $self->{reports}; - return [] unless $self->id; + my $self = shift; + return $self->{reports} if defined $self->{reports}; + return [] unless $self->id; - my $dbh = Bugzilla->dbh; - my $report_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM reports WHERE user_id = ?', undef, $self->id); - require Bugzilla::Report; - $self->{reports} = Bugzilla::Report->new_from_list($report_ids); - return $self->{reports}; + my $dbh = Bugzilla->dbh; + my $report_ids + = $dbh->selectcol_arrayref('SELECT id FROM reports WHERE user_id = ?', + undef, $self->id); + require Bugzilla::Report; + $self->{reports} = Bugzilla::Report->new_from_list($report_ids); + return $self->{reports}; } sub flush_reports_cache { - my $self = shift; + my $self = shift; - delete $self->{reports}; + delete $self->{reports}; } sub settings { - my ($self) = @_; + my ($self) = @_; - return $self->{'settings'} if (defined $self->{'settings'}); + return $self->{'settings'} if (defined $self->{'settings'}); - # IF the user is logged in - # THEN get the user's settings - # ELSE get default settings - if ($self->id) { - $self->{'settings'} = get_all_settings($self->id); - } else { - $self->{'settings'} = get_defaults(); - } + # IF the user is logged in + # THEN get the user's settings + # ELSE get default settings + if ($self->id) { + $self->{'settings'} = get_all_settings($self->id); + } + else { + $self->{'settings'} = get_defaults(); + } - return $self->{'settings'}; + return $self->{'settings'}; } sub setting { - my ($self, $name) = @_; - return $self->settings->{$name}->{'value'}; + my ($self, $name) = @_; + return $self->settings->{$name}->{'value'}; } sub timezone { - my $self = shift; + my $self = shift; - if (!defined $self->{timezone}) { - my $tz = $self->setting('timezone'); - if ($tz eq 'local') { - # The user wants the local timezone of the server. - $self->{timezone} = Bugzilla->local_timezone; - } - else { - $self->{timezone} = DateTime::TimeZone->new(name => $tz); - } + if (!defined $self->{timezone}) { + my $tz = $self->setting('timezone'); + if ($tz eq 'local') { + + # The user wants the local timezone of the server. + $self->{timezone} = Bugzilla->local_timezone; + } + else { + $self->{timezone} = DateTime::TimeZone->new(name => $tz); } - return $self->{timezone}; + } + return $self->{timezone}; } sub flush_queries_cache { - my $self = shift; + my $self = shift; - delete $self->{queries}; - delete $self->{queries_subscribed}; - delete $self->{queries_available}; + delete $self->{queries}; + delete $self->{queries_subscribed}; + delete $self->{queries_available}; } sub groups { - my $self = shift; + my $self = shift; - return $self->{groups} if defined $self->{groups}; - return [] unless $self->id; + return $self->{groups} if defined $self->{groups}; + return [] unless $self->id; - my $user_groups_key = "user_groups." . $self->id; - my $groups = Bugzilla->memcached->get_config({ - key => $user_groups_key - }); + my $user_groups_key = "user_groups." . $self->id; + my $groups = Bugzilla->memcached->get_config({key => $user_groups_key}); - if (!$groups) { - my $dbh = Bugzilla->dbh; - my $groups_to_check = $dbh->selectcol_arrayref( - "SELECT DISTINCT group_id + if (!$groups) { + my $dbh = Bugzilla->dbh; + my $groups_to_check = $dbh->selectcol_arrayref( + "SELECT DISTINCT group_id FROM user_group_map - WHERE user_id = ? AND isbless = 0", undef, $self->id); - - my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP; - my $membership_rows = Bugzilla->memcached->get_config({ - key => $grant_type_key, - }); - if (!$membership_rows) { - $membership_rows = $dbh->selectall_arrayref( - "SELECT DISTINCT grantor_id, member_id + WHERE user_id = ? AND isbless = 0", undef, $self->id + ); + + my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP; + my $membership_rows + = Bugzilla->memcached->get_config({key => $grant_type_key,}); + if (!$membership_rows) { + $membership_rows = $dbh->selectall_arrayref( + "SELECT DISTINCT grantor_id, member_id FROM group_group_map - WHERE grant_type = " . GROUP_MEMBERSHIP); - Bugzilla->memcached->set_config({ - key => $grant_type_key, - data => $membership_rows, - }); - } + WHERE grant_type = " . GROUP_MEMBERSHIP + ); + Bugzilla->memcached->set_config( + {key => $grant_type_key, data => $membership_rows,}); + } - my %group_membership; - foreach my $row (@$membership_rows) { - my ($grantor_id, $member_id) = @$row; - push (@{ $group_membership{$member_id} }, $grantor_id); - } + my %group_membership; + foreach my $row (@$membership_rows) { + my ($grantor_id, $member_id) = @$row; + push(@{$group_membership{$member_id}}, $grantor_id); + } - # Let's walk the groups hierarchy tree (using FIFO) - # On the first iteration it's pre-filled with direct groups - # membership. Later on, each group can add its own members into the - # FIFO. Circular dependencies are eliminated by checking - # $checked_groups{$member_id} hash values. - # As a result, %groups will have all the groups we are the member of. - my %checked_groups; - my %groups; - while (scalar(@$groups_to_check) > 0) { - # Pop the head group from FIFO - my $member_id = shift @$groups_to_check; - - # Skip the group if we have already checked it - if (!$checked_groups{$member_id}) { - # Mark group as checked - $checked_groups{$member_id} = 1; - - # Add all its members to the FIFO check list - # %group_membership contains arrays of group members - # for all groups. Accessible by group number. - my $members = $group_membership{$member_id}; - my @new_to_check = grep(!$checked_groups{$_}, @$members); - push(@$groups_to_check, @new_to_check); - - $groups{$member_id} = 1; - } - } - $groups = [ keys %groups ]; + # Let's walk the groups hierarchy tree (using FIFO) + # On the first iteration it's pre-filled with direct groups + # membership. Later on, each group can add its own members into the + # FIFO. Circular dependencies are eliminated by checking + # $checked_groups{$member_id} hash values. + # As a result, %groups will have all the groups we are the member of. + my %checked_groups; + my %groups; + while (scalar(@$groups_to_check) > 0) { + + # Pop the head group from FIFO + my $member_id = shift @$groups_to_check; - Bugzilla->memcached->set_config({ - key => $user_groups_key, - data => $groups, - }); + # Skip the group if we have already checked it + if (!$checked_groups{$member_id}) { + + # Mark group as checked + $checked_groups{$member_id} = 1; + + # Add all its members to the FIFO check list + # %group_membership contains arrays of group members + # for all groups. Accessible by group number. + my $members = $group_membership{$member_id}; + my @new_to_check = grep(!$checked_groups{$_}, @$members); + push(@$groups_to_check, @new_to_check); + + $groups{$member_id} = 1; + } } + $groups = [keys %groups]; - $self->{groups} = Bugzilla::Group->new_from_list($groups); - return $self->{groups}; + Bugzilla->memcached->set_config({key => $user_groups_key, data => $groups,}); + } + + $self->{groups} = Bugzilla::Group->new_from_list($groups); + return $self->{groups}; } sub last_visited { - my ($self, $ids) = @_; + my ($self, $ids) = @_; - return Bugzilla::BugUserLastVisit->match({ user_id => $self->id, - $ids ? ( bug_id => $ids ) : () }); + return Bugzilla::BugUserLastVisit->match( + {user_id => $self->id, $ids ? (bug_id => $ids) : ()}); } sub is_involved_in_bug { - my ($self, $bug) = @_; - my $user_id = $self->id; - my $user_login = $self->login; + my ($self, $bug) = @_; + my $user_id = $self->id; + my $user_login = $self->login; - return unless $user_id; - return 1 if $user_id == $bug->assigned_to->id; - return 1 if $user_id == $bug->reporter->id; + return unless $user_id; + return 1 if $user_id == $bug->assigned_to->id; + return 1 if $user_id == $bug->reporter->id; - if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) { - return 1 if $user_id == $bug->qa_contact->id; - } + if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) { + return 1 if $user_id == $bug->qa_contact->id; + } - return any { $user_login eq $_ } @{ $bug->cc }; + return any { $user_login eq $_ } @{$bug->cc}; } # It turns out that calling ->id on objects a few hundred thousand @@ -949,296 +961,311 @@ sub is_involved_in_bug { # when profiling xt/search.t.) So we cache the group ids separately from # groups for functions that need the group ids. sub _group_ids { - my ($self) = @_; - $self->{group_ids} ||= [map { $_->id } @{ $self->groups }]; - return $self->{group_ids}; + my ($self) = @_; + $self->{group_ids} ||= [map { $_->id } @{$self->groups}]; + return $self->{group_ids}; } sub groups_as_string { - my $self = shift; - my $ids = $self->_group_ids; - return scalar(@$ids) ? join(',', @$ids) : '-1'; + my $self = shift; + my $ids = $self->_group_ids; + return scalar(@$ids) ? join(',', @$ids) : '-1'; } sub groups_in_sql { - my ($self, $field) = @_; - $field ||= 'group_id'; - my $ids = $self->_group_ids; - $ids = [-1] if !scalar @$ids; - return Bugzilla->dbh->sql_in($field, $ids); + my ($self, $field) = @_; + $field ||= 'group_id'; + my $ids = $self->_group_ids; + $ids = [-1] if !scalar @$ids; + return Bugzilla->dbh->sql_in($field, $ids); } sub bless_groups { - my $self = shift; + my $self = shift; - return $self->{'bless_groups'} if defined $self->{'bless_groups'}; - return [] unless $self->id; + return $self->{'bless_groups'} if defined $self->{'bless_groups'}; + return [] unless $self->id; - if ($self->in_group('editusers')) { - # Users having editusers permissions may bless all groups. - $self->{'bless_groups'} = [Bugzilla::Group->get_all]; - return $self->{'bless_groups'}; - } + if ($self->in_group('editusers')) { - if (Bugzilla->params->{usevisibilitygroups} - && !@{ $self->visible_groups_inherited }) { - return []; - } + # Users having editusers permissions may bless all groups. + $self->{'bless_groups'} = [Bugzilla::Group->get_all]; + return $self->{'bless_groups'}; + } + + if (Bugzilla->params->{usevisibilitygroups} + && !@{$self->visible_groups_inherited}) + { + return []; + } - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - # Get all groups for the user where they have direct bless privileges. - my $query = " + # Get all groups for the user where they have direct bless privileges. + my $query = " SELECT DISTINCT group_id FROM user_group_map WHERE user_id = ? AND isbless = 1"; - if (Bugzilla->params->{usevisibilitygroups}) { - $query .= " AND " - . $dbh->sql_in('group_id', $self->visible_groups_inherited); - } - - # Get all groups for the user where they are a member of a group that - # inherits bless privs. - my @group_ids = map { $_->id } @{ $self->groups }; - if (@group_ids) { - $query .= " + if (Bugzilla->params->{usevisibilitygroups}) { + $query .= " AND " . $dbh->sql_in('group_id', $self->visible_groups_inherited); + } + + # Get all groups for the user where they are a member of a group that + # inherits bless privs. + my @group_ids = map { $_->id } @{$self->groups}; + if (@group_ids) { + $query .= " UNION SELECT DISTINCT grantor_id FROM group_group_map WHERE grant_type = " . GROUP_BLESS . " AND " . $dbh->sql_in('member_id', \@group_ids); - if (Bugzilla->params->{usevisibilitygroups}) { - $query .= " AND " - . $dbh->sql_in('grantor_id', $self->visible_groups_inherited); - } + if (Bugzilla->params->{usevisibilitygroups}) { + $query .= " AND " . $dbh->sql_in('grantor_id', $self->visible_groups_inherited); } + } - my $ids = $dbh->selectcol_arrayref($query, undef, $self->id); - return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids); + my $ids = $dbh->selectcol_arrayref($query, undef, $self->id); + return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids); } sub in_group { - my ($self, $group, $product_id) = @_; - $group = $group->name if blessed $group; - if (scalar grep($_->name eq $group, @{ $self->groups })) { - return 1; - } - elsif ($product_id && detaint_natural($product_id)) { - # Make sure $group exists on a per-product basis. - return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES); - - $self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"}; - if (!defined $self->{"product_$product_id"}->{$group}) { - my $dbh = Bugzilla->dbh; - my $in_group = $dbh->selectrow_array( - "SELECT 1 + my ($self, $group, $product_id) = @_; + $group = $group->name if blessed $group; + if (scalar grep($_->name eq $group, @{$self->groups})) { + return 1; + } + elsif ($product_id && detaint_natural($product_id)) { + + # Make sure $group exists on a per-product basis. + return 0 unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES); + + $self->{"product_$product_id"} = {} + unless exists $self->{"product_$product_id"}; + if (!defined $self->{"product_$product_id"}->{$group}) { + my $dbh = Bugzilla->dbh; + my $in_group = $dbh->selectrow_array( + "SELECT 1 FROM group_control_map WHERE product_id = ? AND $group != 0 - AND " . $self->groups_in_sql . ' ' . - $dbh->sql_limit(1), - undef, $product_id); + AND " + . $self->groups_in_sql . ' ' . $dbh->sql_limit(1), undef, $product_id + ); - $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0; - } - return $self->{"product_$product_id"}->{$group}; + $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0; } - # If we come here, then the user is not in the requested group. - return 0; + return $self->{"product_$product_id"}->{$group}; + } + + # If we come here, then the user is not in the requested group. + return 0; } sub in_group_id { - my ($self, $id) = @_; - return grep($_->id == $id, @{ $self->groups }) ? 1 : 0; + my ($self, $id) = @_; + return grep($_->id == $id, @{$self->groups}) ? 1 : 0; } # This is a helper to get all groups which have an icon to be displayed # besides the name of the commenter. sub groups_with_icon { - my $self = shift; + my $self = shift; - return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }]; + return $self->{groups_with_icon} //= [grep { $_->icon_url } @{$self->groups}]; } sub get_products_by_permission { - my ($self, $group) = @_; - # Make sure $group exists on a per-product basis. - return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES); + my ($self, $group) = @_; + + # Make sure $group exists on a per-product basis. + return [] unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES); - my $product_ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT DISTINCT product_id + my $product_ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT DISTINCT product_id FROM group_control_map WHERE $group != 0 - AND " . $self->groups_in_sql); + AND " . $self->groups_in_sql + ); - # No need to go further if the user has no "special" privs. - return [] unless scalar(@$product_ids); - my %product_map = map { $_ => 1 } @$product_ids; + # No need to go further if the user has no "special" privs. + return [] unless scalar(@$product_ids); + my %product_map = map { $_ => 1 } @$product_ids; - # We will restrict the list to products the user can see. - my $selectable_products = $self->get_selectable_products; - my @products = grep { $product_map{$_->id} } @$selectable_products; - return \@products; + # We will restrict the list to products the user can see. + my $selectable_products = $self->get_selectable_products; + my @products = grep { $product_map{$_->id} } @$selectable_products; + return \@products; } sub can_see_user { - my ($self, $otherUser) = @_; - my $query; + my ($self, $otherUser) = @_; + my $query; - if (Bugzilla->params->{'usevisibilitygroups'}) { - # If the user can see no groups, then no users are visible either. - my $visibleGroups = $self->visible_groups_as_string() || return 0; - $query = qq{SELECT COUNT(DISTINCT userid) + if (Bugzilla->params->{'usevisibilitygroups'}) { + + # If the user can see no groups, then no users are visible either. + my $visibleGroups = $self->visible_groups_as_string() || return 0; + $query = qq{SELECT COUNT(DISTINCT userid) FROM profiles, user_group_map WHERE userid = ? AND user_id = userid AND isbless = 0 AND group_id IN ($visibleGroups) }; - } else { - $query = qq{SELECT COUNT(userid) + } + else { + $query = qq{SELECT COUNT(userid) FROM profiles WHERE userid = ? }; - } - return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); + } + return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); } sub can_edit_product { - my ($self, $prod_id) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $prod_id) = @_; + my $dbh = Bugzilla->dbh; - if (Bugzilla->params->{'or_groups'}) { - my $groups = $self->groups_as_string; - # For or-groups, we check if there are any can_edit groups for the - # product, and if the user is in any of them. If there are none or - # the user is in at least one of them, they can edit the product - my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array( - "SELECT SUM(p.cnt_can_edit), + if (Bugzilla->params->{'or_groups'}) { + my $groups = $self->groups_as_string; + + # For or-groups, we check if there are any can_edit groups for the + # product, and if the user is in any of them. If there are none or + # the user is in at least one of them, they can edit the product + my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array( + "SELECT SUM(p.cnt_can_edit), SUM(p.cnt_group_member) FROM (SELECT CASE WHEN canedit = 1 THEN 1 ELSE 0 END AS cnt_can_edit, CASE WHEN canedit = 1 AND group_id IN ($groups) THEN 1 ELSE 0 END AS cnt_group_member FROM group_control_map - WHERE product_id = $prod_id) AS p"); - return (!$cnt_can_edit or $cnt_group_member); - } - else { - # For and-groups, a user needs to be in all canedit groups. Therefore - # if the user is not in a can_edit group for the product, they cannot - # edit the product. - my $has_external_groups = - $dbh->selectrow_array('SELECT 1 + WHERE product_id = $prod_id) AS p" + ); + return (!$cnt_can_edit or $cnt_group_member); + } + else { + # For and-groups, a user needs to be in all canedit groups. Therefore + # if the user is not in a can_edit group for the product, they cannot + # edit the product. + my $has_external_groups = $dbh->selectrow_array( + 'SELECT 1 FROM group_control_map WHERE product_id = ? AND canedit != 0 - AND group_id NOT IN(' . $self->groups_as_string . ')', - undef, $prod_id); + AND group_id NOT IN(' + . $self->groups_as_string . ')', undef, $prod_id + ); - return !$has_external_groups; - } + return !$has_external_groups; + } } sub can_see_bug { - my ($self, $bug_id) = @_; - return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0; + my ($self, $bug_id) = @_; + return @{$self->visible_bugs([$bug_id])} ? 1 : 0; } sub visible_bugs { - my ($self, $bugs) = @_; - # Allow users to pass in Bug objects and bug ids both. - my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs; - - # We only check the visibility of bugs that we haven't - # checked yet. - # Bugzilla::Bug->update automatically removes updated bugs - # from the cache to force them to be checked again. - my $visible_cache = $self->{_visible_bugs_cache} ||= {}; - my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids); - - if (@check_ids) { - foreach my $id (@check_ids) { - my $orig_id = $id; - detaint_natural($id) - || ThrowCodeError('param_must_be_numeric', { param => $orig_id, - function => 'Bugzilla::User->visible_bugs'}); - } + my ($self, $bugs) = @_; + + # Allow users to pass in Bug objects and bug ids both. + my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs; + + # We only check the visibility of bugs that we haven't + # checked yet. + # Bugzilla::Bug->update automatically removes updated bugs + # from the cache to force them to be checked again. + my $visible_cache = $self->{_visible_bugs_cache} ||= {}; + my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids); - Bugzilla->params->{'or_groups'} - ? $self->_visible_bugs_check_or(\@check_ids) - : $self->_visible_bugs_check_and(\@check_ids); + if (@check_ids) { + foreach my $id (@check_ids) { + my $orig_id = $id; + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {param => $orig_id, function => 'Bugzilla::User->visible_bugs'}); } - return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs]; + Bugzilla->params->{'or_groups'} + ? $self->_visible_bugs_check_or(\@check_ids) + : $self->_visible_bugs_check_and(\@check_ids); + } + + return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs]; } sub _visible_bugs_check_or { - my ($self, $check_ids) = @_; - my $visible_cache = $self->{_visible_bugs_cache}; - my $dbh = Bugzilla->dbh; - my $user_id = $self->id; - - my $sth; - # Speed up the can_see_bug case. - if (scalar(@$check_ids) == 1) { - $sth = $self->{_sth_one_visible_bug}; - } - my $query = qq{ + my ($self, $check_ids) = @_; + my $visible_cache = $self->{_visible_bugs_cache}; + my $dbh = Bugzilla->dbh; + my $user_id = $self->id; + + my $sth; + + # Speed up the can_see_bug case. + if (scalar(@$check_ids) == 1) { + $sth = $self->{_sth_one_visible_bug}; + } + my $query = qq{ SELECT DISTINCT bugs.bug_id FROM bugs LEFT JOIN bug_group_map AS security_map ON bugs.bug_id = security_map.bug_id LEFT JOIN cc AS security_cc ON bugs.bug_id = security_cc.bug_id AND security_cc.who = $user_id WHERE bugs.bug_id IN (} . join(',', ('?') x @$check_ids) . qq{) - AND ((security_map.group_id IS NULL OR security_map.group_id IN (} . $self->groups_as_string . qq{)) + AND ((security_map.group_id IS NULL OR security_map.group_id IN (} + . $self->groups_as_string . qq{)) OR (bugs.reporter_accessible = 1 AND bugs.reporter = $user_id) OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL) OR bugs.assigned_to = $user_id }; - if (Bugzilla->params->{'useqacontact'}) { - $query .= " OR bugs.qa_contact = $user_id"; - } - $query .= ')'; + if (Bugzilla->params->{'useqacontact'}) { + $query .= " OR bugs.qa_contact = $user_id"; + } + $query .= ')'; - $sth ||= $dbh->prepare($query); - if (scalar(@$check_ids) == 1) { - $self->{_sth_one_visible_bug} = $sth; - } + $sth ||= $dbh->prepare($query); + if (scalar(@$check_ids) == 1) { + $self->{_sth_one_visible_bug} = $sth; + } - # Set all bugs as non visible - foreach my $bug_id (@$check_ids) { - $visible_cache->{$bug_id} = 0; - } + # Set all bugs as non visible + foreach my $bug_id (@$check_ids) { + $visible_cache->{$bug_id} = 0; + } - # Now get the bugs the user can see - my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids); - foreach my $bug_id (@$visible_bug_ids) { - $visible_cache->{$bug_id} = 1; - } + # Now get the bugs the user can see + my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids); + foreach my $bug_id (@$visible_bug_ids) { + $visible_cache->{$bug_id} = 1; + } } sub _visible_bugs_check_and { - my ($self, $check_ids) = @_; - my $visible_cache = $self->{_visible_bugs_cache}; - my $dbh = Bugzilla->dbh; - my $user_id = $self->id; - - my $sth; - # Speed up the can_see_bug case. - if (scalar(@$check_ids) == 1) { - $sth = $self->{_sth_one_visible_bug}; - } - $sth ||= $dbh->prepare( - # This checks for groups that the bug is in that the user - # *isn't* in. Then, in the Perl code below, we check if - # the user can otherwise access the bug (for example, by being - # the assignee or QA Contact). - # - # The DISTINCT exists because the bug could be in *several* - # groups that the user isn't in, but they will all return the - # same result for bug_group_map.bug_id (so DISTINCT filters - # out duplicate rows). - "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact, + my ($self, $check_ids) = @_; + my $visible_cache = $self->{_visible_bugs_cache}; + my $dbh = Bugzilla->dbh; + my $user_id = $self->id; + + my $sth; + + # Speed up the can_see_bug case. + if (scalar(@$check_ids) == 1) { + $sth = $self->{_sth_one_visible_bug}; + } + $sth ||= $dbh->prepare( + + # This checks for groups that the bug is in that the user + # *isn't* in. Then, in the Perl code below, we check if + # the user can otherwise access the bug (for example, by being + # the assignee or QA Contact). + # + # The DISTINCT exists because the bug could be in *several* + # groups that the user isn't in, but they will all return the + # same result for bug_group_map.bug_id (so DISTINCT filters + # out duplicate rows). + "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact, reporter_accessible, cclist_accessible, cc.who, bug_group_map.bug_id FROM bugs @@ -1248,1056 +1275,1124 @@ sub _visible_bugs_check_and { LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id AND bug_group_map.group_id NOT IN (" - . $self->groups_as_string . ') + . $self->groups_as_string . ') WHERE bugs.bug_id IN (' . join(',', ('?') x @$check_ids) . ') - AND creation_ts IS NOT NULL '); - if (scalar(@$check_ids) == 1) { - $self->{_sth_one_visible_bug} = $sth; - } - - $sth->execute(@$check_ids); - my $use_qa_contact = Bugzilla->params->{'useqacontact'}; - while (my $row = $sth->fetchrow_arrayref) { - my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, - $cclist_access, $isoncclist, $missinggroup) = @$row; - $visible_cache->{$bug_id} ||= - ((($reporter == $user_id) && $reporter_access) - || ($use_qa_contact - && $qacontact && ($qacontact == $user_id)) - || ($owner == $user_id) - || ($isoncclist && $cclist_access) - || !$missinggroup) ? 1 : 0; - } + AND creation_ts IS NOT NULL ' + ); + if (scalar(@$check_ids) == 1) { + $self->{_sth_one_visible_bug} = $sth; + } + + $sth->execute(@$check_ids); + my $use_qa_contact = Bugzilla->params->{'useqacontact'}; + while (my $row = $sth->fetchrow_arrayref) { + my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, $cclist_access, + $isoncclist, $missinggroup) + = @$row; + $visible_cache->{$bug_id} + ||= ((($reporter == $user_id) && $reporter_access) + || ($use_qa_contact && $qacontact && ($qacontact == $user_id)) + || ($owner == $user_id) + || ($isoncclist && $cclist_access) + || !$missinggroup) ? 1 : 0; + } } sub clear_product_cache { - my $self = shift; - delete $self->{enterable_products}; - delete $self->{selectable_products}; - delete $self->{selectable_classifications}; + my $self = shift; + delete $self->{enterable_products}; + delete $self->{selectable_products}; + delete $self->{selectable_classifications}; } sub can_see_product { - my ($self, $product_name) = @_; + my ($self, $product_name) = @_; - return scalar(grep {$_->name eq $product_name} @{$self->get_selectable_products}); + return + scalar(grep { $_->name eq $product_name } @{$self->get_selectable_products}); } sub get_selectable_products { - my $self = shift; - my $class_id = shift; - my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id; + my $self = shift; + my $class_id = shift; + my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id; - if (!defined $self->{selectable_products}) { - my $query = "SELECT id + if (!defined $self->{selectable_products}) { + my $query = "SELECT id FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id - AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY; - - if (Bugzilla->params->{'or_groups'}) { - # Either the user is in at least one of the MANDATORY groups, or - # there are no such groups for the product. - $query .= " WHERE group_id IN (" . $self->groups_as_string . ") + AND group_control_map.membercontrol = " + . CONTROLMAPMANDATORY; + + if (Bugzilla->params->{'or_groups'}) { + + # Either the user is in at least one of the MANDATORY groups, or + # there are no such groups for the product. + $query .= " WHERE group_id IN (" . $self->groups_as_string . ") OR group_id IS NULL"; - } - else { - # There must be no MANDATORY groups that the user is not in. - $query .= " AND group_id NOT IN (" . $self->groups_as_string . ") + } + else { + # There must be no MANDATORY groups that the user is not in. + $query .= " AND group_id NOT IN (" . $self->groups_as_string . ") WHERE group_id IS NULL"; - } - - my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query); - $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids); } - # Restrict the list of products to those being in the classification, if any. - if ($class_restricted) { - return [grep {$_->classification_id == $class_id} @{$self->{selectable_products}}]; - } - # If we come here, then we want all selectable products. - return $self->{selectable_products}; + my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query); + $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids); + } + + # Restrict the list of products to those being in the classification, if any. + if ($class_restricted) { + return [grep { $_->classification_id == $class_id } + @{$self->{selectable_products}}]; + } + + # If we come here, then we want all selectable products. + return $self->{selectable_products}; } sub get_selectable_classifications { - my ($self) = @_; + my ($self) = @_; - if (!defined $self->{selectable_classifications}) { - my $products = $self->get_selectable_products; - my %class_ids = map { $_->classification_id => 1 } @$products; + if (!defined $self->{selectable_classifications}) { + my $products = $self->get_selectable_products; + my %class_ids = map { $_->classification_id => 1 } @$products; - $self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]); - } - return $self->{selectable_classifications}; + $self->{selectable_classifications} + = Bugzilla::Classification->new_from_list([keys %class_ids]); + } + return $self->{selectable_classifications}; } sub can_enter_product { - my ($self, $input, $warn) = @_; - my $dbh = Bugzilla->dbh; - $warn ||= 0; - - $input = trim($input) if !ref $input; - if (!defined $input or $input eq '') { - return unless $warn == THROW_ERROR; - ThrowUserError('object_not_specified', - { class => 'Bugzilla::Product' }); - } + my ($self, $input, $warn) = @_; + my $dbh = Bugzilla->dbh; + $warn ||= 0; - if (!scalar @{ $self->get_enterable_products }) { - return unless $warn == THROW_ERROR; - ThrowUserError('no_products'); - } + $input = trim($input) if !ref $input; + if (!defined $input or $input eq '') { + return unless $warn == THROW_ERROR; + ThrowUserError('object_not_specified', {class => 'Bugzilla::Product'}); + } - my $product = blessed($input) ? $input - : new Bugzilla::Product({ name => $input }); - my $can_enter = - $product && grep($_->name eq $product->name, - @{ $self->get_enterable_products }); + if (!scalar @{$self->get_enterable_products}) { + return unless $warn == THROW_ERROR; + ThrowUserError('no_products'); + } - return $product if $can_enter; + my $product + = blessed($input) ? $input : new Bugzilla::Product({name => $input}); + my $can_enter = $product + && grep($_->name eq $product->name, @{$self->get_enterable_products}); - return 0 unless $warn == THROW_ERROR; + return $product if $can_enter; - # Check why access was denied. These checks are slow, - # but that's fine, because they only happen if we fail. + return 0 unless $warn == THROW_ERROR; - # We don't just use $product->name for error messages, because if it - # changes case from $input, then that's a clue that the product does - # exist but is hidden. - my $name = blessed($input) ? $input->name : $input; + # Check why access was denied. These checks are slow, + # but that's fine, because they only happen if we fail. - # The product could not exist or you could be denied... - if (!$product || !$product->user_has_access($self)) { - ThrowUserError('entry_access_denied', { product => $name }); - } - # It could be closed for bug entry... - elsif (!$product->is_active) { - ThrowUserError('product_disabled', { product => $product }); - } - # It could have no components... - elsif (!@{$product->components} - || !grep { $_->is_active } @{$product->components}) - { - ThrowUserError('missing_component', { product => $product }); - } - # It could have no versions... - elsif (!@{$product->versions} - || !grep { $_->is_active } @{$product->versions}) - { - ThrowUserError ('missing_version', { product => $product }); - } + # We don't just use $product->name for error messages, because if it + # changes case from $input, then that's a clue that the product does + # exist but is hidden. + my $name = blessed($input) ? $input->name : $input; + + # The product could not exist or you could be denied... + if (!$product || !$product->user_has_access($self)) { + ThrowUserError('entry_access_denied', {product => $name}); + } + + # It could be closed for bug entry... + elsif (!$product->is_active) { + ThrowUserError('product_disabled', {product => $product}); + } + + # It could have no components... + elsif (!@{$product->components} + || !grep { $_->is_active } @{$product->components}) + { + ThrowUserError('missing_component', {product => $product}); + } - die "can_enter_product reached an unreachable location."; + # It could have no versions... + elsif (!@{$product->versions} || !grep { $_->is_active } @{$product->versions}) + { + ThrowUserError('missing_version', {product => $product}); + } + + die "can_enter_product reached an unreachable location."; } sub get_enterable_products { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (defined $self->{enterable_products}) { - return $self->{enterable_products}; - } + if (defined $self->{enterable_products}) { + return $self->{enterable_products}; + } - # All products which the user has "Entry" access to. - my $query = - 'SELECT products.id FROM products + # All products which the user has "Entry" access to. + my $query = 'SELECT products.id FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id AND group_control_map.entry != 0'; - if (Bugzilla->params->{'or_groups'}) { - $query .= " WHERE (group_id IN (" . $self->groups_as_string . ")" . - " OR group_id IS NULL)"; - } else { - $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")" . - " WHERE group_id IS NULL" - } - $query .= " AND products.isactive = 1"; - my $enterable_ids = $dbh->selectcol_arrayref($query); - - if (scalar @$enterable_ids) { - # And all of these products must have at least one component - # and one version. - $enterable_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT products.id FROM products - WHERE ' . $dbh->sql_in('products.id', $enterable_ids) . - ' AND products.id IN (SELECT DISTINCT components.product_id + if (Bugzilla->params->{'or_groups'}) { + $query + .= " WHERE (group_id IN (" + . $self->groups_as_string . ")" + . " OR group_id IS NULL)"; + } + else { + $query + .= " AND group_id NOT IN (" + . $self->groups_as_string . ")" + . " WHERE group_id IS NULL"; + } + $query .= " AND products.isactive = 1"; + my $enterable_ids = $dbh->selectcol_arrayref($query); + + if (scalar @$enterable_ids) { + + # And all of these products must have at least one component + # and one version. + $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT products.id FROM products + WHERE ' + . $dbh->sql_in('products.id', $enterable_ids) + . ' AND products.id IN (SELECT DISTINCT components.product_id FROM components WHERE components.isactive = 1) AND products.id IN (SELECT DISTINCT versions.product_id FROM versions - WHERE versions.isactive = 1)'); - } + WHERE versions.isactive = 1)' + ); + } - $self->{enterable_products} = - Bugzilla::Product->new_from_list($enterable_ids); - return $self->{enterable_products}; + $self->{enterable_products} = Bugzilla::Product->new_from_list($enterable_ids); + return $self->{enterable_products}; } sub can_access_product { - my ($self, $product) = @_; - my $product_name = blessed($product) ? $product->name : $product; - return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products}); + my ($self, $product) = @_; + my $product_name = blessed($product) ? $product->name : $product; + return + scalar(grep { $_->name eq $product_name } @{$self->get_accessible_products}); } sub get_accessible_products { - my $self = shift; - - # Map the objects into a hash using the ids as keys - my %products = map { $_->id => $_ } - @{$self->get_selectable_products}, - @{$self->get_enterable_products}; - - return [ sort { $a->name cmp $b->name } values %products ]; + my $self = shift; + + # Map the objects into a hash using the ids as keys + my %products = map { $_->id => $_ } @{$self->get_selectable_products}, + @{$self->get_enterable_products}; + + return [sort { $a->name cmp $b->name } values %products]; } sub can_administer { - my $self = shift; - - if (not defined $self->{can_administer}) { - my $can_administer = 0; - - $can_administer = 1 if $self->in_group('admin') - || $self->in_group('tweakparams') - || $self->in_group('editusers') - || $self->can_bless - || (Bugzilla->params->{'useclassification'} && $self->in_group('editclassifications')) - || $self->in_group('editcomponents') - || scalar(@{$self->get_products_by_permission('editcomponents')}) - || $self->in_group('creategroups') - || $self->in_group('editkeywords') - || $self->in_group('bz_canusewhines'); - - Bugzilla::Hook::process('user_can_administer', { can_administer => \$can_administer }); - $self->{can_administer} = $can_administer; - } + my $self = shift; + + if (not defined $self->{can_administer}) { + my $can_administer = 0; + + $can_administer = 1 + if $self->in_group('admin') + || $self->in_group('tweakparams') + || $self->in_group('editusers') + || $self->can_bless + || (Bugzilla->params->{'useclassification'} + && $self->in_group('editclassifications')) + || $self->in_group('editcomponents') + || scalar(@{$self->get_products_by_permission('editcomponents')}) + || $self->in_group('creategroups') + || $self->in_group('editkeywords') + || $self->in_group('bz_canusewhines'); - return $self->{can_administer}; + Bugzilla::Hook::process('user_can_administer', + {can_administer => \$can_administer}); + $self->{can_administer} = $can_administer; + } + + return $self->{can_administer}; } sub check_can_admin_product { - my ($self, $product_name) = @_; + my ($self, $product_name) = @_; - # First make sure the product name is valid. - my $product = Bugzilla::Product->check($product_name); + # First make sure the product name is valid. + my $product = Bugzilla::Product->check($product_name); - ($self->in_group('editcomponents', $product->id) - && $self->can_see_product($product->name)) - || ThrowUserError('product_admin_denied', {product => $product->name}); + ( $self->in_group('editcomponents', $product->id) + && $self->can_see_product($product->name)) + || ThrowUserError('product_admin_denied', {product => $product->name}); - # Return the validated product object. - return $product; + # Return the validated product object. + return $product; } sub check_can_admin_flagtype { - my ($self, $flagtype_id) = @_; - - my $flagtype = Bugzilla::FlagType->check({ id => $flagtype_id }); - my $can_fully_edit = 1; - - if (!$self->in_group('editcomponents')) { - my $products = $self->get_products_by_permission('editcomponents'); - # You need editcomponents privs for at least one product to have - # a chance to edit the flagtype. - scalar(@$products) - || ThrowUserError('auth_failure', {group => 'editcomponents', - action => 'edit', - object => 'flagtypes'}); - my $can_admin = 0; - my $i = $flagtype->inclusions_as_hash; - my $e = $flagtype->exclusions_as_hash; - - # If there is at least one product for which the user doesn't have - # editcomponents privs, then don't allow them to do everything with - # this flagtype, independently of whether this product is in the - # exclusion list or not. - my %product_ids; - map { $product_ids{$_->id} = 1 } @$products; - $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i; - - unless ($e->{0}->{0}) { - foreach my $product (@$products) { - my $id = $product->id; - next if $e->{$id}->{0}; - # If we are here, the product has not been explicitly excluded. - # Check whether it's explicitly included, or at least one of - # its components. - $can_admin = ($i->{0}->{0} || $i->{$id}->{0} - || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}})); - last if $can_admin; - } - } - $can_admin || ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); - } - return wantarray ? ($flagtype, $can_fully_edit) : $flagtype; + my ($self, $flagtype_id) = @_; + + my $flagtype = Bugzilla::FlagType->check({id => $flagtype_id}); + my $can_fully_edit = 1; + + if (!$self->in_group('editcomponents')) { + my $products = $self->get_products_by_permission('editcomponents'); + + # You need editcomponents privs for at least one product to have + # a chance to edit the flagtype. + scalar(@$products) + || ThrowUserError('auth_failure', + {group => 'editcomponents', action => 'edit', object => 'flagtypes'}); + my $can_admin = 0; + my $i = $flagtype->inclusions_as_hash; + my $e = $flagtype->exclusions_as_hash; + + # If there is at least one product for which the user doesn't have + # editcomponents privs, then don't allow them to do everything with + # this flagtype, independently of whether this product is in the + # exclusion list or not. + my %product_ids; + map { $product_ids{$_->id} = 1 } @$products; + $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i; + + unless ($e->{0}->{0}) { + foreach my $product (@$products) { + my $id = $product->id; + next if $e->{$id}->{0}; + + # If we are here, the product has not been explicitly excluded. + # Check whether it's explicitly included, or at least one of + # its components. + $can_admin + = ( $i->{0}->{0} + || $i->{$id}->{0} + || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}})); + last if $can_admin; + } + } + $can_admin || ThrowUserError('flag_type_not_editable', {flagtype => $flagtype}); + } + return wantarray ? ($flagtype, $can_fully_edit) : $flagtype; } sub can_request_flag { - my ($self, $flag_type) = @_; + my ($self, $flag_type) = @_; - return ($self->can_set_flag($flag_type) - || !$flag_type->request_group_id - || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0; + return ($self->can_set_flag($flag_type) + || !$flag_type->request_group_id + || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0; } sub can_set_flag { - my ($self, $flag_type) = @_; + my ($self, $flag_type) = @_; - return (!$flag_type->grant_group_id - || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0; + return (!$flag_type->grant_group_id + || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0; } # visible_groups_inherited returns a reference to a list of all the groups # whose members are visible to this user. sub visible_groups_inherited { - my $self = shift; - return $self->{visible_groups_inherited} if defined $self->{visible_groups_inherited}; - return [] unless $self->id; - my @visgroups = @{$self->visible_groups_direct}; - @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)}; - $self->{visible_groups_inherited} = \@visgroups; - return $self->{visible_groups_inherited}; + my $self = shift; + return $self->{visible_groups_inherited} + if defined $self->{visible_groups_inherited}; + return [] unless $self->id; + my @visgroups = @{$self->visible_groups_direct}; + @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)}; + $self->{visible_groups_inherited} = \@visgroups; + return $self->{visible_groups_inherited}; } # visible_groups_direct returns a reference to a list of all the groups that # are visible to this user. sub visible_groups_direct { - my $self = shift; - my @visgroups = (); - return $self->{visible_groups_direct} if defined $self->{visible_groups_direct}; - return [] unless $self->id; - - my $dbh = Bugzilla->dbh; - my $sth; - - if (Bugzilla->params->{'usevisibilitygroups'}) { - $sth = $dbh->prepare("SELECT DISTINCT grantor_id + my $self = shift; + my @visgroups = (); + return $self->{visible_groups_direct} if defined $self->{visible_groups_direct}; + return [] unless $self->id; + + my $dbh = Bugzilla->dbh; + my $sth; + + if (Bugzilla->params->{'usevisibilitygroups'}) { + $sth = $dbh->prepare( + "SELECT DISTINCT grantor_id FROM group_group_map WHERE " . $self->groups_in_sql('member_id') . " - AND grant_type=" . GROUP_VISIBLE); - } - else { - # All groups are visible if usevisibilitygroups is off. - $sth = $dbh->prepare('SELECT id FROM groups'); - } - $sth->execute(); + AND grant_type=" . GROUP_VISIBLE + ); + } + else { + # All groups are visible if usevisibilitygroups is off. + $sth = $dbh->prepare('SELECT id FROM groups'); + } + $sth->execute(); - while (my ($row) = $sth->fetchrow_array) { - push @visgroups,$row; - } - $self->{visible_groups_direct} = \@visgroups; + while (my ($row) = $sth->fetchrow_array) { + push @visgroups, $row; + } + $self->{visible_groups_direct} = \@visgroups; - return $self->{visible_groups_direct}; + return $self->{visible_groups_direct}; } sub visible_groups_as_string { - my $self = shift; - return join(', ', @{$self->visible_groups_inherited()}); + my $self = shift; + return join(', ', @{$self->visible_groups_inherited()}); } # This function defines the groups a user may share a query with. # More restrictive sites may want to build this reference to a list of group IDs # from bless_groups instead of mirroring visible_groups_inherited, perhaps. sub queryshare_groups { - my $self = shift; - my @queryshare_groups; - - return $self->{queryshare_groups} if defined $self->{queryshare_groups}; - - if ($self->in_group(Bugzilla->params->{'querysharegroup'})) { - # We want to be allowed to share with groups we're in only. - # If usevisibilitygroups is on, then we need to restrict this to groups - # we may see. - if (Bugzilla->params->{'usevisibilitygroups'}) { - foreach(@{$self->visible_groups_inherited()}) { - next unless $self->in_group_id($_); - push(@queryshare_groups, $_); - } - } - else { - @queryshare_groups = @{ $self->_group_ids }; - } + my $self = shift; + my @queryshare_groups; + + return $self->{queryshare_groups} if defined $self->{queryshare_groups}; + + if ($self->in_group(Bugzilla->params->{'querysharegroup'})) { + + # We want to be allowed to share with groups we're in only. + # If usevisibilitygroups is on, then we need to restrict this to groups + # we may see. + if (Bugzilla->params->{'usevisibilitygroups'}) { + foreach (@{$self->visible_groups_inherited()}) { + next unless $self->in_group_id($_); + push(@queryshare_groups, $_); + } } + else { + @queryshare_groups = @{$self->_group_ids}; + } + } - return $self->{queryshare_groups} = \@queryshare_groups; + return $self->{queryshare_groups} = \@queryshare_groups; } sub queryshare_groups_as_string { - my $self = shift; - return join(', ', @{$self->queryshare_groups()}); + my $self = shift; + return join(', ', @{$self->queryshare_groups()}); } sub derive_regexp_groups { - my ($self) = @_; + my ($self) = @_; - my $id = $self->id; - return unless $id; + my $id = $self->id; + return unless $id; - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $sth; + my $sth; - # add derived records for any matching regexps + # add derived records for any matching regexps - $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id + $sth = $dbh->prepare( + "SELECT id, userregexp, user_group_map.group_id FROM groups LEFT JOIN user_group_map ON groups.id = user_group_map.group_id AND user_group_map.user_id = ? - AND user_group_map.grant_type = ?"); - $sth->execute($id, GRANT_REGEXP); + AND user_group_map.grant_type = ?" + ); + $sth->execute($id, GRANT_REGEXP); - my $group_insert = $dbh->prepare(q{INSERT INTO user_group_map + my $group_insert = $dbh->prepare( + q{INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, 0, ?)}); - my $group_delete = $dbh->prepare(q{DELETE FROM user_group_map + VALUES (?, ?, 0, ?)} + ); + my $group_delete = $dbh->prepare( + q{DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = 0 - AND grant_type = ?}); - while (my ($group, $regexp, $present) = $sth->fetchrow_array()) { - if (($regexp ne '') && ($self->login =~ m/$regexp/i)) { - $group_insert->execute($id, $group, GRANT_REGEXP) unless $present; - } else { - $group_delete->execute($id, $group, GRANT_REGEXP) if $present; - } + AND grant_type = ?} + ); + while (my ($group, $regexp, $present) = $sth->fetchrow_array()) { + if (($regexp ne '') && ($self->login =~ m/$regexp/i)) { + $group_insert->execute($id, $group, GRANT_REGEXP) unless $present; + } + else { + $group_delete->execute($id, $group, GRANT_REGEXP) if $present; } + } - Bugzilla->memcached->clear_config({ key => "user_groups.$id" }); + Bugzilla->memcached->clear_config({key => "user_groups.$id"}); } sub product_responsibilities { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - return $self->{'product_resp'} if defined $self->{'product_resp'}; - return [] unless $self->id; + return $self->{'product_resp'} if defined $self->{'product_resp'}; + return [] unless $self->id; - my $list = $dbh->selectall_arrayref('SELECT components.product_id, components.id + my $list = $dbh->selectall_arrayref( + 'SELECT components.product_id, components.id FROM components LEFT JOIN component_cc ON components.id = component_cc.component_id WHERE components.initialowner = ? OR components.initialqacontact = ? OR component_cc.user_id = ?', - {Slice => {}}, ($self->id, $self->id, $self->id)); - - unless ($list) { - $self->{'product_resp'} = []; - return $self->{'product_resp'}; - } + {Slice => {}}, ($self->id, $self->id, $self->id) + ); - my @prod_ids = map {$_->{'product_id'}} @$list; - my $products = Bugzilla::Product->new_from_list(\@prod_ids); - # We cannot |use| it, because Component.pm already |use|s User.pm. - require Bugzilla::Component; - my @comp_ids = map {$_->{'id'}} @$list; - my $components = Bugzilla::Component->new_from_list(\@comp_ids); - - my @prod_list; - # @$products is already sorted alphabetically. - foreach my $prod (@$products) { - # We use @components instead of $prod->components because we only want - # components where the user is either the default assignee or QA contact. - push(@prod_list, {product => $prod, - components => [grep {$_->product_id == $prod->id} @$components]}); - } - $self->{'product_resp'} = \@prod_list; + unless ($list) { + $self->{'product_resp'} = []; return $self->{'product_resp'}; + } + + my @prod_ids = map { $_->{'product_id'} } @$list; + my $products = Bugzilla::Product->new_from_list(\@prod_ids); + + # We cannot |use| it, because Component.pm already |use|s User.pm. + require Bugzilla::Component; + my @comp_ids = map { $_->{'id'} } @$list; + my $components = Bugzilla::Component->new_from_list(\@comp_ids); + + my @prod_list; + + # @$products is already sorted alphabetically. + foreach my $prod (@$products) { + + # We use @components instead of $prod->components because we only want + # components where the user is either the default assignee or QA contact. + push( + @prod_list, + { + product => $prod, + components => [grep { $_->product_id == $prod->id } @$components] + } + ); + } + $self->{'product_resp'} = \@prod_list; + return $self->{'product_resp'}; } sub can_bless { - my $self = shift; + my $self = shift; - if (!scalar(@_)) { - # If we're called without an argument, just return - # whether or not we can bless at all. - return scalar(@{ $self->bless_groups }) ? 1 : 0; - } + if (!scalar(@_)) { - # Otherwise, we're checking a specific group - my $group_id = shift; - return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0; + # If we're called without an argument, just return + # whether or not we can bless at all. + return scalar(@{$self->bless_groups}) ? 1 : 0; + } + + # Otherwise, we're checking a specific group + my $group_id = shift; + return grep($_->id == $group_id, @{$self->bless_groups}) ? 1 : 0; } sub match { - # Generates a list of users whose login name (email address) or real name - # matches a substring or wildcard. - # This is also called if matches are disabled (for error checking), but - # in this case only the exact match code will end up running. - - # $str contains the string to match, while $limit contains the - # maximum number of records to retrieve. - my ($str, $limit, $exclude_disabled) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - $str = trim($str); - - my @users = (); - return \@users if $str =~ /^\s*$/; - - # The search order is wildcards, then exact match, then substring search. - # Wildcard matching is skipped if there is no '*', and exact matches will - # not (?) have a '*' in them. If any search comes up with something, the - # ones following it will not execute. - - # first try wildcards - my $wildstr = $str; - - # Do not do wildcards if there is no '*' in the string. - if ($wildstr =~ s/\*/\%/g && $user->id) { - # Build the query. - trick_taint($wildstr); - my $query = "SELECT DISTINCT userid FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "INNER JOIN user_group_map - ON user_group_map.user_id = profiles.userid "; - } - $query .= "WHERE (" - . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " . - $dbh->sql_istrcmp('realname', '?', "LIKE") . ") "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "AND isbless = 0 " . - "AND group_id IN(" . - join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; - } - $query .= " AND is_enabled = 1 " if $exclude_disabled; - $query .= $dbh->sql_limit($limit) if $limit; - # Execute the query, retrieve the results, and make them into - # User objects. - my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr)); - @users = @{Bugzilla::User->new_from_list($user_ids)}; - } - else { # try an exact match - # Exact matches don't care if a user is disabled. - trick_taint($str); - my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles - WHERE ' . $dbh->sql_istrcmp('login_name', '?'), - undef, $str); - - push(@users, new Bugzilla::User($user_id)) if $user_id; + # Generates a list of users whose login name (email address) or real name + # matches a substring or wildcard. + # This is also called if matches are disabled (for error checking), but + # in this case only the exact match code will end up running. + + # $str contains the string to match, while $limit contains the + # maximum number of records to retrieve. + my ($str, $limit, $exclude_disabled) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + $str = trim($str); + + my @users = (); + return \@users if $str =~ /^\s*$/; + + # The search order is wildcards, then exact match, then substring search. + # Wildcard matching is skipped if there is no '*', and exact matches will + # not (?) have a '*' in them. If any search comes up with something, the + # ones following it will not execute. + + # first try wildcards + my $wildstr = $str; + + # Do not do wildcards if there is no '*' in the string. + if ($wildstr =~ s/\*/\%/g && $user->id) { + + # Build the query. + trick_taint($wildstr); + my $query = "SELECT DISTINCT userid FROM profiles "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query .= "INNER JOIN user_group_map + ON user_group_map.user_id = profiles.userid "; } + $query + .= "WHERE (" + . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " + . $dbh->sql_istrcmp('realname', '?', "LIKE") . ") "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= "AND isbless = 0 " + . "AND group_id IN(" + . join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; + } + $query .= " AND is_enabled = 1 " if $exclude_disabled; + $query .= $dbh->sql_limit($limit) if $limit; + + # Execute the query, retrieve the results, and make them into + # User objects. + my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr)); + @users = @{Bugzilla::User->new_from_list($user_ids)}; + } + else { # try an exact match + # Exact matches don't care if a user is disabled. + trick_taint($str); + my $user_id = $dbh->selectrow_array( + 'SELECT userid FROM profiles + WHERE ' + . $dbh->sql_istrcmp('login_name', '?'), undef, $str + ); - # then try substring search - if (!scalar(@users) && length($str) >= 3 && $user->id) { - trick_taint($str); + push(@users, new Bugzilla::User($user_id)) if $user_id; + } - my $query = "SELECT DISTINCT userid FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "INNER JOIN user_group_map + # then try substring search + if (!scalar(@users) && length($str) >= 3 && $user->id) { + trick_taint($str); + + my $query = "SELECT DISTINCT userid FROM profiles "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query .= "INNER JOIN user_group_map ON user_group_map.user_id = profiles.userid "; - } - $query .= " WHERE (" . - $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " . - $dbh->sql_iposition('?', 'realname') . " > 0) "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= " AND isbless = 0" . - " AND group_id IN(" . - join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; - } - $query .= " AND is_enabled = 1 " if $exclude_disabled; - $query .= $dbh->sql_limit($limit) if $limit; - my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str)); - @users = @{Bugzilla::User->new_from_list($user_ids)}; } - return \@users; + $query + .= " WHERE (" + . $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " + . $dbh->sql_iposition('?', 'realname') + . " > 0) "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= " AND isbless = 0" + . " AND group_id IN(" + . join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; + } + $query .= " AND is_enabled = 1 " if $exclude_disabled; + $query .= $dbh->sql_limit($limit) if $limit; + my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str)); + @users = @{Bugzilla::User->new_from_list($user_ids)}; + } + return \@users; } sub match_field { - my $fields = shift; # arguments as a hash - my $data = shift || Bugzilla->input_params; # hash to look up fields in - my $behavior = shift || 0; # A constant that tells us how to act - my $matches = {}; # the values sent to the template - my $matchsuccess = 1; # did the match fail? - my $need_confirm = 0; # whether to display confirmation screen - my $match_multiple = 0; # whether we ever matched more than one user - my @non_conclusive_fields; # fields which don't have a unique user. - - my $params = Bugzilla->params; - - # prepare default form values - - # Fields can be regular expressions matching multiple form fields - # (f.e. "requestee-(\d+)"), so expand each non-literal field - # into the list of form fields it matches. - my $expanded_fields = {}; - foreach my $field_pattern (keys %{$fields}) { - # Check if the field has any non-word characters. Only those fields - # can be regular expressions, so don't expand the field if it doesn't - # have any of those characters. - if ($field_pattern =~ /^\w+$/) { - $expanded_fields->{$field_pattern} = $fields->{$field_pattern}; - } - else { - my @field_names = grep(/$field_pattern/, keys %$data); - - foreach my $field_name (@field_names) { - $expanded_fields->{$field_name} = - { type => $fields->{$field_pattern}->{'type'} }; - - # The field is a requestee field; in order for its name - # to show up correctly on the confirmation page, we need - # to find out the name of its flag type. - if ($field_name =~ /^requestee(_type)?-(\d+)$/) { - my $flag_type; - if ($1) { - require Bugzilla::FlagType; - $flag_type = new Bugzilla::FlagType($2); - } - else { - require Bugzilla::Flag; - my $flag = new Bugzilla::Flag($2); - $flag_type = $flag->type if $flag; - } - if ($flag_type) { - $expanded_fields->{$field_name}->{'flag_type'} = $flag_type; - } - else { - # No need to look for a valid requestee if the flag(type) - # has been deleted (may occur in race conditions). - delete $expanded_fields->{$field_name}; - delete $data->{$field_name}; - } - } - } + my $fields = shift; # arguments as a hash + my $data = shift || Bugzilla->input_params; # hash to look up fields in + my $behavior = shift || 0; # A constant that tells us how to act + my $matches = {}; # the values sent to the template + my $matchsuccess = 1; # did the match fail? + my $need_confirm = 0; # whether to display confirmation screen + my $match_multiple = 0; # whether we ever matched more than one user + my @non_conclusive_fields; # fields which don't have a unique user. + + my $params = Bugzilla->params; + + # prepare default form values + + # Fields can be regular expressions matching multiple form fields + # (f.e. "requestee-(\d+)"), so expand each non-literal field + # into the list of form fields it matches. + my $expanded_fields = {}; + foreach my $field_pattern (keys %{$fields}) { + + # Check if the field has any non-word characters. Only those fields + # can be regular expressions, so don't expand the field if it doesn't + # have any of those characters. + if ($field_pattern =~ /^\w+$/) { + $expanded_fields->{$field_pattern} = $fields->{$field_pattern}; + } + else { + my @field_names = grep(/$field_pattern/, keys %$data); + + foreach my $field_name (@field_names) { + $expanded_fields->{$field_name} = {type => $fields->{$field_pattern}->{'type'}}; + + # The field is a requestee field; in order for its name + # to show up correctly on the confirmation page, we need + # to find out the name of its flag type. + if ($field_name =~ /^requestee(_type)?-(\d+)$/) { + my $flag_type; + if ($1) { + require Bugzilla::FlagType; + $flag_type = new Bugzilla::FlagType($2); + } + else { + require Bugzilla::Flag; + my $flag = new Bugzilla::Flag($2); + $flag_type = $flag->type if $flag; + } + if ($flag_type) { + $expanded_fields->{$field_name}->{'flag_type'} = $flag_type; + } + else { + # No need to look for a valid requestee if the flag(type) + # has been deleted (may occur in race conditions). + delete $expanded_fields->{$field_name}; + delete $data->{$field_name}; + } } + } } - $fields = $expanded_fields; + } + $fields = $expanded_fields; - foreach my $field (keys %{$fields}) { - next unless defined $data->{$field}; + foreach my $field (keys %{$fields}) { + next unless defined $data->{$field}; - #Concatenate login names, so that we have a common way to handle them. - my $raw_field; - if (ref $data->{$field}) { - $raw_field = join(",", @{$data->{$field}}); - } - else { - $raw_field = $data->{$field}; - } - $raw_field = clean_text($raw_field || ''); - - # Now we either split $raw_field by spaces/commas and put the list - # into @queries, or in the case of fields which only accept single - # entries, we simply use the verbatim text. - my @queries; - if ($fields->{$field}->{'type'} eq 'single') { - @queries = ($raw_field); - # We will repopulate it later if a match is found, else it must - # be set to an empty string so that the field remains defined. - $data->{$field} = ''; - } - elsif ($fields->{$field}->{'type'} eq 'multi') { - @queries = split(/[,;]+/, $raw_field); - # We will repopulate it later if a match is found, else it must - # be undefined. - delete $data->{$field}; - } - else { - # bad argument - ThrowCodeError('bad_arg', - { argument => $fields->{$field}->{'type'}, - function => 'Bugzilla::User::match_field', - }); - } + #Concatenate login names, so that we have a common way to handle them. + my $raw_field; + if (ref $data->{$field}) { + $raw_field = join(",", @{$data->{$field}}); + } + else { + $raw_field = $data->{$field}; + } + $raw_field = clean_text($raw_field || ''); - # Tolerate fields that do not exist (in case you specify - # e.g. the QA contact, and it's currently not in use). - next unless (defined $raw_field && $raw_field ne ''); + # Now we either split $raw_field by spaces/commas and put the list + # into @queries, or in the case of fields which only accept single + # entries, we simply use the verbatim text. + my @queries; + if ($fields->{$field}->{'type'} eq 'single') { + @queries = ($raw_field); - my $limit = 0; - if ($params->{'maxusermatches'}) { - $limit = $params->{'maxusermatches'} + 1; - } + # We will repopulate it later if a match is found, else it must + # be set to an empty string so that the field remains defined. + $data->{$field} = ''; + } + elsif ($fields->{$field}->{'type'} eq 'multi') { + @queries = split(/[,;]+/, $raw_field); - my @logins; - for my $query (@queries) { - $query = trim($query); - next if $query eq ''; - - my $users = match( - $query, # match string - $limit, # match limit - 1 # exclude_disabled - ); - - # here is where it checks for multiple matches - if (scalar(@{$users}) == 1) { # exactly one match - push(@logins, @{$users}[0]->login); - - # skip confirmation for exact matches - next if (lc(@{$users}[0]->login) eq lc($query)); - - $matches->{$field}->{$query}->{'status'} = 'success'; - $need_confirm = 1 if $params->{'confirmuniqueusermatch'}; - - } - elsif ((scalar(@{$users}) > 1) - && ($params->{'maxusermatches'} != 1)) { - $need_confirm = 1; - $match_multiple = 1; - push(@non_conclusive_fields, $field); - - if (($params->{'maxusermatches'}) - && (scalar(@{$users}) > $params->{'maxusermatches'})) - { - $matches->{$field}->{$query}->{'status'} = 'trunc'; - pop @{$users}; # take the last one out - } - else { - $matches->{$field}->{$query}->{'status'} = 'success'; - } - - } - else { - # everything else fails - $matchsuccess = 0; # fail - push(@non_conclusive_fields, $field); - $matches->{$field}->{$query}->{'status'} = 'fail'; - $need_confirm = 1; # confirmation screen shows failures - } - - $matches->{$field}->{$query}->{'users'} = $users; + # We will repopulate it later if a match is found, else it must + # be undefined. + delete $data->{$field}; + } + else { + # bad argument + ThrowCodeError( + 'bad_arg', + { + argument => $fields->{$field}->{'type'}, + function => 'Bugzilla::User::match_field', } + ); + } + + # Tolerate fields that do not exist (in case you specify + # e.g. the QA contact, and it's currently not in use). + next unless (defined $raw_field && $raw_field ne ''); + + my $limit = 0; + if ($params->{'maxusermatches'}) { + $limit = $params->{'maxusermatches'} + 1; + } + + my @logins; + for my $query (@queries) { + $query = trim($query); + next if $query eq ''; + + my $users = match( + $query, # match string + $limit, # match limit + 1 # exclude_disabled + ); + + # here is where it checks for multiple matches + if (scalar(@{$users}) == 1) { # exactly one match + push(@logins, @{$users}[0]->login); + + # skip confirmation for exact matches + next if (lc(@{$users}[0]->login) eq lc($query)); + + $matches->{$field}->{$query}->{'status'} = 'success'; + $need_confirm = 1 if $params->{'confirmuniqueusermatch'}; - # If no match or more than one match has been found for a field - # expecting only one match (type eq "single"), we set it back to '' - # so that the caller of this function can still check whether this - # field was defined or not (and it was if we came here). - if ($fields->{$field}->{'type'} eq 'single') { - $data->{$field} = $logins[0] || ''; + } + elsif ((scalar(@{$users}) > 1) && ($params->{'maxusermatches'} != 1)) { + $need_confirm = 1; + $match_multiple = 1; + push(@non_conclusive_fields, $field); + + if ( ($params->{'maxusermatches'}) + && (scalar(@{$users}) > $params->{'maxusermatches'})) + { + $matches->{$field}->{$query}->{'status'} = 'trunc'; + pop @{$users}; # take the last one out } - elsif (scalar @logins) { - $data->{$field} = \@logins; + else { + $matches->{$field}->{$query}->{'status'} = 'success'; } + + } + else { + # everything else fails + $matchsuccess = 0; # fail + push(@non_conclusive_fields, $field); + $matches->{$field}->{$query}->{'status'} = 'fail'; + $need_confirm = 1; # confirmation screen shows failures + } + + $matches->{$field}->{$query}->{'users'} = $users; } - my $retval; - if (!$matchsuccess) { - $retval = USER_MATCH_FAILED; + # If no match or more than one match has been found for a field + # expecting only one match (type eq "single"), we set it back to '' + # so that the caller of this function can still check whether this + # field was defined or not (and it was if we came here). + if ($fields->{$field}->{'type'} eq 'single') { + $data->{$field} = $logins[0] || ''; } - elsif ($match_multiple) { - $retval = USER_MATCH_MULTIPLE; - } - else { - $retval = USER_MATCH_SUCCESS; + elsif (scalar @logins) { + $data->{$field} = \@logins; } + } - # Skip confirmation if we were told to, or if we don't need to confirm. - if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) { - return wantarray ? ($retval, \@non_conclusive_fields) : $retval; - } + my $retval; + if (!$matchsuccess) { + $retval = USER_MATCH_FAILED; + } + elsif ($match_multiple) { + $retval = USER_MATCH_MULTIPLE; + } + else { + $retval = USER_MATCH_SUCCESS; + } + + # Skip confirmation if we were told to, or if we don't need to confirm. + if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) { + return wantarray ? ($retval, \@non_conclusive_fields) : $retval; + } - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $vars = {}; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $vars = {}; - $vars->{'script'} = $cgi->url(-relative => 1); # for self-referencing URLs - $vars->{'fields'} = $fields; # fields being matched - $vars->{'matches'} = $matches; # matches that were made - $vars->{'matchsuccess'} = $matchsuccess; # continue or fail - $vars->{'matchmultiple'} = $match_multiple; + $vars->{'script'} = $cgi->url(-relative => 1); # for self-referencing URLs + $vars->{'fields'} = $fields; # fields being matched + $vars->{'matches'} = $matches; # matches that were made + $vars->{'matchsuccess'} = $matchsuccess; # continue or fail + $vars->{'matchmultiple'} = $match_multiple; - print $cgi->header(); + print $cgi->header(); - $template->process("global/confirm-user-match.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("global/confirm-user-match.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # Changes in some fields automatically trigger events. The field names are # from the fielddefs table. our %names_to_events = ( - 'resolution' => EVT_OPENED_CLOSED, - 'keywords' => EVT_KEYWORD, - 'cc' => EVT_CC, - 'bug_severity' => EVT_PROJ_MANAGEMENT, - 'priority' => EVT_PROJ_MANAGEMENT, - 'bug_status' => EVT_PROJ_MANAGEMENT, - 'target_milestone' => EVT_PROJ_MANAGEMENT, - 'attachments.description' => EVT_ATTACHMENT_DATA, - 'attachments.mimetype' => EVT_ATTACHMENT_DATA, - 'attachments.ispatch' => EVT_ATTACHMENT_DATA, - 'dependson' => EVT_DEPEND_BLOCK, - 'blocked' => EVT_DEPEND_BLOCK, - 'product' => EVT_COMPONENT, - 'component' => EVT_COMPONENT); + 'resolution' => EVT_OPENED_CLOSED, + 'keywords' => EVT_KEYWORD, + 'cc' => EVT_CC, + 'bug_severity' => EVT_PROJ_MANAGEMENT, + 'priority' => EVT_PROJ_MANAGEMENT, + 'bug_status' => EVT_PROJ_MANAGEMENT, + 'target_milestone' => EVT_PROJ_MANAGEMENT, + 'attachments.description' => EVT_ATTACHMENT_DATA, + 'attachments.mimetype' => EVT_ATTACHMENT_DATA, + 'attachments.ispatch' => EVT_ATTACHMENT_DATA, + 'dependson' => EVT_DEPEND_BLOCK, + 'blocked' => EVT_DEPEND_BLOCK, + 'product' => EVT_COMPONENT, + 'component' => EVT_COMPONENT +); # Returns true if the user wants mail for a given bug change. # Note: the "+" signs before the constants suppress bareword quoting. sub wants_bug_mail { - my $self = shift; - my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_; - - # Make a list of the events which have happened during this bug change, - # from the point of view of this user. - my %events; - foreach my $change (@$fieldDiffs) { - my $fieldName = $change->{field_name}; - # A change to any of the above fields sets the corresponding event - if (defined($names_to_events{$fieldName})) { - $events{$names_to_events{$fieldName}} = 1; - } - else { - # Catch-all for any change not caught by a more specific event - $events{+EVT_OTHER} = 1; - } + my $self = shift; + my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_; - # If the user is in a particular role and the value of that role - # changed, we need the ADDED_REMOVED event. - if (($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) || - ($fieldName eq "qa_contact" && $relationship == REL_QA)) - { - $events{+EVT_ADDED_REMOVED} = 1; - } - - if ($fieldName eq "cc") { - my $login = $self->login; - my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); - my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); - if ($inold != $innew) - { - $events{+EVT_ADDED_REMOVED} = 1; - } - } - } + # Make a list of the events which have happened during this bug change, + # from the point of view of this user. + my %events; + foreach my $change (@$fieldDiffs) { + my $fieldName = $change->{field_name}; - if (!$bug->lastdiffed) { - # Notify about new bugs. - $events{+EVT_BUG_CREATED} = 1; - - # You role is new if the bug itself is. - # Only makes sense for the assignee, QA contact and the CC list. - if ($relationship == REL_ASSIGNEE - || $relationship == REL_QA - || $relationship == REL_CC) - { - $events{+EVT_ADDED_REMOVED} = 1; - } + # A change to any of the above fields sets the corresponding event + if (defined($names_to_events{$fieldName})) { + $events{$names_to_events{$fieldName}} = 1; } - - if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { - $events{+EVT_ATTACHMENT} = 1; + else { + # Catch-all for any change not caught by a more specific event + $events{+EVT_OTHER} = 1; } - elsif (defined($$comments[0])) { - $events{+EVT_COMMENT} = 1; + + # If the user is in a particular role and the value of that role + # changed, we need the ADDED_REMOVED event. + if ( ($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) + || ($fieldName eq "qa_contact" && $relationship == REL_QA)) + { + $events{+EVT_ADDED_REMOVED} = 1; } - - # Dependent changed bugmails must have an event to ensure the bugmail is - # emailed. - if ($dep_mail) { - $events{+EVT_DEPEND_BLOCK} = 1; + + if ($fieldName eq "cc") { + my $login = $self->login; + my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); + my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); + if ($inold != $innew) { + $events{+EVT_ADDED_REMOVED} = 1; + } } + } - my @event_list = keys %events; - - my $wants_mail = $self->wants_mail(\@event_list, $relationship); - - # The negative events are handled separately - they can't be incorporated - # into the first wants_mail call, because they are of the opposite sense. - # - # We do them separately because if _any_ of them are set, we don't want - # the mail. - if ($wants_mail && $changer && ($self->id == $changer->id)) { - $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship); - } - - if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') { - $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship); + if (!$bug->lastdiffed) { + + # Notify about new bugs. + $events{+EVT_BUG_CREATED} = 1; + + # You role is new if the bug itself is. + # Only makes sense for the assignee, QA contact and the CC list. + if ( $relationship == REL_ASSIGNEE + || $relationship == REL_QA + || $relationship == REL_CC) + { + $events{+EVT_ADDED_REMOVED} = 1; } - - return $wants_mail; + } + + if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { + $events{+EVT_ATTACHMENT} = 1; + } + elsif (defined($$comments[0])) { + $events{+EVT_COMMENT} = 1; + } + + # Dependent changed bugmails must have an event to ensure the bugmail is + # emailed. + if ($dep_mail) { + $events{+EVT_DEPEND_BLOCK} = 1; + } + + my @event_list = keys %events; + + my $wants_mail = $self->wants_mail(\@event_list, $relationship); + + # The negative events are handled separately - they can't be incorporated + # into the first wants_mail call, because they are of the opposite sense. + # + # We do them separately because if _any_ of them are set, we don't want + # the mail. + if ($wants_mail && $changer && ($self->id == $changer->id)) { + $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship); + } + + if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') { + $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship); + } + + return $wants_mail; } # Returns true if the user wants mail for a given set of events. sub wants_mail { - my $self = shift; - my ($events, $relationship) = @_; - - # Don't send any mail, ever, if account is disabled - # XXX Temporary Compatibility Change 1 of 2: - # This code is disabled for the moment to make the behaviour like the old - # system, which sent bugmail to disabled accounts. - # return 0 if $self->{'disabledtext'}; - - # No mail if there are no events - return 0 if !scalar(@$events); - - # If a relationship isn't given, default to REL_ANY. - if (!defined($relationship)) { - $relationship = REL_ANY; - } + my $self = shift; + my ($events, $relationship) = @_; + + # Don't send any mail, ever, if account is disabled + # XXX Temporary Compatibility Change 1 of 2: + # This code is disabled for the moment to make the behaviour like the old + # system, which sent bugmail to disabled accounts. + # return 0 if $self->{'disabledtext'}; + + # No mail if there are no events + return 0 if !scalar(@$events); - # Skip DB query if relationship is explicit - return 1 if $relationship == REL_GLOBAL_WATCHER; + # If a relationship isn't given, default to REL_ANY. + if (!defined($relationship)) { + $relationship = REL_ANY; + } - my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events; - return $wants_mail ? 1 : 0; + # Skip DB query if relationship is explicit + return 1 if $relationship == REL_GLOBAL_WATCHER; + + my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events; + return $wants_mail ? 1 : 0; } sub mail_settings { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'mail_settings'}) { - my $data = - $dbh->selectall_arrayref('SELECT relationship, event FROM email_setting - WHERE user_id = ?', undef, $self->id); - my %mail; - # The hash is of the form $mail{$relationship}{$event} = 1. - $mail{$_->[0]}{$_->[1]} = 1 foreach @$data; - - $self->{'mail_settings'} = \%mail; - } - return $self->{'mail_settings'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'mail_settings'}) { + my $data = $dbh->selectall_arrayref( + 'SELECT relationship, event FROM email_setting + WHERE user_id = ?', undef, $self->id + ); + my %mail; + + # The hash is of the form $mail{$relationship}{$event} = 1. + $mail{$_->[0]}{$_->[1]} = 1 foreach @$data; + + $self->{'mail_settings'} = \%mail; + } + return $self->{'mail_settings'}; } sub has_audit_entries { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!exists $self->{'has_audit_entries'}) { - $self->{'has_audit_entries'} = - $dbh->selectrow_array('SELECT 1 FROM audit_log WHERE user_id = ? ' . - $dbh->sql_limit(1), undef, $self->id); - } - return $self->{'has_audit_entries'}; + if (!exists $self->{'has_audit_entries'}) { + $self->{'has_audit_entries'} + = $dbh->selectrow_array( + 'SELECT 1 FROM audit_log WHERE user_id = ? ' . $dbh->sql_limit(1), + undef, $self->id); + } + return $self->{'has_audit_entries'}; } sub is_insider { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_insider'}) { - my $insider_group = Bugzilla->params->{'insidergroup'}; - $self->{'is_insider'} = - ($insider_group && $self->in_group($insider_group)) ? 1 : 0; - } - return $self->{'is_insider'}; + if (!defined $self->{'is_insider'}) { + my $insider_group = Bugzilla->params->{'insidergroup'}; + $self->{'is_insider'} + = ($insider_group && $self->in_group($insider_group)) ? 1 : 0; + } + return $self->{'is_insider'}; } sub is_global_watcher { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_global_watcher'}) { - my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'}); - $self->{'is_global_watcher'} = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0; - } - return $self->{'is_global_watcher'}; + if (!defined $self->{'is_global_watcher'}) { + my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'}); + $self->{'is_global_watcher'} + = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0; + } + return $self->{'is_global_watcher'}; } sub is_timetracker { - my $self = shift; + my $self = shift; - if (!defined $self->{'is_timetracker'}) { - my $tt_group = Bugzilla->params->{'timetrackinggroup'}; - $self->{'is_timetracker'} = - ($tt_group && $self->in_group($tt_group)) ? 1 : 0; - } - return $self->{'is_timetracker'}; + if (!defined $self->{'is_timetracker'}) { + my $tt_group = Bugzilla->params->{'timetrackinggroup'}; + $self->{'is_timetracker'} = ($tt_group && $self->in_group($tt_group)) ? 1 : 0; + } + return $self->{'is_timetracker'}; } sub can_tag_comments { - my $self = shift; + my $self = shift; - if (!defined $self->{'can_tag_comments'}) { - my $group = Bugzilla->params->{'comment_taggers_group'}; - $self->{'can_tag_comments'} = - ($group && $self->in_group($group)) ? 1 : 0; - } - return $self->{'can_tag_comments'}; + if (!defined $self->{'can_tag_comments'}) { + my $group = Bugzilla->params->{'comment_taggers_group'}; + $self->{'can_tag_comments'} = ($group && $self->in_group($group)) ? 1 : 0; + } + return $self->{'can_tag_comments'}; } sub get_userlist { - my $self = shift; - - return $self->{'userlist'} if defined $self->{'userlist'}; - - my $dbh = Bugzilla->dbh; - my $query = "SELECT DISTINCT login_name, realname,"; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= " COUNT(group_id) "; - } else { - $query .= " 1 "; - } - $query .= "FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "LEFT JOIN user_group_map " . - "ON user_group_map.user_id = userid AND isbless = 0 " . - "AND group_id IN(" . - join(', ', (-1, @{$self->visible_groups_inherited})) . ")"; - } - $query .= " WHERE is_enabled = 1 "; - $query .= $dbh->sql_group_by('userid', 'login_name, realname'); - - my $sth = $dbh->prepare($query); - $sth->execute; - - my @userlist; - while (my($login, $name, $visible) = $sth->fetchrow_array) { - push @userlist, { - login => $login, - identity => $name ? "$name <$login>" : $login, - visible => $visible, - }; - } - @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist; - - $self->{'userlist'} = \@userlist; - return $self->{'userlist'}; + my $self = shift; + + return $self->{'userlist'} if defined $self->{'userlist'}; + + my $dbh = Bugzilla->dbh; + my $query = "SELECT DISTINCT login_name, realname,"; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query .= " COUNT(group_id) "; + } + else { + $query .= " 1 "; + } + $query .= "FROM profiles "; + if (Bugzilla->params->{'usevisibilitygroups'}) { + $query + .= "LEFT JOIN user_group_map " + . "ON user_group_map.user_id = userid AND isbless = 0 " + . "AND group_id IN(" + . join(', ', (-1, @{$self->visible_groups_inherited})) . ")"; + } + $query .= " WHERE is_enabled = 1 "; + $query .= $dbh->sql_group_by('userid', 'login_name, realname'); + + my $sth = $dbh->prepare($query); + $sth->execute; + + my @userlist; + while (my ($login, $name, $visible) = $sth->fetchrow_array) { + push @userlist, + { + login => $login, + identity => $name ? "$name <$login>" : $login, + visible => $visible, + }; + } + @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist; + + $self->{'userlist'} = \@userlist; + return $self->{'userlist'}; } sub create { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - my $user = $class->SUPER::create(@_); - - # Turn on all email for the new user - require Bugzilla::BugMail; - my %relationships = Bugzilla::BugMail::relationships(); - foreach my $rel (keys %relationships) { - foreach my $event (POS_EVENTS, NEG_EVENTS) { - # These "exceptions" define the default email preferences. - # - # We enable mail unless the change was made by the user, or it's - # just a CC list addition and the user is not the reporter. - next if ($event == EVT_CHANGED_BY_ME); - next if (($event == EVT_CC) && ($rel != REL_REPORTER)); - - $dbh->do('INSERT INTO email_setting (user_id, relationship, event) - VALUES (?, ?, ?)', undef, ($user->id, $rel, $event)); - } - } - - foreach my $event (GLOBAL_EVENTS) { - $dbh->do('INSERT INTO email_setting (user_id, relationship, event) - VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event)); - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + my $user = $class->SUPER::create(@_); + + # Turn on all email for the new user + require Bugzilla::BugMail; + my %relationships = Bugzilla::BugMail::relationships(); + foreach my $rel (keys %relationships) { + foreach my $event (POS_EVENTS, NEG_EVENTS) { + + # These "exceptions" define the default email preferences. + # + # We enable mail unless the change was made by the user, or it's + # just a CC list addition and the user is not the reporter. + next if ($event == EVT_CHANGED_BY_ME); + next if (($event == EVT_CC) && ($rel != REL_REPORTER)); + + $dbh->do( + 'INSERT INTO email_setting (user_id, relationship, event) + VALUES (?, ?, ?)', undef, ($user->id, $rel, $event) + ); + } + } + + foreach my $event (GLOBAL_EVENTS) { + $dbh->do( + 'INSERT INTO email_setting (user_id, relationship, event) + VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event) + ); + } - $user->derive_regexp_groups(); + $user->derive_regexp_groups(); - # Add the creation date to the profiles_activity table. - # $who is the user who created the new user account, i.e. either an - # admin or the new user himself. - my $who = Bugzilla->user->id || $user->id; - my $creation_date_fieldid = get_field_id('creation_ts'); + # Add the creation date to the profiles_activity table. + # $who is the user who created the new user account, i.e. either an + # admin or the new user himself. + my $who = Bugzilla->user->id || $user->id; + my $creation_date_fieldid = get_field_id('creation_ts'); - $dbh->do('INSERT INTO profiles_activity + $dbh->do( + 'INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, newvalue) - VALUES (?, ?, NOW(), ?, NOW())', - undef, ($user->id, $who, $creation_date_fieldid)); + VALUES (?, ?, NOW(), ?, NOW())', undef, + ($user->id, $who, $creation_date_fieldid) + ); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Return the newly created user account. - return $user; + # Return the newly created user account. + return $user; } ########################### @@ -2305,44 +2400,45 @@ sub create { ########################### sub account_is_locked_out { - my $self = shift; - my $login_failures = scalar @{ $self->account_ip_login_failures }; - return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0; + my $self = shift; + my $login_failures = scalar @{$self->account_ip_login_failures}; + return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0; } sub note_login_failure { - my $self = shift; - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - Bugzilla->dbh->do("INSERT INTO login_failure (user_id, ip_addr, login_time) - VALUES (?, ?, LOCALTIMESTAMP(0))", - undef, $self->id, $ip_addr); - delete $self->{account_ip_login_failures}; + my $self = shift; + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + Bugzilla->dbh->do( + "INSERT INTO login_failure (user_id, ip_addr, login_time) + VALUES (?, ?, LOCALTIMESTAMP(0))", undef, $self->id, $ip_addr + ); + delete $self->{account_ip_login_failures}; } sub clear_login_failures { - my $self = shift; - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - Bugzilla->dbh->do( - 'DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?', - undef, $self->id, $ip_addr); - delete $self->{account_ip_login_failures}; + my $self = shift; + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + Bugzilla->dbh->do('DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?', + undef, $self->id, $ip_addr); + delete $self->{account_ip_login_failures}; } sub account_ip_login_failures { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', - LOGIN_LOCKOUT_INTERVAL, 'MINUTE'); - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref( - "SELECT login_time, ip_addr, user_id FROM login_failure + my $self = shift; + my $dbh = Bugzilla->dbh; + my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', LOGIN_LOCKOUT_INTERVAL, + 'MINUTE'); + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref( + "SELECT login_time, ip_addr, user_id FROM login_failure WHERE user_id = ? AND login_time > $time AND ip_addr = ? - ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr); - return $self->{account_ip_login_failures}; + ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr + ); + return $self->{account_ip_login_failures}; } ############### @@ -2350,145 +2446,162 @@ sub account_ip_login_failures { ############### sub is_available_username { - my ($username, $old_username) = @_; - - if(login_to_id($username) != 0) { - return 0; - } + my ($username, $old_username) = @_; - my $dbh = Bugzilla->dbh; - # $username is safe because it is only used in SELECT placeholders. - trick_taint($username); - # Reject if the new login is part of an email change which is - # still in progress - # - # substring/locate stuff: bug 165221; this used to use regexes, but that - # was unsafe and required weird escaping; using substring to pull out - # the new/old email addresses and sql_position() to find the delimiter (':') - # is cleaner/safer - my ($tokentype, $eventdata) = $dbh->selectrow_array( - "SELECT tokentype, eventdata + if (login_to_id($username) != 0) { + return 0; + } + + my $dbh = Bugzilla->dbh; + + # $username is safe because it is only used in SELECT placeholders. + trick_taint($username); + + # Reject if the new login is part of an email change which is + # still in progress + # + # substring/locate stuff: bug 165221; this used to use regexes, but that + # was unsafe and required weird escaping; using substring to pull out + # the new/old email addresses and sql_position() to find the delimiter (':') + # is cleaner/safer + my ($tokentype, $eventdata) = $dbh->selectrow_array( + "SELECT tokentype, eventdata FROM tokens WHERE (tokentype = 'emailold' - AND SUBSTRING(eventdata, 1, (" . - $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?) + AND SUBSTRING(eventdata, 1, (" + . $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?) OR (tokentype = 'emailnew' - AND SUBSTRING(eventdata, (" . - $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)", - undef, ($username, $username)); - - if ($eventdata) { - # Allow thru owner of token - if ($old_username - && (($tokentype eq 'emailnew' && $eventdata eq "$old_username:$username") - || ($tokentype eq 'emailold' && $eventdata eq "$username:$old_username"))) - { - return 1; - } - return 0; + AND SUBSTRING(eventdata, (" + . $dbh->sql_position(q{':'}, 'eventdata') + . "+ 1), LENGTH(eventdata)) = ?)", + undef, ($username, $username) + ); + + if ($eventdata) { + + # Allow thru owner of token + if ( + $old_username + && ( ($tokentype eq 'emailnew' && $eventdata eq "$old_username:$username") + || ($tokentype eq 'emailold' && $eventdata eq "$username:$old_username")) + ) + { + return 1; } + return 0; + } - return 1; + return 1; } sub check_account_creation_enabled { - my $self = shift; + my $self = shift; - # If we're using e.g. LDAP for login, then we can't create a new account. - $self->authorizer->user_can_create_account - || ThrowUserError('auth_cant_create_account'); + # If we're using e.g. LDAP for login, then we can't create a new account. + $self->authorizer->user_can_create_account + || ThrowUserError('auth_cant_create_account'); - Bugzilla->params->{'createemailregexp'} - || ThrowUserError('account_creation_disabled'); + Bugzilla->params->{'createemailregexp'} + || ThrowUserError('account_creation_disabled'); } sub check_and_send_account_creation_confirmation { - my ($self, $login) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $login) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction; + $dbh->bz_start_transaction; - $login = $self->check_login_name($login); - my $creation_regexp = Bugzilla->params->{'createemailregexp'}; + $login = $self->check_login_name($login); + my $creation_regexp = Bugzilla->params->{'createemailregexp'}; - if ($login !~ /$creation_regexp/i) { - ThrowUserError('account_creation_restricted'); - } + if ($login !~ /$creation_regexp/i) { + ThrowUserError('account_creation_restricted'); + } - # Allow extensions to do extra checks. - Bugzilla::Hook::process('user_check_account_creation', { login => $login }); + # Allow extensions to do extra checks. + Bugzilla::Hook::process('user_check_account_creation', {login => $login}); - # Create and send a token for this new account. - require Bugzilla::Token; - Bugzilla::Token::issue_new_user_account_token($login); + # Create and send a token for this new account. + require Bugzilla::Token; + Bugzilla::Token::issue_new_user_account_token($login); - $dbh->bz_commit_transaction; + $dbh->bz_commit_transaction; } # This is used in a few performance-critical areas where we don't want to # do check() and pull all the user data from the database. sub login_to_id { - my ($login, $throw_error) = @_; - my $dbh = Bugzilla->dbh; - my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {}; - - # We cache lookups because this function showed up as taking up a - # significant amount of time in profiles of xt/search.t. However, - # for users that don't exist, we re-do the check every time, because - # otherwise we break is_available_username. - my $user_id; - if (defined $cache->{$login}) { - $user_id = $cache->{$login}; - } - else { - # No need to validate $login -- it will be used by the following SELECT - # statement only, so it's safe to simply trick_taint. - trick_taint($login); - $user_id = $dbh->selectrow_array( - "SELECT userid FROM profiles - WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login); - $cache->{$login} = $user_id; - } - - if ($user_id) { - return $user_id; - } elsif ($throw_error) { - ThrowUserError('invalid_username', { name => $login }); - } else { - return 0; - } + my ($login, $throw_error) = @_; + my $dbh = Bugzilla->dbh; + my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {}; + + # We cache lookups because this function showed up as taking up a + # significant amount of time in profiles of xt/search.t. However, + # for users that don't exist, we re-do the check every time, because + # otherwise we break is_available_username. + my $user_id; + if (defined $cache->{$login}) { + $user_id = $cache->{$login}; + } + else { + # No need to validate $login -- it will be used by the following SELECT + # statement only, so it's safe to simply trick_taint. + trick_taint($login); + $user_id = $dbh->selectrow_array( + "SELECT userid FROM profiles + WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login + ); + $cache->{$login} = $user_id; + } + + if ($user_id) { + return $user_id; + } + elsif ($throw_error) { + ThrowUserError('invalid_username', {name => $login}); + } + else { + return 0; + } } sub validate_password { - my $check = validate_password_check(@_); - ThrowUserError($check) if $check; - return 1; + my $check = validate_password_check(@_); + ThrowUserError($check) if $check; + return 1; } sub validate_password_check { - my ($password, $matchpassword) = @_; - - if (length($password) < USER_PASSWORD_MIN_LENGTH) { - return 'password_too_short'; - } elsif ((defined $matchpassword) && ($password ne $matchpassword)) { - return 'passwords_dont_match'; - } - - my $complexity_level = Bugzilla->params->{password_complexity}; - if ($complexity_level eq 'letters_numbers_specialchars') { - return 'password_not_complex' - if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/); - } elsif ($complexity_level eq 'letters_numbers') { - return 'password_not_complex' - if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/); - } elsif ($complexity_level eq 'mixed_letters') { - return 'password_not_complex' - if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/); - } - - # Having done these checks makes us consider the password untainted. - trick_taint($_[0]); - return; + my ($password, $matchpassword) = @_; + + if (length($password) < USER_PASSWORD_MIN_LENGTH) { + return 'password_too_short'; + } + elsif ((defined $matchpassword) && ($password ne $matchpassword)) { + return 'passwords_dont_match'; + } + + my $complexity_level = Bugzilla->params->{password_complexity}; + if ($complexity_level eq 'letters_numbers_specialchars') { + return 'password_not_complex' + if ($password !~ /[[:alpha:]]/ + || $password !~ /\d/ + || $password !~ /[[:punct:]]/); + } + elsif ($complexity_level eq 'letters_numbers') { + return 'password_not_complex' + if ($password !~ /[[:lower:]]/ + || $password !~ /[[:upper:]]/ + || $password !~ /\d/); + } + elsif ($complexity_level eq 'mixed_letters') { + return 'password_not_complex' + if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/); + } + + # Having done these checks makes us consider the password untainted. + trick_taint($_[0]); + return; } diff --git a/Bugzilla/User/APIKey.pm b/Bugzilla/User/APIKey.pm index d268a0a93..d2e337c5e 100644 --- a/Bugzilla/User/APIKey.pm +++ b/Bugzilla/User/APIKey.pm @@ -20,52 +20,54 @@ use Bugzilla::Util qw(generate_random_password trim); # Overriden Constants that are used as methods ##################################################################### -use constant DB_TABLE => 'user_api_keys'; -use constant DB_COLUMNS => qw( - id - user_id - api_key - description - revoked - last_used +use constant DB_TABLE => 'user_api_keys'; +use constant DB_COLUMNS => qw( + id + user_id + api_key + description + revoked + last_used ); use constant UPDATE_COLUMNS => qw(description revoked last_used); use constant VALIDATORS => { - api_key => \&_check_api_key, - description => \&_check_description, - revoked => \&Bugzilla::Object::check_boolean, + api_key => \&_check_api_key, + description => \&_check_description, + revoked => \&Bugzilla::Object::check_boolean, }; -use constant LIST_ORDER => 'id'; -use constant NAME_FIELD => 'api_key'; +use constant LIST_ORDER => 'id'; +use constant NAME_FIELD => 'api_key'; # turn off auditing and exclude these objects from memcached -use constant { AUDIT_CREATES => 0, - AUDIT_UPDATES => 0, - AUDIT_REMOVES => 0, - USE_MEMCACHED => 0 }; +use constant { + AUDIT_CREATES => 0, + AUDIT_UPDATES => 0, + AUDIT_REMOVES => 0, + USE_MEMCACHED => 0 +}; # Accessors -sub id { return $_[0]->{id} } -sub user_id { return $_[0]->{user_id} } -sub api_key { return $_[0]->{api_key} } -sub description { return $_[0]->{description} } -sub revoked { return $_[0]->{revoked} } -sub last_used { return $_[0]->{last_used} } +sub id { return $_[0]->{id} } +sub user_id { return $_[0]->{user_id} } +sub api_key { return $_[0]->{api_key} } +sub description { return $_[0]->{description} } +sub revoked { return $_[0]->{revoked} } +sub last_used { return $_[0]->{last_used} } # Helpers sub user { - my $self = shift; - $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1}); - return $self->{user}; + my $self = shift; + $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1}); + return $self->{user}; } sub update_last_used { - my $self = shift; - my $timestamp = shift - || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $self->set('last_used', $timestamp); - $self->update; + my $self = shift; + my $timestamp + = shift || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $self->set('last_used', $timestamp); + $self->update; } # Setters @@ -73,8 +75,8 @@ sub set_description { $_[0]->set('description', $_[1]); } sub set_revoked { $_[0]->set('revoked', $_[1]); } # Validators -sub _check_api_key { return generate_random_password(40); } -sub _check_description { return trim($_[1]) || ''; } +sub _check_api_key { return generate_random_password(40); } +sub _check_description { return trim($_[1]) || ''; } 1; __END__ diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm index aece3b7de..94171a5d9 100644 --- a/Bugzilla/User/Setting.pm +++ b/Bugzilla/User/Setting.pm @@ -17,10 +17,10 @@ use parent qw(Exporter); # Module stuff @Bugzilla::User::Setting::EXPORT = qw( - get_all_settings - get_defaults - add_setting - clear_settings_cache + get_all_settings + get_defaults + add_setting + clear_settings_cache ); use Bugzilla::Error; @@ -31,88 +31,84 @@ use Bugzilla::Util qw(trick_taint get_text); ############################### sub new { - my $invocant = shift; - my $setting_name = shift; - my $user_id = shift; - - my $class = ref($invocant) || $invocant; - my $subclass = ''; - - # Create a ref to an empty hash and bless it - my $self = {}; - - my $dbh = Bugzilla->dbh; - - # Confirm that the $setting_name is properly formed; - # if not, throw a code error. - # - # NOTE: due to the way that setting names are used in templates, - # they must conform to to the limitations set for HTML NAMEs and IDs. - # - if ( !($setting_name =~ /^[a-zA-Z][-.:\w]*$/) ) { - ThrowCodeError("setting_name_invalid", { name => $setting_name }); - } - - # If there were only two parameters passed in, then we need - # to retrieve the information for this setting ourselves. - if (scalar @_ == 0) { - - my ($default, $is_enabled, $value); - ($default, $is_enabled, $value, $subclass) = - $dbh->selectrow_array( - q{SELECT default_value, is_enabled, setting_value, subclass + my $invocant = shift; + my $setting_name = shift; + my $user_id = shift; + + my $class = ref($invocant) || $invocant; + my $subclass = ''; + + # Create a ref to an empty hash and bless it + my $self = {}; + + my $dbh = Bugzilla->dbh; + + # Confirm that the $setting_name is properly formed; + # if not, throw a code error. + # + # NOTE: due to the way that setting names are used in templates, + # they must conform to to the limitations set for HTML NAMEs and IDs. + # + if (!($setting_name =~ /^[a-zA-Z][-.:\w]*$/)) { + ThrowCodeError("setting_name_invalid", {name => $setting_name}); + } + + # If there were only two parameters passed in, then we need + # to retrieve the information for this setting ourselves. + if (scalar @_ == 0) { + + my ($default, $is_enabled, $value); + ($default, $is_enabled, $value, $subclass) = $dbh->selectrow_array( + q{SELECT default_value, is_enabled, setting_value, subclass FROM setting LEFT JOIN profile_setting ON setting.name = profile_setting.setting_name WHERE name = ? - AND profile_setting.user_id = ?}, - undef, - $setting_name, $user_id); - - # if not defined, then grab the default value - if (! defined $value) { - ($default, $is_enabled, $subclass) = - $dbh->selectrow_array( - q{SELECT default_value, is_enabled, subclass + AND profile_setting.user_id = ?}, undef, $setting_name, $user_id + ); + + # if not defined, then grab the default value + if (!defined $value) { + ($default, $is_enabled, $subclass) = $dbh->selectrow_array( + q{SELECT default_value, is_enabled, subclass FROM setting - WHERE name = ?}, - undef, - $setting_name); - } - - $self->{'is_enabled'} = $is_enabled; - $self->{'default_value'} = $default; - - # IF the setting is enabled, AND the user has chosen a setting - # THEN return that value - # ELSE return the site default, and note that it is the default. - if ( ($is_enabled) && (defined $value) ) { - $self->{'value'} = $value; - } else { - $self->{'value'} = $default; - $self->{'isdefault'} = 1; - } - } - else { - # If the values were passed in, simply assign them and return. - $self->{'is_enabled'} = shift; - $self->{'default_value'} = shift; - $self->{'value'} = shift; - $self->{'is_default'} = shift; - $subclass = shift; - } - if ($subclass) { - eval('require ' . $class . '::' . $subclass); - $@ && ThrowCodeError('setting_subclass_invalid', - {'subclass' => $subclass}); - $class = $class . '::' . $subclass; + WHERE name = ?}, undef, $setting_name + ); } - bless($self, $class); - $self->{'_setting_name'} = $setting_name; - $self->{'_user_id'} = $user_id; + $self->{'is_enabled'} = $is_enabled; + $self->{'default_value'} = $default; - return $self; + # IF the setting is enabled, AND the user has chosen a setting + # THEN return that value + # ELSE return the site default, and note that it is the default. + if (($is_enabled) && (defined $value)) { + $self->{'value'} = $value; + } + else { + $self->{'value'} = $default; + $self->{'isdefault'} = 1; + } + } + else { + # If the values were passed in, simply assign them and return. + $self->{'is_enabled'} = shift; + $self->{'default_value'} = shift; + $self->{'value'} = shift; + $self->{'is_default'} = shift; + $subclass = shift; + } + if ($subclass) { + eval('require ' . $class . '::' . $subclass); + $@ && ThrowCodeError('setting_subclass_invalid', {'subclass' => $subclass}); + $class = $class . '::' . $subclass; + } + bless($self, $class); + + $self->{'_setting_name'} = $setting_name; + $self->{'_user_id'} = $user_id; + + return $self; } ############################### @@ -120,191 +116,205 @@ sub new { ############################### sub add_setting { - my ($name, $values, $default_value, $subclass, $force_check, - $silently) = @_; - my $dbh = Bugzilla->dbh; - - my $exists = _setting_exists($name); - return if ($exists && !$force_check); - - ($name && length( $default_value // '' )) - || ThrowCodeError("setting_info_invalid"); - - if ($exists) { - # If this setting exists, we delete it and regenerate it. - $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name); - $dbh->do('DELETE FROM setting WHERE name = ?', undef, $name); - # Remove obsolete user preferences for this setting. - if (defined $values && scalar(@$values)) { - my $list = join(', ', map {$dbh->quote($_)} @$values); - $dbh->do("DELETE FROM profile_setting - WHERE setting_name = ? AND setting_value NOT IN ($list)", - undef, $name); - } - } - elsif (!$silently) { - print get_text('install_setting_new', { name => $name }) . "\n"; - } - $dbh->do(q{INSERT INTO setting (name, default_value, is_enabled, subclass) - VALUES (?, ?, 1, ?)}, - undef, ($name, $default_value, $subclass)); + my ($name, $values, $default_value, $subclass, $force_check, $silently) = @_; + my $dbh = Bugzilla->dbh; + + my $exists = _setting_exists($name); + return if ($exists && !$force_check); + + ($name && length($default_value // '')) + || ThrowCodeError("setting_info_invalid"); - my $sth = $dbh->prepare(q{INSERT INTO setting_value (name, value, sortindex) - VALUES (?, ?, ?)}); + if ($exists) { - my $sortindex = 5; - foreach my $key (@$values){ - $sth->execute($name, $key, $sortindex); - $sortindex += 5; + # If this setting exists, we delete it and regenerate it. + $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name); + $dbh->do('DELETE FROM setting WHERE name = ?', undef, $name); + + # Remove obsolete user preferences for this setting. + if (defined $values && scalar(@$values)) { + my $list = join(', ', map { $dbh->quote($_) } @$values); + $dbh->do( + "DELETE FROM profile_setting + WHERE setting_name = ? AND setting_value NOT IN ($list)", undef, + $name + ); } + } + elsif (!$silently) { + print get_text('install_setting_new', {name => $name}) . "\n"; + } + $dbh->do( + q{INSERT INTO setting (name, default_value, is_enabled, subclass) + VALUES (?, ?, 1, ?)}, undef, ($name, $default_value, $subclass) + ); + + my $sth = $dbh->prepare( + q{INSERT INTO setting_value (name, value, sortindex) + VALUES (?, ?, ?)} + ); + + my $sortindex = 5; + foreach my $key (@$values) { + $sth->execute($name, $key, $sortindex); + $sortindex += 5; + } } sub get_all_settings { - my ($user_id) = @_; - my $settings = {}; - my $dbh = Bugzilla->dbh; - - my $cache_key = "user_settings.$user_id"; - my $rows = Bugzilla->memcached->get_config({ key => $cache_key }); - if (!$rows) { - $rows = $dbh->selectall_arrayref( - q{SELECT name, default_value, is_enabled, setting_value, subclass + my ($user_id) = @_; + my $settings = {}; + my $dbh = Bugzilla->dbh; + + my $cache_key = "user_settings.$user_id"; + my $rows = Bugzilla->memcached->get_config({key => $cache_key}); + if (!$rows) { + $rows = $dbh->selectall_arrayref( + q{SELECT name, default_value, is_enabled, setting_value, subclass FROM setting LEFT JOIN profile_setting ON setting.name = profile_setting.setting_name - AND profile_setting.user_id = ?}, undef, ($user_id)); - Bugzilla->memcached->set_config({ key => $cache_key, data => $rows }); - } + AND profile_setting.user_id = ?}, undef, ($user_id) + ); + Bugzilla->memcached->set_config({key => $cache_key, data => $rows}); + } - foreach my $row (@$rows) { - my ($name, $default_value, $is_enabled, $value, $subclass) = @$row; + foreach my $row (@$rows) { + my ($name, $default_value, $is_enabled, $value, $subclass) = @$row; - my $is_default; + my $is_default; - if ( ($is_enabled) && (defined $value) ) { - $is_default = 0; - } else { - $value = $default_value; - $is_default = 1; - } - - $settings->{$name} = new Bugzilla::User::Setting( - $name, $user_id, $is_enabled, - $default_value, $value, $is_default, $subclass); + if (($is_enabled) && (defined $value)) { + $is_default = 0; } + else { + $value = $default_value; + $is_default = 1; + } + + $settings->{$name} + = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value, + $value, $is_default, $subclass); + } - return $settings; + return $settings; } sub clear_settings_cache { - my ($user_id) = @_; - Bugzilla->memcached->clear_config({ key => "user_settings.$user_id" }); + my ($user_id) = @_; + Bugzilla->memcached->clear_config({key => "user_settings.$user_id"}); } sub get_defaults { - my ($user_id) = @_; - my $dbh = Bugzilla->dbh; - my $default_settings = {}; + my ($user_id) = @_; + my $dbh = Bugzilla->dbh; + my $default_settings = {}; - $user_id ||= 0; + $user_id ||= 0; - my $rows = $dbh->selectall_arrayref(q{SELECT name, default_value, is_enabled, subclass - FROM setting}); + my $rows = $dbh->selectall_arrayref( + q{SELECT name, default_value, is_enabled, subclass + FROM setting} + ); - foreach my $row (@$rows) { - my ($name, $default_value, $is_enabled, $subclass) = @$row; + foreach my $row (@$rows) { + my ($name, $default_value, $is_enabled, $subclass) = @$row; - $default_settings->{$name} = new Bugzilla::User::Setting( - $name, $user_id, $is_enabled, $default_value, $default_value, 1, - $subclass); - } + $default_settings->{$name} + = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value, + $default_value, 1, $subclass); + } - return $default_settings; + return $default_settings; } sub set_default { - my ($setting_name, $default_value, $is_enabled) = @_; - my $dbh = Bugzilla->dbh; + my ($setting_name, $default_value, $is_enabled) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(q{UPDATE setting + my $sth = $dbh->prepare( + q{UPDATE setting SET default_value = ?, is_enabled = ? - WHERE name = ?}); - $sth->execute($default_value, $is_enabled, $setting_name); + WHERE name = ?} + ); + $sth->execute($default_value, $is_enabled, $setting_name); } sub _setting_exists { - my ($setting_name) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_arrayref( - "SELECT 1 FROM setting WHERE name = ?", undef, $setting_name) || 0; + my ($setting_name) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_arrayref("SELECT 1 FROM setting WHERE name = ?", + undef, $setting_name) + || 0; } sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my $dbh = Bugzilla->dbh; - $self->{'legal_values'} = $dbh->selectcol_arrayref( - q{SELECT value + my $dbh = Bugzilla->dbh; + $self->{'legal_values'} = $dbh->selectcol_arrayref( + q{SELECT value FROM setting_value WHERE name = ? - ORDER BY sortindex}, - undef, $self->{'_setting_name'}); + ORDER BY sortindex}, undef, $self->{'_setting_name'} + ); - return $self->{'legal_values'}; + return $self->{'legal_values'}; } sub validate_value { - my $self = shift; - - if (grep(/^$_[0]$/, @{$self->legal_values()})) { - trick_taint($_[0]); - } - else { - ThrowCodeError('setting_value_invalid', - {'name' => $self->{'_setting_name'}, - 'value' => $_[0]}); - } + my $self = shift; + + if (grep(/^$_[0]$/, @{$self->legal_values()})) { + trick_taint($_[0]); + } + else { + ThrowCodeError('setting_value_invalid', + {'name' => $self->{'_setting_name'}, 'value' => $_[0]}); + } } sub reset_to_default { - my ($self) = @_; + my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->do(q{ DELETE + my $dbh = Bugzilla->dbh; + my $sth = $dbh->do( + q{ DELETE FROM profile_setting WHERE setting_name = ? - AND user_id = ?}, - undef, $self->{'_setting_name'}, $self->{'_user_id'}); - $self->{'value'} = $self->{'default_value'}; - $self->{'is_default'} = 1; + AND user_id = ?}, undef, $self->{'_setting_name'}, + $self->{'_user_id'} + ); + $self->{'value'} = $self->{'default_value'}; + $self->{'is_default'} = 1; } sub set { - my ($self, $value) = @_; - my $dbh = Bugzilla->dbh; - my $query; + my ($self, $value) = @_; + my $dbh = Bugzilla->dbh; + my $query; - if ($self->{'is_default'}) { - $query = q{INSERT INTO profile_setting + if ($self->{'is_default'}) { + $query = q{INSERT INTO profile_setting (setting_value, setting_name, user_id) VALUES (?,?,?)}; - } else { - $query = q{UPDATE profile_setting + } + else { + $query = q{UPDATE profile_setting SET setting_value = ? WHERE setting_name = ? AND user_id = ?}; - } - $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'}); + } + $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'}); - $self->{'value'} = $value; - $self->{'is_default'} = 0; + $self->{'value'} = $value; + $self->{'is_default'} = 0; } - 1; __END__ diff --git a/Bugzilla/User/Setting/Lang.pm b/Bugzilla/User/Setting/Lang.pm index d980b7a92..d1aeb3421 100644 --- a/Bugzilla/User/Setting/Lang.pm +++ b/Bugzilla/User/Setting/Lang.pm @@ -16,11 +16,11 @@ use parent qw(Bugzilla::User::Setting); use Bugzilla::Constants; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - return $self->{'legal_values'} = Bugzilla->languages; + return $self->{'legal_values'} = Bugzilla->languages; } 1; diff --git a/Bugzilla/User/Setting/Skin.pm b/Bugzilla/User/Setting/Skin.pm index 7b0688c0c..0447b02ab 100644 --- a/Bugzilla/User/Setting/Skin.pm +++ b/Bugzilla/User/Setting/Skin.pm @@ -21,24 +21,26 @@ use File::Basename; use constant BUILTIN_SKIN_NAMES => ['standard']; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my $dirbase = bz_locations()->{'skinsdir'} . '/contrib'; - # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the - # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES. - my @legal_values = @{(BUILTIN_SKIN_NAMES)}; + my $dirbase = bz_locations()->{'skinsdir'} . '/contrib'; - foreach my $direntry (glob(catdir($dirbase, '*'))) { - if (-d $direntry) { - next if basename($direntry) =~ /^cvs$/i; - # Stylesheet set found - push(@legal_values, basename($direntry)); - } + # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the + # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES. + my @legal_values = @{(BUILTIN_SKIN_NAMES)}; + + foreach my $direntry (glob(catdir($dirbase, '*'))) { + if (-d $direntry) { + next if basename($direntry) =~ /^cvs$/i; + + # Stylesheet set found + push(@legal_values, basename($direntry)); } + } - return $self->{'legal_values'} = \@legal_values; + return $self->{'legal_values'} = \@legal_values; } 1; diff --git a/Bugzilla/User/Setting/Timezone.pm b/Bugzilla/User/Setting/Timezone.pm index 8959d1dda..b6b2503b5 100644 --- a/Bugzilla/User/Setting/Timezone.pm +++ b/Bugzilla/User/Setting/Timezone.pm @@ -18,19 +18,21 @@ use parent qw(Bugzilla::User::Setting); use Bugzilla::Constants; sub legal_values { - my ($self) = @_; + my ($self) = @_; - return $self->{'legal_values'} if defined $self->{'legal_values'}; + return $self->{'legal_values'} if defined $self->{'legal_values'}; - my @timezones = DateTime::TimeZone->all_names; - # Remove old formats, such as CST6CDT, EST, EST5EDT. - @timezones = grep { $_ =~ m#.+/.+#} @timezones; - # Append 'local' to the list, which will use the timezone - # given by the server. - push(@timezones, 'local'); - push(@timezones, 'UTC'); + my @timezones = DateTime::TimeZone->all_names; - return $self->{'legal_values'} = \@timezones; + # Remove old formats, such as CST6CDT, EST, EST5EDT. + @timezones = grep { $_ =~ m#.+/.+# } @timezones; + + # Append 'local' to the list, which will use the timezone + # given by the server. + push(@timezones, 'local'); + push(@timezones, 'UTC'); + + return $self->{'legal_values'} = \@timezones; } 1; diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm index 14637038c..1995cc82f 100644 --- a/Bugzilla/UserAgent.pm +++ b/Bugzilla/UserAgent.pm @@ -20,176 +20,200 @@ use List::MoreUtils qw(natatime); use constant DEFAULT_VALUE => 'Other'; use constant PLATFORMS_MAP => ( - # PowerPC - qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"], - # AMD64, Intel x86_64 - qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"], - qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"], - # Intel IA64 - qr/\(.*IA64.*\)/ => ["IA64", "PC"], - # Intel x86 - qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"], - # Versions of Windows that only run on Intel x86 - qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"], - qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"], - # Sparc - qr/\(.*sparc.*\)/ => ["Sparc", "Sun"], - qr/\(.*sun4.*\)/ => ["Sparc", "Sun"], - # Alpha - qr/\(.*AXP.*\)/i => ["Alpha", "DEC"], - qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"], - qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"], - # MIPS - qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], - qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"], - # 68k - qr/\(.*68K.*\)/ => ["68k", "Macintosh"], - qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"], - # HP - qr/\(.*9000.*\)/ => ["PA-RISC", "HP"], - # ARM - qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["ARM"], - qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"], - # PocketPC intentionally before PowerPC - qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"], - # PowerPC - qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"], - qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"], - # Stereotypical and broken - qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"], - qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"], - qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"], - qr/\(.*WOW64.*\)/ => ["x86_64"], - qr/\(.*Win64.*\)/ => ["IA64"], - qr/\(Win.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"], - qr/\(.*OSF.*\)/ => ["Alpha", "DEC"], - qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"], - qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], - qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"], - # Braindead old browsers who didn't follow convention: - qr/Amiga/ => ["68k", "Macintosh"], - qr/WinMosaic/ => ["IA32", "x86", "PC"], + + # PowerPC + qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"], + + # AMD64, Intel x86_64 + qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"], + qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"], + + # Intel IA64 + qr/\(.*IA64.*\)/ => ["IA64", "PC"], + + # Intel x86 + qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"], + + # Versions of Windows that only run on Intel x86 + qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"], + qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"], + + # Sparc + qr/\(.*sparc.*\)/ => ["Sparc", "Sun"], + qr/\(.*sun4.*\)/ => ["Sparc", "Sun"], + + # Alpha + qr/\(.*AXP.*\)/i => ["Alpha", "DEC"], + qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"], + qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"], + + # MIPS + qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], + qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"], + + # 68k + qr/\(.*68K.*\)/ => ["68k", "Macintosh"], + qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"], + + # HP + qr/\(.*9000.*\)/ => ["PA-RISC", "HP"], + + # ARM + qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["ARM"], + qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"], + + # PocketPC intentionally before PowerPC + qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"], + + # PowerPC + qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"], + qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"], + + # Stereotypical and broken + qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"], + qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"], + qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"], + qr/\(.*WOW64.*\)/ => ["x86_64"], + qr/\(.*Win64.*\)/ => ["IA64"], + qr/\(Win.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"], + qr/\(.*OSF.*\)/ => ["Alpha", "DEC"], + qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"], + qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"], + qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"], + + # Braindead old browsers who didn't follow convention: + qr/Amiga/ => ["68k", "Macintosh"], + qr/WinMosaic/ => ["IA32", "x86", "PC"], ); use constant OS_MAP => ( - # Sun - qr/\(.*Solaris.*\)/ => ["Solaris"], - qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")], - qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"], - qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"], - qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"], - qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"], - qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"], - qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"], - qr/\(.*SunOS 5.*\)/ => ["Solaris"], - qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"], - qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"], - qr/\(.*SunOS.*\)/ => ["SunOS"], - # BSD - qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"], - qr/\(.*FreeBSD.*\)/ => ["FreeBSD"], - qr/\(.*OpenBSD.*\)/ => ["OpenBSD"], - qr/\(.*NetBSD.*\)/ => ["NetBSD"], - # Misc POSIX - qr/\(.*IRIX.*\)/ => ["IRIX"], - qr/\(.*OSF.*\)/ => ["OSF/1"], - qr/\(.*Linux.*\)/ => ["Linux"], - qr/\(.*BeOS.*\)/ => ["BeOS"], - qr/\(.*AIX.*\)/ => ["AIX"], - qr/\(.*OS\/2.*\)/ => ["OS/2"], - qr/\(.*QNX.*\)/ => ["Neutrino"], - qr/\(.*VMS.*\)/ => ["OpenVMS"], - qr/\(.*HP-?UX.*\)/ => ["HP-UX"], - qr/\(.*Android.*\)/ => ["Android"], - # Windows - qr/\(.*Windows XP.*\)/ => ["Windows XP"], - qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"], - qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"], - qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"], - qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"], - qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"], - qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"], - qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"], - qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"], - qr/\(.*Windows 2000.*\)/ => ["Windows 2000"], - qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"], - qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"], - qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"], - qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"], - qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"], - qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"], - qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"], - qr/\(.*Windows.*NT.*\)/ => ["Windows NT"], - # OS X - qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"], - qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"], - qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"], - qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"], - qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"], - qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["iOS"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ => ["Mac OS X 10.8"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ => ["Mac OS X 10.7"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"], - qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"], - # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback - # support because some browsers refused to include the OS Version. - qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"], - # OS X 10.3 is the most likely default version of PowerPC Macs - # OS X 10.0 is more for configurations which didn't setup 10.x versions - qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")], - qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")], - qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")], - qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"], - qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")], - qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"], - qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"], - qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"], - qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")], - # Silly - qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"], - qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"], - qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"], - # Evil - qr/Amiga/i => ["Other"], - qr/WinMosaic/ => ["Windows 95"], - qr/\(.*32bit.*\)/ => ["Windows 95"], - qr/\(.*16bit.*\)/ => ["Windows 3.1"], - qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"], - qr/\(.*PPC.*\)/ => ["Mac System 9.x"], - qr/\(.*68K.*\)/ => ["Mac System 8.0"], + + # Sun + qr/\(.*Solaris.*\)/ => ["Solaris"], + qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")], + qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"], + qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"], + qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"], + qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"], + qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"], + qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"], + qr/\(.*SunOS 5.*\)/ => ["Solaris"], + qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"], + qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"], + qr/\(.*SunOS.*\)/ => ["SunOS"], + + # BSD + qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"], + qr/\(.*FreeBSD.*\)/ => ["FreeBSD"], + qr/\(.*OpenBSD.*\)/ => ["OpenBSD"], + qr/\(.*NetBSD.*\)/ => ["NetBSD"], + + # Misc POSIX + qr/\(.*IRIX.*\)/ => ["IRIX"], + qr/\(.*OSF.*\)/ => ["OSF/1"], + qr/\(.*Linux.*\)/ => ["Linux"], + qr/\(.*BeOS.*\)/ => ["BeOS"], + qr/\(.*AIX.*\)/ => ["AIX"], + qr/\(.*OS\/2.*\)/ => ["OS/2"], + qr/\(.*QNX.*\)/ => ["Neutrino"], + qr/\(.*VMS.*\)/ => ["OpenVMS"], + qr/\(.*HP-?UX.*\)/ => ["HP-UX"], + qr/\(.*Android.*\)/ => ["Android"], + + # Windows + qr/\(.*Windows XP.*\)/ => ["Windows XP"], + qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"], + qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"], + qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"], + qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"], + qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"], + qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"], + qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"], + qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"], + qr/\(.*Windows 2000.*\)/ => ["Windows 2000"], + qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"], + qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"], + qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"], + qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"], + qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"], + qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"], + qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"], + qr/\(.*Windows.*NT.*\)/ => ["Windows NT"], + + # OS X + qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"], + qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"], + qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"], + qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"], + qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"], + qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["iOS"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ => ["Mac OS X 10.8"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ => ["Mac OS X 10.7"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"], + qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"], + + # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback + # support because some browsers refused to include the OS Version. + qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"], + + # OS X 10.3 is the most likely default version of PowerPC Macs + # OS X 10.0 is more for configurations which didn't setup 10.x versions + qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")], + qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")], + qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")], + qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"], + qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")], + qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"], + qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"], + qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"], + qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")], + + # Silly + qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"], + qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"], + qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"], + + # Evil + qr/Amiga/i => ["Other"], + qr/WinMosaic/ => ["Windows 95"], + qr/\(.*32bit.*\)/ => ["Windows 95"], + qr/\(.*16bit.*\)/ => ["Windows 3.1"], + qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"], + qr/\(.*PPC.*\)/ => ["Mac System 9.x"], + qr/\(.*68K.*\)/ => ["Mac System 8.0"], ); sub detect_platform { - my $userAgent = $ENV{'HTTP_USER_AGENT'}; - my @detected; - my $iterator = natatime(2, PLATFORMS_MAP); - while (my($re, $ra) = $iterator->()) { - if ($userAgent =~ $re) { - push @detected, @$ra; - } + my $userAgent = $ENV{'HTTP_USER_AGENT'}; + my @detected; + my $iterator = natatime(2, PLATFORMS_MAP); + while (my ($re, $ra) = $iterator->()) { + if ($userAgent =~ $re) { + push @detected, @$ra; } - return _pick_valid_field_value('rep_platform', @detected); + } + return _pick_valid_field_value('rep_platform', @detected); } sub detect_op_sys { - my $userAgent = $ENV{'HTTP_USER_AGENT'} || ''; - my @detected; - my $iterator = natatime(2, OS_MAP); - while (my($re, $ra) = $iterator->()) { - if ($userAgent =~ $re) { - push @detected, @$ra; - } + my $userAgent = $ENV{'HTTP_USER_AGENT'} || ''; + my @detected; + my $iterator = natatime(2, OS_MAP); + while (my ($re, $ra) = $iterator->()) { + if ($userAgent =~ $re) { + push @detected, @$ra; } - push(@detected, "Windows") if grep(/^Windows /, @detected); - push(@detected, "Mac OS") if grep(/^Mac /, @detected); - return _pick_valid_field_value('op_sys', @detected); + } + push(@detected, "Windows") if grep(/^Windows /, @detected); + push(@detected, "Mac OS") if grep(/^Mac /, @detected); + return _pick_valid_field_value('op_sys', @detected); } # Takes the name of a field and a list of possible values for that field. @@ -197,11 +221,11 @@ sub detect_op_sys { # field. # Returns 'Other' if none of the values match. sub _pick_valid_field_value { - my ($field, @values) = @_; - foreach my $value (@values) { - return $value if check_field($field, $value, undef, 1); - } - return DEFAULT_VALUE; + my ($field, @values) = @_; + foreach my $value (@values) { + return $value if check_field($field, $value, undef, 1); + } + return DEFAULT_VALUE; } 1; diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm index 57ce5f6b6..0edd361ce 100644 --- a/Bugzilla/Util.pm +++ b/Bugzilla/Util.pm @@ -13,18 +13,18 @@ use warnings; use parent qw(Exporter); @Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed - html_quote url_quote xml_quote - css_class_quote html_light_quote - i_am_cgi i_am_webservice correct_urlbase remote_ip - validate_ip do_ssl_redirect_if_required use_attachbase - diff_arrays on_main_db - trim wrap_hard wrap_comment find_wrap_point - format_time validate_date validate_time datetime_from - is_7bit_clean bz_crypt generate_random_password - validate_email_syntax check_email_syntax clean_text - get_text template_var display_value disable_utf8 - detect_encoding email_filter - join_activity_entries read_text write_text); + html_quote url_quote xml_quote + css_class_quote html_light_quote + i_am_cgi i_am_webservice correct_urlbase remote_ip + validate_ip do_ssl_redirect_if_required use_attachbase + diff_arrays on_main_db + trim wrap_hard wrap_comment find_wrap_point + format_time validate_date validate_time datetime_from + is_7bit_clean bz_crypt generate_random_password + validate_email_syntax check_email_syntax clean_text + get_text template_var display_value disable_utf8 + detect_encoding email_filter + join_activity_entries read_text write_text); use Bugzilla::Constants; use Bugzilla::RNG qw(irand); @@ -43,642 +43,684 @@ use File::Basename qw(dirname); use File::Temp qw(tempfile); sub trick_taint { - require Carp; - Carp::confess("Undef to trick_taint") unless defined $_[0]; - my $match = $_[0] =~ /^(.*)$/s; - $_[0] = $match ? $1 : undef; - return (defined($_[0])); + require Carp; + Carp::confess("Undef to trick_taint") unless defined $_[0]; + my $match = $_[0] =~ /^(.*)$/s; + $_[0] = $match ? $1 : undef; + return (defined($_[0])); } sub detaint_natural { - my $match = $_[0] =~ /^([0-9]+)$/; - $_[0] = $match ? int($1) : undef; - return (defined($_[0])); + my $match = $_[0] =~ /^([0-9]+)$/; + $_[0] = $match ? int($1) : undef; + return (defined($_[0])); } sub detaint_signed { - my $match = $_[0] =~ /^([-+]?[0-9]+)$/; - # The "int()" call removes any leading plus sign. - $_[0] = $match ? int($1) : undef; - return (defined($_[0])); + my $match = $_[0] =~ /^([-+]?[0-9]+)$/; + + # The "int()" call removes any leading plus sign. + $_[0] = $match ? int($1) : undef; + return (defined($_[0])); } # Bug 120030: Override html filter to obscure the '@' in user # visible strings. # Bug 319331: Handle BiDi disruptions. sub html_quote { - my $var = shift; - $var =~ s/&/&/g; - $var =~ s/</</g; - $var =~ s/>/>/g; - $var =~ s/"/"/g; - # Obscure '@'. - $var =~ s/\@/\@/g; - - state $use_utf8 = Bugzilla->params->{'utf8'}; - - if ($use_utf8) { - # Remove control characters if the encoding is utf8. - # Other multibyte encodings may be using this range; so ignore if not utf8. - $var =~ s/(?![\t\r\n])[[:cntrl:]]//g; - - # Remove the following characters because they're - # influencing BiDi: - # -------------------------------------------------------- - # |Code |Name |UTF-8 representation| - # |------|--------------------------|--------------------| - # |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa | - # |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab | - # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac | - # |U+202d|Left-To-Right Override |0xe2 0x80 0xad | - # |U+202e|Right-To-Left Override |0xe2 0x80 0xae | - # -------------------------------------------------------- - # - # The following are characters influencing BiDi, too, but - # they can be spared from filtering because they don't - # influence more than one character right or left: - # -------------------------------------------------------- - # |Code |Name |UTF-8 representation| - # |------|--------------------------|--------------------| - # |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e | - # |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f | - # -------------------------------------------------------- - $var =~ tr/\x{202a}-\x{202e}//d; - } - return $var; + my $var = shift; + $var =~ s/&/&/g; + $var =~ s/</</g; + $var =~ s/>/>/g; + $var =~ s/"/"/g; + + # Obscure '@'. + $var =~ s/\@/\@/g; + + state $use_utf8 = Bugzilla->params->{'utf8'}; + + if ($use_utf8) { + + # Remove control characters if the encoding is utf8. + # Other multibyte encodings may be using this range; so ignore if not utf8. + $var =~ s/(?![\t\r\n])[[:cntrl:]]//g; + + # Remove the following characters because they're + # influencing BiDi: + # -------------------------------------------------------- + # |Code |Name |UTF-8 representation| + # |------|--------------------------|--------------------| + # |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa | + # |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab | + # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac | + # |U+202d|Left-To-Right Override |0xe2 0x80 0xad | + # |U+202e|Right-To-Left Override |0xe2 0x80 0xae | + # -------------------------------------------------------- + # + # The following are characters influencing BiDi, too, but + # they can be spared from filtering because they don't + # influence more than one character right or left: + # -------------------------------------------------------- + # |Code |Name |UTF-8 representation| + # |------|--------------------------|--------------------| + # |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e | + # |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f | + # -------------------------------------------------------- + $var =~ tr/\x{202a}-\x{202e}//d; + } + return $var; } sub read_text { - my ($filename) = @_; - open my $fh, '<:encoding(utf-8)', $filename; - local $/ = undef; - my $content = <$fh>; - close $fh; - return $content; + my ($filename) = @_; + open my $fh, '<:encoding(utf-8)', $filename; + local $/ = undef; + my $content = <$fh>; + close $fh; + return $content; } sub write_text { - my ($filename, $content) = @_; - my ($tmp_fh, $tmp_filename) = tempfile('.tmp.XXXXXXXXXX', - DIR => dirname($filename), - UNLINK => 0, - ); - binmode $tmp_fh, ':encoding(utf-8)'; - print $tmp_fh $content; - close $tmp_fh; - # File::Temp tries for secure files, but File::Slurp used the umask. - chmod(0666 & ~umask, $tmp_filename); - rename $tmp_filename, $filename; + my ($filename, $content) = @_; + my ($tmp_fh, $tmp_filename) + = tempfile('.tmp.XXXXXXXXXX', DIR => dirname($filename), UNLINK => 0,); + binmode $tmp_fh, ':encoding(utf-8)'; + print $tmp_fh $content; + close $tmp_fh; + + # File::Temp tries for secure files, but File::Slurp used the umask. + chmod(0666 & ~umask, $tmp_filename); + rename $tmp_filename, $filename; } sub html_light_quote { - my ($text) = @_; - # admin/table.html.tmpl calls |FILTER html_light| many times. - # There is no need to recreate the HTML::Scrubber object again and again. - my $scrubber = Bugzilla->process_cache->{html_scrubber}; - - # List of allowed HTML elements having no attributes. - my @allow = qw(b strong em i u p br abbr acronym ins del cite code var - dfn samp kbd big small sub sup tt dd dt dl ul li ol - fieldset legend); - - if (!Bugzilla->feature('html_desc')) { - my $safe = join('|', @allow); - my $chr = chr(1); - - # First, escape safe elements. - $text =~ s#<($safe)>#$chr$1$chr#go; - $text =~ s#</($safe)>#$chr/$1$chr#go; - # Now filter < and >. - $text =~ s#<#<#g; - $text =~ s#>#>#g; - # Restore safe elements. - $text =~ s#$chr/($safe)$chr#</$1>#go; - $text =~ s#$chr($safe)$chr#<$1>#go; - return $text; - } - elsif (!$scrubber) { - # We can be less restrictive. We can accept elements with attributes. - push(@allow, qw(a blockquote q span)); - - # Allowed protocols. - my $safe_protocols = join('|', SAFE_PROTOCOLS); - my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i; - - # Deny all elements and attributes unless explicitly authorized. - my @default = (0 => { - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - } - ); - - # Specific rules for allowed elements. If no specific rule is set - # for a given element, then the default is used. - my @rules = (a => { - href => $protocol_regexp, - target => qr{^(?:_blank|_parent|_self|_top)$}i, - title => 1, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - blockquote => { - cite => $protocol_regexp, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - 'q' => { - cite => $protocol_regexp, - id => 1, - name => 1, - class => 1, - '*' => 0, # Reject all other attributes. - }, - ); - - Bugzilla->process_cache->{html_scrubber} = $scrubber = - HTML::Scrubber->new(default => \@default, - allow => \@allow, - rules => \@rules, - comment => 0, - process => 0); - } - return $scrubber->scrub($text); + my ($text) = @_; + + # admin/table.html.tmpl calls |FILTER html_light| many times. + # There is no need to recreate the HTML::Scrubber object again and again. + my $scrubber = Bugzilla->process_cache->{html_scrubber}; + + # List of allowed HTML elements having no attributes. + my @allow = qw(b strong em i u p br abbr acronym ins del cite code var + dfn samp kbd big small sub sup tt dd dt dl ul li ol + fieldset legend); + + if (!Bugzilla->feature('html_desc')) { + my $safe = join('|', @allow); + my $chr = chr(1); + + # First, escape safe elements. + $text =~ s#<($safe)>#$chr$1$chr#go; + $text =~ s#</($safe)>#$chr/$1$chr#go; + + # Now filter < and >. + $text =~ s#<#<#g; + $text =~ s#>#>#g; + + # Restore safe elements. + $text =~ s#$chr/($safe)$chr#</$1>#go; + $text =~ s#$chr($safe)$chr#<$1>#go; + return $text; + } + elsif (!$scrubber) { + + # We can be less restrictive. We can accept elements with attributes. + push(@allow, qw(a blockquote q span)); + + # Allowed protocols. + my $safe_protocols = join('|', SAFE_PROTOCOLS); + my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i; + + # Deny all elements and attributes unless explicitly authorized. + my @default = ( + 0 => { + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + } + ); + + # Specific rules for allowed elements. If no specific rule is set + # for a given element, then the default is used. + my @rules = ( + a => { + href => $protocol_regexp, + target => qr{^(?:_blank|_parent|_self|_top)$}i, + title => 1, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + blockquote => { + cite => $protocol_regexp, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + 'q' => { + cite => $protocol_regexp, + id => 1, + name => 1, + class => 1, + '*' => 0, # Reject all other attributes. + }, + ); + + Bugzilla->process_cache->{html_scrubber} = $scrubber = HTML::Scrubber->new( + default => \@default, + allow => \@allow, + rules => \@rules, + comment => 0, + process => 0 + ); + } + return $scrubber->scrub($text); } sub email_filter { - my ($toencode) = @_; - if (!Bugzilla->user->id) { - my @emails = Email::Address->parse($toencode); - if (scalar @emails) { - my @hosts = map { quotemeta($_->host) } @emails; - my $hosts_re = join('|', @hosts); - $toencode =~ s/\@(?:$hosts_re)//g; - return $toencode; - } + my ($toencode) = @_; + if (!Bugzilla->user->id) { + my @emails = Email::Address->parse($toencode); + if (scalar @emails) { + my @hosts = map { quotemeta($_->host) } @emails; + my $hosts_re = join('|', @hosts); + $toencode =~ s/\@(?:$hosts_re)//g; + return $toencode; } - return $toencode; + } + return $toencode; } # This originally came from CGI.pm, by Lincoln D. Stein sub url_quote { - my ($toencode) = (@_); - utf8::encode($toencode) # The below regex works only on bytes - if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode); - $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; - return $toencode; + my ($toencode) = (@_); + utf8::encode($toencode) # The below regex works only on bytes + if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode); + $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; + return $toencode; } sub css_class_quote { - my ($toencode) = (@_); - $toencode =~ s#[ /]#_#g; - $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg; - return $toencode; + my ($toencode) = (@_); + $toencode =~ s#[ /]#_#g; + $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg; + return $toencode; } sub xml_quote { - my ($var) = (@_); - $var =~ s/\&/\&/g; - $var =~ s/</\</g; - $var =~ s/>/\>/g; - $var =~ s/\"/\"/g; - $var =~ s/\'/\'/g; - - # the following nukes characters disallowed by the XML 1.0 - # spec, Production 2.2. 1.0 declares that only the following - # are valid: - # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]) - $var =~ s/([\x{0001}-\x{0008}]| + my ($var) = (@_); + $var =~ s/\&/\&/g; + $var =~ s/</\</g; + $var =~ s/>/\>/g; + $var =~ s/\"/\"/g; + $var =~ s/\'/\'/g; + + # the following nukes characters disallowed by the XML 1.0 + # spec, Production 2.2. 1.0 declares that only the following + # are valid: + # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]) + $var =~ s/([\x{0001}-\x{0008}]| [\x{000B}-\x{000C}]| [\x{000E}-\x{001F}]| [\x{D800}-\x{DFFF}]| [\x{FFFE}-\x{FFFF}])//gx; - return $var; + return $var; } sub i_am_cgi { - # I use SERVER_SOFTWARE because it's required to be - # defined for all requests in the CGI spec. - return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0; + + # I use SERVER_SOFTWARE because it's required to be + # defined for all requests in the CGI spec. + return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0; } sub i_am_webservice { - my $usage_mode = Bugzilla->usage_mode; - return $usage_mode == USAGE_MODE_XMLRPC - || $usage_mode == USAGE_MODE_JSON - || $usage_mode == USAGE_MODE_REST; + my $usage_mode = Bugzilla->usage_mode; + return + $usage_mode == USAGE_MODE_XMLRPC + || $usage_mode == USAGE_MODE_JSON + || $usage_mode == USAGE_MODE_REST; } # This exists as a separate function from Bugzilla::CGI::redirect_to_https # because we don't want to create a CGI object during XML-RPC calls # (doing so can mess up XML-RPC). sub do_ssl_redirect_if_required { - return if !i_am_cgi(); - return if !Bugzilla->params->{'ssl_redirect'}; - - my $sslbase = Bugzilla->params->{'sslbase'}; - - # If we're already running under SSL, never redirect. - return if uc($ENV{HTTPS} || '') eq 'ON'; - # Never redirect if there isn't an sslbase. - return if !$sslbase; - Bugzilla->cgi->redirect_to_https(); + return if !i_am_cgi(); + return if !Bugzilla->params->{'ssl_redirect'}; + + my $sslbase = Bugzilla->params->{'sslbase'}; + + # If we're already running under SSL, never redirect. + return if uc($ENV{HTTPS} || '') eq 'ON'; + + # Never redirect if there isn't an sslbase. + return if !$sslbase; + Bugzilla->cgi->redirect_to_https(); } sub correct_urlbase { - my $ssl = Bugzilla->params->{'ssl_redirect'}; - my $urlbase = Bugzilla->params->{'urlbase'}; - my $sslbase = Bugzilla->params->{'sslbase'}; - - if (!$sslbase) { - return $urlbase; - } - elsif ($ssl) { - return $sslbase; - } - else { - # Return what the user currently uses. - return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase; - } + my $ssl = Bugzilla->params->{'ssl_redirect'}; + my $urlbase = Bugzilla->params->{'urlbase'}; + my $sslbase = Bugzilla->params->{'sslbase'}; + + if (!$sslbase) { + return $urlbase; + } + elsif ($ssl) { + return $sslbase; + } + else { + # Return what the user currently uses. + return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase; + } } sub remote_ip { - my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1'; - my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'}); - - # If the IP address is one of our trusted proxies, then we look at - # the X-Forwarded-For header to determine the real remote IP address. - if ($ENV{'HTTP_X_FORWARDED_FOR'} && first { $_ eq $ip } @proxies) { - my @ips = split(/[\s,]+/, $ENV{'HTTP_X_FORWARDED_FOR'}); - # This header can contain several IP addresses. We want the - # IP address of the machine which connected to our proxies as - # all other IP addresses may be fake or internal ones. - # Note that this may block a whole external proxy, but we have - # no way to determine if this proxy is malicious or trustable. - foreach my $remote_ip (reverse @ips) { - if (!first { $_ eq $remote_ip } @proxies) { - # Keep the original IP address if the remote IP is invalid. - $ip = validate_ip($remote_ip) || $ip; - last; - } - } + my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1'; + my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'}); + + # If the IP address is one of our trusted proxies, then we look at + # the X-Forwarded-For header to determine the real remote IP address. + if ($ENV{'HTTP_X_FORWARDED_FOR'} && first { $_ eq $ip } @proxies) { + my @ips = split(/[\s,]+/, $ENV{'HTTP_X_FORWARDED_FOR'}); + + # This header can contain several IP addresses. We want the + # IP address of the machine which connected to our proxies as + # all other IP addresses may be fake or internal ones. + # Note that this may block a whole external proxy, but we have + # no way to determine if this proxy is malicious or trustable. + foreach my $remote_ip (reverse @ips) { + if (!first { $_ eq $remote_ip } @proxies) { + + # Keep the original IP address if the remote IP is invalid. + $ip = validate_ip($remote_ip) || $ip; + last; + } } - return $ip; + } + return $ip; } sub validate_ip { - my $ip = shift; - return is_ipv4($ip) || is_ipv6($ip); + my $ip = shift; + return is_ipv4($ip) || is_ipv6($ip); } # Copied from Data::Validate::IP::is_ipv4(). sub is_ipv4 { - my $ip = shift; - return unless defined $ip; + my $ip = shift; + return unless defined $ip; - my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; - return unless scalar(@octets) == 4; + my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + return unless scalar(@octets) == 4; - foreach my $octet (@octets) { - return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/); - } + foreach my $octet (@octets) { + return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/); + } - # The IP address is valid and can now be detainted. - return join('.', @octets); + # The IP address is valid and can now be detainted. + return join('.', @octets); } # Copied from Data::Validate::IP::is_ipv6(). sub is_ipv6 { - my $ip = shift; - return unless defined $ip; - - # If there is a :: then there must be only one :: and the length - # can be variable. Without it, the length must be 8 groups. - my @chunks = split(':', $ip); - - # Need to check if the last chunk is an IPv4 address, if it is we - # pop it off and exempt it from the normal IPv6 checking and stick - # it back on at the end. If there is only one chunk and it's an IPv4 - # address, then it isn't an IPv6 address. - my $ipv4; - my $expected_chunks = 8; - if (@chunks > 1 && is_ipv4($chunks[$#chunks])) { - $ipv4 = pop(@chunks); - $expected_chunks--; - } + my $ip = shift; + return unless defined $ip; + + # If there is a :: then there must be only one :: and the length + # can be variable. Without it, the length must be 8 groups. + my @chunks = split(':', $ip); + + # Need to check if the last chunk is an IPv4 address, if it is we + # pop it off and exempt it from the normal IPv6 checking and stick + # it back on at the end. If there is only one chunk and it's an IPv4 + # address, then it isn't an IPv6 address. + my $ipv4; + my $expected_chunks = 8; + if (@chunks > 1 && is_ipv4($chunks[$#chunks])) { + $ipv4 = pop(@chunks); + $expected_chunks--; + } + + my $empty = 0; + + # Workaround to handle trailing :: being valid. + if ($ip =~ /[0-9a-f]{1,4}::$/) { + $empty++; - my $empty = 0; - # Workaround to handle trailing :: being valid. - if ($ip =~ /[0-9a-f]{1,4}::$/) { - $empty++; # Single trailing ':' is invalid. - } elsif ($ip =~ /:$/) { - return; - } + } + elsif ($ip =~ /:$/) { + return; + } - foreach my $chunk (@chunks) { - return unless $chunk =~ /^[0-9a-f]{0,4}$/i; - $empty++ if $chunk eq ''; - } - # More than one :: block is bad, but if it starts with :: it will - # look like two, so we need an exception. - if ($empty == 2 && $ip =~ /^::/) { - # This is ok - } elsif ($empty > 1) { - return; - } + foreach my $chunk (@chunks) { + return unless $chunk =~ /^[0-9a-f]{0,4}$/i; + $empty++ if $chunk eq ''; + } + + # More than one :: block is bad, but if it starts with :: it will + # look like two, so we need an exception. + if ($empty == 2 && $ip =~ /^::/) { + + # This is ok + } + elsif ($empty > 1) { + return; + } - push(@chunks, $ipv4) if $ipv4; - # Need 8 chunks, or we need an empty section that could be filled - # to represent the missing '0' sections. - return unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty); + push(@chunks, $ipv4) if $ipv4; - my $ipv6 = join(':', @chunks); - # The IP address is valid and can now be detainted. - trick_taint($ipv6); + # Need 8 chunks, or we need an empty section that could be filled + # to represent the missing '0' sections. + return + unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty); - # Need to handle the exception of trailing :: being valid. - return "${ipv6}::" if $ip =~ /::$/; - return $ipv6; + my $ipv6 = join(':', @chunks); + + # The IP address is valid and can now be detainted. + trick_taint($ipv6); + + # Need to handle the exception of trailing :: being valid. + return "${ipv6}::" if $ip =~ /::$/; + return $ipv6; } sub use_attachbase { - my $attachbase = Bugzilla->params->{'attachment_base'}; - return ($attachbase ne '' - && $attachbase ne Bugzilla->params->{'urlbase'} - && $attachbase ne Bugzilla->params->{'sslbase'}) ? 1 : 0; + my $attachbase = Bugzilla->params->{'attachment_base'}; + return ($attachbase ne '' + && $attachbase ne Bugzilla->params->{'urlbase'} + && $attachbase ne Bugzilla->params->{'sslbase'}) ? 1 : 0; } sub diff_arrays { - my ($old_ref, $new_ref, $attrib) = @_; - $attrib ||= 'name'; - - my (%counts, %pos); - # We are going to alter the old array. - my @old = @$old_ref; - my $i = 0; - - # $counts{foo}-- means old, $counts{foo}++ means new. - # If $counts{foo} becomes positive, then we are adding new items, - # else we simply cancel one old existing item. Remaining items - # in the old list have been removed. - foreach (@old) { - next unless defined $_; - my $value = blessed($_) ? $_->$attrib : $_; - $counts{$value}--; - push @{$pos{$value}}, $i++; + my ($old_ref, $new_ref, $attrib) = @_; + $attrib ||= 'name'; + + my (%counts, %pos); + + # We are going to alter the old array. + my @old = @$old_ref; + my $i = 0; + + # $counts{foo}-- means old, $counts{foo}++ means new. + # If $counts{foo} becomes positive, then we are adding new items, + # else we simply cancel one old existing item. Remaining items + # in the old list have been removed. + foreach (@old) { + next unless defined $_; + my $value = blessed($_) ? $_->$attrib : $_; + $counts{$value}--; + push @{$pos{$value}}, $i++; + } + my @added; + foreach (@$new_ref) { + next unless defined $_; + my $value = blessed($_) ? $_->$attrib : $_; + if (++$counts{$value} > 0) { + + # Ignore empty strings, but objects having an empty string + # as attribute are fine. + push(@added, $_) unless ($value eq '' && !blessed($_)); } - my @added; - foreach (@$new_ref) { - next unless defined $_; - my $value = blessed($_) ? $_->$attrib : $_; - if (++$counts{$value} > 0) { - # Ignore empty strings, but objects having an empty string - # as attribute are fine. - push(@added, $_) unless ($value eq '' && !blessed($_)); - } - else { - my $old_pos = shift @{$pos{$value}}; - $old[$old_pos] = undef; - } + else { + my $old_pos = shift @{$pos{$value}}; + $old[$old_pos] = undef; } - # Ignore canceled items as well as empty strings. - my @removed = grep { defined $_ && $_ ne '' } @old; - return (\@removed, \@added); + } + + # Ignore canceled items as well as empty strings. + my @removed = grep { defined $_ && $_ ne '' } @old; + return (\@removed, \@added); } sub trim { - my ($str) = @_; - if ($str) { - $str =~ s/^\s+//g; - $str =~ s/\s+$//g; - } - return $str; + my ($str) = @_; + if ($str) { + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; } sub wrap_comment { - my ($comment, $cols) = @_; - my $wrappedcomment = ""; - - # Use 'local', as recommended by Text::Wrap's perldoc. - local $Text::Wrap::columns = $cols || COMMENT_COLS; - # Make words that are longer than COMMENT_COLS not wrap. - local $Text::Wrap::huge = 'overflow'; - # Don't mess with tabs. - local $Text::Wrap::unexpand = 0; - - # If the line starts with ">", don't wrap it. Otherwise, wrap. - foreach my $line (split(/\r\n|\r|\n/, $comment)) { - if ($line =~ qr/^>/) { - $wrappedcomment .= ($line . "\n"); - } - else { - $wrappedcomment .= (wrap('', '', $line) . "\n"); - } + my ($comment, $cols) = @_; + my $wrappedcomment = ""; + + # Use 'local', as recommended by Text::Wrap's perldoc. + local $Text::Wrap::columns = $cols || COMMENT_COLS; + + # Make words that are longer than COMMENT_COLS not wrap. + local $Text::Wrap::huge = 'overflow'; + + # Don't mess with tabs. + local $Text::Wrap::unexpand = 0; + + # If the line starts with ">", don't wrap it. Otherwise, wrap. + foreach my $line (split(/\r\n|\r|\n/, $comment)) { + if ($line =~ qr/^>/) { + $wrappedcomment .= ($line . "\n"); } + else { + $wrappedcomment .= (wrap('', '', $line) . "\n"); + } + } - chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end. - return $wrappedcomment; + chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end. + return $wrappedcomment; } sub find_wrap_point { - my ($string, $maxpos) = @_; - if (!$string) { return 0 } - if (length($string) < $maxpos) { return length($string) } - my $wrappoint = rindex($string, ",", $maxpos); # look for comma - if ($wrappoint <= 0) { # can't find comma - $wrappoint = rindex($string, " ", $maxpos); # look for space - if ($wrappoint <= 0) { # can't find space - $wrappoint = rindex($string, "-", $maxpos); # look for hyphen - if ($wrappoint <= 0) { # can't find hyphen - $wrappoint = $maxpos; # just truncate it - } else { - $wrappoint++; # leave hyphen on the left side - } - } + my ($string, $maxpos) = @_; + if (!$string) { return 0 } + if (length($string) < $maxpos) { return length($string) } + my $wrappoint = rindex($string, ",", $maxpos); # look for comma + if ($wrappoint <= 0) { # can't find comma + $wrappoint = rindex($string, " ", $maxpos); # look for space + if ($wrappoint <= 0) { # can't find space + $wrappoint = rindex($string, "-", $maxpos); # look for hyphen + if ($wrappoint <= 0) { # can't find hyphen + $wrappoint = $maxpos; # just truncate it + } + else { + $wrappoint++; # leave hyphen on the left side + } } - return $wrappoint; + } + return $wrappoint; } sub join_activity_entries { - my ($field, $current_change, $new_change) = @_; - # We need to insert characters as these were removed by old - # LogActivityEntry code. - - return $new_change if $current_change eq ''; - - # Buglists and see_also need the comma restored - if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { - if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { - return $current_change . $new_change; - } else { - return $current_change . ', ' . $new_change; - } - } + my ($field, $current_change, $new_change) = @_; - # Assume bug_file_loc contain a single url, don't insert a delimiter - if ($field eq 'bug_file_loc') { - return $current_change . $new_change; - } + # We need to insert characters as these were removed by old + # LogActivityEntry code. + + return $new_change if $current_change eq ''; - # All other fields get a space unless the first character of the second - # string is a comma or space + # Buglists and see_also need the comma restored + if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { - return $current_change . $new_change; - } else { - return $current_change . ' ' . $new_change; + return $current_change . $new_change; + } + else { + return $current_change . ', ' . $new_change; } + } + + # Assume bug_file_loc contain a single url, don't insert a delimiter + if ($field eq 'bug_file_loc') { + return $current_change . $new_change; + } + + # All other fields get a space unless the first character of the second + # string is a comma or space + if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { + return $current_change . $new_change; + } + else { + return $current_change . ' ' . $new_change; + } } sub wrap_hard { - my ($string, $columns) = @_; - local $Text::Wrap::columns = $columns; - local $Text::Wrap::unexpand = 0; - local $Text::Wrap::huge = 'wrap'; - - my $wrapped = wrap('', '', $string); - chomp($wrapped); - return $wrapped; + my ($string, $columns) = @_; + local $Text::Wrap::columns = $columns; + local $Text::Wrap::unexpand = 0; + local $Text::Wrap::huge = 'wrap'; + + my $wrapped = wrap('', '', $string); + chomp($wrapped); + return $wrapped; } sub format_time { - my ($date, $format, $timezone) = @_; - - # If $format is not set, try to guess the correct date format. - if (!$format) { - if (!ref $date - && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) - { - my $sec = $7; - if (defined $sec) { - $format = "%Y-%m-%d %T %Z"; - } else { - $format = "%Y-%m-%d %R %Z"; - } - } else { - # Default date format. See DateTime for other formats available. - $format = "%Y-%m-%d %R %Z"; - } - } - - my $dt = ref $date ? $date : datetime_from($date, $timezone); - $date = defined $dt ? $dt->strftime($format) : ''; - return trim($date); -} - -sub datetime_from { - my ($date, $timezone) = @_; - - # In the database, this is the "0" date. - return undef if $date =~ /^0000/; + my ($date, $format, $timezone) = @_; - my @time; - # Most dates will be in this format, avoid strptime's generic parser - if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) { - @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef); + # If $format is not set, try to guess the correct date format. + if (!$format) { + if (!ref $date + && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) + { + my $sec = $7; + if (defined $sec) { + $format = "%Y-%m-%d %T %Z"; + } + else { + $format = "%Y-%m-%d %R %Z"; + } } else { - @time = strptime($date); - } - - unless (scalar @time) { - # If an unknown timezone is passed (such as MSK, for Moskow), - # strptime() is unable to parse the date. We try again, but we first - # remove the timezone. - $date =~ s/\s+\S+$//; - @time = strptime($date); + # Default date format. See DateTime for other formats available. + $format = "%Y-%m-%d %R %Z"; } + } - return undef if !@time; - - # strptime() counts years from 1900, except if they are older than 1901 - # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84, - # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000. - $time[5] += 1900 if $time[5] < 1100; - - my %args = ( - year => $time[5], - # Months start from 0 (January). - month => $time[4] + 1, - day => $time[3], - hour => $time[2], - minute => $time[1], - # DateTime doesn't like fractional seconds. - # Also, sometimes seconds are undef. - second => defined($time[0]) ? int($time[0]) : undef, - # If a timezone was specified, use it. Otherwise, use the - # local timezone. - time_zone => DateTime::TimeZone->offset_as_string($time[6]) - || Bugzilla->local_timezone, - ); - - # If something wasn't specified in the date, it's best to just not - # pass it to DateTime at all. (This is important for doing datetime_from - # on the deadline field, which is usually just a date with no time.) - foreach my $arg (keys %args) { - delete $args{$arg} if !defined $args{$arg}; - } - - # This module takes time to load and is only used here, so we - # |require| it here rather than |use| it. - require DateTime; - my $dt = new DateTime(\%args); + my $dt = ref $date ? $date : datetime_from($date, $timezone); + $date = defined $dt ? $dt->strftime($format) : ''; + return trim($date); +} - # Now display the date using the given timezone, - # or the user's timezone if none is given. - $dt->set_time_zone($timezone || Bugzilla->user->timezone); - return $dt; +sub datetime_from { + my ($date, $timezone) = @_; + + # In the database, this is the "0" date. + return undef if $date =~ /^0000/; + + my @time; + + # Most dates will be in this format, avoid strptime's generic parser + if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) { + @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef); + } + else { + @time = strptime($date); + } + + unless (scalar @time) { + + # If an unknown timezone is passed (such as MSK, for Moskow), + # strptime() is unable to parse the date. We try again, but we first + # remove the timezone. + $date =~ s/\s+\S+$//; + @time = strptime($date); + } + + return undef if !@time; + + # strptime() counts years from 1900, except if they are older than 1901 + # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84, + # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000. + $time[5] += 1900 if $time[5] < 1100; + + my %args = ( + year => $time[5], + + # Months start from 0 (January). + month => $time[4] + 1, + day => $time[3], + hour => $time[2], + minute => $time[1], + + # DateTime doesn't like fractional seconds. + # Also, sometimes seconds are undef. + second => defined($time[0]) ? int($time[0]) : undef, + + # If a timezone was specified, use it. Otherwise, use the + # local timezone. + time_zone => DateTime::TimeZone->offset_as_string($time[6]) + || Bugzilla->local_timezone, + ); + + # If something wasn't specified in the date, it's best to just not + # pass it to DateTime at all. (This is important for doing datetime_from + # on the deadline field, which is usually just a date with no time.) + foreach my $arg (keys %args) { + delete $args{$arg} if !defined $args{$arg}; + } + + # This module takes time to load and is only used here, so we + # |require| it here rather than |use| it. + require DateTime; + my $dt = new DateTime(\%args); + + # Now display the date using the given timezone, + # or the user's timezone if none is given. + $dt->set_time_zone($timezone || Bugzilla->user->timezone); + return $dt; } sub bz_crypt { - my ($password, $salt) = @_; - - my $algorithm; - if (!defined $salt) { - # If you don't use a salt, then people can create tables of - # hashes that map to particular passwords, and then break your - # hashing very easily if they have a large-enough table of common - # (or even uncommon) passwords. So we generate a unique salt for - # each password in the database, and then just prepend it to - # the hash. - $salt = generate_random_password(PASSWORD_SALT_LENGTH); - $algorithm = PASSWORD_DIGEST_ALGORITHM; - } - - # We append the algorithm used to the string. This is good because then - # we can change the algorithm being used, in the future, without - # disrupting the validation of existing passwords. Also, this tells - # us if a password is using the old "crypt" method of hashing passwords, - # because the algorithm will be missing from the string. - if ($salt =~ /{([^}]+)}$/) { - $algorithm = $1; - } - - # Wide characters cause crypt and Digest to die. - if (Bugzilla->params->{'utf8'}) { - utf8::encode($password) if utf8::is_utf8($password); - } - - my $crypted_password; - if (!$algorithm) { - # Crypt the password. - $crypted_password = crypt($password, $salt); - } - else { - my $hasher = Digest->new($algorithm); - # Newly created salts won't yet have a comma. - ($salt) = $salt =~ /^([^,]+),?/; - $hasher->add($password, $salt); - $crypted_password = $salt . ',' . $hasher->b64digest . "{$algorithm}"; - } - - # Return the crypted password. - return $crypted_password; + my ($password, $salt) = @_; + + my $algorithm; + if (!defined $salt) { + + # If you don't use a salt, then people can create tables of + # hashes that map to particular passwords, and then break your + # hashing very easily if they have a large-enough table of common + # (or even uncommon) passwords. So we generate a unique salt for + # each password in the database, and then just prepend it to + # the hash. + $salt = generate_random_password(PASSWORD_SALT_LENGTH); + $algorithm = PASSWORD_DIGEST_ALGORITHM; + } + + # We append the algorithm used to the string. This is good because then + # we can change the algorithm being used, in the future, without + # disrupting the validation of existing passwords. Also, this tells + # us if a password is using the old "crypt" method of hashing passwords, + # because the algorithm will be missing from the string. + if ($salt =~ /{([^}]+)}$/) { + $algorithm = $1; + } + + # Wide characters cause crypt and Digest to die. + if (Bugzilla->params->{'utf8'}) { + utf8::encode($password) if utf8::is_utf8($password); + } + + my $crypted_password; + if (!$algorithm) { + + # Crypt the password. + $crypted_password = crypt($password, $salt); + } + else { + my $hasher = Digest->new($algorithm); + + # Newly created salts won't yet have a comma. + ($salt) = $salt =~ /^([^,]+),?/; + $hasher->add($password, $salt); + $crypted_password = $salt . ',' . $hasher->b64digest . "{$algorithm}"; + } + + # Return the crypted password. + return $crypted_password; } # If you want to understand the security of strings generated by this @@ -688,191 +730,199 @@ sub bz_crypt { # by the number of characters you generate, and that gets you the equivalent # strength of the string in bits. sub generate_random_password { - my $size = shift || 10; # default to 10 chars if nothing specified - return join("", map{ ('0'..'9','a'..'z','A'..'Z')[irand 62] } (1..$size)); + my $size = shift || 10; # default to 10 chars if nothing specified + return + join("", map { ('0' .. '9', 'a' .. 'z', 'A' .. 'Z')[irand 62] } (1 .. $size)); } sub validate_email_syntax { - my ($addr) = @_; - my $match = Bugzilla->params->{'emailregexp'}; - my $email = $addr . Bugzilla->params->{'emailsuffix'}; - # This regexp follows RFC 2822 section 3.4.1. - my $addr_spec = $Email::Address::addr_spec; - # RFC 2822 section 2.1 specifies that email addresses must - # be made of US-ASCII characters only. - # Email::Address::addr_spec doesn't enforce this. - # We set the max length to 127 to ensure addresses aren't truncated when - # inserted into the tokens.eventdata field. - if ($addr =~ /$match/ - && $email !~ /\P{ASCII}/ - && $email =~ /^$addr_spec$/ - && length($email) <= 127) - { - # We assume these checks to suffice to consider the address untainted. - trick_taint($_[0]); - return 1; - } - return 0; + my ($addr) = @_; + my $match = Bugzilla->params->{'emailregexp'}; + my $email = $addr . Bugzilla->params->{'emailsuffix'}; + + # This regexp follows RFC 2822 section 3.4.1. + my $addr_spec = $Email::Address::addr_spec; + + # RFC 2822 section 2.1 specifies that email addresses must + # be made of US-ASCII characters only. + # Email::Address::addr_spec doesn't enforce this. + # We set the max length to 127 to ensure addresses aren't truncated when + # inserted into the tokens.eventdata field. + if ( $addr =~ /$match/ + && $email !~ /\P{ASCII}/ + && $email =~ /^$addr_spec$/ + && length($email) <= 127) + { + # We assume these checks to suffice to consider the address untainted. + trick_taint($_[0]); + return 1; + } + return 0; } sub check_email_syntax { - my ($addr) = @_; + my ($addr) = @_; - unless (validate_email_syntax(@_)) { - my $email = $addr . Bugzilla->params->{'emailsuffix'}; - ThrowUserError('illegal_email_address', { addr => $email }); - } + unless (validate_email_syntax(@_)) { + my $email = $addr . Bugzilla->params->{'emailsuffix'}; + ThrowUserError('illegal_email_address', {addr => $email}); + } } sub validate_date { - my ($date) = @_; - my $date2; - - # $ts is undefined if the parser fails. - my $ts = str2time($date); - if ($ts) { - $date2 = time2str("%Y-%m-%d", $ts); - - $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; - $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; - } - my $ret = ($ts && $date eq $date2); - return $ret ? 1 : 0; + my ($date) = @_; + my $date2; + + # $ts is undefined if the parser fails. + my $ts = str2time($date); + if ($ts) { + $date2 = time2str("%Y-%m-%d", $ts); + + $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; + $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; + } + my $ret = ($ts && $date eq $date2); + return $ret ? 1 : 0; } sub validate_time { - my ($time) = @_; - my $time2; - - # $ts is undefined if the parser fails. - my $ts = str2time($time); - if ($ts) { - $time2 = time2str("%H:%M:%S", $ts); - if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) { - $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0); - } + my ($time) = @_; + my $time2; + + # $ts is undefined if the parser fails. + my $ts = str2time($time); + if ($ts) { + $time2 = time2str("%H:%M:%S", $ts); + if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) { + $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0); } - my $ret = ($ts && $time eq $time2); - return $ret ? 1 : 0; + } + my $ret = ($ts && $time eq $time2); + return $ret ? 1 : 0; } sub is_7bit_clean { - return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/; + return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/; } sub clean_text { - my $dtext = shift; - if ($dtext) { - # change control characters into a space - $dtext =~ s/[\x00-\x1F\x7F]+/ /g; - } - return trim($dtext); + my $dtext = shift; + if ($dtext) { + + # change control characters into a space + $dtext =~ s/[\x00-\x1F\x7F]+/ /g; + } + return trim($dtext); } sub on_main_db (&) { - my $code = shift; - my $original_dbh = Bugzilla->dbh; - Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main; - $code->(); - Bugzilla->request_cache->{dbh} = $original_dbh; + my $code = shift; + my $original_dbh = Bugzilla->dbh; + Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main; + $code->(); + Bugzilla->request_cache->{dbh} = $original_dbh; } sub get_text { - my ($name, $vars) = @_; - my $template = Bugzilla->template_inner; - $vars ||= {}; - $vars->{'message'} = $name; - my $message; - $template->process('global/message.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); - - # Remove the indenting that exists in messages.html.tmpl. - $message =~ s/^ //gm; - return $message; + my ($name, $vars) = @_; + my $template = Bugzilla->template_inner; + $vars ||= {}; + $vars->{'message'} = $name; + my $message; + $template->process('global/message.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); + + # Remove the indenting that exists in messages.html.tmpl. + $message =~ s/^ //gm; + return $message; } sub template_var { - my $name = shift; - my $request_cache = Bugzilla->request_cache; - my $cache = $request_cache->{util_template_var} ||= {}; - my $lang = $request_cache->{template_current_lang}->[0] || ''; - return $cache->{$lang}->{$name} if defined $cache->{$lang}; - - my $template = Bugzilla->template_inner($lang); - my %vars; - # Note: If we suddenly start needing a lot of template_var variables, - # they should move into their own template, not field-descs. - $template->process('global/field-descs.none.tmpl', - { vars => \%vars, in_template_var => 1 }) - || ThrowTemplateError($template->error()); - - $cache->{$lang} = \%vars; - return $vars{$name}; + my $name = shift; + my $request_cache = Bugzilla->request_cache; + my $cache = $request_cache->{util_template_var} ||= {}; + my $lang = $request_cache->{template_current_lang}->[0] || ''; + return $cache->{$lang}->{$name} if defined $cache->{$lang}; + + my $template = Bugzilla->template_inner($lang); + my %vars; + + # Note: If we suddenly start needing a lot of template_var variables, + # they should move into their own template, not field-descs. + $template->process('global/field-descs.none.tmpl', + {vars => \%vars, in_template_var => 1}) + || ThrowTemplateError($template->error()); + + $cache->{$lang} = \%vars; + return $vars{$name}; } sub display_value { - my ($field, $value) = @_; - return template_var('value_descs')->{$field}->{$value} // $value; + my ($field, $value) = @_; + return template_var('value_descs')->{$field}->{$value} // $value; } sub disable_utf8 { - if (Bugzilla->params->{'utf8'}) { - binmode STDOUT, ':bytes'; # Turn off UTF8 encoding. - } + if (Bugzilla->params->{'utf8'}) { + binmode STDOUT, ':bytes'; # Turn off UTF8 encoding. + } } use constant UTF8_ACCIDENTAL => qw(shiftjis big5-eten euc-kr euc-jp); sub detect_encoding { - my $data = shift; - - Bugzilla->feature('detect_charset') - || ThrowUserError('feature_disabled', { feature => 'detect_charset' }); - - require Encode::Detect::Detector; - import Encode::Detect::Detector 'detect'; - - my $encoding = detect($data); - $encoding = resolve_alias($encoding) if $encoding; - - # Encode::Detect is bad at detecting certain charsets, but Encode::Guess - # is better at them. Here's the details: - - # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect - # tends to accidentally mis-detect UTF-8 strings as being - # these encodings.) - if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) { - $encoding = undef; - my $decoder = guess_encoding($data, UTF8_ACCIDENTAL); - $encoding = $decoder->name if ref $decoder; - } - - # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8, - # or cp1255, but Encode::Guess can usually tell which one it is. - if ($encoding && ($encoding eq 'iso-8859-8' || $encoding eq 'cp1255')) { - my $decoded_as = _guess_iso($data, 'iso-8859-8', - # These are ordered this way because it gives the most - # accurate results. - qw(cp1252 iso-8859-7 iso-8859-2)); - $encoding = $decoded_as if $decoded_as; - } + my $data = shift; + + Bugzilla->feature('detect_charset') + || ThrowUserError('feature_disabled', {feature => 'detect_charset'}); + + require Encode::Detect::Detector; + import Encode::Detect::Detector 'detect'; + + my $encoding = detect($data); + $encoding = resolve_alias($encoding) if $encoding; + + # Encode::Detect is bad at detecting certain charsets, but Encode::Guess + # is better at them. Here's the details: + + # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect + # tends to accidentally mis-detect UTF-8 strings as being + # these encodings.) + if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) { + $encoding = undef; + my $decoder = guess_encoding($data, UTF8_ACCIDENTAL); + $encoding = $decoder->name if ref $decoder; + } + + # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8, + # or cp1255, but Encode::Guess can usually tell which one it is. + if ($encoding && ($encoding eq 'iso-8859-8' || $encoding eq 'cp1255')) { + my $decoded_as = _guess_iso( + $data, 'iso-8859-8', + + # These are ordered this way because it gives the most + # accurate results. + qw(cp1252 iso-8859-7 iso-8859-2) + ); + $encoding = $decoded_as if $decoded_as; + } - return $encoding; + return $encoding; } # A helper for detect_encoding. sub _guess_iso { - my ($data, $versus, @isos) = (shift, shift, shift); - - my $encoding; - foreach my $iso (@isos) { - my $decoder = guess_encoding($data, ($iso, $versus)); - if (ref $decoder) { - $encoding = $decoder->name if ref $decoder; - last; - } + my ($data, $versus, @isos) = (shift, shift, shift); + + my $encoding; + foreach my $iso (@isos) { + my $decoder = guess_encoding($data, ($iso, $versus)); + if (ref $decoder) { + $encoding = $decoder->name if ref $decoder; + last; } - return $encoding; + } + return $encoding; } 1; diff --git a/Bugzilla/Version.pm b/Bugzilla/Version.pm index 4b332ff2b..6a5930574 100644 --- a/Bugzilla/Version.pm +++ b/Bugzilla/Version.pm @@ -26,134 +26,131 @@ use Scalar::Util qw(blessed); use constant DEFAULT_VERSION => 'unspecified'; -use constant DB_TABLE => 'versions'; +use constant DB_TABLE => 'versions'; use constant NAME_FIELD => 'value'; + # This is "id" because it has to be filled in and id is probably the fastest. # We do a custom sort in new_from_list below. use constant LIST_ORDER => 'id'; use constant DB_COLUMNS => qw( - id - value - product_id - isactive + id + value + product_id + isactive ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; use constant UPDATE_COLUMNS => qw( - value - isactive + value + isactive ); use constant VALIDATORS => { - product => \&_check_product, - value => \&_check_value, - isactive => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + value => \&_check_value, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - value => ['product'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['product'],}; ################################ # Methods ################################ sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param and !defined $param->{id}) { - $product = $param->{product}; - my $name = $param->{name}; - if (!defined $product) { - ThrowCodeError('bad_arg', - {argument => 'product', - function => "${class}::new"}); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - - my $condition = 'product_id = ? AND value = ?'; - my @values = ($product->id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $product; + if (ref $param and !defined $param->{id}) { + $product = $param->{product}; + my $name = $param->{name}; + if (!defined $product) { + ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"}); + } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - unshift @_, $param; - return $class->SUPER::new(@_); + my $condition = 'product_id = ? AND value = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + return $class->SUPER::new(@_); } sub new_from_list { - my $self = shift; - my $list = $self->SUPER::new_from_list(@_); - return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list]; + my $self = shift; + my $list = $self->SUPER::new_from_list(@_); + return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list]; } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - return $params; + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + return $params; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(qq{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + qq{ SELECT COUNT(*) FROM bugs WHERE product_id = ? AND version = ?}, undef, - ($self->product_id, $self->name)) || 0; - } - return $self->{'bug_count'}; + ($self->product_id, $self->name) + ) || 0; + } + return $self->{'bug_count'}; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my ($changes, $old_self) = $self->SUPER::update(@_); - - if (exists $changes->{value}) { - $dbh->do('UPDATE bugs SET version = ? - WHERE version = ? AND product_id = ?', - undef, ($self->name, $old_self->name, $self->product_id)); - } - $dbh->bz_commit_transaction(); - - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my ($changes, $old_self) = $self->SUPER::update(@_); + + if (exists $changes->{value}) { + $dbh->do( + 'UPDATE bugs SET version = ? + WHERE version = ? AND product_id = ?', undef, + ($self->name, $old_self->name, $self->product_id) + ); + } + $dbh->bz_commit_transaction(); + + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Products must have at least one version. - if (scalar(@{$self->product->versions}) == 1) { - ThrowUserError('version_is_last', { version => $self }); - } + # Products must have at least one version. + if (scalar(@{$self->product->versions}) == 1) { + ThrowUserError('version_is_last', {version => $self}); + } - # The version cannot be removed if there are bugs - # associated with it. - if ($self->bug_count) { - ThrowUserError("version_has_bugs", { nb => $self->bug_count }); - } - $self->SUPER::remove_from_db(); + # The version cannot be removed if there are bugs + # associated with it. + if ($self->bug_count) { + ThrowUserError("version_has_bugs", {nb => $self->bug_count}); + } + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } ############################### @@ -161,45 +158,47 @@ sub remove_from_db { ############################### sub product_id { return $_[0]->{'product_id'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub is_active { return $_[0]->{'isactive'}; } sub product { - my $self = shift; + my $self = shift; - require Bugzilla::Product; - $self->{'product'} ||= new Bugzilla::Product($self->product_id); - return $self->{'product'}; + require Bugzilla::Product; + $self->{'product'} ||= new Bugzilla::Product($self->product_id); + return $self->{'product'}; } ################################ # Validators ################################ -sub set_value { $_[0]->set('value', $_[1]); } +sub set_value { $_[0]->set('value', $_[1]); } sub set_isactive { $_[0]->set('isactive', $_[1]); } sub _check_value { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('version_blank_name'); - # Remove unprintable characters - $name = clean_text($name); - - my $version = new Bugzilla::Version({ product => $product, name => $name }); - if ($version && (!ref $invocant || $version->id != $invocant->id)) { - ThrowUserError('version_already_exists', { name => $version->name, - product => $product->name }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('version_blank_name'); + + # Remove unprintable characters + $name = clean_text($name); + + my $version = new Bugzilla::Version({product => $product, name => $name}); + if ($version && (!ref $invocant || $version->id != $invocant->id)) { + ThrowUserError('version_already_exists', + {name => $version->name, product => $product->name}); + } + return $name; } sub _check_product { - my ($invocant, $product) = @_; - $product || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'product' }); - return Bugzilla->user->check_can_admin_product($product->name); + my ($invocant, $product) = @_; + $product + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'product'}); + return Bugzilla->user->check_can_admin_product($product->name); } ############################### @@ -209,44 +208,52 @@ sub _check_product { # This is taken straight from Sort::Versions 1.5, which is not included # with perl by default. sub vers_cmp { - my ($a, $b) = @_; - - # Remove leading zeroes - Bug 344661 - $a =~ s/^0*(\d.+)/$1/; - $b =~ s/^0*(\d.+)/$1/; - - my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); - my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); - - my ($A, $B); - while (@A and @B) { - $A = shift @A; - $B = shift @B; - if ($A eq '-' and $B eq '-') { - next; - } elsif ( $A eq '-' ) { - return -1; - } elsif ( $B eq '-') { - return 1; - } elsif ($A eq '.' and $B eq '.') { - next; - } elsif ( $A eq '.' ) { - return -1; - } elsif ( $B eq '.' ) { - return 1; - } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { - if ($A =~ /^0/ || $B =~ /^0/) { - return $A cmp $B if $A cmp $B; - } else { - return $A <=> $B if $A <=> $B; - } - } else { - $A = uc $A; - $B = uc $B; - return $A cmp $B if $A cmp $B; - } + my ($a, $b) = @_; + + # Remove leading zeroes - Bug 344661 + $a =~ s/^0*(\d.+)/$1/; + $b =~ s/^0*(\d.+)/$1/; + + my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); + my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); + + my ($A, $B); + while (@A and @B) { + $A = shift @A; + $B = shift @B; + if ($A eq '-' and $B eq '-') { + next; + } + elsif ($A eq '-') { + return -1; + } + elsif ($B eq '-') { + return 1; + } + elsif ($A eq '.' and $B eq '.') { + next; + } + elsif ($A eq '.') { + return -1; + } + elsif ($B eq '.') { + return 1; + } + elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { + if ($A =~ /^0/ || $B =~ /^0/) { + return $A cmp $B if $A cmp $B; + } + else { + return $A <=> $B if $A <=> $B; + } + } + else { + $A = uc $A; + $B = uc $B; + return $A cmp $B if $A cmp $B; } - return @A <=> @B; + } + return @A <=> @B; } 1; diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm index f80813744..2630e3565 100644 --- a/Bugzilla/WebService.pm +++ b/Bugzilla/WebService.pm @@ -5,7 +5,7 @@ # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. -# This is the base class for $self in WebService method calls. For the +# This is the base class for $self in WebService method calls. For the # actual RPC server, see Bugzilla::WebService::Server and its subclasses. package Bugzilla::WebService; @@ -17,11 +17,12 @@ use Bugzilla::WebService::Server; # Used by the JSON-RPC server to convert incoming date fields apprpriately. use constant DATE_FIELDS => {}; + # Used by the JSON-RPC server to convert incoming base64 fields appropriately. use constant BASE64_FIELDS => {}; # For some methods, we shouldn't call Bugzilla->login before we call them -use constant LOGIN_EXEMPT => { }; +use constant LOGIN_EXEMPT => {}; # Used to allow methods to be called in the JSON-RPC WebService via GET. # Methods that can modify data MUST not be listed here. @@ -32,8 +33,8 @@ use constant READ_ONLY => (); use constant PUBLIC_METHODS => (); sub login_exempt { - my ($class, $method) = @_; - return $class->LOGIN_EXEMPT->{$method}; + my ($class, $method) = @_; + return $class->LOGIN_EXEMPT->{$method}; } 1; diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index b07d3cb01..2cdc37443 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -19,7 +19,8 @@ use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Field; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate); +use Bugzilla::WebService::Util + qw(extract_flags filter filter_wants validate translate); use Bugzilla::Bug; use Bugzilla::BugMail; use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural); @@ -43,58 +44,54 @@ use Storable qw(dclone); use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component); use constant DATE_FIELDS => { - comments => ['new_since'], - history => ['new_since'], - search => ['last_change_time', 'creation_time'], + comments => ['new_since'], + history => ['new_since'], + search => ['last_change_time', 'creation_time'], }; -use constant BASE64_FIELDS => { - add_attachment => ['data'], -}; +use constant BASE64_FIELDS => {add_attachment => ['data'],}; use constant READ_ONLY => qw( - attachments - comments - fields - get - history - legal_values - search + attachments + comments + fields + get + history + legal_values + search ); use constant PUBLIC_METHODS => qw( - add_attachment - add_comment - attachments - comments - create - fields - get - history - legal_values - possible_duplicates - render_comment - search - search_comment_tags - update - update_attachment - update_comment_tags - update_see_also - update_tags + add_attachment + add_comment + attachments + comments + create + fields + get + history + legal_values + possible_duplicates + render_comment + search + search_comment_tags + update + update_attachment + update_comment_tags + update_see_also + update_tags ); -use constant ATTACHMENT_MAPPED_SETTERS => { - file_name => 'filename', - summary => 'description', -}; +use constant ATTACHMENT_MAPPED_SETTERS => + {file_name => 'filename', summary => 'description',}; use constant ATTACHMENT_MAPPED_RETURNS => { - description => 'summary', - ispatch => 'is_patch', - isprivate => 'is_private', - isobsolete => 'is_obsolete', - filename => 'file_name', - mimetype => 'content_type', + description => 'summary', + ispatch => 'is_patch', + isprivate => 'is_private', + isobsolete => 'is_obsolete', + filename => 'file_name', + mimetype => 'content_type', }; ########### @@ -102,1089 +99,1101 @@ use constant ATTACHMENT_MAPPED_RETURNS => { ########### sub fields { - my ($self, $params) = validate(@_, 'ids', 'names'); + my ($self, $params) = validate(@_, 'ids', 'names'); - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - my @fields; - if (defined $params->{ids}) { - my $ids = $params->{ids}; - foreach my $id (@$ids) { - my $loop_field = Bugzilla::Field->check({ id => $id }); - push(@fields, $loop_field); - } + my @fields; + if (defined $params->{ids}) { + my $ids = $params->{ids}; + foreach my $id (@$ids) { + my $loop_field = Bugzilla::Field->check({id => $id}); + push(@fields, $loop_field); } - - if (defined $params->{names}) { - my $names = $params->{names}; - foreach my $field_name (@$names) { - my $loop_field = Bugzilla::Field->check($field_name); - # Don't push in duplicate fields if we also asked for this field - # in "ids". - if (!grep($_->id == $loop_field->id, @fields)) { - push(@fields, $loop_field); - } - } + } + + if (defined $params->{names}) { + my $names = $params->{names}; + foreach my $field_name (@$names) { + my $loop_field = Bugzilla::Field->check($field_name); + + # Don't push in duplicate fields if we also asked for this field + # in "ids". + if (!grep($_->id == $loop_field->id, @fields)) { + push(@fields, $loop_field); + } } - - if (!defined $params->{ids} and !defined $params->{names}) { - @fields = @{ Bugzilla->fields({ obsolete => 0 }) }; + } + + if (!defined $params->{ids} and !defined $params->{names}) { + @fields = @{Bugzilla->fields({obsolete => 0})}; + } + + my @fields_out; + foreach my $field (@fields) { + my $visibility_field + = $field->visibility_field ? $field->visibility_field->name : undef; + my $vis_values = $field->visibility_values; + my $value_field = $field->value_field ? $field->value_field->name : undef; + + my (@values, $has_values); + if ( ($field->is_select and $field->name ne 'product') + or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) + or $field->name eq 'keywords') + { + $has_values = 1; + @values = @{$self->_legal_field_values({field => $field})}; } - my @fields_out; - foreach my $field (@fields) { - my $visibility_field = $field->visibility_field - ? $field->visibility_field->name : undef; - my $vis_values = $field->visibility_values; - my $value_field = $field->value_field - ? $field->value_field->name : undef; - - my (@values, $has_values); - if ( ($field->is_select and $field->name ne 'product') - or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS) - or $field->name eq 'keywords') - { - $has_values = 1; - @values = @{ $self->_legal_field_values({ field => $field }) }; - } - - if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { - $value_field = 'product'; - } + if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) { + $value_field = 'product'; + } - my %field_data = ( - id => $self->type('int', $field->id), - type => $self->type('int', $field->type), - is_custom => $self->type('boolean', $field->custom), - name => $self->type('string', $field->name), - display_name => $self->type('string', $field->description), - is_mandatory => $self->type('boolean', $field->is_mandatory), - is_on_bug_entry => $self->type('boolean', $field->enter_bug), - visibility_field => $self->type('string', $visibility_field), - visibility_values => - [ map { $self->type('string', $_->name) } @$vis_values ], - ); - if ($has_values) { - $field_data{value_field} = $self->type('string', $value_field); - $field_data{values} = \@values; - }; - push(@fields_out, filter $params, \%field_data); + my %field_data = ( + id => $self->type('int', $field->id), + type => $self->type('int', $field->type), + is_custom => $self->type('boolean', $field->custom), + name => $self->type('string', $field->name), + display_name => $self->type('string', $field->description), + is_mandatory => $self->type('boolean', $field->is_mandatory), + is_on_bug_entry => $self->type('boolean', $field->enter_bug), + visibility_field => $self->type('string', $visibility_field), + visibility_values => [map { $self->type('string', $_->name) } @$vis_values], + ); + if ($has_values) { + $field_data{value_field} = $self->type('string', $value_field); + $field_data{values} = \@values; } + push(@fields_out, filter $params, \%field_data); + } - return { fields => \@fields_out }; + return {fields => \@fields_out}; } sub _legal_field_values { - my ($self, $params) = @_; - my $field = $params->{field}; - my $field_name = $field->name; - my $user = Bugzilla->user; - - my @result; - if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { - my @list; - if ($field_name eq 'version') { - @list = Bugzilla::Version->get_all; - } - elsif ($field_name eq 'component') { - @list = Bugzilla::Component->get_all; - } - else { - @list = Bugzilla::Milestone->get_all; - } + my ($self, $params) = @_; + my $field = $params->{field}; + my $field_name = $field->name; + my $user = Bugzilla->user; + + my @result; + if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) { + my @list; + if ($field_name eq 'version') { + @list = Bugzilla::Version->get_all; + } + elsif ($field_name eq 'component') { + @list = Bugzilla::Component->get_all; + } + else { + @list = Bugzilla::Milestone->get_all; + } - foreach my $value (@list) { - my $sortkey = $field_name eq 'target_milestone' - ? $value->sortkey : 0; - # XXX This is very slow for large numbers of values. - my $product_name = $value->product->name; - if ($user->can_see_product($product_name)) { - push(@result, { - name => $self->type('string', $value->name), - sort_key => $self->type('int', $sortkey), - sortkey => $self->type('int', $sortkey), # deprecated - visibility_values => [$self->type('string', $product_name)], - is_active => $self->type('boolean', $value->is_active), - }); - } - } + foreach my $value (@list) { + my $sortkey = $field_name eq 'target_milestone' ? $value->sortkey : 0; + + # XXX This is very slow for large numbers of values. + my $product_name = $value->product->name; + if ($user->can_see_product($product_name)) { + push( + @result, + { + name => $self->type('string', $value->name), + sort_key => $self->type('int', $sortkey), + sortkey => $self->type('int', $sortkey), # deprecated + visibility_values => [$self->type('string', $product_name)], + is_active => $self->type('boolean', $value->is_active), + } + ); + } } + } + + elsif ($field_name eq 'bug_status') { + my @status_all = Bugzilla::Status->get_all; + my $initial_status = bless( + { + id => 0, + name => '', + is_open => 1, + sortkey => 0, + can_change_to => Bugzilla::Status->can_change_to + }, + 'Bugzilla::Status' + ); + unshift(@status_all, $initial_status); + + foreach my $status (@status_all) { + my @can_change_to; + foreach my $change_to (@{$status->can_change_to}) { + + # There's no need to note that a status can transition + # to itself. + next if $change_to->id == $status->id; + my %change_to_hash = ( + name => $self->type('string', $change_to->name), + comment_required => + $self->type('boolean', $change_to->comment_required_on_change_from($status)), + ); + push(@can_change_to, \%change_to_hash); + } - elsif ($field_name eq 'bug_status') { - my @status_all = Bugzilla::Status->get_all; - my $initial_status = bless({ id => 0, name => '', is_open => 1, sortkey => 0, - can_change_to => Bugzilla::Status->can_change_to }, - 'Bugzilla::Status'); - unshift(@status_all, $initial_status); - - foreach my $status (@status_all) { - my @can_change_to; - foreach my $change_to (@{ $status->can_change_to }) { - # There's no need to note that a status can transition - # to itself. - next if $change_to->id == $status->id; - my %change_to_hash = ( - name => $self->type('string', $change_to->name), - comment_required => $self->type('boolean', - $change_to->comment_required_on_change_from($status)), - ); - push(@can_change_to, \%change_to_hash); - } - - push (@result, { - name => $self->type('string', $status->name), - is_open => $self->type('boolean', $status->is_open), - sort_key => $self->type('int', $status->sortkey), - sortkey => $self->type('int', $status->sortkey), # deprecated - can_change_to => \@can_change_to, - visibility_values => [], - }); + push( + @result, + { + name => $self->type('string', $status->name), + is_open => $self->type('boolean', $status->is_open), + sort_key => $self->type('int', $status->sortkey), + sortkey => $self->type('int', $status->sortkey), # deprecated + can_change_to => \@can_change_to, + visibility_values => [], } + ); } + } - elsif ($field_name eq 'keywords') { - my @legal_keywords = Bugzilla::Keyword->get_all; - foreach my $value (@legal_keywords) { - push (@result, { - name => $self->type('string', $value->name), - description => $self->type('string', $value->description), - }); + elsif ($field_name eq 'keywords') { + my @legal_keywords = Bugzilla::Keyword->get_all; + foreach my $value (@legal_keywords) { + push( + @result, + { + name => $self->type('string', $value->name), + description => $self->type('string', $value->description), } + ); } - else { - my @values = Bugzilla::Field::Choice->type($field)->get_all(); - foreach my $value (@values) { - my $vis_val = $value->visibility_value; - push(@result, { - name => $self->type('string', $value->name), - sort_key => $self->type('int' , $value->sortkey), - sortkey => $self->type('int' , $value->sortkey), # deprecated - visibility_values => [ - defined $vis_val ? $self->type('string', $vis_val->name) - : () - ], - }); + } + else { + my @values = Bugzilla::Field::Choice->type($field)->get_all(); + foreach my $value (@values) { + my $vis_val = $value->visibility_value; + push( + @result, + { + name => $self->type('string', $value->name), + sort_key => $self->type('int', $value->sortkey), + sortkey => $self->type('int', $value->sortkey), # deprecated + visibility_values => + [defined $vis_val ? $self->type('string', $vis_val->name) : ()], } + ); } + } - return \@result; + return \@result; } sub comments { - my ($self, $params) = validate(@_, 'ids', 'comment_ids'); + my ($self, $params) = validate(@_, 'ids', 'comment_ids'); - if (!(defined $params->{ids} || defined $params->{comment_ids})) { - ThrowCodeError('params_required', - { function => 'Bug.comments', - params => ['ids', 'comment_ids'] }); - } + if (!(defined $params->{ids} || defined $params->{comment_ids})) { + ThrowCodeError('params_required', + {function => 'Bug.comments', params => ['ids', 'comment_ids']}); + } - my $bug_ids = $params->{ids} || []; - my $comment_ids = $params->{comment_ids} || []; - - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - my %bugs; - foreach my $bug_id (@$bug_ids) { - my $bug = Bugzilla::Bug->check($bug_id); - # We want the API to always return comments in the same order. - - my $comments = $bug->comments({ order => 'oldest_to_newest', - after => $params->{new_since} }); - my @result; - foreach my $comment (@$comments) { - next if $comment->is_private && !$user->is_insider; - push(@result, $self->_translate_comment($comment, $params)); - } - $bugs{$bug->id}{'comments'} = \@result; + my $bug_ids = $params->{ids} || []; + my $comment_ids = $params->{comment_ids} || []; + + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + my %bugs; + foreach my $bug_id (@$bug_ids) { + my $bug = Bugzilla::Bug->check($bug_id); + + # We want the API to always return comments in the same order. + + my $comments = $bug->comments( + {order => 'oldest_to_newest', after => $params->{new_since}}); + my @result; + foreach my $comment (@$comments) { + next if $comment->is_private && !$user->is_insider; + push(@result, $self->_translate_comment($comment, $params)); + } + $bugs{$bug->id}{'comments'} = \@result; + } + + my %comments; + if (scalar @$comment_ids) { + my @ids = map { trim($_) } @$comment_ids; + my $comment_data = Bugzilla::Comment->new_from_list(\@ids); + + # See if we were passed any invalid comment ids. + my %got_ids = map { $_->id => 1 } @$comment_data; + foreach my $comment_id (@ids) { + if (!$got_ids{$comment_id}) { + ThrowUserError('comment_id_invalid', {id => $comment_id}); + } } - my %comments; - if (scalar @$comment_ids) { - my @ids = map { trim($_) } @$comment_ids; - my $comment_data = Bugzilla::Comment->new_from_list(\@ids); - - # See if we were passed any invalid comment ids. - my %got_ids = map { $_->id => 1 } @$comment_data; - foreach my $comment_id (@ids) { - if (!$got_ids{$comment_id}) { - ThrowUserError('comment_id_invalid', { id => $comment_id }); - } - } - - # Now make sure that we can see all the associated bugs. - my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; - Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); - - foreach my $comment (@$comment_data) { - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment->id }); - } - $comments{$comment->id} = - $self->_translate_comment($comment, $params); - } + # Now make sure that we can see all the associated bugs. + my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; + Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); + + foreach my $comment (@$comment_data) { + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment->id}); + } + $comments{$comment->id} = $self->_translate_comment($comment, $params); } + } - return { bugs => \%bugs, comments => \%comments }; + return {bugs => \%bugs, comments => \%comments}; } sub render_comment { - my ($self, $params) = @_; + my ($self, $params) = @_; - unless (defined $params->{text}) { - ThrowCodeError('params_required', - { function => 'Bug.render_comment', - params => ['text'] }); - } + unless (defined $params->{text}) { + ThrowCodeError('params_required', + {function => 'Bug.render_comment', params => ['text']}); + } - Bugzilla->switch_to_shadow_db(); - my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; + Bugzilla->switch_to_shadow_db(); + my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef; - my $tmpl = '[% text FILTER quoteUrls(bug) %]'; - my $html; - my $template = Bugzilla->template; - $template->process( - \$tmpl, - { bug => $bug, text => $params->{text}}, - \$html - ); + my $tmpl = '[% text FILTER quoteUrls(bug) %]'; + my $html; + my $template = Bugzilla->template; + $template->process(\$tmpl, {bug => $bug, text => $params->{text}}, \$html); - return { html => $html }; + return {html => $html}; } # Helper for Bug.comments sub _translate_comment { - my ($self, $comment, $filters, $types, $prefix) = @_; - my $attach_id = $comment->is_about_attachment ? $comment->extra_data - : undef; - - my $comment_hash = { - id => $self->type('int', $comment->id), - bug_id => $self->type('int', $comment->bug_id), - creator => $self->type('email', $comment->author->login), - time => $self->type('dateTime', $comment->creation_ts), - creation_time => $self->type('dateTime', $comment->creation_ts), - is_private => $self->type('boolean', $comment->is_private), - text => $self->type('string', $comment->body_full), - attachment_id => $self->type('int', $attach_id), - count => $self->type('int', $comment->count), - }; - - # Don't load comment tags unless enabled - if (Bugzilla->params->{'comment_taggers_group'}) { - $comment_hash->{tags} = [ - map { $self->type('string', $_) } - @{ $comment->tags } - ]; - } - - return filter($filters, $comment_hash, $types, $prefix); + my ($self, $comment, $filters, $types, $prefix) = @_; + my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef; + + my $comment_hash = { + id => $self->type('int', $comment->id), + bug_id => $self->type('int', $comment->bug_id), + creator => $self->type('email', $comment->author->login), + time => $self->type('dateTime', $comment->creation_ts), + creation_time => $self->type('dateTime', $comment->creation_ts), + is_private => $self->type('boolean', $comment->is_private), + text => $self->type('string', $comment->body_full), + attachment_id => $self->type('int', $attach_id), + count => $self->type('int', $comment->count), + }; + + # Don't load comment tags unless enabled + if (Bugzilla->params->{'comment_taggers_group'}) { + $comment_hash->{tags} = [map { $self->type('string', $_) } @{$comment->tags}]; + } + + return filter($filters, $comment_hash, $types, $prefix); } sub get { - my ($self, $params) = validate(@_, 'ids'); - - Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - - my $ids = $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - my (@bugs, @faults, @hashes); - - # Cache permissions for bugs. This highly reduces the number of calls to the DB. - # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. - my @int = grep { $_ =~ /^\d+$/ } @$ids; - Bugzilla->user->visible_bugs(\@int); - - foreach my $bug_id (@$ids) { - my $bug; - if ($params->{permissive}) { - eval { $bug = Bugzilla::Bug->check($bug_id); }; - if ($@) { - push(@faults, {id => $bug_id, - faultString => $@->faultstring, - faultCode => $@->faultcode, - } - ); - undef $@; - next; - } - } - else { - $bug = Bugzilla::Bug->check($bug_id); - } - push(@bugs, $bug); - push(@hashes, $self->_bug_to_hash($bug, $params)); + my ($self, $params) = validate(@_, 'ids'); + + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + + my $ids = $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + my (@bugs, @faults, @hashes); + + # Cache permissions for bugs. This highly reduces the number of calls to the DB. + # visible_bugs() is only able to handle bug IDs, so we have to skip aliases. + my @int = grep { $_ =~ /^\d+$/ } @$ids; + Bugzilla->user->visible_bugs(\@int); + + foreach my $bug_id (@$ids) { + my $bug; + if ($params->{permissive}) { + eval { $bug = Bugzilla::Bug->check($bug_id); }; + if ($@) { + push(@faults, + {id => $bug_id, faultString => $@->faultstring, faultCode => $@->faultcode,}); + undef $@; + next; + } } + else { + $bug = Bugzilla::Bug->check($bug_id); + } + push(@bugs, $bug); + push(@hashes, $self->_bug_to_hash($bug, $params)); + } - # Set the ETag before inserting the update tokens - # since the tokens will always be unique even if - # the data has not changed. - $self->bz_etag(\@hashes); + # Set the ETag before inserting the update tokens + # since the tokens will always be unique even if + # the data has not changed. + $self->bz_etag(\@hashes); - $self->_add_update_tokens($params, \@bugs, \@hashes); + $self->_add_update_tokens($params, \@bugs, \@hashes); - return { bugs => \@hashes, faults => \@faults }; + return {bugs => \@hashes, faults => \@faults}; } -# this is a function that gets bug activity for list of bug ids +# this is a function that gets bug activity for list of bug ids # it can be called as the following: # $call = $rpc->call( 'Bug.history', { ids => [1,2] }); sub history { - my ($self, $params) = validate(@_, 'ids'); - - Bugzilla->switch_to_shadow_db(); - - my $ids = $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); - - my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - $api_name{'bug_group'} = 'groups'; - - my @return; - foreach my $bug_id (@$ids) { - my %item; - my $bug = Bugzilla::Bug->check($bug_id); - $bug_id = $bug->id; - $item{id} = $self->type('int', $bug_id); - - my ($activity) = $bug->get_activity(undef, $params->{new_since}); - - my @history; - foreach my $changeset (@$activity) { - my %bug_history; - $bug_history{when} = $self->type('dateTime', $changeset->{when}); - $bug_history{who} = $self->type('string', $changeset->{who}); - $bug_history{changes} = []; - foreach my $change (@{ $changeset->{changes} }) { - my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; - my $attach_id = delete $change->{attachid}; - if ($attach_id) { - $change->{attachment_id} = $self->type('int', $attach_id); - } - $change->{removed} = $self->type('string', $change->{removed}); - $change->{added} = $self->type('string', $change->{added}); - $change->{field_name} = $self->type('string', $api_field); - delete $change->{fieldname}; - push (@{$bug_history{changes}}, $change); - } - - push (@history, \%bug_history); + my ($self, $params) = validate(@_, 'ids'); + + Bugzilla->switch_to_shadow_db(); + + my $ids = $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()}; + $api_name{'bug_group'} = 'groups'; + + my @return; + foreach my $bug_id (@$ids) { + my %item; + my $bug = Bugzilla::Bug->check($bug_id); + $bug_id = $bug->id; + $item{id} = $self->type('int', $bug_id); + + my ($activity) = $bug->get_activity(undef, $params->{new_since}); + + my @history; + foreach my $changeset (@$activity) { + my %bug_history; + $bug_history{when} = $self->type('dateTime', $changeset->{when}); + $bug_history{who} = $self->type('string', $changeset->{who}); + $bug_history{changes} = []; + foreach my $change (@{$changeset->{changes}}) { + my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname}; + my $attach_id = delete $change->{attachid}; + if ($attach_id) { + $change->{attachment_id} = $self->type('int', $attach_id); } + $change->{removed} = $self->type('string', $change->{removed}); + $change->{added} = $self->type('string', $change->{added}); + $change->{field_name} = $self->type('string', $api_field); + delete $change->{fieldname}; + push(@{$bug_history{changes}}, $change); + } + + push(@history, \%bug_history); + } - $item{history} = \@history; + $item{history} = \@history; - # alias is returned in case users passes a mixture of ids and aliases - # then they get to know which bug activity relates to which value - # they passed - $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; + # alias is returned in case users passes a mixture of ids and aliases + # then they get to know which bug activity relates to which value + # they passed + $item{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; - push(@return, \%item); - } + push(@return, \%item); + } - return { bugs => \@return }; + return {bugs => \@return}; } sub search { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - Bugzilla->switch_to_shadow_db(); - - my $match_params = dclone($params); - delete $match_params->{include_fields}; - delete $match_params->{exclude_fields}; - - # Determine whether this is a quicksearch query - if (exists $match_params->{quicksearch}) { - my $quicksearch = quicksearch($match_params->{'quicksearch'}); - my $cgi = Bugzilla::CGI->new($quicksearch); - $match_params = $cgi->Vars; - } - - if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) { - ThrowCodeError('param_required', - { param => 'limit', function => 'Bug.search()' }); - } - - my $max_results = Bugzilla->params->{max_search_results}; - unless (defined $match_params->{limit} && $match_params->{limit} == 0) { - if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { - $match_params->{limit} = $max_results; - } - } - else { - delete $match_params->{limit}; - delete $match_params->{offset}; - } + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; - $match_params = Bugzilla::Bug::map_fields($match_params); + Bugzilla->switch_to_shadow_db(); - my %options = ( fields => ['bug_id'] ); + my $match_params = dclone($params); + delete $match_params->{include_fields}; + delete $match_params->{exclude_fields}; - # Find the highest custom field id - my @field_ids = grep(/^f(\d+)$/, keys %$match_params); - my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + # Determine whether this is a quicksearch query + if (exists $match_params->{quicksearch}) { + my $quicksearch = quicksearch($match_params->{'quicksearch'}); + my $cgi = Bugzilla::CGI->new($quicksearch); + $match_params = $cgi->Vars; + } - # Do special search types for certain fields. - if (my $change_when = delete $match_params->{'delta_ts'}) { - $match_params->{"f${last_field_id}"} = 'delta_ts'; - $match_params->{"o${last_field_id}"} = 'greaterthaneq'; - $match_params->{"v${last_field_id}"} = $change_when; - $last_field_id++; - } - if (my $creation_when = delete $match_params->{'creation_ts'}) { - $match_params->{"f${last_field_id}"} = 'creation_ts'; - $match_params->{"o${last_field_id}"} = 'greaterthaneq'; - $match_params->{"v${last_field_id}"} = $creation_when; - $last_field_id++; - } - - # Some fields require a search type such as short desc, keywords, etc. - foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { - if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) { - $match_params->{$param . '_type'} = 'allwordssubstr'; - } - } - if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) { - $match_params->{'keywords_type'} = 'allwords'; - } + if (defined($match_params->{offset}) and !defined($match_params->{limit})) { + ThrowCodeError('param_required', + {param => 'limit', function => 'Bug.search()'}); + } - # Backwards compatibility with old method regarding role search - $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'}; - foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) { - next if !exists $match_params->{$role}; - my $value = delete $match_params->{$role}; - $match_params->{"f${last_field_id}"} = $role; - $match_params->{"o${last_field_id}"} = "anywordssubstr"; - $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value; - $last_field_id++; + my $max_results = Bugzilla->params->{max_search_results}; + unless (defined $match_params->{limit} && $match_params->{limit} == 0) { + if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) { + $match_params->{limit} = $max_results; } - - # If no other parameters have been passed other than limit and offset - # then we throw error if system is configured to do so. - if (!grep(!/^(limit|offset)$/, keys %$match_params) - && !Bugzilla->params->{search_allow_no_criteria}) + } + else { + delete $match_params->{limit}; + delete $match_params->{offset}; + } + + $match_params = Bugzilla::Bug::map_fields($match_params); + + my %options = (fields => ['bug_id']); + + # Find the highest custom field id + my @field_ids = grep(/^f(\d+)$/, keys %$match_params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + + # Do special search types for certain fields. + if (my $change_when = delete $match_params->{'delta_ts'}) { + $match_params->{"f${last_field_id}"} = 'delta_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $change_when; + $last_field_id++; + } + if (my $creation_when = delete $match_params->{'creation_ts'}) { + $match_params->{"f${last_field_id}"} = 'creation_ts'; + $match_params->{"o${last_field_id}"} = 'greaterthaneq'; + $match_params->{"v${last_field_id}"} = $creation_when; + $last_field_id++; + } + + # Some fields require a search type such as short desc, keywords, etc. + foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) { + if (defined $match_params->{$param} + && !defined $match_params->{$param . '_type'}) { - ThrowUserError('buglist_parameters_required'); + $match_params->{$param . '_type'} = 'allwordssubstr'; } - - $options{order} = [ split(/\s*,\s*/, delete $match_params->{order}) ] if $match_params->{order}; - $options{params} = $match_params; - - my $search = new Bugzilla::Search(%options); - my ($data) = $search->data; - - if (!scalar @$data) { - return { bugs => [] }; - } - - # Search.pm won't return bugs that the user shouldn't see so no filtering is needed. - my @bug_ids = map { $_->[0] } @$data; - my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) }; - my @bugs = map { $bug_objects{$_} } @bug_ids; - @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; - - return { bugs => \@bugs }; + } + if (defined $match_params->{'keywords'} + && !defined $match_params->{'keywords_type'}) + { + $match_params->{'keywords_type'} = 'allwords'; + } + + # Backwards compatibility with old method regarding role search + $match_params->{'reporter'} = delete $match_params->{'creator'} + if $match_params->{'creator'}; + foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) { + next if !exists $match_params->{$role}; + my $value = delete $match_params->{$role}; + $match_params->{"f${last_field_id}"} = $role; + $match_params->{"o${last_field_id}"} = "anywordssubstr"; + $match_params->{"v${last_field_id}"} + = ref $value ? join(" ", @{$value}) : $value; + $last_field_id++; + } + + # If no other parameters have been passed other than limit and offset + # then we throw error if system is configured to do so. + if ( !grep(!/^(limit|offset)$/, keys %$match_params) + && !Bugzilla->params->{search_allow_no_criteria}) + { + ThrowUserError('buglist_parameters_required'); + } + + $options{order} = [split(/\s*,\s*/, delete $match_params->{order})] + if $match_params->{order}; + $options{params} = $match_params; + + my $search = new Bugzilla::Search(%options); + my ($data) = $search->data; + + if (!scalar @$data) { + return {bugs => []}; + } + +# Search.pm won't return bugs that the user shouldn't see so no filtering is needed. + my @bug_ids = map { $_->[0] } @$data; + my %bug_objects + = map { $_->id => $_ } @{Bugzilla::Bug->new_from_list(\@bug_ids)}; + my @bugs = map { $bug_objects{$_} } @bug_ids; + @bugs = map { $self->_bug_to_hash($_, $params) } @bugs; + + return {bugs => \@bugs}; } sub possible_duplicates { - my ($self, $params) = validate(@_, 'products'); - my $user = Bugzilla->user; - - Bugzilla->switch_to_shadow_db(); - - # Undo the array-ification that validate() does, for "summary". - $params->{summary} || ThrowCodeError('param_required', - { function => 'Bug.possible_duplicates', param => 'summary' }); - - my @products; - foreach my $name (@{ $params->{'products'} || [] }) { - my $object = $user->can_enter_product($name, THROW_ERROR); - push(@products, $object); - } - - my $possible_dupes = Bugzilla::Bug->possible_duplicates( - { summary => $params->{summary}, products => \@products, - limit => $params->{limit} }); - my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; - $self->_add_update_tokens($params, $possible_dupes, \@hashes); - return { bugs => \@hashes }; + my ($self, $params) = validate(@_, 'products'); + my $user = Bugzilla->user; + + Bugzilla->switch_to_shadow_db(); + + # Undo the array-ification that validate() does, for "summary". + $params->{summary} + || ThrowCodeError('param_required', + {function => 'Bug.possible_duplicates', param => 'summary'}); + + my @products; + foreach my $name (@{$params->{'products'} || []}) { + my $object = $user->can_enter_product($name, THROW_ERROR); + push(@products, $object); + } + + my $possible_dupes = Bugzilla::Bug->possible_duplicates({ + summary => $params->{summary}, + products => \@products, + limit => $params->{limit} + }); + my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes; + $self->_add_update_tokens($params, $possible_dupes, \@hashes); + return {bugs => \@hashes}; } sub update { - my ($self, $params) = validate(@_, 'ids'); + my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; - # We skip certain fields because their set_ methods actually use - # the external names instead of the internal names. - $params = Bugzilla::Bug::map_fields($params, - { summary => 1, platform => 1, severity => 1, url => 1 }); + # We skip certain fields because their set_ methods actually use + # the external names instead of the internal names. + $params = Bugzilla::Bug::map_fields($params, + {summary => 1, platform => 1, severity => 1, url => 1}); - my $ids = delete $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); - my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids; + my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids; - my %values = %$params; - $values{other_bugs} = \@bugs; + my %values = %$params; + $values{other_bugs} = \@bugs; - if (exists $values{comment} and exists $values{comment}{comment}) { - $values{comment}{body} = delete $values{comment}{comment}; - } + if (exists $values{comment} and exists $values{comment}{comment}) { + $values{comment}{body} = delete $values{comment}{comment}; + } - # Prevent bugs that could be triggered by specifying fields that - # have valid "set_" functions in Bugzilla::Bug, but shouldn't be - # called using those field names. - delete $values{dependencies}; + # Prevent bugs that could be triggered by specifying fields that + # have valid "set_" functions in Bugzilla::Bug, but shouldn't be + # called using those field names. + delete $values{dependencies}; - # For backwards compatibility, treat alias string or array as a set action - if (exists $values{alias}) { - if (not ref $values{alias}) { - $values{alias} = { set => [ $values{alias} ] }; - } - elsif (ref $values{alias} eq 'ARRAY') { - $values{alias} = { set => $values{alias} }; - } + # For backwards compatibility, treat alias string or array as a set action + if (exists $values{alias}) { + if (not ref $values{alias}) { + $values{alias} = {set => [$values{alias}]}; } - - my $flags = delete $values{flags}; - - foreach my $bug (@bugs) { - $bug->set_all(\%values); - if ($flags) { - my ($old_flags, $new_flags) = extract_flags($flags, $bug); - $bug->set_flags($old_flags, $new_flags); - } + elsif (ref $values{alias} eq 'ARRAY') { + $values{alias} = {set => $values{alias}}; } + } - my %all_changes; - $dbh->bz_start_transaction(); - foreach my $bug (@bugs) { - $all_changes{$bug->id} = $bug->update(); - } - $dbh->bz_commit_transaction(); + my $flags = delete $values{flags}; - foreach my $bug (@bugs) { - $bug->send_changes($all_changes{$bug->id}); + foreach my $bug (@bugs) { + $bug->set_all(\%values); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($old_flags, $new_flags); } + } + + my %all_changes; + $dbh->bz_start_transaction(); + foreach my $bug (@bugs) { + $all_changes{$bug->id} = $bug->update(); + } + $dbh->bz_commit_transaction(); + + foreach my $bug (@bugs) { + $bug->send_changes($all_changes{$bug->id}); + } + + my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()}; + + # This doesn't normally belong in FIELD_MAP, but we do want to translate + # "bug_group" back into "groups". + $api_name{'bug_group'} = 'groups'; + + my @result; + foreach my $bug (@bugs) { + my %hash = ( + id => $self->type('int', $bug->id), + last_change_time => $self->type('dateTime', $bug->delta_ts), + changes => {}, + ); - my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - # This doesn't normally belong in FIELD_MAP, but we do want to translate - # "bug_group" back into "groups". - $api_name{'bug_group'} = 'groups'; - - my @result; - foreach my $bug (@bugs) { - my %hash = ( - id => $self->type('int', $bug->id), - last_change_time => $self->type('dateTime', $bug->delta_ts), - changes => {}, - ); - - # alias is returned in case users pass a mixture of ids and aliases, - # so that they can know which set of changes relates to which value - # they passed. - $hash{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; - - my %changes = %{ $all_changes{$bug->id} }; - foreach my $field (keys %changes) { - my $change = $changes{$field}; - my $api_field = $api_name{$field} || $field; - # We normalize undef to an empty string, so that the API - # stays consistent for things like Deadline that can become - # empty. - $change->[0] = '' if !defined $change->[0]; - $change->[1] = '' if !defined $change->[1]; - $hash{changes}->{$api_field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - - push(@result, \%hash); + # alias is returned in case users pass a mixture of ids and aliases, + # so that they can know which set of changes relates to which value + # they passed. + $hash{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; + + my %changes = %{$all_changes{$bug->id}}; + foreach my $field (keys %changes) { + my $change = $changes{$field}; + my $api_field = $api_name{$field} || $field; + + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; + $hash{changes}->{$api_field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - return { bugs => \@result }; + push(@result, \%hash); + } + + return {bugs => \@result}; } sub create { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); - $params = Bugzilla::Bug::map_fields($params); + $params = Bugzilla::Bug::map_fields($params); - my $flags = delete $params->{flags}; + my $flags = delete $params->{flags}; - # We start a nested transaction in case flag setting fails - # we want the bug creation to roll back as well. - $dbh->bz_start_transaction(); + # We start a nested transaction in case flag setting fails + # we want the bug creation to roll back as well. + $dbh->bz_start_transaction(); - my $bug = Bugzilla::Bug->create($params); + my $bug = Bugzilla::Bug->create($params); - # Set bug flags - if ($flags) { - my ($flags, $new_flags) = extract_flags($flags, $bug); - $bug->set_flags($flags, $new_flags); - $bug->update($bug->creation_ts); - } + # Set bug flags + if ($flags) { + my ($flags, $new_flags) = extract_flags($flags, $bug); + $bug->set_flags($flags, $new_flags); + $bug->update($bug->creation_ts); + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - $bug->send_changes(); + $bug->send_changes(); - return { id => $self->type('int', $bug->bug_id) }; + return {id => $self->type('int', $bug->bug_id)}; } sub legal_values { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - defined $params->{field} - or ThrowCodeError('param_required', { param => 'field' }); + defined $params->{field} + or ThrowCodeError('param_required', {param => 'field'}); - my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} - || $params->{field}; + my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field}; - my @global_selects = - @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) }; + my @global_selects = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})}; - my $values; - if (grep($_->name eq $field, @global_selects)) { - # The field is a valid one. - trick_taint($field); - $values = get_legal_field_values($field); - } - elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { - my $id = $params->{product_id}; - defined $id || ThrowCodeError('param_required', - { function => 'Bug.legal_values', param => 'product_id' }); - grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) - || ThrowUserError('product_access_denied', { id => $id }); - - my $product = new Bugzilla::Product($id); - my @objects; - if ($field eq 'version') { - @objects = @{$product->versions}; - } - elsif ($field eq 'target_milestone') { - @objects = @{$product->milestones}; - } - elsif ($field eq 'component') { - @objects = @{$product->components}; - } + my $values; + if (grep($_->name eq $field, @global_selects)) { - $values = [map { $_->name } @objects]; + # The field is a valid one. + trick_taint($field); + $values = get_legal_field_values($field); + } + elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) { + my $id = $params->{product_id}; + defined $id + || ThrowCodeError('param_required', + {function => 'Bug.legal_values', param => 'product_id'}); + grep($_->id eq $id, @{Bugzilla->user->get_accessible_products}) + || ThrowUserError('product_access_denied', {id => $id}); + + my $product = new Bugzilla::Product($id); + my @objects; + if ($field eq 'version') { + @objects = @{$product->versions}; } - else { - ThrowCodeError('invalid_field_name', { field => $params->{field} }); + elsif ($field eq 'target_milestone') { + @objects = @{$product->milestones}; } - - my @result; - foreach my $val (@$values) { - push(@result, $self->type('string', $val)); + elsif ($field eq 'component') { + @objects = @{$product->components}; } - return { values => \@result }; + $values = [map { $_->name } @objects]; + } + else { + ThrowCodeError('invalid_field_name', {field => $params->{field}}); + } + + my @result; + foreach my $val (@$values) { + push(@result, $self->type('string', $val)); + } + + return {values => \@result}; } sub add_attachment { - my ($self, $params) = validate(@_, 'ids'); - my $dbh = Bugzilla->dbh; - - Bugzilla->login(LOGIN_REQUIRED); - defined $params->{ids} - || ThrowCodeError('param_required', { param => 'ids' }); - defined $params->{data} - || ThrowCodeError('param_required', { param => 'data' }); - - my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{ $params->{ids} }; - - my @created; - $dbh->bz_start_transaction(); - my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - my $flags = delete $params->{flags}; - - foreach my $bug (@bugs) { - my $attachment = Bugzilla::Attachment->create({ - bug => $bug, - creation_ts => $timestamp, - data => $params->{data}, - description => $params->{summary}, - filename => $params->{file_name}, - mimetype => $params->{content_type}, - ispatch => $params->{is_patch}, - isprivate => $params->{is_private}, - }); - - if ($flags) { - my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment); - $attachment->set_flags($old_flags, $new_flags); - } + my ($self, $params) = validate(@_, 'ids'); + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + defined $params->{ids} || ThrowCodeError('param_required', {param => 'ids'}); + defined $params->{data} || ThrowCodeError('param_required', {param => 'data'}); + + my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{$params->{ids}}; + + my @created; + $dbh->bz_start_transaction(); + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my $flags = delete $params->{flags}; + + foreach my $bug (@bugs) { + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $params->{data}, + description => $params->{summary}, + filename => $params->{file_name}, + mimetype => $params->{content_type}, + ispatch => $params->{is_patch}, + isprivate => $params->{is_private}, + }); - $attachment->update($timestamp); - my $comment = $params->{comment} || ''; - $attachment->bug->add_comment($comment, - { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - push(@created, $attachment); + if ($flags) { + my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment); + $attachment->set_flags($old_flags, $new_flags); } - $_->bug->update($timestamp) foreach @created; - $dbh->bz_commit_transaction(); - $_->send_changes() foreach @bugs; + $attachment->update($timestamp); + my $comment = $params->{comment} || ''; + $attachment->bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_CREATED, + extra_data => $attachment->id + } + ); + push(@created, $attachment); + } + $_->bug->update($timestamp) foreach @created; + $dbh->bz_commit_transaction(); + + $_->send_changes() foreach @bugs; - my @created_ids = map { $_->id } @created; + my @created_ids = map { $_->id } @created; - return { ids => \@created_ids }; + return {ids => \@created_ids}; } sub update_attachment { - my ($self, $params) = validate(@_, 'ids'); + my ($self, $params) = validate(@_, 'ids'); + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + + my $ids = delete $params->{ids}; + defined $ids || ThrowCodeError('param_required', {param => 'ids'}); + + # Some fields cannot be sent to set_all + foreach my $key (qw(login password token)) { + delete $params->{$key}; + } + + $params = translate($params, ATTACHMENT_MAPPED_SETTERS); + + # Get all the attachments, after verifying that they exist and are editable + my @attachments = (); + my %bugs = (); + foreach my $id (@$ids) { + my $attachment = Bugzilla::Attachment->new($id) + || ThrowUserError("invalid_attach_id", {attach_id => $id}); + my $bug = $attachment->bug; + $attachment->_check_bug; + + push @attachments, $attachment; + $bugs{$bug->id} = $bug; + } + + my $flags = delete $params->{flags}; + my $comment = delete $params->{comment}; + + # Update the values + foreach my $attachment (@attachments) { + my ($update_flags, $new_flags) + = $flags ? extract_flags($flags, $attachment->bug, $attachment) : ([], []); + if ($attachment->validate_can_edit) { + $attachment->set_all($params); + $attachment->set_flags($update_flags, $new_flags) if $flags; + } + elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) { + + # Requestees can set flags targetted to them, even if they cannot + # edit the attachment. Flag setters can edit their own flags too. + my %flag_list = map { $_->{id} => $_ } @$update_flags; + my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]); + my @editable_flags; + foreach my $flag_obj (@$flag_objs) { + if ($flag_obj->setter_id == $user->id + || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) + { + push(@editable_flags, $flag_list{$flag_obj->id}); + } + } + if (!scalar @editable_flags) { + ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id}); + } + $attachment->set_flags(\@editable_flags, []); + } + else { + ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id}); + } + } - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); - my $ids = delete $params->{ids}; - defined $ids || ThrowCodeError('param_required', { param => 'ids' }); + # Do the actual update and get information to return to user + my @result; + foreach my $attachment (@attachments) { + my $changes = $attachment->update(); - # Some fields cannot be sent to set_all - foreach my $key (qw(login password token)) { - delete $params->{$key}; + if ($comment = trim($comment)) { + $attachment->bug->add_comment( + $comment, + { + isprivate => $attachment->isprivate, + type => CMT_ATTACHMENT_UPDATED, + extra_data => $attachment->id + } + ); } - $params = translate($params, ATTACHMENT_MAPPED_SETTERS); + $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); - # Get all the attachments, after verifying that they exist and are editable - my @attachments = (); - my %bugs = (); - foreach my $id (@$ids) { - my $attachment = Bugzilla::Attachment->new($id) - || ThrowUserError("invalid_attach_id", { attach_id => $id }); - my $bug = $attachment->bug; - $attachment->_check_bug; + my %hash = ( + id => $self->type('int', $attachment->id), + last_change_time => $self->type('dateTime', $attachment->modification_time), + changes => {}, + ); - push @attachments, $attachment; - $bugs{$bug->id} = $bug; - } + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; - my $flags = delete $params->{flags}; - my $comment = delete $params->{comment}; - - # Update the values - foreach my $attachment (@attachments) { - my ($update_flags, $new_flags) = $flags - ? extract_flags($flags, $attachment->bug, $attachment) - : ([], []); - if ($attachment->validate_can_edit) { - $attachment->set_all($params); - $attachment->set_flags($update_flags, $new_flags) if $flags; - } - elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) { - # Requestees can set flags targetted to them, even if they cannot - # edit the attachment. Flag setters can edit their own flags too. - my %flag_list = map { $_->{id} => $_ } @$update_flags; - my $flag_objs = Bugzilla::Flag->new_from_list([ keys %flag_list ]); - my @editable_flags; - foreach my $flag_obj (@$flag_objs) { - if ($flag_obj->setter_id == $user->id - || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id)) - { - push(@editable_flags, $flag_list{$flag_obj->id}); - } - } - if (!scalar @editable_flags) { - ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id }); - } - $attachment->set_flags(\@editable_flags, []); - } - else { - ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id }); - } + # We normalize undef to an empty string, so that the API + # stays consistent for things like Deadline that can become + # empty. + $hash{changes}->{$field} = { + removed => $self->type('string', $change->[0] // ''), + added => $self->type('string', $change->[1] // '') + }; } - $dbh->bz_start_transaction(); - - # Do the actual update and get information to return to user - my @result; - foreach my $attachment (@attachments) { - my $changes = $attachment->update(); - - if ($comment = trim($comment)) { - $attachment->bug->add_comment($comment, - { isprivate => $attachment->isprivate, - type => CMT_ATTACHMENT_UPDATED, - extra_data => $attachment->id }); - } + push(@result, \%hash); + } - $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS); + $dbh->bz_commit_transaction(); - my %hash = ( - id => $self->type('int', $attachment->id), - last_change_time => $self->type('dateTime', $attachment->modification_time), - changes => {}, - ); + # Email users about the change + foreach my $bug (values %bugs) { + $bug->update(); + $bug->send_changes(); + } - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; + # Return the information to the user + return {attachments => \@result}; +} - # We normalize undef to an empty string, so that the API - # stays consistent for things like Deadline that can become - # empty. - $hash{changes}->{$field} = { - removed => $self->type('string', $change->[0] // ''), - added => $self->type('string', $change->[1] // '') - }; - } +sub add_comment { + my ($self, $params) = @_; - push(@result, \%hash); - } + # The user must login in order add a comment + my $user = Bugzilla->login(LOGIN_REQUIRED); - $dbh->bz_commit_transaction(); + # Check parameters + defined $params->{id} || ThrowCodeError('param_required', {param => 'id'}); + my $comment = $params->{comment}; + (defined $comment && trim($comment) ne '') + || ThrowCodeError('param_required', {param => 'comment'}); - # Email users about the change - foreach my $bug (values %bugs) { - $bug->update(); - $bug->send_changes(); - } + my $bug = Bugzilla::Bug->check_for_edit($params->{id}); - # Return the information to the user - return { attachments => \@result }; -} + # Backwards-compatibility for versions before 3.6 + if (defined $params->{private}) { + $params->{is_private} = delete $params->{private}; + } -sub add_comment { - my ($self, $params) = @_; - - # The user must login in order add a comment - my $user = Bugzilla->login(LOGIN_REQUIRED); - - # Check parameters - defined $params->{id} - || ThrowCodeError('param_required', { param => 'id' }); - my $comment = $params->{comment}; - (defined $comment && trim($comment) ne '') - || ThrowCodeError('param_required', { param => 'comment' }); - - my $bug = Bugzilla::Bug->check_for_edit($params->{id}); - - # Backwards-compatibility for versions before 3.6 - if (defined $params->{private}) { - $params->{is_private} = delete $params->{private}; - } - # Append comment - $bug->add_comment($comment, { isprivate => $params->{is_private}, - work_time => $params->{work_time} }); - $bug->update(); + # Append comment + $bug->add_comment($comment, + {isprivate => $params->{is_private}, work_time => $params->{work_time}}); + $bug->update(); - my $new_comment_id = $bug->{added_comments}[0]->id; + my $new_comment_id = $bug->{added_comments}[0]->id; - # Send mail. - Bugzilla::BugMail::Send($bug->bug_id, { changer => $user }); + # Send mail. + Bugzilla::BugMail::Send($bug->bug_id, {changer => $user}); - return { id => $self->type('int', $new_comment_id) }; + return {id => $self->type('int', $new_comment_id)}; } sub update_see_also { - my ($self, $params) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - - # Check parameters - $params->{ids} - || ThrowCodeError('param_required', { param => 'id' }); - my ($add, $remove) = @$params{qw(add remove)}; - ($add || $remove) - or ThrowCodeError('params_required', { params => ['add', 'remove'] }); - - my @bugs; - foreach my $id (@{ $params->{ids} }) { - my $bug = Bugzilla::Bug->check_for_edit($id); - push(@bugs, $bug); - if ($remove) { - $bug->remove_see_also($_) foreach @$remove; - } - if ($add) { - $bug->add_see_also($_) foreach @$add; - } + my ($self, $params) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Check parameters + $params->{ids} || ThrowCodeError('param_required', {param => 'id'}); + my ($add, $remove) = @$params{qw(add remove)}; + ($add || $remove) + or ThrowCodeError('params_required', {params => ['add', 'remove']}); + + my @bugs; + foreach my $id (@{$params->{ids}}) { + my $bug = Bugzilla::Bug->check_for_edit($id); + push(@bugs, $bug); + if ($remove) { + $bug->remove_see_also($_) foreach @$remove; } - - my %changes; - foreach my $bug (@bugs) { - my $change = $bug->update(); - if (my $see_also = $change->{see_also}) { - $changes{$bug->id}->{see_also} = { - removed => [split(', ', $see_also->[0])], - added => [split(', ', $see_also->[1])], - }; - } - else { - # We still want a changes entry, for API consistency. - $changes{$bug->id}->{see_also} = { added => [], removed => [] }; - } - - Bugzilla::BugMail::Send($bug->id, { changer => $user }); + if ($add) { + $bug->add_see_also($_) foreach @$add; + } + } + + my %changes; + foreach my $bug (@bugs) { + my $change = $bug->update(); + if (my $see_also = $change->{see_also}) { + $changes{$bug->id}->{see_also} = { + removed => [split(', ', $see_also->[0])], + added => [split(', ', $see_also->[1])], + }; } + else { + # We still want a changes entry, for API consistency. + $changes{$bug->id}->{see_also} = {added => [], removed => []}; + } + + Bugzilla::BugMail::Send($bug->id, {changer => $user}); + } - return { changes => \%changes }; + return {changes => \%changes}; } sub attachments { - my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); + my ($self, $params) = validate(@_, 'ids', 'attachment_ids'); - Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; + Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id; - if (!(defined $params->{ids} - or defined $params->{attachment_ids})) - { - ThrowCodeError('param_required', - { function => 'Bug.attachments', - params => ['ids', 'attachment_ids'] }); - } - - my $ids = $params->{ids} || []; - my $attach_ids = $params->{attachment_ids} || []; - - my %bugs; - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check($bug_id); - $bugs{$bug->id} = []; - foreach my $attach (@{$bug->attachments}) { - push @{$bugs{$bug->id}}, - $self->_attachment_to_hash($attach, $params); - } + if (!(defined $params->{ids} or defined $params->{attachment_ids})) { + ThrowCodeError('param_required', + {function => 'Bug.attachments', params => ['ids', 'attachment_ids']}); + } + + my $ids = $params->{ids} || []; + my $attach_ids = $params->{attachment_ids} || []; + + my %bugs; + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check($bug_id); + $bugs{$bug->id} = []; + foreach my $attach (@{$bug->attachments}) { + push @{$bugs{$bug->id}}, $self->_attachment_to_hash($attach, $params); } - - my %attachments; - foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) { - Bugzilla::Bug->check($attach->bug_id); - if ($attach->isprivate && !Bugzilla->user->is_insider) { - ThrowUserError('auth_failure', {action => 'access', - object => 'attachment', - attach_id => $attach->id}); - } - $attachments{$attach->id} = - $self->_attachment_to_hash($attach, $params); + } + + my %attachments; + foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) { + Bugzilla::Bug->check($attach->bug_id); + if ($attach->isprivate && !Bugzilla->user->is_insider) { + ThrowUserError('auth_failure', + {action => 'access', object => 'attachment', attach_id => $attach->id}); } + $attachments{$attach->id} = $self->_attachment_to_hash($attach, $params); + } - return { bugs => \%bugs, attachments => \%attachments }; + return {bugs => \%bugs, attachments => \%attachments}; } sub update_tags { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->login(LOGIN_REQUIRED); - my $ids = $params->{ids}; - my $tags = $params->{tags}; + my $ids = $params->{ids}; + my $tags = $params->{tags}; - ThrowCodeError('param_required', - { function => 'Bug.update_tags', - param => 'ids' }) if !defined $ids; + ThrowCodeError('param_required', + {function => 'Bug.update_tags', param => 'ids'}) + if !defined $ids; - ThrowCodeError('param_required', - { function => 'Bug.update_tags', - param => 'tags' }) if !defined $tags; + ThrowCodeError('param_required', + {function => 'Bug.update_tags', param => 'tags'}) + if !defined $tags; - my %changes; - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check($bug_id); - my @old_tags = @{ $bug->tags }; + my %changes; + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check($bug_id); + my @old_tags = @{$bug->tags}; - $bug->remove_tag($_) foreach @{ $tags->{remove} || [] }; - $bug->add_tag($_) foreach @{ $tags->{add} || [] }; + $bug->remove_tag($_) foreach @{$tags->{remove} || []}; + $bug->add_tag($_) foreach @{$tags->{add} || []}; - my ($removed, $added) = diff_arrays(\@old_tags, $bug->tags); + my ($removed, $added) = diff_arrays(\@old_tags, $bug->tags); - my @removed = map { $self->type('string', $_) } @$removed; - my @added = map { $self->type('string', $_) } @$added; + my @removed = map { $self->type('string', $_) } @$removed; + my @added = map { $self->type('string', $_) } @$added; - $changes{$bug->id}->{tags} = { - removed => \@removed, - added => \@added - }; - } + $changes{$bug->id}->{tags} = {removed => \@removed, added => \@added}; + } - return { changes => \%changes }; + return {changes => \%changes}; } sub update_comment_tags { - my ($self, $params) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->params->{'comment_taggers_group'} - || ThrowUserError("comment_tag_disabled"); - $user->can_tag_comments - || ThrowUserError("auth_failure", - { group => Bugzilla->params->{'comment_taggers_group'}, - action => "update", - object => "comment_tags" }); - - my $comment_id = $params->{comment_id} - // ThrowCodeError('param_required', - { function => 'Bug.update_comment_tags', - param => 'comment_id' }); - - ThrowCodeError('param_integer_required', { function => 'Bug.update_comment_tags', - param => 'comment_id' }) - unless $comment_id =~ /^[0-9]+$/; - - my $comment = Bugzilla::Comment->new($comment_id) - || return []; - $comment->bug->check_is_visible(); - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment_id }); - } + my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - foreach my $tag (@{ $params->{add} || [] }) { - $comment->add_tag($tag) if defined $tag; - } - foreach my $tag (@{ $params->{remove} || [] }) { - $comment->remove_tag($tag) if defined $tag; + my $user = Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + $user->can_tag_comments || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{'comment_taggers_group'}, + action => "update", + object => "comment_tags" } - $comment->update(); - $dbh->bz_commit_transaction(); - - return $comment->tags; + ); + + my $comment_id = $params->{comment_id} // ThrowCodeError('param_required', + {function => 'Bug.update_comment_tags', param => 'comment_id'}); + + ThrowCodeError('param_integer_required', + {function => 'Bug.update_comment_tags', param => 'comment_id'}) + unless $comment_id =~ /^[0-9]+$/; + + my $comment = Bugzilla::Comment->new($comment_id) || return []; + $comment->bug->check_is_visible(); + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment_id}); + } + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + foreach my $tag (@{$params->{add} || []}) { + $comment->add_tag($tag) if defined $tag; + } + foreach my $tag (@{$params->{remove} || []}) { + $comment->remove_tag($tag) if defined $tag; + } + $comment->update(); + $dbh->bz_commit_transaction(); + + return $comment->tags; } sub search_comment_tags { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->params->{'comment_taggers_group'} - || ThrowUserError("comment_tag_disabled"); - Bugzilla->user->can_tag_comments - || ThrowUserError("auth_failure", { group => Bugzilla->params->{'comment_taggers_group'}, - action => "search", - object => "comment_tags"}); - - my $query = $params->{query}; - $query - // ThrowCodeError('param_required', { param => 'query' }); - my $limit = $params->{limit} || 7; - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', { param => 'limit', - function => 'Bug.search_comment_tags' }); - - - my $tags = Bugzilla::Comment::TagWeights->match({ - WHERE => { - 'tag LIKE ?' => "\%$query\%", - }, - LIMIT => $limit, - }); - return [ map { $_->tag } @$tags ]; + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->params->{'comment_taggers_group'} + || ThrowUserError("comment_tag_disabled"); + Bugzilla->user->can_tag_comments || ThrowUserError( + "auth_failure", + { + group => Bugzilla->params->{'comment_taggers_group'}, + action => "search", + object => "comment_tags" + } + ); + + my $query = $params->{query}; + $query // ThrowCodeError('param_required', {param => 'query'}); + my $limit = $params->{limit} || 7; + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {param => 'limit', function => 'Bug.search_comment_tags'}); + + + my $tags = Bugzilla::Comment::TagWeights->match( + {WHERE => {'tag LIKE ?' => "\%$query\%",}, LIMIT => $limit,}); + return [map { $_->tag } @$tags]; } ############################## @@ -1197,232 +1206,238 @@ sub search_comment_tags { # return them directly. sub _bug_to_hash { - my ($self, $bug, $params) = @_; - - # All the basic bug attributes are here, in alphabetical order. - # A bug attribute is "basic" if it doesn't require an additional - # database call to get the info. - my %item = %{ filter $params, { - # No need to format $bug->deadline specially, because Bugzilla::Bug - # already does it for us. - deadline => $self->type('string', $bug->deadline), - id => $self->type('int', $bug->bug_id), - is_confirmed => $self->type('boolean', $bug->everconfirmed), - op_sys => $self->type('string', $bug->op_sys), - platform => $self->type('string', $bug->rep_platform), - priority => $self->type('string', $bug->priority), - resolution => $self->type('string', $bug->resolution), - severity => $self->type('string', $bug->bug_severity), - status => $self->type('string', $bug->bug_status), - summary => $self->type('string', $bug->short_desc), - target_milestone => $self->type('string', $bug->target_milestone), - url => $self->type('string', $bug->bug_file_loc), - version => $self->type('string', $bug->version), - whiteboard => $self->type('string', $bug->status_whiteboard), - } }; - - # First we handle any fields that require extra work (such as date parsing - # or SQL calls). - if (filter_wants $params, 'alias') { - $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ]; - } - if (filter_wants $params, 'assigned_to') { - $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login); - $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); - } - if (filter_wants $params, 'blocks') { - my @blocks = map { $self->type('int', $_) } @{ $bug->blocked }; - $item{'blocks'} = \@blocks; - } - if (filter_wants $params, 'classification') { - $item{classification} = $self->type('string', $bug->classification); - } - if (filter_wants $params, 'component') { - $item{component} = $self->type('string', $bug->component); - } - if (filter_wants $params, 'cc') { - my @cc = map { $self->type('email', $_) } @{ $bug->cc }; - $item{'cc'} = \@cc; - $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ]; - } - if (filter_wants $params, 'creation_time') { - $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts); - } - if (filter_wants $params, 'creator') { - $item{'creator'} = $self->type('email', $bug->reporter->login); - $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); - } - if (filter_wants $params, 'depends_on') { - my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson }; - $item{'depends_on'} = \@depends_on; - } - if (filter_wants $params, 'dupe_of') { - $item{'dupe_of'} = $self->type('int', $bug->dup_id); - } - if (filter_wants $params, 'groups') { - my @groups = map { $self->type('string', $_->name) } - @{ $bug->groups_in }; - $item{'groups'} = \@groups; - } - if (filter_wants $params, 'is_open') { - $item{'is_open'} = $self->type('boolean', $bug->status->is_open); - } - if (filter_wants $params, 'keywords') { - my @keywords = map { $self->type('string', $_->name) } - @{ $bug->keyword_objects }; - $item{'keywords'} = \@keywords; - } - if (filter_wants $params, 'last_change_time') { - $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts); - } - if (filter_wants $params, 'product') { - $item{product} = $self->type('string', $bug->product); + my ($self, $bug, $params) = @_; + + # All the basic bug attributes are here, in alphabetical order. + # A bug attribute is "basic" if it doesn't require an additional + # database call to get the info. + my %item = %{filter $params, + { + # No need to format $bug->deadline specially, because Bugzilla::Bug + # already does it for us. + deadline => $self->type('string', $bug->deadline), + id => $self->type('int', $bug->bug_id), + is_confirmed => $self->type('boolean', $bug->everconfirmed), + op_sys => $self->type('string', $bug->op_sys), + platform => $self->type('string', $bug->rep_platform), + priority => $self->type('string', $bug->priority), + resolution => $self->type('string', $bug->resolution), + severity => $self->type('string', $bug->bug_severity), + status => $self->type('string', $bug->bug_status), + summary => $self->type('string', $bug->short_desc), + target_milestone => $self->type('string', $bug->target_milestone), + url => $self->type('string', $bug->bug_file_loc), + version => $self->type('string', $bug->version), + whiteboard => $self->type('string', $bug->status_whiteboard), } - if (filter_wants $params, 'qa_contact') { - my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; - $item{'qa_contact'} = $self->type('email', $qa_login); - if ($bug->qa_contact) { - $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); - } + }; + + # First we handle any fields that require extra work (such as date parsing + # or SQL calls). + if (filter_wants $params, 'alias') { + $item{alias} = [map { $self->type('string', $_) } @{$bug->alias}]; + } + if (filter_wants $params, 'assigned_to') { + $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login); + $item{'assigned_to_detail'} + = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to'); + } + if (filter_wants $params, 'blocks') { + my @blocks = map { $self->type('int', $_) } @{$bug->blocked}; + $item{'blocks'} = \@blocks; + } + if (filter_wants $params, 'classification') { + $item{classification} = $self->type('string', $bug->classification); + } + if (filter_wants $params, 'component') { + $item{component} = $self->type('string', $bug->component); + } + if (filter_wants $params, 'cc') { + my @cc = map { $self->type('email', $_) } @{$bug->cc}; + $item{'cc'} = \@cc; + $item{'cc_detail'} + = [map { $self->_user_to_hash($_, $params, undef, 'cc') } @{$bug->cc_users}]; + } + if (filter_wants $params, 'creation_time') { + $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts); + } + if (filter_wants $params, 'creator') { + $item{'creator'} = $self->type('email', $bug->reporter->login); + $item{'creator_detail'} + = $self->_user_to_hash($bug->reporter, $params, undef, 'creator'); + } + if (filter_wants $params, 'depends_on') { + my @depends_on = map { $self->type('int', $_) } @{$bug->dependson}; + $item{'depends_on'} = \@depends_on; + } + if (filter_wants $params, 'dupe_of') { + $item{'dupe_of'} = $self->type('int', $bug->dup_id); + } + if (filter_wants $params, 'groups') { + my @groups = map { $self->type('string', $_->name) } @{$bug->groups_in}; + $item{'groups'} = \@groups; + } + if (filter_wants $params, 'is_open') { + $item{'is_open'} = $self->type('boolean', $bug->status->is_open); + } + if (filter_wants $params, 'keywords') { + my @keywords = map { $self->type('string', $_->name) } @{$bug->keyword_objects}; + $item{'keywords'} = \@keywords; + } + if (filter_wants $params, 'last_change_time') { + $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts); + } + if (filter_wants $params, 'product') { + $item{product} = $self->type('string', $bug->product); + } + if (filter_wants $params, 'qa_contact') { + my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : ''; + $item{'qa_contact'} = $self->type('email', $qa_login); + if ($bug->qa_contact) { + $item{'qa_contact_detail'} + = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact'); } - if (filter_wants $params, 'see_also') { - my @see_also = map { $self->type('string', $_->name) } - @{ $bug->see_also }; - $item{'see_also'} = \@see_also; + } + if (filter_wants $params, 'see_also') { + my @see_also = map { $self->type('string', $_->name) } @{$bug->see_also}; + $item{'see_also'} = \@see_also; + } + if (filter_wants $params, 'flags') { + $item{'flags'} = [map { $self->_flag_to_hash($_) } @{$bug->flags}]; + } + if (filter_wants $params, 'tags', 'extra') { + $item{'tags'} = $bug->tags; + } + + # And now custom fields + my @custom_fields = Bugzilla->active_custom_fields; + foreach my $field (@custom_fields) { + my $name = $field->name; + next if !filter_wants($params, $name, ['default', 'custom']); + if ($field->type == FIELD_TYPE_BUG_ID) { + $item{$name} = $self->type('int', $bug->$name); } - if (filter_wants $params, 'flags') { - $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ]; + elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) { + $item{$name} = $self->type('dateTime', $bug->$name); } - if (filter_wants $params, 'tags', 'extra') { - $item{'tags'} = $bug->tags; + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + my @values = map { $self->type('string', $_) } @{$bug->$name}; + $item{$name} = \@values; } - - # And now custom fields - my @custom_fields = Bugzilla->active_custom_fields; - foreach my $field (@custom_fields) { - my $name = $field->name; - next if !filter_wants($params, $name, ['default', 'custom']); - if ($field->type == FIELD_TYPE_BUG_ID) { - $item{$name} = $self->type('int', $bug->$name); - } - elsif ($field->type == FIELD_TYPE_DATETIME - || $field->type == FIELD_TYPE_DATE) - { - $item{$name} = $self->type('dateTime', $bug->$name); - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - my @values = map { $self->type('string', $_) } @{ $bug->$name }; - $item{$name} = \@values; - } - else { - $item{$name} = $self->type('string', $bug->$name); - } + else { + $item{$name} = $self->type('string', $bug->$name); } + } - # Timetracking fields are only sent if the user can see them. - if (Bugzilla->user->is_timetracker) { - if (filter_wants $params, 'estimated_time') { - $item{'estimated_time'} = $self->type('double', $bug->estimated_time); - } - if (filter_wants $params, 'remaining_time') { - $item{'remaining_time'} = $self->type('double', $bug->remaining_time); - } - if (filter_wants $params, 'actual_time') { - $item{'actual_time'} = $self->type('double', $bug->actual_time); - } + # Timetracking fields are only sent if the user can see them. + if (Bugzilla->user->is_timetracker) { + if (filter_wants $params, 'estimated_time') { + $item{'estimated_time'} = $self->type('double', $bug->estimated_time); } - - # The "accessible" bits go here because they have long names and it - # makes the code look nicer to separate them out. - if (filter_wants $params, 'is_cc_accessible') { - $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); + if (filter_wants $params, 'remaining_time') { + $item{'remaining_time'} = $self->type('double', $bug->remaining_time); } - if (filter_wants $params, 'is_creator_accessible') { - $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible); + if (filter_wants $params, 'actual_time') { + $item{'actual_time'} = $self->type('double', $bug->actual_time); } - - return \%item; + } + + # The "accessible" bits go here because they have long names and it + # makes the code look nicer to separate them out. + if (filter_wants $params, 'is_cc_accessible') { + $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible); + } + if (filter_wants $params, 'is_creator_accessible') { + $item{'is_creator_accessible'} + = $self->type('boolean', $bug->reporter_accessible); + } + + return \%item; } sub _user_to_hash { - my ($self, $user, $filters, $types, $prefix) = @_; - my $item = filter $filters, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), - email => $self->type('email', $user->email), - }, $types, $prefix; - return $item; + my ($self, $user, $filters, $types, $prefix) = @_; + my $item = filter $filters, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + email => $self->type('email', $user->email), + }, + $types, $prefix; + return $item; } sub _attachment_to_hash { - my ($self, $attach, $filters, $types, $prefix) = @_; - - my $item = filter $filters, { - creation_time => $self->type('dateTime', $attach->attached), - last_change_time => $self->type('dateTime', $attach->modification_time), - id => $self->type('int', $attach->id), - bug_id => $self->type('int', $attach->bug_id), - file_name => $self->type('string', $attach->filename), - summary => $self->type('string', $attach->description), - content_type => $self->type('string', $attach->contenttype), - is_private => $self->type('int', $attach->isprivate), - is_obsolete => $self->type('int', $attach->isobsolete), - is_patch => $self->type('int', $attach->ispatch), - }, $types, $prefix; - - # creator requires an extra lookup, so we only send them if - # the filter wants them. - if (filter_wants $filters, 'creator', $types, $prefix) { - $item->{'creator'} = $self->type('email', $attach->attacher->login); - } - - if (filter_wants $filters, 'data', $types, $prefix) { - $item->{'data'} = $self->type('base64', $attach->data); - } - - if (filter_wants $filters, 'size', $types, $prefix) { - $item->{'size'} = $self->type('int', $attach->datasize); - } - - if (filter_wants $filters, 'flags', $types, $prefix) { - $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ]; - } + my ($self, $attach, $filters, $types, $prefix) = @_; - return $item; + my $item = filter $filters, + { + creation_time => $self->type('dateTime', $attach->attached), + last_change_time => $self->type('dateTime', $attach->modification_time), + id => $self->type('int', $attach->id), + bug_id => $self->type('int', $attach->bug_id), + file_name => $self->type('string', $attach->filename), + summary => $self->type('string', $attach->description), + content_type => $self->type('string', $attach->contenttype), + is_private => $self->type('int', $attach->isprivate), + is_obsolete => $self->type('int', $attach->isobsolete), + is_patch => $self->type('int', $attach->ispatch), + }, + $types, $prefix; + + # creator requires an extra lookup, so we only send them if + # the filter wants them. + if (filter_wants $filters, 'creator', $types, $prefix) { + $item->{'creator'} = $self->type('email', $attach->attacher->login); + } + + if (filter_wants $filters, 'data', $types, $prefix) { + $item->{'data'} = $self->type('base64', $attach->data); + } + + if (filter_wants $filters, 'size', $types, $prefix) { + $item->{'size'} = $self->type('int', $attach->datasize); + } + + if (filter_wants $filters, 'flags', $types, $prefix) { + $item->{'flags'} = [map { $self->_flag_to_hash($_) } @{$attach->flags}]; + } + + return $item; } sub _flag_to_hash { - my ($self, $flag) = @_; - - my $item = { - id => $self->type('int', $flag->id), - name => $self->type('string', $flag->name), - type_id => $self->type('int', $flag->type_id), - creation_date => $self->type('dateTime', $flag->creation_date), - modification_date => $self->type('dateTime', $flag->modification_date), - status => $self->type('string', $flag->status) - }; - - foreach my $field (qw(setter requestee)) { - my $field_id = $field . "_id"; - $item->{$field} = $self->type('email', $flag->$field->login) - if $flag->$field_id; - } - - return $item; + my ($self, $flag) = @_; + + my $item = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $item->{$field} = $self->type('email', $flag->$field->login) + if $flag->$field_id; + } + + return $item; } sub _add_update_tokens { - my ($self, $params, $bugs, $hashes) = @_; + my ($self, $params, $bugs, $hashes) = @_; - return if !Bugzilla->user->id; - return if !filter_wants($params, 'update_token'); + return if !Bugzilla->user->id; + return if !filter_wants($params, 'update_token'); - for(my $i = 0; $i < @$bugs; $i++) { - my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); - $hashes->[$i]->{'update_token'} = $self->type('string', $token); - } + for (my $i = 0; $i < @$bugs; $i++) { + my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]); + $hashes->[$i]->{'update_token'} = $self->type('string', $token); + } } 1; diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm index 56e91ec31..128507376 100644 --- a/Bugzilla/WebService/BugUserLastVisit.pm +++ b/Bugzilla/WebService/BugUserLastVisit.pm @@ -19,80 +19,83 @@ use Bugzilla::WebService::Util qw( validate filter ); use Bugzilla::Constants; use constant PUBLIC_METHODS => qw( - get - update + get + update ); sub update { - my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; - $user->login(LOGIN_REQUIRED); + $user->login(LOGIN_REQUIRED); - my $ids = $params->{ids} // []; - ThrowCodeError('param_required', { param => 'ids' }) unless @$ids; + my $ids = $params->{ids} // []; + ThrowCodeError('param_required', {param => 'ids'}) unless @$ids; - # Cache permissions for bugs. This highly reduces the number of calls to the - # DB. visible_bugs() is only able to handle bug IDs, so we have to skip - # aliases. - $user->visible_bugs([grep /^[0-9]+$/, @$ids]); + # Cache permissions for bugs. This highly reduces the number of calls to the + # DB. visible_bugs() is only able to handle bug IDs, so we have to skip + # aliases. + $user->visible_bugs([grep /^[0-9]+$/, @$ids]); - $dbh->bz_start_transaction(); - my @results; - my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); - foreach my $bug_id (@$ids) { - my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }); + $dbh->bz_start_transaction(); + my @results; + my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()'); + foreach my $bug_id (@$ids) { + my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1}); - ThrowUserError('user_not_involved', { bug_id => $bug->id }) - unless $user->is_involved_in_bug($bug); + ThrowUserError('user_not_involved', {bug_id => $bug->id}) + unless $user->is_involved_in_bug($bug); - $bug->update_user_last_visit($user, $last_visit_ts); + $bug->update_user_last_visit($user, $last_visit_ts); - push( - @results, - $self->_bug_user_last_visit_to_hash( - $bug->id, $last_visit_ts, $params - )); - } - $dbh->bz_commit_transaction(); + push(@results, + $self->_bug_user_last_visit_to_hash($bug->id, $last_visit_ts, $params)); + } + $dbh->bz_commit_transaction(); - return \@results; + return \@results; } sub get { - my ($self, $params) = validate(@_, 'ids'); - my $user = Bugzilla->user; - my $ids = $params->{ids}; - - $user->login(LOGIN_REQUIRED); - - my @last_visits; - if ($ids) { - # Cache permissions for bugs. This highly reduces the number of calls to - # the DB. visible_bugs() is only able to handle bug IDs, so we have to - # skip aliases. - $user->visible_bugs([grep /^[0-9]+$/, @$ids]); - - my %last_visit = map { $_->bug_id => $_->last_visit_ts } @{ $user->last_visited($ids) }; - @last_visits = map { $self->_bug_user_last_visit_to_hash($_->id, $last_visit{$_}, $params) } @$ids; - } - else { - @last_visits = map { - $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params) - } @{ $user->last_visited }; - } - - return \@last_visits; + my ($self, $params) = validate(@_, 'ids'); + my $user = Bugzilla->user; + my $ids = $params->{ids}; + + $user->login(LOGIN_REQUIRED); + + my @last_visits; + if ($ids) { + + # Cache permissions for bugs. This highly reduces the number of calls to + # the DB. visible_bugs() is only able to handle bug IDs, so we have to + # skip aliases. + $user->visible_bugs([grep /^[0-9]+$/, @$ids]); + + my %last_visit + = map { $_->bug_id => $_->last_visit_ts } @{$user->last_visited($ids)}; + @last_visits + = map { $self->_bug_user_last_visit_to_hash($_->id, $last_visit{$_}, $params) } + @$ids; + } + else { + @last_visits = map { + $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params) + } @{$user->last_visited}; + } + + return \@last_visits; } sub _bug_user_last_visit_to_hash { - my ($self, $bug_id, $last_visit_ts, $params) = @_; + my ($self, $bug_id, $last_visit_ts, $params) = @_; - my %result = (id => $self->type('int', $bug_id), - last_visit_ts => $self->type('dateTime', $last_visit_ts)); + my %result = ( + id => $self->type('int', $bug_id), + last_visit_ts => $self->type('dateTime', $last_visit_ts) + ); - return filter($params, \%result); + return filter($params, \%result); } 1; diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm index 848cffd30..6d9563d61 100644 --- a/Bugzilla/WebService/Bugzilla.pm +++ b/Bugzilla/WebService/Bugzilla.pm @@ -20,158 +20,155 @@ use Bugzilla::Util qw(trick_taint); use DateTime; # Basic info that is needed before logins -use constant LOGIN_EXEMPT => { - parameters => 1, - timezone => 1, - version => 1, -}; +use constant LOGIN_EXEMPT => {parameters => 1, timezone => 1, version => 1,}; use constant READ_ONLY => qw( - extensions - parameters - timezone - time - version + extensions + parameters + timezone + time + version ); use constant PUBLIC_METHODS => qw( - extensions - last_audit_time - parameters - time - timezone - version + extensions + last_audit_time + parameters + time + timezone + version ); # Logged-out users do not need to know more than that. use constant PARAMETERS_LOGGED_OUT => qw( - maintainer - requirelogin + maintainer + requirelogin ); # These parameters are guessable from the web UI when the user # is logged in. So it's safe to access them. use constant PARAMETERS_LOGGED_IN => qw( - allowemailchange - attachment_base - commentonchange_resolution - commentonduplicate - cookiepath - defaultopsys - defaultplatform - defaultpriority - defaultseverity - duplicate_or_move_bug_status - emailregexpdesc - emailsuffix - letsubmitterchoosemilestone - letsubmitterchoosepriority - mailfrom - maintainer - maxattachmentsize - maxlocalattachment - musthavemilestoneonaccept - noresolveonopenblockers - password_complexity - rememberlogin - requirelogin - search_allow_no_criteria - urlbase - use_see_also - useclassification - usemenuforusers - useqacontact - usestatuswhiteboard - usetargetmilestone + allowemailchange + attachment_base + commentonchange_resolution + commentonduplicate + cookiepath + defaultopsys + defaultplatform + defaultpriority + defaultseverity + duplicate_or_move_bug_status + emailregexpdesc + emailsuffix + letsubmitterchoosemilestone + letsubmitterchoosepriority + mailfrom + maintainer + maxattachmentsize + maxlocalattachment + musthavemilestoneonaccept + noresolveonopenblockers + password_complexity + rememberlogin + requirelogin + search_allow_no_criteria + urlbase + use_see_also + useclassification + usemenuforusers + useqacontact + usestatuswhiteboard + usetargetmilestone ); sub version { - my $self = shift; - return { version => $self->type('string', BUGZILLA_VERSION) }; + my $self = shift; + return {version => $self->type('string', BUGZILLA_VERSION)}; } sub extensions { - my $self = shift; - - my %retval; - foreach my $extension (@{ Bugzilla->extensions }) { - my $version = $extension->VERSION || 0; - my $name = $extension->NAME; - $retval{$name}->{version} = $self->type('string', $version); - } - return { extensions => \%retval }; + my $self = shift; + + my %retval; + foreach my $extension (@{Bugzilla->extensions}) { + my $version = $extension->VERSION || 0; + my $name = $extension->NAME; + $retval{$name}->{version} = $self->type('string', $version); + } + return {extensions => \%retval}; } sub timezone { - my $self = shift; - # All Webservices return times in UTC; Use UTC here for backwards compat. - return { timezone => $self->type('string', "+0000") }; + my $self = shift; + + # All Webservices return times in UTC; Use UTC here for backwards compat. + return {timezone => $self->type('string', "+0000")}; } sub time { - my ($self) = @_; - # All Webservices return times in UTC; Use UTC here for backwards compat. - # Hardcode values where appropriate - my $dbh = Bugzilla->dbh; - - my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $db_time = datetime_from($db_time, 'UTC'); - my $now_utc = DateTime->now(); - - return { - db_time => $self->type('dateTime', $db_time), - web_time => $self->type('dateTime', $now_utc), - web_time_utc => $self->type('dateTime', $now_utc), - tz_name => $self->type('string', 'UTC'), - tz_offset => $self->type('string', '+0000'), - tz_short_name => $self->type('string', 'UTC'), - }; + my ($self) = @_; + + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + my $dbh = Bugzilla->dbh; + + my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $db_time = datetime_from($db_time, 'UTC'); + my $now_utc = DateTime->now(); + + return { + db_time => $self->type('dateTime', $db_time), + web_time => $self->type('dateTime', $now_utc), + web_time_utc => $self->type('dateTime', $now_utc), + tz_name => $self->type('string', 'UTC'), + tz_offset => $self->type('string', '+0000'), + tz_short_name => $self->type('string', 'UTC'), + }; } sub last_audit_time { - my ($self, $params) = validate(@_, 'class'); - my $dbh = Bugzilla->dbh; - - my $sql_statement = "SELECT MAX(at_time) FROM audit_log"; - my $class_values = $params->{class}; - my @class_values_quoted; - foreach my $class_value (@$class_values) { - push (@class_values_quoted, $dbh->quote($class_value)) - if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/; - } - - if (@class_values_quoted) { - $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted); - } - - my $last_audit_time = $dbh->selectrow_array("$sql_statement"); - - # All Webservices return times in UTC; Use UTC here for backwards compat. - # Hardcode values where appropriate - $last_audit_time = datetime_from($last_audit_time, 'UTC'); - - return { - last_audit_time => $self->type('dateTime', $last_audit_time) - }; + my ($self, $params) = validate(@_, 'class'); + my $dbh = Bugzilla->dbh; + + my $sql_statement = "SELECT MAX(at_time) FROM audit_log"; + my $class_values = $params->{class}; + my @class_values_quoted; + foreach my $class_value (@$class_values) { + push(@class_values_quoted, $dbh->quote($class_value)) + if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/; + } + + if (@class_values_quoted) { + $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted); + } + + my $last_audit_time = $dbh->selectrow_array("$sql_statement"); + + # All Webservices return times in UTC; Use UTC here for backwards compat. + # Hardcode values where appropriate + $last_audit_time = datetime_from($last_audit_time, 'UTC'); + + return {last_audit_time => $self->type('dateTime', $last_audit_time)}; } sub parameters { - my ($self, $args) = @_; - my $user = Bugzilla->login(LOGIN_OPTIONAL); - my $params = Bugzilla->params; - $args ||= {}; - - my @params_list = $user->in_group('tweakparams') - ? keys(%$params) - : $user->id ? PARAMETERS_LOGGED_IN : PARAMETERS_LOGGED_OUT; - - my %parameters; - foreach my $param (@params_list) { - next unless filter_wants($args, $param); - $parameters{$param} = $self->type('string', $params->{$param}); - } - - return { parameters => \%parameters }; + my ($self, $args) = @_; + my $user = Bugzilla->login(LOGIN_OPTIONAL); + my $params = Bugzilla->params; + $args ||= {}; + + my @params_list + = $user->in_group('tweakparams') ? keys(%$params) + : $user->id ? PARAMETERS_LOGGED_IN + : PARAMETERS_LOGGED_OUT; + + my %parameters; + foreach my $param (@params_list) { + next unless filter_wants($args, $param); + $parameters{$param} = $self->type('string', $params->{$param}); + } + + return {parameters => \%parameters}; } 1; diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm index cee597b68..ab539b339 100644 --- a/Bugzilla/WebService/Classification.pm +++ b/Bugzilla/WebService/Classification.pm @@ -18,65 +18,76 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(filter validate params_to_objects); use constant READ_ONLY => qw( - get + get ); use constant PUBLIC_METHODS => qw( - get + get ); sub get { - my ($self, $params) = validate(@_, 'names', 'ids'); + my ($self, $params) = validate(@_, 'names', 'ids'); - defined $params->{names} || defined $params->{ids} - || ThrowCodeError('params_required', { function => 'Classification.get', - params => ['names', 'ids'] }); + defined $params->{names} + || defined $params->{ids} + || ThrowCodeError('params_required', + {function => 'Classification.get', params => ['names', 'ids']}); - my $user = Bugzilla->user; + my $user = Bugzilla->user; - Bugzilla->params->{'useclassification'} - || $user->in_group('editclassifications') - || ThrowUserError('auth_classification_not_enabled'); + Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications') + || ThrowUserError('auth_classification_not_enabled'); - Bugzilla->switch_to_shadow_db; + Bugzilla->switch_to_shadow_db; - my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') }; - unless ($user->in_group('editclassifications')) { - my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications}; - @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; - } + my @classification_objs + = @{params_to_objects($params, 'Bugzilla::Classification')}; + unless ($user->in_group('editclassifications')) { + my %selectable_class + = map { $_->id => 1 } @{$user->get_selectable_classifications}; + @classification_objs = grep { $selectable_class{$_->id} } @classification_objs; + } - my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs; + my @classifications + = map { $self->_classification_to_hash($_, $params) } @classification_objs; - return { classifications => \@classifications }; + return {classifications => \@classifications}; } sub _classification_to_hash { - my ($self, $classification, $params) = @_; - - my $user = Bugzilla->user; - return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications')); - - my $products = $user->in_group('editclassifications') ? - $classification->products : $user->get_selectable_products($classification->id); - - return filter $params, { - id => $self->type('int', $classification->id), - name => $self->type('string', $classification->name), - description => $self->type('string', $classification->description), - sort_key => $self->type('int', $classification->sortkey), - products => [ map { $self->_product_to_hash($_, $params) } @$products ], + my ($self, $classification, $params) = @_; + + my $user = Bugzilla->user; + return + unless (Bugzilla->params->{'useclassification'} + || $user->in_group('editclassifications')); + + my $products + = $user->in_group('editclassifications') + ? $classification->products + : $user->get_selectable_products($classification->id); + + return filter $params, + { + id => $self->type('int', $classification->id), + name => $self->type('string', $classification->name), + description => $self->type('string', $classification->description), + sort_key => $self->type('int', $classification->sortkey), + products => [map { $self->_product_to_hash($_, $params) } @$products], }; } sub _product_to_hash { - my ($self, $product, $params) = @_; - - return filter $params, { - id => $self->type('int', $product->id), - name => $self->type('string', $product->name), - description => $self->type('string', $product->description), - }, undef, 'products'; + my ($self, $product, $params) = @_; + + return filter $params, + { + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + }, + undef, 'products'; } 1; diff --git a/Bugzilla/WebService/Component.pm b/Bugzilla/WebService/Component.pm index 4d6723d8b..802f40c73 100644 --- a/Bugzilla/WebService/Component.pm +++ b/Bugzilla/WebService/Component.pm @@ -19,37 +19,36 @@ use Bugzilla::Error; use Bugzilla::WebService::Constants; use Bugzilla::WebService::Util qw(translate params_to_objects validate); -use constant PUBLIC_METHODS => qw( - create +use constant PUBLIC_METHODS => qw( + create ); use constant MAPPED_FIELDS => { - default_assignee => 'initialowner', - default_qa_contact => 'initialqacontact', - default_cc => 'initial_cc', - is_open => 'isactive', + default_assignee => 'initialowner', + default_qa_contact => 'initialqacontact', + default_cc => 'initial_cc', + is_open => 'isactive', }; sub create { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $user->in_group('editcomponents') - || scalar @{ $user->get_products_by_permission('editcomponents') } - || ThrowUserError('auth_failure', { group => 'editcomponents', - action => 'edit', - object => 'components' }); + $user->in_group('editcomponents') + || scalar @{$user->get_products_by_permission('editcomponents')} + || ThrowUserError('auth_failure', + {group => 'editcomponents', action => 'edit', object => 'components'}); - my $product = $user->check_can_admin_product($params->{product}); + my $product = $user->check_can_admin_product($params->{product}); - # Translate the fields - my $values = translate($params, MAPPED_FIELDS); - $values->{product} = $product; + # Translate the fields + my $values = translate($params, MAPPED_FIELDS); + $values->{product} = $product; - # Create the component and return the newly created id. - my $component = Bugzilla::Component->create($values); - return { id => $self->type('int', $component->id) }; + # Create the component and return the newly created id. + my $component = Bugzilla::Component->create($values); + return {id => $self->type('int', $component->id)}; } 1; diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 557a996f8..a2f83f528 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -14,25 +14,25 @@ use warnings; use parent qw(Exporter); our @EXPORT = qw( - WS_ERROR_CODE + WS_ERROR_CODE - STATUS_OK - STATUS_CREATED - STATUS_ACCEPTED - STATUS_NO_CONTENT - STATUS_MULTIPLE_CHOICES - STATUS_BAD_REQUEST - STATUS_NOT_FOUND - STATUS_GONE - REST_STATUS_CODE_MAP + STATUS_OK + STATUS_CREATED + STATUS_ACCEPTED + STATUS_NO_CONTENT + STATUS_MULTIPLE_CHOICES + STATUS_BAD_REQUEST + STATUS_NOT_FOUND + STATUS_GONE + REST_STATUS_CODE_MAP - ERROR_UNKNOWN_FATAL - ERROR_UNKNOWN_TRANSIENT + ERROR_UNKNOWN_FATAL + ERROR_UNKNOWN_TRANSIENT - XMLRPC_CONTENT_TYPE_WHITELIST - REST_CONTENT_TYPE_WHITELIST + XMLRPC_CONTENT_TYPE_WHITELIST + REST_CONTENT_TYPE_WHITELIST - WS_DISPATCH + WS_DISPATCH ); # This maps the error names in global/*-error.html.tmpl to numbers. @@ -54,173 +54,196 @@ our @EXPORT = qw( # comment that it was retired. Also, if an error changes its name, you'll # have to fix it here. use constant WS_ERROR_CODE => { - # Generic errors (Bugzilla::Object and others) are 50-99. - object_not_specified => 50, - reassign_to_empty => 50, - param_required => 50, - params_required => 50, - undefined_field => 50, - object_does_not_exist => 51, - param_must_be_numeric => 52, - number_not_numeric => 52, - param_invalid => 53, - number_too_large => 54, - number_too_small => 55, - illegal_date => 56, - param_integer_required => 57, - param_scalar_array_required => 58, - # Bug errors usually occupy the 100-200 range. - improper_bug_id_field_value => 100, - bug_id_does_not_exist => 101, - bug_access_denied => 102, - bug_access_query => 102, - # These all mean "invalid alias" - alias_too_long => 103, - alias_in_use => 103, - alias_is_numeric => 103, - alias_has_comma_or_space => 103, - multiple_alias_not_allowed => 103, - # Misc. bug field errors - illegal_field => 104, - freetext_too_long => 104, - # Component errors - require_component => 105, - component_name_too_long => 105, - product_unknown_component => 105, - # Invalid Product - no_products => 106, - entry_access_denied => 106, - product_access_denied => 106, - product_disabled => 106, - # Invalid Summary - require_summary => 107, - # Invalid field name - invalid_field_name => 108, - # Not authorized to edit the bug - product_edit_denied => 109, - # Comment-related errors - comment_is_private => 110, - comment_id_invalid => 111, - comment_too_long => 114, - comment_invalid_isprivate => 117, - # Comment tagging - comment_tag_disabled => 125, - comment_tag_invalid => 126, - comment_tag_too_long => 127, - comment_tag_too_short => 128, - # See Also errors - bug_url_invalid => 112, - bug_url_too_long => 112, - # Insidergroup Errors - user_not_insider => 113, - # Note: 114 is above in the Comment-related section. - # Bug update errors - illegal_change => 115, - # Dependency errors - dependency_loop_single => 116, - dependency_loop_multi => 116, - # Note: 117 is above in the Comment-related section. - # Dup errors - dupe_loop_detected => 118, - dupe_id_required => 119, - # Bug-related group errors - group_invalid_removal => 120, - group_restriction_not_allowed => 120, - # Status/Resolution errors - missing_resolution => 121, - resolution_not_allowed => 122, - illegal_bug_status_transition => 123, - # Flag errors - flag_status_invalid => 129, - flag_update_denied => 130, - flag_type_requestee_disabled => 131, - flag_not_unique => 132, - flag_type_not_unique => 133, - flag_type_inactive => 134, - - # Authentication errors are usually 300-400. - invalid_login_or_password => 300, - account_disabled => 301, - auth_invalid_email => 302, - extern_id_conflict => -303, - auth_failure => 304, - password_too_short => 305, - password_not_complex => 305, - api_key_not_valid => 306, - api_key_revoked => 306, - auth_invalid_token => 307, - - # Except, historically, AUTH_NODATA, which is 410. - login_required => 410, - - # User errors are 500-600. - account_exists => 500, - illegal_email_address => 501, - auth_cant_create_account => 501, - account_creation_disabled => 501, - account_creation_restricted => 501, - password_too_short => 502, - # Error 503 password_too_long no longer exists. - invalid_username => 504, - # This is from strict_isolation, but it also basically means - # "invalid user." - invalid_user_group => 504, - user_access_by_id_denied => 505, - user_access_by_match_denied => 505, - - # Attachment errors are 600-700. - file_too_large => 600, - invalid_content_type => 601, - # Error 602 attachment_illegal_url no longer exists. - file_not_specified => 603, - missing_attachment_description => 604, - # Error 605 attachment_url_disabled no longer exists. - zero_length_file => 606, - - # Product erros are 700-800 - product_blank_name => 700, - product_name_too_long => 701, - product_name_already_in_use => 702, - product_name_diff_in_case => 702, - product_must_have_description => 703, - product_must_have_version => 704, - product_must_define_defaultmilestone => 705, - - # Group errors are 800-900 - empty_group_name => 800, - group_exists => 801, - empty_group_description => 802, - invalid_regexp => 803, - invalid_group_name => 804, - group_cannot_view => 805, - - # Classification errors are 900-1000 - auth_classification_not_enabled => 900, - - # Search errors are 1000-1100 - buglist_parameters_required => 1000, - - # Flag type errors are 1100-1200 - flag_type_name_invalid => 1101, - flag_type_description_invalid => 1102, - flag_type_cc_list_invalid => 1103, - flag_type_sortkey_invalid => 1104, - flag_type_not_editable => 1105, - - # Component errors are 1200-1300 - component_already_exists => 1200, - component_is_last => 1201, - component_has_bugs => 1202, - - # Errors thrown by the WebService itself. The ones that are negative - # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php - xmlrpc_invalid_value => -32600, - unknown_method => -32601, - json_rpc_post_only => 32610, - json_rpc_invalid_callback => 32611, - xmlrpc_illegal_content_type => 32612, - json_rpc_illegal_content_type => 32613, - rest_invalid_resource => 32614, + + # Generic errors (Bugzilla::Object and others) are 50-99. + object_not_specified => 50, + reassign_to_empty => 50, + param_required => 50, + params_required => 50, + undefined_field => 50, + object_does_not_exist => 51, + param_must_be_numeric => 52, + number_not_numeric => 52, + param_invalid => 53, + number_too_large => 54, + number_too_small => 55, + illegal_date => 56, + param_integer_required => 57, + param_scalar_array_required => 58, + + # Bug errors usually occupy the 100-200 range. + improper_bug_id_field_value => 100, + bug_id_does_not_exist => 101, + bug_access_denied => 102, + bug_access_query => 102, + + # These all mean "invalid alias" + alias_too_long => 103, + alias_in_use => 103, + alias_is_numeric => 103, + alias_has_comma_or_space => 103, + multiple_alias_not_allowed => 103, + + # Misc. bug field errors + illegal_field => 104, + freetext_too_long => 104, + + # Component errors + require_component => 105, + component_name_too_long => 105, + product_unknown_component => 105, + + # Invalid Product + no_products => 106, + entry_access_denied => 106, + product_access_denied => 106, + product_disabled => 106, + + # Invalid Summary + require_summary => 107, + + # Invalid field name + invalid_field_name => 108, + + # Not authorized to edit the bug + product_edit_denied => 109, + + # Comment-related errors + comment_is_private => 110, + comment_id_invalid => 111, + comment_too_long => 114, + comment_invalid_isprivate => 117, + + # Comment tagging + comment_tag_disabled => 125, + comment_tag_invalid => 126, + comment_tag_too_long => 127, + comment_tag_too_short => 128, + + # See Also errors + bug_url_invalid => 112, + bug_url_too_long => 112, + + # Insidergroup Errors + user_not_insider => 113, + + # Note: 114 is above in the Comment-related section. + # Bug update errors + illegal_change => 115, + + # Dependency errors + dependency_loop_single => 116, + dependency_loop_multi => 116, + + # Note: 117 is above in the Comment-related section. + # Dup errors + dupe_loop_detected => 118, + dupe_id_required => 119, + + # Bug-related group errors + group_invalid_removal => 120, + group_restriction_not_allowed => 120, + + # Status/Resolution errors + missing_resolution => 121, + resolution_not_allowed => 122, + illegal_bug_status_transition => 123, + + # Flag errors + flag_status_invalid => 129, + flag_update_denied => 130, + flag_type_requestee_disabled => 131, + flag_not_unique => 132, + flag_type_not_unique => 133, + flag_type_inactive => 134, + + # Authentication errors are usually 300-400. + invalid_login_or_password => 300, + account_disabled => 301, + auth_invalid_email => 302, + extern_id_conflict => -303, + auth_failure => 304, + password_too_short => 305, + password_not_complex => 305, + api_key_not_valid => 306, + api_key_revoked => 306, + auth_invalid_token => 307, + + # Except, historically, AUTH_NODATA, which is 410. + login_required => 410, + + # User errors are 500-600. + account_exists => 500, + illegal_email_address => 501, + auth_cant_create_account => 501, + account_creation_disabled => 501, + account_creation_restricted => 501, + password_too_short => 502, + + # Error 503 password_too_long no longer exists. + invalid_username => 504, + + # This is from strict_isolation, but it also basically means + # "invalid user." + invalid_user_group => 504, + user_access_by_id_denied => 505, + user_access_by_match_denied => 505, + + # Attachment errors are 600-700. + file_too_large => 600, + invalid_content_type => 601, + + # Error 602 attachment_illegal_url no longer exists. + file_not_specified => 603, + missing_attachment_description => 604, + + # Error 605 attachment_url_disabled no longer exists. + zero_length_file => 606, + + # Product erros are 700-800 + product_blank_name => 700, + product_name_too_long => 701, + product_name_already_in_use => 702, + product_name_diff_in_case => 702, + product_must_have_description => 703, + product_must_have_version => 704, + product_must_define_defaultmilestone => 705, + + # Group errors are 800-900 + empty_group_name => 800, + group_exists => 801, + empty_group_description => 802, + invalid_regexp => 803, + invalid_group_name => 804, + group_cannot_view => 805, + + # Classification errors are 900-1000 + auth_classification_not_enabled => 900, + + # Search errors are 1000-1100 + buglist_parameters_required => 1000, + + # Flag type errors are 1100-1200 + flag_type_name_invalid => 1101, + flag_type_description_invalid => 1102, + flag_type_cc_list_invalid => 1103, + flag_type_sortkey_invalid => 1104, + flag_type_not_editable => 1105, + + # Component errors are 1200-1300 + component_already_exists => 1200, + component_is_last => 1201, + component_has_bugs => 1202, + + # Errors thrown by the WebService itself. The ones that are negative + # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + xmlrpc_invalid_value => -32600, + unknown_method => -32601, + json_rpc_post_only => 32610, + json_rpc_invalid_callback => 32611, + xmlrpc_illegal_content_type => 32612, + json_rpc_illegal_content_type => 32613, + rest_invalid_resource => 32614, }; # RESTful webservices use the http status code @@ -241,73 +264,74 @@ use constant STATUS_GONE => 410; # http status code based on the error code or use the # default STATUS_BAD_REQUEST. sub REST_STATUS_CODE_MAP { - my $status_code_map = { - 51 => STATUS_NOT_FOUND, - 101 => STATUS_NOT_FOUND, - 102 => STATUS_NOT_AUTHORIZED, - 106 => STATUS_NOT_AUTHORIZED, - 109 => STATUS_NOT_AUTHORIZED, - 110 => STATUS_NOT_AUTHORIZED, - 113 => STATUS_NOT_AUTHORIZED, - 115 => STATUS_NOT_AUTHORIZED, - 120 => STATUS_NOT_AUTHORIZED, - 300 => STATUS_NOT_AUTHORIZED, - 301 => STATUS_NOT_AUTHORIZED, - 302 => STATUS_NOT_AUTHORIZED, - 303 => STATUS_NOT_AUTHORIZED, - 304 => STATUS_NOT_AUTHORIZED, - 410 => STATUS_NOT_AUTHORIZED, - 504 => STATUS_NOT_AUTHORIZED, - 505 => STATUS_NOT_AUTHORIZED, - 32614 => STATUS_NOT_FOUND, - _default => STATUS_BAD_REQUEST - }; - - Bugzilla::Hook::process('webservice_status_code_map', - { status_code_map => $status_code_map }); - - return $status_code_map; -}; + my $status_code_map = { + 51 => STATUS_NOT_FOUND, + 101 => STATUS_NOT_FOUND, + 102 => STATUS_NOT_AUTHORIZED, + 106 => STATUS_NOT_AUTHORIZED, + 109 => STATUS_NOT_AUTHORIZED, + 110 => STATUS_NOT_AUTHORIZED, + 113 => STATUS_NOT_AUTHORIZED, + 115 => STATUS_NOT_AUTHORIZED, + 120 => STATUS_NOT_AUTHORIZED, + 300 => STATUS_NOT_AUTHORIZED, + 301 => STATUS_NOT_AUTHORIZED, + 302 => STATUS_NOT_AUTHORIZED, + 303 => STATUS_NOT_AUTHORIZED, + 304 => STATUS_NOT_AUTHORIZED, + 410 => STATUS_NOT_AUTHORIZED, + 504 => STATUS_NOT_AUTHORIZED, + 505 => STATUS_NOT_AUTHORIZED, + 32614 => STATUS_NOT_FOUND, + _default => STATUS_BAD_REQUEST + }; + + Bugzilla::Hook::process('webservice_status_code_map', + {status_code_map => $status_code_map}); + + return $status_code_map; +} # These are the fallback defaults for errors not in ERROR_CODE. use constant ERROR_UNKNOWN_FATAL => -32000; use constant ERROR_UNKNOWN_TRANSIENT => 32000; -use constant ERROR_GENERAL => 999; +use constant ERROR_GENERAL => 999; use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw( - text/xml - application/xml + text/xml + application/xml ); # The first content type specified is used as the default. use constant REST_CONTENT_TYPE_WHITELIST => qw( - application/json - application/javascript - text/javascript - text/html + application/json + application/javascript + text/javascript + text/html ); sub WS_DISPATCH { - # We "require" here instead of "use" above to avoid a dependency loop. - require Bugzilla::Hook; - my %hook_dispatch; - Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch }); - - my $dispatch = { - 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', - 'Bug' => 'Bugzilla::WebService::Bug', - 'Classification' => 'Bugzilla::WebService::Classification', - 'Component' => 'Bugzilla::WebService::Component', - 'FlagType' => 'Bugzilla::WebService::FlagType', - 'Group' => 'Bugzilla::WebService::Group', - 'Product' => 'Bugzilla::WebService::Product', - 'User' => 'Bugzilla::WebService::User', - 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', - %hook_dispatch - }; - return $dispatch; -}; + + # We "require" here instead of "use" above to avoid a dependency loop. + require Bugzilla::Hook; + my %hook_dispatch; + Bugzilla::Hook::process('webservice', {dispatch => \%hook_dispatch}); + + my $dispatch = { + 'Bugzilla' => 'Bugzilla::WebService::Bugzilla', + 'Bug' => 'Bugzilla::WebService::Bug', + 'Classification' => 'Bugzilla::WebService::Classification', + 'Component' => 'Bugzilla::WebService::Component', + 'FlagType' => 'Bugzilla::WebService::FlagType', + 'Group' => 'Bugzilla::WebService::Group', + 'Product' => 'Bugzilla::WebService::Product', + 'User' => 'Bugzilla::WebService::User', + 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit', + %hook_dispatch + }; + return $dispatch; +} 1; diff --git a/Bugzilla/WebService/FlagType.pm b/Bugzilla/WebService/FlagType.pm index 9d7cce037..9dc240c7f 100644 --- a/Bugzilla/WebService/FlagType.pm +++ b/Bugzilla/WebService/FlagType.pm @@ -22,292 +22,308 @@ use Bugzilla::Util qw(trim); use List::MoreUtils qw(uniq); use constant PUBLIC_METHODS => qw( - create - get - update + create + get + update ); sub get { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - defined $params->{product} - || ThrowCodeError('param_required', - { function => 'Bug.flag_types', - param => 'product' }); - - my $product = delete $params->{product}; - my $component = delete $params->{component}; - - $product = Bugzilla::Product->check({ name => $product, cache => 1 }); - $component = Bugzilla::Component->check( - { name => $component, product => $product, cache => 1 }) if $component; - - my $flag_params = { product_id => $product->id }; - $flag_params->{component_id} = $component->id if $component; - my $matched_flag_types = Bugzilla::FlagType::match($flag_params); - - my $flag_types = { bug => [], attachment => [] }; - foreach my $flag_type (@$matched_flag_types) { - push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'bug'; - push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product)) - if $flag_type->target_type eq 'attachment'; - } - - return $flag_types; + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + defined $params->{product} + || ThrowCodeError('param_required', + {function => 'Bug.flag_types', param => 'product'}); + + my $product = delete $params->{product}; + my $component = delete $params->{component}; + + $product = Bugzilla::Product->check({name => $product, cache => 1}); + $component + = Bugzilla::Component->check( + {name => $component, product => $product, cache => 1}) + if $component; + + my $flag_params = {product_id => $product->id}; + $flag_params->{component_id} = $component->id if $component; + my $matched_flag_types = Bugzilla::FlagType::match($flag_params); + + my $flag_types = {bug => [], attachment => []}; + foreach my $flag_type (@$matched_flag_types) { + push(@{$flag_types->{bug}}, $self->_flagtype_to_hash($flag_type, $product)) + if $flag_type->target_type eq 'bug'; + push( + @{$flag_types->{attachment}}, + $self->_flagtype_to_hash($flag_type, $product) + ) if $flag_type->target_type eq 'attachment'; + } + + return $flag_types; } sub create { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - $user->in_group('editcomponents') - || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "add", - object => "flagtypes" }); - - $params->{name} || ThrowCodeError('param_required', { param => 'name' }); - $params->{description} || ThrowCodeError('param_required', { param => 'description' }); - - my %args = ( - sortkey => 1, - name => undef, - inclusions => ['0:0'], # Default to __ALL__:__ALL__ - cc_list => '', - description => undef, - is_requestable => 'on', - exclusions => [], - is_multiplicable => 'on', - request_group => '', - is_active => 'on', - is_specifically_requestable => 'on', - target_type => 'bug', - grant_group => '', - ); - - foreach my $key (keys %args) { - $args{$key} = $params->{$key} if defined($params->{$key}); - } - - $args{name} = trim($params->{name}); - $args{description} = trim($params->{description}); - - # Is specifically requestable is actually is_requesteeable - if (exists $args{is_specifically_requestable}) { - $args{is_requesteeble} = delete $args{is_specifically_requestable}; - } - - # Default is on for the tickbox flags. - # If the user has set them to 'off' then undefine them so the flags are not ticked - foreach my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) { - if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) { - $args{$arg_name} = undef; - } + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "flagtypes"}); + + $params->{name} || ThrowCodeError('param_required', {param => 'name'}); + $params->{description} + || ThrowCodeError('param_required', {param => 'description'}); + + my %args = ( + sortkey => 1, + name => undef, + inclusions => ['0:0'], # Default to __ALL__:__ALL__ + cc_list => '', + description => undef, + is_requestable => 'on', + exclusions => [], + is_multiplicable => 'on', + request_group => '', + is_active => 'on', + is_specifically_requestable => 'on', + target_type => 'bug', + grant_group => '', + ); + + foreach my $key (keys %args) { + $args{$key} = $params->{$key} if defined($params->{$key}); + } + + $args{name} = trim($params->{name}); + $args{description} = trim($params->{description}); + + # Is specifically requestable is actually is_requesteeable + if (exists $args{is_specifically_requestable}) { + $args{is_requesteeble} = delete $args{is_specifically_requestable}; + } + +# Default is on for the tickbox flags. +# If the user has set them to 'off' then undefine them so the flags are not ticked + foreach + my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) + { + if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) { + $args{$arg_name} = undef; } + } - # Process group inclusions and exclusions - $args{inclusions} = _process_lists($params->{inclusions}) if defined $params->{inclusions}; - $args{exclusions} = _process_lists($params->{exclusions}) if defined $params->{exclusions}; + # Process group inclusions and exclusions + $args{inclusions} = _process_lists($params->{inclusions}) + if defined $params->{inclusions}; + $args{exclusions} = _process_lists($params->{exclusions}) + if defined $params->{exclusions}; - my $flagtype = Bugzilla::FlagType->create(\%args); + my $flagtype = Bugzilla::FlagType->create(\%args); - return { id => $self->type('int', $flagtype->id) }; + return {id => $self->type('int', $flagtype->id)}; } sub update { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - $user->in_group('editcomponents') - || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "edit", - object => "flagtypes" }); - - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'FlagType.update', params => ['ids', 'names'] }); - - # Get the list of unique flag type ids we are updating - my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : (); - if (defined $params->{names}) { - push @flag_type_ids, map { $_->id } - @{ Bugzilla::FlagType::match({ name => $params->{names} }) }; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + $user->in_group('editcomponents') + || scalar(@{$user->get_products_by_permission('editcomponents')}) + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "flagtypes"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'FlagType.update', params => ['ids', 'names']}); + + # Get the list of unique flag type ids we are updating + my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : (); + if (defined $params->{names}) { + push @flag_type_ids, + map { $_->id } @{Bugzilla::FlagType::match({name => $params->{names}})}; + } + @flag_type_ids = uniq @flag_type_ids; + + # We delete names and ids to keep only new values to set. + delete $params->{names}; + delete $params->{ids}; + + # Process group inclusions and exclusions + # We removed them from $params because these are handled differently + my $inclusions = _process_lists(delete $params->{inclusions}) + if defined $params->{inclusions}; + my $exclusions = _process_lists(delete $params->{exclusions}) + if defined $params->{exclusions}; + + $dbh->bz_start_transaction(); + my %changes = (); + + foreach my $flag_type_id (@flag_type_ids) { + my ($flagtype, $can_fully_edit) + = $user->check_can_admin_flagtype($flag_type_id); + + if ($can_fully_edit) { + $flagtype->set_all($params); + } + elsif (scalar keys %$params) { + ThrowUserError('flag_type_not_editable', {flagtype => $flagtype}); } - @flag_type_ids = uniq @flag_type_ids; - - # We delete names and ids to keep only new values to set. - delete $params->{names}; - delete $params->{ids}; - - # Process group inclusions and exclusions - # We removed them from $params because these are handled differently - my $inclusions = _process_lists(delete $params->{inclusions}) if defined $params->{inclusions}; - my $exclusions = _process_lists(delete $params->{exclusions}) if defined $params->{exclusions}; - - $dbh->bz_start_transaction(); - my %changes = (); - foreach my $flag_type_id (@flag_type_ids) { - my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id); + # Process the clusions + foreach my $type ('inclusions', 'exclusions') { + my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions; + next if not defined $clusions; - if ($can_fully_edit) { - $flagtype->set_all($params); - } - elsif (scalar keys %$params) { - ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); - } + my @extra_clusions = (); + if (!$user->in_group('editcomponents')) { + my $products = $user->get_products_by_permission('editcomponents'); - # Process the clusions - foreach my $type ('inclusions', 'exclusions') { - my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions; - next if not defined $clusions; - - my @extra_clusions = (); - if (!$user->in_group('editcomponents')) { - my $products = $user->get_products_by_permission('editcomponents'); - # Bring back the products the user cannot edit. - foreach my $item (values %{$flagtype->$type}) { - my ($prod_id, $comp_id) = split(':', $item); - push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products; - } - } - - $flagtype->set_clusions({ - $type => [@$clusions, @extra_clusions], - }); + # Bring back the products the user cannot edit. + foreach my $item (values %{$flagtype->$type}) { + my ($prod_id, $comp_id) = split(':', $item); + push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products; } + } - my $returned_changes = $flagtype->update(); - $changes{$flagtype->id} = { - name => $flagtype->name, - changes => $returned_changes, - }; + $flagtype->set_clusions({$type => [@$clusions, @extra_clusions],}); } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $flag_type_id (keys %changes) { - my %hash = ( - id => $self->type('int', $flag_type_id), - name => $self->type('string', $changes{$flag_type_id}{name}), - changes => {}, - ); - - foreach my $field (keys %{ $changes{$flag_type_id}{changes} }) { - my $change = $changes{$flag_type_id}{changes}{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - push(@result, \%hash); + my $returned_changes = $flagtype->update(); + $changes{$flagtype->id} + = {name => $flagtype->name, changes => $returned_changes,}; + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $flag_type_id (keys %changes) { + my %hash = ( + id => $self->type('int', $flag_type_id), + name => $self->type('string', $changes{$flag_type_id}{name}), + changes => {}, + ); + + foreach my $field (keys %{$changes{$flag_type_id}{changes}}) { + my $change = $changes{$flag_type_id}{changes}{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - return { flagtypes => \@result }; + push(@result, \%hash); + } + + return {flagtypes => \@result}; } sub _flagtype_to_hash { - my ($self, $flagtype, $product) = @_; - my $user = Bugzilla->user; - - my @values = ('X'); - push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); - push(@values, '+', '-') if $user->can_set_flag($flagtype); - - my $item = { - id => $self->type('int' , $flagtype->id), - name => $self->type('string' , $flagtype->name), - description => $self->type('string' , $flagtype->description), - type => $self->type('string' , $flagtype->target_type), - values => \@values, - is_active => $self->type('boolean', $flagtype->is_active), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) - }; - - if ($product) { - my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); - my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); - # if we have both inclusions and exclusions, the exclusions are redundant - $exclusions = [] if @$inclusions && @$exclusions; - # no need to return anything if there's just "any component" - $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; - $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; - } - - return $item; + my ($self, $flagtype, $product) = @_; + my $user = Bugzilla->user; + + my @values = ('X'); + push(@values, '?') + if ($flagtype->is_requestable && $user->can_request_flag($flagtype)); + push(@values, '+', '-') if $user->can_set_flag($flagtype); + + my $item = { + id => $self->type('int', $flagtype->id), + name => $self->type('string', $flagtype->name), + description => $self->type('string', $flagtype->description), + type => $self->type('string', $flagtype->target_type), + values => \@values, + is_active => $self->type('boolean', $flagtype->is_active), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable) + }; + + if ($product) { + my $inclusions + = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id); + my $exclusions + = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id); + + # if we have both inclusions and exclusions, the exclusions are redundant + $exclusions = [] if @$inclusions && @$exclusions; + + # no need to return anything if there's just "any component" + $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; + $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; + } + + return $item; } sub _flagtype_clusions_to_hash { - my ($self, $clusions, $product_id) = @_; - my $result = []; - foreach my $key (keys %$clusions) { - my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); - if ($prod_id == 0 || $prod_id == $product_id) { - if ($comp_id) { - my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 }); - push @$result, $component->name; - } - else { - return [ '' ]; - } - } + my ($self, $clusions, $product_id) = @_; + my $result = []; + foreach my $key (keys %$clusions) { + my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); + if ($prod_id == 0 || $prod_id == $product_id) { + if ($comp_id) { + my $component = Bugzilla::Component->new({id => $comp_id, cache => 1}); + push @$result, $component->name; + } + else { + return ['']; + } } - return $result; + } + return $result; } sub _process_lists { - my $list = shift; - my $user = Bugzilla->user; - - my @products; - if ($user->in_group('editcomponents')) { - @products = Bugzilla::Product->get_all; - } - else { - @products = @{$user->get_products_by_permission('editcomponents')}; - } - - my @component_list; - - foreach my $item (@$list) { - # A hash with products as the key and component names as the values - if(ref($item) eq 'HASH') { - while (my ($product_name, $component_names) = each %$item) { - my $product = Bugzilla::Product->check({name => $product_name}); - unless (grep { $product->name eq $_->name } @products) { - ThrowUserError('product_access_denied', { name => $product_name }); - } - my @component_ids; - - foreach my $comp_name (@$component_names) { - my $component = Bugzilla::Component->check({product => $product, name => $comp_name}); - ThrowCodeError('param_invalid', { param => $comp_name}) unless defined $component; - push @component_list, $product->id . ':' . $component->id; - } - } + my $list = shift; + my $user = Bugzilla->user; + + my @products; + if ($user->in_group('editcomponents')) { + @products = Bugzilla::Product->get_all; + } + else { + @products = @{$user->get_products_by_permission('editcomponents')}; + } + + my @component_list; + + foreach my $item (@$list) { + + # A hash with products as the key and component names as the values + if (ref($item) eq 'HASH') { + while (my ($product_name, $component_names) = each %$item) { + my $product = Bugzilla::Product->check({name => $product_name}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', {name => $product_name}); } - elsif(!ref($item)) { - # These are whole products - my $product = Bugzilla::Product->check({name => $item}); - unless (grep { $product->name eq $_->name } @products) { - ThrowUserError('product_access_denied', { name => $item }); - } - push @component_list, $product->id . ':0'; - } - else { - # The user has passed something invalid - ThrowCodeError('param_invalid', { param => $item }); + my @component_ids; + + foreach my $comp_name (@$component_names) { + my $component + = Bugzilla::Component->check({product => $product, name => $comp_name}); + ThrowCodeError('param_invalid', {param => $comp_name}) + unless defined $component; + push @component_list, $product->id . ':' . $component->id; } + } + } + elsif (!ref($item)) { + + # These are whole products + my $product = Bugzilla::Product->check({name => $item}); + unless (grep { $product->name eq $_->name } @products) { + ThrowUserError('product_access_denied', {name => $item}); + } + push @component_list, $product->id . ':0'; + } + else { + # The user has passed something invalid + ThrowCodeError('param_invalid', {param => $item}); } + } - return \@component_list; + return \@component_list; } 1; diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm index 468575a35..c92583d0b 100644 --- a/Bugzilla/WebService/Group.pm +++ b/Bugzilla/WebService/Group.pm @@ -17,207 +17,210 @@ use Bugzilla::Error; use Bugzilla::WebService::Util qw(validate translate params_to_objects); use constant PUBLIC_METHODS => qw( - create - get - update + create + get + update ); -use constant MAPPED_RETURNS => { - userregexp => 'user_regexp', - isactive => 'is_active' -}; +use constant MAPPED_RETURNS => + {userregexp => 'user_regexp', isactive => 'is_active'}; sub create { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('creategroups') - || ThrowUserError("auth_failure", { group => "creategroups", - action => "add", - object => "group"}); - # Create group - my $group = Bugzilla::Group->create({ - name => $params->{name}, - description => $params->{description}, - userregexp => $params->{user_regexp}, - isactive => $params->{is_active}, - isbuggroup => 1, - icon_url => $params->{icon_url} - }); - return { id => $self->type('int', $group->id) }; + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", + {group => "creategroups", action => "add", object => "group"}); + + # Create group + my $group = Bugzilla::Group->create({ + name => $params->{name}, + description => $params->{description}, + userregexp => $params->{user_regexp}, + isactive => $params->{is_active}, + isbuggroup => 1, + icon_url => $params->{icon_url} + }); + return {id => $self->type('int', $group->id)}; } sub update { - my ($self, $params) = @_; + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('creategroups') + || ThrowUserError("auth_failure", + {group => "creategroups", action => "edit", object => "group"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Group.update', params => ['ids', 'names']}); + + my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + + my %values = %$params; + + # We delete names and ids to keep only new values to set. + delete $values{names}; + delete $values{ids}; + + $dbh->bz_start_transaction(); + foreach my $group (@$group_objects) { + $group->set_all(\%values); + } + + my %changes; + foreach my $group (@$group_objects) { + my $returned_changes = $group->update(); + $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $group (@$group_objects) { + my %hash = (id => $group->id, changes => {},); + foreach my $field (keys %{$changes{$group->id}}) { + my $change = $changes{$group->id}->{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; + } + push(@result, \%hash); + } - my $dbh = Bugzilla->dbh; + return {groups => \@result}; +} - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('creategroups') - || ThrowUserError("auth_failure", { group => "creategroups", - action => "edit", - object => "group" }); +sub get { + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Group.update', params => ['ids', 'names'] }); + Bugzilla->login(LOGIN_REQUIRED); - my $group_objects = params_to_objects($params, 'Bugzilla::Group'); + # Reject access if there is no sense in continuing. + my $user = Bugzilla->user; + my $all_groups + = $user->in_group('editusers') || $user->in_group('creategroups'); + if (!$all_groups && !$user->can_bless) { + ThrowUserError('group_cannot_view'); + } - my %values = %$params; - - # We delete names and ids to keep only new values to set. - delete $values{names}; - delete $values{ids}; + Bugzilla->switch_to_shadow_db(); - $dbh->bz_start_transaction(); - foreach my $group (@$group_objects) { - $group->set_all(\%values); - } + my $groups = []; - my %changes; - foreach my $group (@$group_objects) { - my $returned_changes = $group->update(); - $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $group (@$group_objects) { - my %hash = ( - id => $group->id, - changes => {}, - ); - foreach my $field (keys %{ $changes{$group->id} }) { - my $change = $changes{$group->id}->{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - push(@result, \%hash); - } + if (defined $params->{ids}) { - return { groups => \@result }; -} - -sub get { - my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + # Get the groups by id + $groups = Bugzilla::Group->new_from_list($params->{ids}); + } - Bugzilla->login(LOGIN_REQUIRED); - - # Reject access if there is no sense in continuing. - my $user = Bugzilla->user; - my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups'); - if (!$all_groups && !$user->can_bless) { - ThrowUserError('group_cannot_view'); - } + if (defined $params->{names}) { - Bugzilla->switch_to_shadow_db(); + # Get the groups by name. Check will throw an error if a bad name is given + foreach my $name (@{$params->{names}}) { - my $groups = []; + # Skip if we got this from params->{id} + next if grep { $_->name eq $name } @$groups; - if (defined $params->{ids}) { - # Get the groups by id - $groups = Bugzilla::Group->new_from_list($params->{ids}); + push @$groups, Bugzilla::Group->check({name => $name}); } + } - if (defined $params->{names}) { - # Get the groups by name. Check will throw an error if a bad name is given - foreach my $name (@{$params->{names}}) { - # Skip if we got this from params->{id} - next if grep { $_->name eq $name } @$groups; - - push @$groups, Bugzilla::Group->check({ name => $name }); - } + if (!defined $params->{ids} && !defined $params->{names}) { + if ($all_groups) { + @$groups = Bugzilla::Group->get_all; } - - if (!defined $params->{ids} && !defined $params->{names}) { - if ($all_groups) { - @$groups = Bugzilla::Group->get_all; - } - else { - # Get only groups the user has bless groups too - $groups = $user->bless_groups; - } + else { + # Get only groups the user has bless groups too + $groups = $user->bless_groups; } + } - # Now create a result entry for each. - my @groups = map { $self->_group_to_hash($params, $_) } @$groups; - return { groups => \@groups }; + # Now create a result entry for each. + my @groups = map { $self->_group_to_hash($params, $_) } @$groups; + return {groups => \@groups}; } sub _group_to_hash { - my ($self, $params, $group) = @_; - my $user = Bugzilla->user; - - my $field_data = { - id => $self->type('int', $group->id), - name => $self->type('string', $group->name), - description => $self->type('string', $group->description), - }; - - if ($user->in_group('creategroups')) { - $field_data->{is_active} = $self->type('boolean', $group->is_active); - $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group); - $field_data->{user_regexp} = $self->type('string', $group->user_regexp); - } - - if ($params->{membership}) { - $field_data->{membership} = $self->_get_group_membership($group, $params); - } - return $field_data; + my ($self, $params, $group) = @_; + my $user = Bugzilla->user; + + my $field_data = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + + if ($user->in_group('creategroups')) { + $field_data->{is_active} = $self->type('boolean', $group->is_active); + $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group); + $field_data->{user_regexp} = $self->type('string', $group->user_regexp); + } + + if ($params->{membership}) { + $field_data->{membership} = $self->_get_group_membership($group, $params); + } + return $field_data; } sub _get_group_membership { - my ($self, $group, $params) = @_; - my $user = Bugzilla->user; + my ($self, $group, $params) = @_; + my $user = Bugzilla->user; - my %users_only; - my $dbh = Bugzilla->dbh; - my $editusers = $user->in_group('editusers'); + my %users_only; + my $dbh = Bugzilla->dbh; + my $editusers = $user->in_group('editusers'); - my $query = 'SELECT userid FROM profiles'; - my $visibleGroups; + my $query = 'SELECT userid FROM profiles'; + my $visibleGroups; - if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - # Show only users in visible groups. - $visibleGroups = $user->visible_groups_inherited; + if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - if (scalar @$visibleGroups) { - $query .= qq{, user_group_map AS ugm + # Show only users in visible groups. + $visibleGroups = $user->visible_groups_inherited; + + if (scalar @$visibleGroups) { + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 AND } . $dbh->sql_in('ugm.group_id', $visibleGroups); - } - } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) { - $visibleGroups = 1; - $query .= qq{, user_group_map AS ugm + } + } + elsif ($editusers + || $user->can_bless($group->id) + || $user->in_group('creategroups')) + { + $visibleGroups = 1; + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 }; - } - if (!$visibleGroups) { - ThrowUserError('group_not_visible', { group => $group }); - } - - my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); - $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); - - my $userids = $dbh->selectcol_arrayref($query); - my $user_objects = Bugzilla::User->new_from_list($userids); - my @users = - map {{ - id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('string', $_->login), - email => $self->type('string', $_->email), - can_login => $self->type('boolean', $_->is_enabled), - email_enabled => $self->type('boolean', $_->email_enabled), - login_denied_text => $self->type('string', $_->disabledtext), - }} @$user_objects; - - return \@users; + } + if (!$visibleGroups) { + ThrowUserError('group_not_visible', {group => $group}); + } + + my $grouplist = Bugzilla::Group->flatten_group_membership($group->id); + $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist); + + my $userids = $dbh->selectcol_arrayref($query); + my $user_objects = Bugzilla::User->new_from_list($userids); + my @users = map { { + id => $self->type('int', $_->id), + real_name => $self->type('string', $_->name), + name => $self->type('string', $_->login), + email => $self->type('string', $_->email), + can_login => $self->type('boolean', $_->is_enabled), + email_enabled => $self->type('boolean', $_->email_enabled), + login_denied_text => $self->type('string', $_->disabledtext), + } } @$user_objects; + + return \@users; } 1; diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm index 7d9e7f181..e0f357e61 100644 --- a/Bugzilla/WebService/Product.pm +++ b/Bugzilla/WebService/Product.pm @@ -17,40 +17,37 @@ use Bugzilla::User; use Bugzilla::Error; use Bugzilla::Constants; use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(validate filter filter_wants translate params_to_objects); +use Bugzilla::WebService::Util + qw(validate filter filter_wants translate params_to_objects); use constant READ_ONLY => qw( - get - get_accessible_products - get_enterable_products - get_selectable_products + get + get_accessible_products + get_enterable_products + get_selectable_products ); use constant PUBLIC_METHODS => qw( - create - get - get_accessible_products - get_enterable_products - get_products - get_selectable_products - update + create + get + get_accessible_products + get_enterable_products + get_products + get_selectable_products + update ); -use constant MAPPED_FIELDS => { - has_unconfirmed => 'allows_unconfirmed', - is_open => 'is_active', -}; +use constant MAPPED_FIELDS => + {has_unconfirmed => 'allows_unconfirmed', is_open => 'is_active',}; use constant MAPPED_RETURNS => { - allows_unconfirmed => 'has_unconfirmed', - defaultmilestone => 'default_milestone', - isactive => 'is_open', + allows_unconfirmed => 'has_unconfirmed', + defaultmilestone => 'default_milestone', + isactive => 'is_open', }; -use constant FIELD_MAP => { - has_unconfirmed => 'allows_unconfirmed', - is_open => 'isactive', -}; +use constant FIELD_MAP => + {has_unconfirmed => 'allows_unconfirmed', is_open => 'isactive',}; ################################################## # Add aliases here for method name compatibility # @@ -58,300 +55,277 @@ use constant FIELD_MAP => { # Get the ids of the products the user can search sub get_selectable_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_selectable_products}]}; } # Get the ids of the products the user can enter bugs against sub get_enterable_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_enterable_products}]}; } # Get the union of the products the user can search and enter bugs against. sub get_accessible_products { - Bugzilla->switch_to_shadow_db(); - return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]}; + Bugzilla->switch_to_shadow_db(); + return {ids => [map { $_->id } @{Bugzilla->user->get_accessible_products}]}; } # Get a list of actual products, based on list of ids or names sub get { - my ($self, $params) = validate(@_, 'ids', 'names', 'type'); - my $user = Bugzilla->user; - - defined $params->{ids} || defined $params->{names} || defined $params->{type} - || ThrowCodeError("params_required", { function => "Product.get", - params => ['ids', 'names', 'type'] }); - Bugzilla->switch_to_shadow_db(); - - my $products = []; - if (defined $params->{type}) { - my %product_hash; - foreach my $type (@{ $params->{type} }) { - my $result = []; - if ($type eq 'accessible') { - $result = $user->get_accessible_products(); - } - elsif ($type eq 'enterable') { - $result = $user->get_enterable_products(); - } - elsif ($type eq 'selectable') { - $result = $user->get_selectable_products(); - } - else { - ThrowUserError('get_products_invalid_type', - { type => $type }); - } - map { $product_hash{$_->id} = $_ } @$result; - } - $products = [ values %product_hash ]; - } - else { - $products = $user->get_accessible_products; + my ($self, $params) = validate(@_, 'ids', 'names', 'type'); + my $user = Bugzilla->user; + + defined $params->{ids} + || defined $params->{names} + || defined $params->{type} + || ThrowCodeError("params_required", + {function => "Product.get", params => ['ids', 'names', 'type']}); + Bugzilla->switch_to_shadow_db(); + + my $products = []; + if (defined $params->{type}) { + my %product_hash; + foreach my $type (@{$params->{type}}) { + my $result = []; + if ($type eq 'accessible') { + $result = $user->get_accessible_products(); + } + elsif ($type eq 'enterable') { + $result = $user->get_enterable_products(); + } + elsif ($type eq 'selectable') { + $result = $user->get_selectable_products(); + } + else { + ThrowUserError('get_products_invalid_type', {type => $type}); + } + map { $product_hash{$_->id} = $_ } @$result; } + $products = [values %product_hash]; + } + else { + $products = $user->get_accessible_products; + } - my @requested_products; + my @requested_products; - if (defined $params->{ids}) { - # Create a hash with the ids the user wants - my %ids = map { $_ => 1 } @{$params->{ids}}; + if (defined $params->{ids}) { - # Return the intersection of this, by grepping the ids from $products. - push(@requested_products, - grep { $ids{$_->id} } @$products); - } + # Create a hash with the ids the user wants + my %ids = map { $_ => 1 } @{$params->{ids}}; - if (defined $params->{names}) { - # Create a hash with the names the user wants - my %names = map { lc($_) => 1 } @{$params->{names}}; - - # Return the intersection of this, by grepping the names - # from $products, union'ed with products found by ID to - # avoid duplicates - foreach my $product (grep { $names{lc $_->name} } - @$products) { - next if grep { $_->id == $product->id } - @requested_products; - push @requested_products, $product; - } - } + # Return the intersection of this, by grepping the ids from $products. + push(@requested_products, grep { $ids{$_->id} } @$products); + } + + if (defined $params->{names}) { - # If we just requested a specific type of products without - # specifying ids or names, then return the entire list. - if (!defined $params->{ids} && !defined $params->{names}) { - @requested_products = @$products; + # Create a hash with the names the user wants + my %names = map { lc($_) => 1 } @{$params->{names}}; + + # Return the intersection of this, by grepping the names + # from $products, union'ed with products found by ID to + # avoid duplicates + foreach my $product (grep { $names{lc $_->name} } @$products) { + next if grep { $_->id == $product->id } @requested_products; + push @requested_products, $product; } + } + + # If we just requested a specific type of products without + # specifying ids or names, then return the entire list. + if (!defined $params->{ids} && !defined $params->{names}) { + @requested_products = @$products; + } - # Now create a result entry for each. - my @products = map { $self->_product_to_hash($params, $_) } - @requested_products; - return { products => \@products }; + # Now create a result entry for each. + my @products = map { $self->_product_to_hash($params, $_) } @requested_products; + return {products => \@products}; } sub create { - my ($self, $params) = @_; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('editcomponents') - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "add", - object => "products"}); - # Create product - my $args = { - name => $params->{name}, - description => $params->{description}, - version => $params->{version}, - defaultmilestone => $params->{default_milestone}, - # create_series has no default value. - create_series => defined $params->{create_series} ? - $params->{create_series} : 1 - }; - foreach my $field (qw(has_unconfirmed is_open classification)) { - if (defined $params->{$field}) { - my $name = FIELD_MAP->{$field} || $field; - $args->{$name} = $params->{$field}; - } + my ($self, $params) = @_; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "products"}); + + # Create product + my $args = { + name => $params->{name}, + description => $params->{description}, + version => $params->{version}, + defaultmilestone => $params->{default_milestone}, + + # create_series has no default value. + create_series => defined $params->{create_series} + ? $params->{create_series} + : 1 + }; + foreach my $field (qw(has_unconfirmed is_open classification)) { + if (defined $params->{$field}) { + my $name = FIELD_MAP->{$field} || $field; + $args->{$name} = $params->{$field}; } - my $product = Bugzilla::Product->create($args); - return { id => $self->type('int', $product->id) }; + } + my $product = Bugzilla::Product->create($args); + return {id => $self->type('int', $product->id)}; } sub update { - my ($self, $params) = @_; - - my $dbh = Bugzilla->dbh; - - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->user->in_group('editcomponents') - || ThrowUserError("auth_failure", { group => "editcomponents", - action => "edit", - object => "products" }); - - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'Product.update', params => ['ids', 'names'] }); - - my $product_objects = params_to_objects($params, 'Bugzilla::Product'); - - my $values = translate($params, MAPPED_FIELDS); - - # We delete names and ids to keep only new values to set. - delete $values->{names}; - delete $values->{ids}; - - $dbh->bz_start_transaction(); - foreach my $product (@$product_objects) { - $product->set_all($values); + my ($self, $params) = @_; + + my $dbh = Bugzilla->dbh; + + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "products"}); + + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'Product.update', params => ['ids', 'names']}); + + my $product_objects = params_to_objects($params, 'Bugzilla::Product'); + + my $values = translate($params, MAPPED_FIELDS); + + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; + + $dbh->bz_start_transaction(); + foreach my $product (@$product_objects) { + $product->set_all($values); + } + + my %changes; + foreach my $product (@$product_objects) { + my $returned_changes = $product->update(); + $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); + + my @result; + foreach my $product (@$product_objects) { + my %hash = (id => $product->id, changes => {},); + + foreach my $field (keys %{$changes{$product->id}}) { + my $change = $changes{$product->id}->{$field}; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - my %changes; - foreach my $product (@$product_objects) { - my $returned_changes = $product->update(); - $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $product (@$product_objects) { - my %hash = ( - id => $product->id, - changes => {}, - ); - - foreach my $field (keys %{ $changes{$product->id} }) { - my $change = $changes{$product->id}->{$field}; - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } - - push(@result, \%hash); - } + push(@result, \%hash); + } - return { products => \@result }; + return {products => \@result}; } sub _product_to_hash { - my ($self, $params, $product) = @_; - - my $field_data = { - id => $self->type('int', $product->id), - name => $self->type('string', $product->name), - description => $self->type('string', $product->description), - is_active => $self->type('boolean', $product->is_active), - default_milestone => $self->type('string', $product->default_milestone), - has_unconfirmed => $self->type('boolean', $product->allows_unconfirmed), - classification => $self->type('string', $product->classification->name), - }; - if (filter_wants($params, 'components')) { - $field_data->{components} = [map { - $self->_component_to_hash($_, $params) - } @{$product->components}]; - } - if (filter_wants($params, 'versions')) { - $field_data->{versions} = [map { - $self->_version_to_hash($_, $params) - } @{$product->versions}]; - } - if (filter_wants($params, 'milestones')) { - $field_data->{milestones} = [map { - $self->_milestone_to_hash($_, $params) - } @{$product->milestones}]; - } - return filter($params, $field_data); + my ($self, $params, $product) = @_; + + my $field_data = { + id => $self->type('int', $product->id), + name => $self->type('string', $product->name), + description => $self->type('string', $product->description), + is_active => $self->type('boolean', $product->is_active), + default_milestone => $self->type('string', $product->default_milestone), + has_unconfirmed => $self->type('boolean', $product->allows_unconfirmed), + classification => $self->type('string', $product->classification->name), + }; + if (filter_wants($params, 'components')) { + $field_data->{components} + = [map { $self->_component_to_hash($_, $params) } @{$product->components}]; + } + if (filter_wants($params, 'versions')) { + $field_data->{versions} + = [map { $self->_version_to_hash($_, $params) } @{$product->versions}]; + } + if (filter_wants($params, 'milestones')) { + $field_data->{milestones} + = [map { $self->_milestone_to_hash($_, $params) } @{$product->milestones}]; + } + return filter($params, $field_data); } sub _component_to_hash { - my ($self, $component, $params) = @_; - my $field_data = filter $params, { - id => - $self->type('int', $component->id), - name => - $self->type('string', $component->name), - description => - $self->type('string' , $component->description), - default_assigned_to => - $self->type('email', $component->default_assignee->login), - default_qa_contact => - $self->type('email', $component->default_qa_contact ? - $component->default_qa_contact->login : ""), - sort_key => # sort_key is returned to match Bug.fields - 0, - is_active => - $self->type('boolean', $component->is_active), - }, undef, 'components'; - - if (filter_wants($params, 'flag_types', undef, 'components')) { - $field_data->{flag_types} = { - bug => - [map { - $self->_flag_type_to_hash($_) - } @{$component->flag_types->{'bug'}}], - attachment => - [map { - $self->_flag_type_to_hash($_) - } @{$component->flag_types->{'attachment'}}], - }; - } + my ($self, $component, $params) = @_; + my $field_data = filter $params, { + id => $self->type('int', $component->id), + name => $self->type('string', $component->name), + description => $self->type('string', $component->description), + default_assigned_to => + $self->type('email', $component->default_assignee->login), + default_qa_contact => $self->type( + 'email', + $component->default_qa_contact ? $component->default_qa_contact->login : "" + ), + sort_key => # sort_key is returned to match Bug.fields + 0, + is_active => $self->type('boolean', $component->is_active), + }, + undef, 'components'; + + if (filter_wants($params, 'flag_types', undef, 'components')) { + $field_data->{flag_types} = { + bug => + [map { $self->_flag_type_to_hash($_) } @{$component->flag_types->{'bug'}}], + attachment => [ + map { $self->_flag_type_to_hash($_) } @{$component->flag_types->{'attachment'}} + ], + }; + } - return $field_data; + return $field_data; } sub _flag_type_to_hash { - my ($self, $flag_type, $params) = @_; - return filter $params, { - id => - $self->type('int', $flag_type->id), - name => - $self->type('string', $flag_type->name), - description => - $self->type('string', $flag_type->description), - cc_list => - $self->type('string', $flag_type->cc_list), - sort_key => - $self->type('int', $flag_type->sortkey), - is_active => - $self->type('boolean', $flag_type->is_active), - is_requestable => - $self->type('boolean', $flag_type->is_requestable), - is_requesteeble => - $self->type('boolean', $flag_type->is_requesteeble), - is_multiplicable => - $self->type('boolean', $flag_type->is_multiplicable), - grant_group => - $self->type('int', $flag_type->grant_group_id), - request_group => - $self->type('int', $flag_type->request_group_id), - }, undef, 'flag_types'; + my ($self, $flag_type, $params) = @_; + return filter $params, + { + id => $self->type('int', $flag_type->id), + name => $self->type('string', $flag_type->name), + description => $self->type('string', $flag_type->description), + cc_list => $self->type('string', $flag_type->cc_list), + sort_key => $self->type('int', $flag_type->sortkey), + is_active => $self->type('boolean', $flag_type->is_active), + is_requestable => $self->type('boolean', $flag_type->is_requestable), + is_requesteeble => $self->type('boolean', $flag_type->is_requesteeble), + is_multiplicable => $self->type('boolean', $flag_type->is_multiplicable), + grant_group => $self->type('int', $flag_type->grant_group_id), + request_group => $self->type('int', $flag_type->request_group_id), + }, + undef, 'flag_types'; } sub _version_to_hash { - my ($self, $version, $params) = @_; - return filter $params, { - id => - $self->type('int', $version->id), - name => - $self->type('string', $version->name), - sort_key => # sort_key is returened to match Bug.fields - 0, - is_active => - $self->type('boolean', $version->is_active), - }, undef, 'versions'; + my ($self, $version, $params) = @_; + return filter $params, { + id => $self->type('int', $version->id), + name => $self->type('string', $version->name), + sort_key => # sort_key is returened to match Bug.fields + 0, + is_active => $self->type('boolean', $version->is_active), + }, + undef, 'versions'; } sub _milestone_to_hash { - my ($self, $milestone, $params) = @_; - return filter $params, { - id => - $self->type('int', $milestone->id), - name => - $self->type('string', $milestone->name), - sort_key => - $self->type('int', $milestone->sortkey), - is_active => - $self->type('boolean', $milestone->is_active), - }, undef, 'milestones'; + my ($self, $milestone, $params) = @_; + return filter $params, + { + id => $self->type('int', $milestone->id), + name => $self->type('string', $milestone->name), + sort_key => $self->type('int', $milestone->sortkey), + is_active => $self->type('boolean', $milestone->is_active), + }, + undef, 'milestones'; } 1; diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm index 7950c7a3b..f2844cdcb 100644 --- a/Bugzilla/WebService/Server.pm +++ b/Bugzilla/WebService/Server.pm @@ -20,72 +20,77 @@ use Digest::MD5 qw(md5_base64); use Storable qw(freeze); sub handle_login { - my ($self, $class, $method, $full_method) = @_; - # Throw error if the supplied class does not exist or the method is private - ThrowCodeError('unknown_method', {method => $full_method}) if (!$class or $method =~ /^_/); - - eval "require $class"; - ThrowCodeError('unknown_method', {method => $full_method}) if $@; - return if ($class->login_exempt($method) - and !defined Bugzilla->input_params->{Bugzilla_login}); - Bugzilla->login(); - - Bugzilla::Hook::process( - 'webservice_before_call', - { 'method' => $method, full_method => $full_method }); + my ($self, $class, $method, $full_method) = @_; + + # Throw error if the supplied class does not exist or the method is private + ThrowCodeError('unknown_method', {method => $full_method}) + if (!$class or $method =~ /^_/); + + eval "require $class"; + ThrowCodeError('unknown_method', {method => $full_method}) if $@; + return + if ($class->login_exempt($method) + and !defined Bugzilla->input_params->{Bugzilla_login}); + Bugzilla->login(); + + Bugzilla::Hook::process('webservice_before_call', + {'method' => $method, full_method => $full_method}); } sub datetime_format_inbound { - my ($self, $time) = @_; - - my $converted = datetime_from($time, Bugzilla->local_timezone); - if (!defined $converted) { - ThrowUserError('illegal_date', { date => $time }); - } - $time = $converted->ymd() . ' ' . $converted->hms(); - return $time + my ($self, $time) = @_; + + my $converted = datetime_from($time, Bugzilla->local_timezone); + if (!defined $converted) { + ThrowUserError('illegal_date', {date => $time}); + } + $time = $converted->ymd() . ' ' . $converted->hms(); + return $time; } sub datetime_format_outbound { - my ($self, $date) = @_; - - return undef if (!defined $date or $date eq ''); - - my $time = $date; - if (blessed($date)) { - # We expect this to mean we were sent a datetime object - $time->set_time_zone('UTC'); - } else { - # We always send our time in UTC, for consistency. - # passed in value is likely a string, create a datetime object - $time = datetime_from($date, 'UTC'); - } - return $time->iso8601(); + my ($self, $date) = @_; + + return undef if (!defined $date or $date eq ''); + + my $time = $date; + if (blessed($date)) { + + # We expect this to mean we were sent a datetime object + $time->set_time_zone('UTC'); + } + else { + # We always send our time in UTC, for consistency. + # passed in value is likely a string, create a datetime object + $time = datetime_from($date, 'UTC'); + } + return $time->iso8601(); } # ETag support sub bz_etag { - my ($self, $data) = @_; - my $cache = Bugzilla->request_cache; - if (defined $data) { - # Serialize the data if passed a reference - local $Storable::canonical = 1; - $data = freeze($data) if ref $data; - - # Wide characters cause md5_base64() to die. - utf8::encode($data) if utf8::is_utf8($data); - - # Append content_type to the end of the data - # string as we want the etag to be unique to - # the content_type. We do not need this for - # XMLRPC as text/xml is always returned. - if (blessed($self) && $self->can('content_type')) { - $data .= $self->content_type if $self->content_type; - } - - $cache->{'bz_etag'} = md5_base64($data); + my ($self, $data) = @_; + my $cache = Bugzilla->request_cache; + if (defined $data) { + + # Serialize the data if passed a reference + local $Storable::canonical = 1; + $data = freeze($data) if ref $data; + + # Wide characters cause md5_base64() to die. + utf8::encode($data) if utf8::is_utf8($data); + + # Append content_type to the end of the data + # string as we want the etag to be unique to + # the content_type. We do not need this for + # XMLRPC as text/xml is always returned. + if (blessed($self) && $self->can('content_type')) { + $data .= $self->content_type if $self->content_type; } - return $cache->{'bz_etag'}; + + $cache->{'bz_etag'} = md5_base64($data); + } + return $cache->{'bz_etag'}; } 1; diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm index 70b8fd96c..66640beb7 100644 --- a/Bugzilla/WebService/Server/JSONRPC.pm +++ b/Bugzilla/WebService/Server/JSONRPC.pm @@ -12,16 +12,17 @@ use strict; use warnings; use Bugzilla::WebService::Server; -BEGIN { - our @ISA = qw(Bugzilla::WebService::Server); - if (eval { require JSON::RPC::Server::CGI }) { - unshift(@ISA, 'JSON::RPC::Server::CGI'); - } - else { - require JSON::RPC::Legacy::Server::CGI; - unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); - } +BEGIN { + our @ISA = qw(Bugzilla::WebService::Server); + + if (eval { require JSON::RPC::Server::CGI }) { + unshift(@ISA, 'JSON::RPC::Server::CGI'); + } + else { + require JSON::RPC::Legacy::Server::CGI; + unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI'); + } } use Bugzilla::Error; @@ -38,79 +39,83 @@ use List::MoreUtils qw(none); ##################################### sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - Bugzilla->_json_server($self); - $self->dispatch(WS_DISPATCH); - $self->return_die_message(1); - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + Bugzilla->_json_server($self); + $self->dispatch(WS_DISPATCH); + $self->return_die_message(1); + return $self; } sub create_json_coder { - my $self = shift; - my $json = $self->SUPER::create_json_coder(@_); - $json->allow_blessed(1); - $json->convert_blessed(1); - # This may seem a little backwards, but what this really means is - # "don't convert our utf8 into byte strings, just leave it as a - # utf8 string." - $json->utf8(0) if Bugzilla->params->{'utf8'}; - return $json; + my $self = shift; + my $json = $self->SUPER::create_json_coder(@_); + $json->allow_blessed(1); + $json->convert_blessed(1); + + # This may seem a little backwards, but what this really means is + # "don't convert our utf8 into byte strings, just leave it as a + # utf8 string." + $json->utf8(0) if Bugzilla->params->{'utf8'}; + return $json; } # Override the JSON::RPC method to return our CGI object instead of theirs. sub cgi { return Bugzilla->cgi; } sub response_header { - my $self = shift; - # The HTTP body needs to be bytes (not a utf8 string) for recent - # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this - # properly. $_[1] is the HTTP body content we're going to be sending. - if (utf8::is_utf8($_[1])) { - utf8::encode($_[1]); - # Since we're going to just be sending raw bytes, we need to - # set STDOUT to not expect utf8. - disable_utf8(); - } - return $self->SUPER::response_header(@_); + my $self = shift; + + # The HTTP body needs to be bytes (not a utf8 string) for recent + # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this + # properly. $_[1] is the HTTP body content we're going to be sending. + if (utf8::is_utf8($_[1])) { + utf8::encode($_[1]); + + # Since we're going to just be sending raw bytes, we need to + # set STDOUT to not expect utf8. + disable_utf8(); + } + return $self->SUPER::response_header(@_); } sub response { - my ($self, $response) = @_; - my $cgi = $self->cgi; - - # Implement JSONP. - if (my $callback = $self->_bz_callback) { - my $content = $response->content; - # Prepend the JSONP response with /**/ in order to protect - # against possible encoding attacks (e.g., affecting Flash). - $response->content("/**/$callback($content)"); - } - - # Use $cgi->header properly instead of just printing text directly. - # This fixes various problems, including sending Bugzilla's cookies - # properly. - my $headers = $response->headers; - my @header_args; - foreach my $name ($headers->header_field_names) { - my @values = $headers->header($name); - $name =~ s/-/_/g; - foreach my $value (@values) { - push(@header_args, "-$name", $value); - } - } - - # ETag support - my $etag = $self->bz_etag; - if ($etag && $cgi->check_etag($etag)) { - push(@header_args, "-ETag", $etag); - print $cgi->header(-status => '304 Not Modified', @header_args); - } - else { - push(@header_args, "-ETag", $etag) if $etag; - print $cgi->header(-status => $response->code, @header_args); - print $response->content; - } + my ($self, $response) = @_; + my $cgi = $self->cgi; + + # Implement JSONP. + if (my $callback = $self->_bz_callback) { + my $content = $response->content; + + # Prepend the JSONP response with /**/ in order to protect + # against possible encoding attacks (e.g., affecting Flash). + $response->content("/**/$callback($content)"); + } + + # Use $cgi->header properly instead of just printing text directly. + # This fixes various problems, including sending Bugzilla's cookies + # properly. + my $headers = $response->headers; + my @header_args; + foreach my $name ($headers->header_field_names) { + my @values = $headers->header($name); + $name =~ s/-/_/g; + foreach my $value (@values) { + push(@header_args, "-$name", $value); + } + } + + # ETag support + my $etag = $self->bz_etag; + if ($etag && $cgi->check_etag($etag)) { + push(@header_args, "-ETag", $etag); + print $cgi->header(-status => '304 Not Modified', @header_args); + } + else { + push(@header_args, "-ETag", $etag) if $etag; + print $cgi->header(-status => $response->code, @header_args); + print $response->content; + } } # The JSON-RPC 1.1 GET specification is not so great--you can't specify @@ -122,70 +127,69 @@ sub response { # Base64 encoded, because that is ridiculous and obnoxious for JavaScript # clients. sub retrieve_json_from_get { - my $self = shift; - my $cgi = $self->cgi; - - my %input; - - # Both version and id must be set before any errors are thrown. - if ($cgi->param('version')) { - $self->version(scalar $cgi->param('version')); - $input{version} = $cgi->param('version'); - } - else { - $self->version('1.0'); - } - - # The JSON-RPC 2.0 spec says that any request that omits an id doesn't - # want a response. However, in an HTTP GET situation, it's stupid to - # expect all clients to specify some id parameter just to get a response, - # so we don't require it. - my $id; - if (defined $cgi->param('id')) { - $id = $cgi->param('id'); - } - # However, JSON::RPC does require that an id exist in most cases, in - # order to throw proper errors. We use the installation's urlbase as - # the id, in this case. - else { - $id = correct_urlbase(); - } - # Setting _bz_request_id here is required in case we throw errors early, - # before _handle. - $self->{_bz_request_id} = $input{id} = $id; - - # _bz_callback can throw an error, so we have to set it here, after we're - # ready to throw errors. - $self->_bz_callback(scalar $cgi->param('callback')); - - if (!$cgi->param('method')) { - ThrowUserError('json_rpc_get_method_required'); - } - $input{method} = $cgi->param('method'); - - my $params; - if (defined $cgi->param('params')) { - local $@; - $params = eval { - $self->json->decode(scalar $cgi->param('params')) - }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', - { params => scalar $cgi->param('params'), - err_msg => $@ }); - } - } - elsif (!$self->version or $self->version ne '1.1') { - $params = []; - } - else { - $params = {}; - } - - $input{params} = $params; - - my $json = $self->json->encode(\%input); - return $json; + my $self = shift; + my $cgi = $self->cgi; + + my %input; + + # Both version and id must be set before any errors are thrown. + if ($cgi->param('version')) { + $self->version(scalar $cgi->param('version')); + $input{version} = $cgi->param('version'); + } + else { + $self->version('1.0'); + } + + # The JSON-RPC 2.0 spec says that any request that omits an id doesn't + # want a response. However, in an HTTP GET situation, it's stupid to + # expect all clients to specify some id parameter just to get a response, + # so we don't require it. + my $id; + if (defined $cgi->param('id')) { + $id = $cgi->param('id'); + } + + # However, JSON::RPC does require that an id exist in most cases, in + # order to throw proper errors. We use the installation's urlbase as + # the id, in this case. + else { + $id = correct_urlbase(); + } + + # Setting _bz_request_id here is required in case we throw errors early, + # before _handle. + $self->{_bz_request_id} = $input{id} = $id; + + # _bz_callback can throw an error, so we have to set it here, after we're + # ready to throw errors. + $self->_bz_callback(scalar $cgi->param('callback')); + + if (!$cgi->param('method')) { + ThrowUserError('json_rpc_get_method_required'); + } + $input{method} = $cgi->param('method'); + + my $params; + if (defined $cgi->param('params')) { + local $@; + $params = eval { $self->json->decode(scalar $cgi->param('params')) }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', + {params => scalar $cgi->param('params'), err_msg => $@}); + } + } + elsif (!$self->version or $self->version ne '1.1') { + $params = []; + } + else { + $params = {}; + } + + $input{params} = $params; + + my $json = $self->json->encode(\%input); + return $json; } ####################################### @@ -193,72 +197,76 @@ sub retrieve_json_from_get { ####################################### sub type { - my ($self, $type, $value) = @_; - - # This is the only type that does something special with undef. - if ($type eq 'boolean') { - return $value ? JSON::true : JSON::false; - } - - return JSON::null if !defined $value; - - my $retval = $value; - - if ($type eq 'int') { - $retval = int($value); - } - if ($type eq 'double') { - $retval = 0.0 + $value; - } - elsif ($type eq 'string') { - # Forces string context, so that JSON will make it a string. - $retval = "$value"; - } - elsif ($type eq 'dateTime') { - # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T - $retval = $self->datetime_format_outbound($value); - } - elsif ($type eq 'base64') { - utf8::encode($value) if utf8::is_utf8($value); - $retval = encode_base64($value, ''); - } - elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { - $retval = email_filter($value); - } - - return $retval; + my ($self, $type, $value) = @_; + + # This is the only type that does something special with undef. + if ($type eq 'boolean') { + return $value ? JSON::true : JSON::false; + } + + return JSON::null if !defined $value; + + my $retval = $value; + + if ($type eq 'int') { + $retval = int($value); + } + if ($type eq 'double') { + $retval = 0.0 + $value; + } + elsif ($type eq 'string') { + + # Forces string context, so that JSON will make it a string. + $retval = "$value"; + } + elsif ($type eq 'dateTime') { + + # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T + $retval = $self->datetime_format_outbound($value); + } + elsif ($type eq 'base64') { + utf8::encode($value) if utf8::is_utf8($value); + $retval = encode_base64($value, ''); + } + elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) { + $retval = email_filter($value); + } + + return $retval; } sub datetime_format_outbound { - my $self = shift; - # YUI expects ISO8601 in UTC time; including TZ specifier - return $self->SUPER::datetime_format_outbound(@_) . 'Z'; + my $self = shift; + + # YUI expects ISO8601 in UTC time; including TZ specifier + return $self->SUPER::datetime_format_outbound(@_) . 'Z'; } sub handle_login { - my $self = shift; - - # If we're being called using GET, we don't allow cookie-based or Env - # login, because GET requests can be done cross-domain, and we don't - # want private data showing up on another site unless the user - # explicitly gives that site their username and password. (This is - # particularly important for JSONP, which would allow a remote site - # to use private data without the user's knowledge, unless we had this - # protection in place.) - if ($self->request->method ne 'POST') { - # XXX There's no particularly good way for us to get a parameter - # to Bugzilla->login at this point, so we pass this information - # around using request_cache, which is a bit of a hack. The - # implementation of it is in Bugzilla::Auth::Login::Stack. - Bugzilla->request_cache->{auth_no_automatic_login} = 1; - } - - my $path = $self->path_info; - my $class = $self->{dispatch_path}->{$path}; - my $full_method = $self->_bz_method_name; - $full_method =~ /^\S+\.(\S+)/; - my $method = $1; - $self->SUPER::handle_login($class, $method, $full_method); + my $self = shift; + + # If we're being called using GET, we don't allow cookie-based or Env + # login, because GET requests can be done cross-domain, and we don't + # want private data showing up on another site unless the user + # explicitly gives that site their username and password. (This is + # particularly important for JSONP, which would allow a remote site + # to use private data without the user's knowledge, unless we had this + # protection in place.) + if ($self->request->method ne 'POST') { + + # XXX There's no particularly good way for us to get a parameter + # to Bugzilla->login at this point, so we pass this information + # around using request_cache, which is a bit of a hack. The + # implementation of it is in Bugzilla::Auth::Login::Stack. + Bugzilla->request_cache->{auth_no_automatic_login} = 1; + } + + my $path = $self->path_info; + my $class = $self->{dispatch_path}->{$path}; + my $full_method = $self->_bz_method_name; + $full_method =~ /^\S+\.(\S+)/; + my $method = $1; + $self->SUPER::handle_login($class, $method, $full_method); } ###################################### @@ -267,165 +275,165 @@ sub handle_login { # Store the ID of the current call, because Bugzilla::Error will need it. sub _handle { - my $self = shift; - my ($obj) = @_; - $self->{_bz_request_id} = $obj->{id}; + my $self = shift; + my ($obj) = @_; + $self->{_bz_request_id} = $obj->{id}; - my $result = $self->SUPER::_handle(@_); + my $result = $self->SUPER::_handle(@_); - # Set the ETag if not already set in the webservice methods. - my $etag = $self->bz_etag; - if (!$etag && ref $result) { - my $data = $self->json->decode($result)->{'result'}; - $self->bz_etag($data); - } + # Set the ETag if not already set in the webservice methods. + my $etag = $self->bz_etag; + if (!$etag && ref $result) { + my $data = $self->json->decode($result)->{'result'}; + $self->bz_etag($data); + } - return $result; + return $result; } # Make all error messages returned by JSON::RPC go into the 100000 # range, and bring down all our errors into the normal range. sub _error { - my ($self, $id, $code) = (shift, shift, shift); - # All JSON::RPC errors are less than 1000. - if ($code < 1000) { - $code += 100000; - } - # Bugzilla::Error adds 100,000 to all *our* errors, so - # we know they came from us. - elsif ($code > 100000) { - $code -= 100000; - } - - # We can't just set $_[1] because it's not always settable, - # in JSON::RPC::Server. - unshift(@_, $id, $code); - my $json = $self->SUPER::_error(@_); - - # We want to always send the JSON-RPC 1.1 error format, although - # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter. - if (!$self->version or $self->version ne '1.1') { - my $object = $self->json->decode($json); - my $message = $object->{error}; - # Just assure that future versions of JSON::RPC don't change the - # JSON-RPC 1.0 error format. - if (!ref $message) { - $object->{error} = { - code => $code, - message => $message, - }; - $json = $self->json->encode($object); - } - } - return $json; + my ($self, $id, $code) = (shift, shift, shift); + + # All JSON::RPC errors are less than 1000. + if ($code < 1000) { + $code += 100000; + } + + # Bugzilla::Error adds 100,000 to all *our* errors, so + # we know they came from us. + elsif ($code > 100000) { + $code -= 100000; + } + + # We can't just set $_[1] because it's not always settable, + # in JSON::RPC::Server. + unshift(@_, $id, $code); + my $json = $self->SUPER::_error(@_); + + # We want to always send the JSON-RPC 1.1 error format, although + # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter. + if (!$self->version or $self->version ne '1.1') { + my $object = $self->json->decode($json); + my $message = $object->{error}; + + # Just assure that future versions of JSON::RPC don't change the + # JSON-RPC 1.0 error format. + if (!ref $message) { + $object->{error} = {code => $code, message => $message,}; + $json = $self->json->encode($object); + } + } + return $json; } # This handles dispatching our calls to the appropriate class based on # the name of the method. sub _find_procedure { - my $self = shift; + my $self = shift; - my $method = shift; - $self->{_bz_method_name} = $method; + my $method = shift; + $self->{_bz_method_name} = $method; - # This tricks SUPER::_find_procedure into finding the right class. - $method =~ /^(\S+)\.(\S+)$/; - $self->path_info($1); - unshift(@_, $2); + # This tricks SUPER::_find_procedure into finding the right class. + $method =~ /^(\S+)\.(\S+)$/; + $self->path_info($1); + unshift(@_, $2); - return $self->SUPER::_find_procedure(@_); + return $self->SUPER::_find_procedure(@_); } # This is a hacky way to do something right before methods are called. # This is the last thing that JSON::RPC::Server::_handle calls right before # the method is actually called. sub _argument_type_check { - my $self = shift; - my $params = $self->SUPER::_argument_type_check(@_); - - # JSON-RPC 1.0 requires all parameters to be passed as an array, so - # we just pull out the first item and assume it's an object. - my $params_is_array; - if (ref $params eq 'ARRAY') { - $params = $params->[0]; - $params_is_array = 1; - } - - taint_data($params); - - # Now, convert dateTime fields on input. - $self->_bz_method_name =~ /^(\S+)\.(\S+)$/; - my ($class, $method) = ($1, $2); - my $pkg = $self->{dispatch_path}->{$class}; - my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] }; - foreach my $field (@date_fields) { - if (defined $params->{$field}) { - my $value = $params->{$field}; - if (ref $value eq 'ARRAY') { - $params->{$field} = - [ map { $self->datetime_format_inbound($_) } @$value ]; - } - else { - $params->{$field} = $self->datetime_format_inbound($value); - } - } - } - my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] }; - foreach my $field (@base64_fields) { - if (defined $params->{$field}) { - $params->{$field} = decode_base64($params->{$field}); - } - } - - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params); - - Bugzilla->input_params($params); - - if ($self->request->method eq 'POST') { - # CSRF is possible via XMLHttpRequest when the Content-Type header - # is not application/json (for example: text/plain or - # application/x-www-form-urlencoded). - # application/json is the single official MIME type, per RFC 4627. - my $content_type = $self->cgi->content_type; - # The charset can be appended to the content type, so we use a regexp. - if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { - ThrowUserError('json_rpc_illegal_content_type', - { content_type => $content_type }); - } - } - else { - # When being called using GET, we don't allow calling - # methods that can change data. This protects us against cross-site - # request forgeries. - if (!grep($_ eq $method, $pkg->READ_ONLY)) { - ThrowUserError('json_rpc_post_only', - { method => $self->_bz_method_name }); - } - } - - # Only allowed methods to be used from our whitelist - if (none { $_ eq $method} $pkg->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $self->_bz_method_name }); - } - - # This is the best time to do login checks. - $self->handle_login(); - - # Bugzilla::WebService packages call internal methods like - # $self->_some_private_method. So we have to inherit from - # that class as well as this Server class. - my $new_class = ref($self) . '::' . $pkg; - my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; - eval "package $new_class;$isa_string;"; - bless $self, $new_class; - - if ($params_is_array) { - $params = [$params]; - } - - return $params; + my $self = shift; + my $params = $self->SUPER::_argument_type_check(@_); + + # JSON-RPC 1.0 requires all parameters to be passed as an array, so + # we just pull out the first item and assume it's an object. + my $params_is_array; + if (ref $params eq 'ARRAY') { + $params = $params->[0]; + $params_is_array = 1; + } + + taint_data($params); + + # Now, convert dateTime fields on input. + $self->_bz_method_name =~ /^(\S+)\.(\S+)$/; + my ($class, $method) = ($1, $2); + my $pkg = $self->{dispatch_path}->{$class}; + my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []}; + foreach my $field (@date_fields) { + if (defined $params->{$field}) { + my $value = $params->{$field}; + if (ref $value eq 'ARRAY') { + $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value]; + } + else { + $params->{$field} = $self->datetime_format_inbound($value); + } + } + } + my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []}; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); + } + } + + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); + + Bugzilla->input_params($params); + + if ($self->request->method eq 'POST') { + + # CSRF is possible via XMLHttpRequest when the Content-Type header + # is not application/json (for example: text/plain or + # application/x-www-form-urlencoded). + # application/json is the single official MIME type, per RFC 4627. + my $content_type = $self->cgi->content_type; + + # The charset can be appended to the content type, so we use a regexp. + if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) { + ThrowUserError('json_rpc_illegal_content_type', + {content_type => $content_type}); + } + } + else { + # When being called using GET, we don't allow calling + # methods that can change data. This protects us against cross-site + # request forgeries. + if (!grep($_ eq $method, $pkg->READ_ONLY)) { + ThrowUserError('json_rpc_post_only', {method => $self->_bz_method_name}); + } + } + + # Only allowed methods to be used from our whitelist + if (none { $_ eq $method } $pkg->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $self->_bz_method_name}); + } + + # This is the best time to do login checks. + $self->handle_login(); + + # Bugzilla::WebService packages call internal methods like + # $self->_some_private_method. So we have to inherit from + # that class as well as this Server class. + my $new_class = ref($self) . '::' . $pkg; + my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; + eval "package $new_class;$isa_string;"; + bless $self, $new_class; + + if ($params_is_array) { + $params = [$params]; + } + + return $params; } ########################## @@ -434,22 +442,24 @@ sub _argument_type_check { # _bz_method_name is stored by _find_procedure for later use. sub _bz_method_name { - return $_[0]->{_bz_method_name}; + return $_[0]->{_bz_method_name}; } sub _bz_callback { - my ($self, $value) = @_; - if (defined $value) { - $value = trim($value); - # We don't use \w because we don't want to allow Unicode here. - if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { - ThrowUserError('json_rpc_invalid_callback', { callback => $value }); - } - $self->{_bz_callback} = $value; - # JSONP needs to be parsed by a JS parser, not by a JSON parser. - $self->content_type('text/javascript'); + my ($self, $value) = @_; + if (defined $value) { + $value = trim($value); + + # We don't use \w because we don't want to allow Unicode here. + if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) { + ThrowUserError('json_rpc_invalid_callback', {callback => $value}); } - return $self->{_bz_callback}; + $self->{_bz_callback} = $value; + + # JSONP needs to be parsed by a JS parser, not by a JSON parser. + $self->content_type('text/javascript'); + } + return $self->{_bz_callback}; } 1; diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm index 8450a7a28..8108e1d4f 100644 --- a/Bugzilla/WebService/Server/REST.pm +++ b/Bugzilla/WebService/Server/REST.pm @@ -40,134 +40,134 @@ use MIME::Base64 qw(decode_base64); ########################### sub handle { - my ($self) = @_; - - # Determine how the data should be represented. We do this early so - # errors will also be returned with the proper content type. - # If no accept header was sent or the content types specified were not - # matched, we default to the first type in the whitelist. - $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST())); - - # Using current path information, decide which class/method to - # use to serve the request. Throw error if no resource was found - # unless we were looking for OPTIONS - if (!$self->_find_resource($self->cgi->path_info)) { - if ($self->request->method eq 'OPTIONS' - && $self->bz_rest_options) - { - my $response = $self->response_header(STATUS_OK, ""); - my $options_string = join(', ', @{ $self->bz_rest_options }); - $response->header('Allow' => $options_string, - 'Access-Control-Allow-Methods' => $options_string); - return $self->response($response); - } - - ThrowUserError("rest_invalid_resource", - { path => $self->cgi->path_info, - method => $self->request->method }); + my ($self) = @_; + + # Determine how the data should be represented. We do this early so + # errors will also be returned with the proper content type. + # If no accept header was sent or the content types specified were not + # matched, we default to the first type in the whitelist. + $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST())); + + # Using current path information, decide which class/method to + # use to serve the request. Throw error if no resource was found + # unless we were looking for OPTIONS + if (!$self->_find_resource($self->cgi->path_info)) { + if ($self->request->method eq 'OPTIONS' && $self->bz_rest_options) { + my $response = $self->response_header(STATUS_OK, ""); + my $options_string = join(', ', @{$self->bz_rest_options}); + $response->header( + 'Allow' => $options_string, + 'Access-Control-Allow-Methods' => $options_string + ); + return $self->response($response); } - # Dispatch to the proper module - my $class = $self->bz_class_name; - my ($path) = $class =~ /::([^:]+)$/; - $self->path_info($path); - delete $self->{dispatch_path}; - $self->dispatch({ $path => $class }); + ThrowUserError("rest_invalid_resource", + {path => $self->cgi->path_info, method => $self->request->method}); + } - my $params = $self->_retrieve_json_params; + # Dispatch to the proper module + my $class = $self->bz_class_name; + my ($path) = $class =~ /::([^:]+)$/; + $self->path_info($path); + delete $self->{dispatch_path}; + $self->dispatch({$path => $class}); - fix_credentials($params); + my $params = $self->_retrieve_json_params; - # Fix includes/excludes for each call - rest_include_exclude($params); + fix_credentials($params); - # Set callback name if exists - $self->_bz_callback($params->{'callback'}) if $params->{'callback'}; + # Fix includes/excludes for each call + rest_include_exclude($params); - Bugzilla->input_params($params); + # Set callback name if exists + $self->_bz_callback($params->{'callback'}) if $params->{'callback'}; - # Set the JSON version to 1.1 and the id to the current urlbase - # also set up the correct handler method - my $obj = { - version => '1.1', - id => correct_urlbase(), - method => $self->bz_method_name, - params => $params - }; + Bugzilla->input_params($params); - # Execute the handler - my $result = $self->_handle($obj); + # Set the JSON version to 1.1 and the id to the current urlbase + # also set up the correct handler method + my $obj = { + version => '1.1', + id => correct_urlbase(), + method => $self->bz_method_name, + params => $params + }; - if (!$self->error_response_header) { - return $self->response( - $self->response_header($self->bz_success_code || STATUS_OK, $result)); - } + # Execute the handler + my $result = $self->_handle($obj); + + if (!$self->error_response_header) { + return $self->response( + $self->response_header($self->bz_success_code || STATUS_OK, $result)); + } - $self->response($self->error_response_header); + $self->response($self->error_response_header); } sub response { - my ($self, $response) = @_; - - # If we have thrown an error, the 'error' key will exist - # otherwise we use 'result'. JSONRPC returns other data - # along with the result/error such as version and id which - # we will strip off for REST calls. - my $content = $response->content; - my $json_data = {}; - if ($content) { - $json_data = $self->json->decode($content); - } - - my $result = {}; - if (exists $json_data->{error}) { - $result = $json_data->{error}; - $result->{error} = $self->type('boolean', 1); - $result->{documentation} = REST_DOC; - delete $result->{'name'}; # Remove JSONRPCError - } - elsif (exists $json_data->{result}) { - $result = $json_data->{result}; - } - - # The result needs to be a valid JSON data structure - # and not a undefined or scalar value. - if (!ref $result - || blessed($result) - || (ref $result ne 'HASH' && ref $result ne 'ARRAY')) - { - $result = { result => $result }; - } - - Bugzilla::Hook::process('webservice_rest_response', - { rpc => $self, result => \$result, response => $response }); - - # Access Control - $response->header("Access-Control-Allow-Origin", "*"); - $response->header("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with"); - - # ETag support - my $etag = $self->bz_etag; - $self->bz_etag($result) if !$etag; - - # If accessing through web browser, then display in readable format - if ($self->content_type eq 'text/html') { - $result = $self->json->pretty->canonical->allow_nonref->encode($result); - - my $template = Bugzilla->template; - $content = ""; - $template->process("rest.html.tmpl", { result => $result }, \$content) - || ThrowTemplateError($template->error()); - - $response->content_type('text/html'); - } - else { - $content = $self->json->encode($result); - } - - $response->content($content); - - $self->SUPER::response($response); + my ($self, $response) = @_; + + # If we have thrown an error, the 'error' key will exist + # otherwise we use 'result'. JSONRPC returns other data + # along with the result/error such as version and id which + # we will strip off for REST calls. + my $content = $response->content; + my $json_data = {}; + if ($content) { + $json_data = $self->json->decode($content); + } + + my $result = {}; + if (exists $json_data->{error}) { + $result = $json_data->{error}; + $result->{error} = $self->type('boolean', 1); + $result->{documentation} = REST_DOC; + delete $result->{'name'}; # Remove JSONRPCError + } + elsif (exists $json_data->{result}) { + $result = $json_data->{result}; + } + + # The result needs to be a valid JSON data structure + # and not a undefined or scalar value. + if ( !ref $result + || blessed($result) + || (ref $result ne 'HASH' && ref $result ne 'ARRAY')) + { + $result = {result => $result}; + } + + Bugzilla::Hook::process('webservice_rest_response', + {rpc => $self, result => \$result, response => $response}); + + # Access Control + $response->header("Access-Control-Allow-Origin", "*"); + $response->header("Access-Control-Allow-Headers", + "origin, content-type, accept, x-requested-with"); + + # ETag support + my $etag = $self->bz_etag; + $self->bz_etag($result) if !$etag; + + # If accessing through web browser, then display in readable format + if ($self->content_type eq 'text/html') { + $result = $self->json->pretty->canonical->allow_nonref->encode($result); + + my $template = Bugzilla->template; + $content = ""; + $template->process("rest.html.tmpl", {result => $result}, \$content) + || ThrowTemplateError($template->error()); + + $response->content_type('text/html'); + } + else { + $content = $self->json->encode($result); + } + + $response->content($content); + + $self->SUPER::response($response); } ####################################### @@ -175,36 +175,40 @@ sub response { ####################################### sub handle_login { - my $self = shift; - - # If we're being called using GET, we don't allow cookie-based or Env - # login, because GET requests can be done cross-domain, and we don't - # want private data showing up on another site unless the user - # explicitly gives that site their username and password. (This is - # particularly important for JSONP, which would allow a remote site - # to use private data without the user's knowledge, unless we had this - # protection in place.) We do allow this for GET /login as we need to - # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we - # can also use for Bugzilla_token support. This is OK as it requires - # a login and password to be supplied and will fail if they are not - # valid for the user. - if (!grep($_ eq $self->request->method, ('POST', 'PUT')) - && !($self->bz_class_name eq 'Bugzilla::WebService::User' - && $self->bz_method_name eq 'login')) - { - # XXX There's no particularly good way for us to get a parameter - # to Bugzilla->login at this point, so we pass this information - # around using request_cache, which is a bit of a hack. The - # implementation of it is in Bugzilla::Auth::Login::Stack. - Bugzilla->request_cache->{'auth_no_automatic_login'} = 1; - } - - my $class = $self->bz_class_name; - my $method = $self->bz_method_name; - my $full_method = $class . "." . $method; - - # Bypass JSONRPC::handle_login - Bugzilla::WebService::Server->handle_login($class, $method, $full_method); + my $self = shift; + + # If we're being called using GET, we don't allow cookie-based or Env + # login, because GET requests can be done cross-domain, and we don't + # want private data showing up on another site unless the user + # explicitly gives that site their username and password. (This is + # particularly important for JSONP, which would allow a remote site + # to use private data without the user's knowledge, unless we had this + # protection in place.) We do allow this for GET /login as we need to + # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we + # can also use for Bugzilla_token support. This is OK as it requires + # a login and password to be supplied and will fail if they are not + # valid for the user. + if ( + !grep($_ eq $self->request->method, ('POST', 'PUT')) + && !( + $self->bz_class_name eq 'Bugzilla::WebService::User' + && $self->bz_method_name eq 'login' + ) + ) + { + # XXX There's no particularly good way for us to get a parameter + # to Bugzilla->login at this point, so we pass this information + # around using request_cache, which is a bit of a hack. The + # implementation of it is in Bugzilla::Auth::Login::Stack. + Bugzilla->request_cache->{'auth_no_automatic_login'} = 1; + } + + my $class = $self->bz_class_name; + my $method = $self->bz_method_name; + my $full_method = $class . "." . $method; + + # Bypass JSONRPC::handle_login + Bugzilla::WebService::Server->handle_login($class, $method, $full_method); } ############################ @@ -214,79 +218,78 @@ sub handle_login { # We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure # as it determines the method name differently. sub _find_procedure { - my $self = shift; - if ($self->isa('JSON::RPC::Server::CGI')) { - return JSON::RPC::Server::_find_procedure($self, @_); - } - else { - return JSON::RPC::Legacy::Server::_find_procedure($self, @_); - } + my $self = shift; + if ($self->isa('JSON::RPC::Server::CGI')) { + return JSON::RPC::Server::_find_procedure($self, @_); + } + else { + return JSON::RPC::Legacy::Server::_find_procedure($self, @_); + } } sub _argument_type_check { - my $self = shift; - my $params; - - if ($self->isa('JSON::RPC::Server::CGI')) { - $params = JSON::RPC::Server::_argument_type_check($self, @_); + my $self = shift; + my $params; + + if ($self->isa('JSON::RPC::Server::CGI')) { + $params = JSON::RPC::Server::_argument_type_check($self, @_); + } + else { + $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_); + } + + # JSON-RPC 1.0 requires all parameters to be passed as an array, so + # we just pull out the first item and assume it's an object. + my $params_is_array; + if (ref $params eq 'ARRAY') { + $params = $params->[0]; + $params_is_array = 1; + } + + taint_data($params); + + # Now, convert dateTime fields on input. + my $method = $self->bz_method_name; + my $pkg = $self->{dispatch_path}->{$self->path_info}; + my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []}; + foreach my $field (@date_fields) { + if (defined $params->{$field}) { + my $value = $params->{$field}; + if (ref $value eq 'ARRAY') { + $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value]; + } + else { + $params->{$field} = $self->datetime_format_inbound($value); + } } - else { - $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_); + } + my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []}; + foreach my $field (@base64_fields) { + if (defined $params->{$field}) { + $params->{$field} = decode_base64($params->{$field}); } + } - # JSON-RPC 1.0 requires all parameters to be passed as an array, so - # we just pull out the first item and assume it's an object. - my $params_is_array; - if (ref $params eq 'ARRAY') { - $params = $params->[0]; - $params_is_array = 1; - } + # This is the best time to do login checks. + $self->handle_login(); - taint_data($params); - - # Now, convert dateTime fields on input. - my $method = $self->bz_method_name; - my $pkg = $self->{dispatch_path}->{$self->path_info}; - my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] }; - foreach my $field (@date_fields) { - if (defined $params->{$field}) { - my $value = $params->{$field}; - if (ref $value eq 'ARRAY') { - $params->{$field} = - [ map { $self->datetime_format_inbound($_) } @$value ]; - } - else { - $params->{$field} = $self->datetime_format_inbound($value); - } - } - } - my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] }; - foreach my $field (@base64_fields) { - if (defined $params->{$field}) { - $params->{$field} = decode_base64($params->{$field}); - } - } + # Bugzilla::WebService packages call internal methods like + # $self->_some_private_method. So we have to inherit from + # that class as well as this Server class. + my $new_class = ref($self) . '::' . $pkg; + my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; + eval "package $new_class;$isa_string;"; + bless $self, $new_class; - # This is the best time to do login checks. - $self->handle_login(); + # Allow extensions to modify the params post login + Bugzilla::Hook::process('webservice_rest_request', + {rpc => $self, params => $params}); - # Bugzilla::WebService packages call internal methods like - # $self->_some_private_method. So we have to inherit from - # that class as well as this Server class. - my $new_class = ref($self) . '::' . $pkg; - my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)"; - eval "package $new_class;$isa_string;"; - bless $self, $new_class; + if ($params_is_array) { + $params = [$params]; + } - # Allow extensions to modify the params post login - Bugzilla::Hook::process('webservice_rest_request', - { rpc => $self, params => $params }); - - if ($params_is_array) { - $params = [$params]; - } - - return $params; + return $params; } ################### @@ -294,46 +297,46 @@ sub _argument_type_check { ################### sub bz_method_name { - my ($self, $method) = @_; - $self->{_bz_method_name} = $method if $method; - return $self->{_bz_method_name}; + my ($self, $method) = @_; + $self->{_bz_method_name} = $method if $method; + return $self->{_bz_method_name}; } sub bz_class_name { - my ($self, $class) = @_; - $self->{_bz_class_name} = $class if $class; - return $self->{_bz_class_name}; + my ($self, $class) = @_; + $self->{_bz_class_name} = $class if $class; + return $self->{_bz_class_name}; } sub bz_success_code { - my ($self, $value) = @_; - $self->{_bz_success_code} = $value if $value; - return $self->{_bz_success_code}; + my ($self, $value) = @_; + $self->{_bz_success_code} = $value if $value; + return $self->{_bz_success_code}; } sub bz_rest_params { - my ($self, $params) = @_; - $self->{_bz_rest_params} = $params if $params; - return $self->{_bz_rest_params}; + my ($self, $params) = @_; + $self->{_bz_rest_params} = $params if $params; + return $self->{_bz_rest_params}; } sub bz_rest_options { - my ($self, $options) = @_; - $self->{_bz_rest_options} = $options if $options; - return $self->{_bz_rest_options}; + my ($self, $options) = @_; + $self->{_bz_rest_options} = $options if $options; + return $self->{_bz_rest_options}; } sub rest_include_exclude { - my ($params) = @_; + my ($params) = @_; - if ($params->{'include_fields'} && !ref $params->{'include_fields'}) { - $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ]; - } - if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { - $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ]; - } + if ($params->{'include_fields'} && !ref $params->{'include_fields'}) { + $params->{'include_fields'} = [split(/[\s+,]/, $params->{'include_fields'})]; + } + if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) { + $params->{'exclude_fields'} = [split(/[\s+,]/, $params->{'exclude_fields'})]; + } - return $params; + return $params; } ########################## @@ -341,184 +344,191 @@ sub rest_include_exclude { ########################## sub _retrieve_json_params { - my $self = shift; - - # Make a copy of the current input_params rather than edit directly - my $params = {}; - %{$params} = %{ Bugzilla->input_params }; - - # First add any parameters we were able to pull out of the path - # based on the resource regexp and combine with the normal URL - # parameters. - if (my $rest_params = $self->bz_rest_params) { - foreach my $param (keys %$rest_params) { - # If the param does not already exist or if the - # rest param is a single value, add it to the - # global params. - if (!exists $params->{$param} || !ref $rest_params->{$param}) { - $params->{$param} = $rest_params->{$param}; - } - # If rest_param is a list then add any extra values to the list - elsif (ref $rest_params->{$param}) { - my @extra_values = ref $params->{$param} - ? @{ $params->{$param} } - : ($params->{$param}); - $params->{$param} - = [ uniq (@{ $rest_params->{$param} }, @extra_values) ]; - } - } + my $self = shift; + + # Make a copy of the current input_params rather than edit directly + my $params = {}; + %{$params} = %{Bugzilla->input_params}; + + # First add any parameters we were able to pull out of the path + # based on the resource regexp and combine with the normal URL + # parameters. + if (my $rest_params = $self->bz_rest_params) { + foreach my $param (keys %$rest_params) { + + # If the param does not already exist or if the + # rest param is a single value, add it to the + # global params. + if (!exists $params->{$param} || !ref $rest_params->{$param}) { + $params->{$param} = $rest_params->{$param}; + } + + # If rest_param is a list then add any extra values to the list + elsif (ref $rest_params->{$param}) { + my @extra_values + = ref $params->{$param} ? @{$params->{$param}} : ($params->{$param}); + $params->{$param} = [uniq(@{$rest_params->{$param}}, @extra_values)]; + } + } + } + + # Any parameters passed in in the body of a non-GET request will override + # any parameters pull from the url path. Otherwise non-unique keys are + # combined. + if ($self->request->method ne 'GET') { + my $extra_params = {}; + + # We do this manually because CGI.pm doesn't understand JSON strings. + my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; + if ($json) { + eval { $extra_params = $self->json->decode($json); }; + if ($@) { + ThrowUserError('json_rpc_invalid_params', {err_msg => $@}); + } } - # Any parameters passed in in the body of a non-GET request will override - # any parameters pull from the url path. Otherwise non-unique keys are - # combined. - if ($self->request->method ne 'GET') { - my $extra_params = {}; - # We do this manually because CGI.pm doesn't understand JSON strings. - my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'}; - if ($json) { - eval { $extra_params = $self->json->decode($json); }; - if ($@) { - ThrowUserError('json_rpc_invalid_params', { err_msg => $@ }); - } - } - - # Allow parameters in the query string if request was non-GET. - # Note: parameters in query string body override any matching - # parameters in the request body. - foreach my $param ($self->cgi->url_param()) { - $extra_params->{$param} = $self->cgi->url_param($param); - } - - %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params}; + # Allow parameters in the query string if request was non-GET. + # Note: parameters in query string body override any matching + # parameters in the request body. + foreach my $param ($self->cgi->url_param()) { + $extra_params->{$param} = $self->cgi->url_param($param); } - return $params; + %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params}; + } + + return $params; } sub _find_resource { - my ($self, $path) = @_; - - # Load in the WebService module from the dispatch map and then call - # $module->rest_resources to get the resources array ref. - my $resources = {}; - foreach my $module (values %{ $self->{dispatch_path} }) { - eval("require $module") || die $@; - next if !$module->can('rest_resources'); - $resources->{$module} = $module->rest_resources; - } - - Bugzilla::Hook::process('webservice_rest_resources', - { rpc => $self, resources => $resources }); - - # Use the resources hash from each module loaded earlier to determine - # which handler to use based on a regex match of the CGI path. - # Also any matches found in the regex will be passed in later to the - # handler for possible use. - my $request_method = $self->request->method; - - my (@matches, $handler_found, $handler_method, $handler_class); - foreach my $class (keys %{ $resources }) { - # The resource data for each module needs to be - # an array ref with an even number of elements - # to work correctly. - next if (ref $resources->{$class} ne 'ARRAY' - || scalar @{ $resources->{$class} } % 2 != 0); - - while (my $regex = shift @{ $resources->{$class} }) { - my $options_data = shift @{ $resources->{$class} }; - next if ref $options_data ne 'HASH'; - - if (@matches = ($path =~ $regex)) { - # If a specific path is accompanied by a OPTIONS request - # method, the user is asking for a list of possible request - # methods for a specific path. - $self->bz_rest_options([ keys %{ $options_data } ]); - - if ($options_data->{$request_method}) { - my $resource_data = $options_data->{$request_method}; - $self->bz_class_name($class); - - # The method key/value can be a simple scalar method name - # or a anonymous subroutine so we execute it here. - my $method = ref $resource_data->{method} eq 'CODE' - ? $resource_data->{method}->($self) - : $resource_data->{method}; - $self->bz_method_name($method); - - # Pull out any parameters parsed from the URL path - # and store them for use by the method. - if ($resource_data->{params}) { - $self->bz_rest_params($resource_data->{params}->(@matches)); - } - - # If a special success code is needed for this particular - # method, then store it for later when generating response. - if ($resource_data->{success_code}) { - $self->bz_success_code($resource_data->{success_code}); - } - $handler_found = 1; - } - } - last if $handler_found; + my ($self, $path) = @_; + + # Load in the WebService module from the dispatch map and then call + # $module->rest_resources to get the resources array ref. + my $resources = {}; + foreach my $module (values %{$self->{dispatch_path}}) { + eval("require $module") || die $@; + next if !$module->can('rest_resources'); + $resources->{$module} = $module->rest_resources; + } + + Bugzilla::Hook::process('webservice_rest_resources', + {rpc => $self, resources => $resources}); + + # Use the resources hash from each module loaded earlier to determine + # which handler to use based on a regex match of the CGI path. + # Also any matches found in the regex will be passed in later to the + # handler for possible use. + my $request_method = $self->request->method; + + my (@matches, $handler_found, $handler_method, $handler_class); + foreach my $class (keys %{$resources}) { + + # The resource data for each module needs to be + # an array ref with an even number of elements + # to work correctly. + next + if (ref $resources->{$class} ne 'ARRAY' + || scalar @{$resources->{$class}} % 2 != 0); + + while (my $regex = shift @{$resources->{$class}}) { + my $options_data = shift @{$resources->{$class}}; + next if ref $options_data ne 'HASH'; + + if (@matches = ($path =~ $regex)) { + + # If a specific path is accompanied by a OPTIONS request + # method, the user is asking for a list of possible request + # methods for a specific path. + $self->bz_rest_options([keys %{$options_data}]); + + if ($options_data->{$request_method}) { + my $resource_data = $options_data->{$request_method}; + $self->bz_class_name($class); + + # The method key/value can be a simple scalar method name + # or a anonymous subroutine so we execute it here. + my $method + = ref $resource_data->{method} eq 'CODE' + ? $resource_data->{method}->($self) + : $resource_data->{method}; + $self->bz_method_name($method); + + # Pull out any parameters parsed from the URL path + # and store them for use by the method. + if ($resource_data->{params}) { + $self->bz_rest_params($resource_data->{params}->(@matches)); + } + + # If a special success code is needed for this particular + # method, then store it for later when generating response. + if ($resource_data->{success_code}) { + $self->bz_success_code($resource_data->{success_code}); + } + $handler_found = 1; } - last if $handler_found; + } + last if $handler_found; } + last if $handler_found; + } - return $handler_found; + return $handler_found; } sub _best_content_type { - my ($self, @types) = @_; - return ($self->_simple_content_negotiation(@types))[0] || '*/*'; + my ($self, @types) = @_; + return ($self->_simple_content_negotiation(@types))[0] || '*/*'; } sub _simple_content_negotiation { - my ($self, @types) = @_; - my @accept_types = $self->_get_content_prefs(); - # Return the types as-is if no accept header sent, since sorting will be a no-op. - if (!@accept_types) { - return @types; - } - my $score = sub { $self->_score_type(shift, @accept_types) }; - return sort {$score->($b) <=> $score->($a)} @types; + my ($self, @types) = @_; + my @accept_types = $self->_get_content_prefs(); + + # Return the types as-is if no accept header sent, since sorting will be a no-op. + if (!@accept_types) { + return @types; + } + my $score = sub { $self->_score_type(shift, @accept_types) }; + return sort { $score->($b) <=> $score->($a) } @types; } sub _score_type { - my ($self, $type, @accept_types) = @_; - my $score = scalar(@accept_types); - for my $accept_type (@accept_types) { - return $score if $type eq $accept_type; - $score--; - } - return 0; + my ($self, $type, @accept_types) = @_; + my $score = scalar(@accept_types); + for my $accept_type (@accept_types) { + return $score if $type eq $accept_type; + $score--; + } + return 0; } sub _get_content_prefs { - my $self = shift; - my $default_weight = 1; - my @prefs; - - # Parse the Accept header, and save type name, score, and position. - my @accept_types = split /,/, $self->cgi->http('accept') || ''; - my $order = 0; - for my $accept_type (@accept_types) { - my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/); - my ($name) = ($accept_type =~ m#(\S+/[^;]+)#); - next unless $name; - push @prefs, { name => $name, order => $order++}; - if (defined $weight) { - $prefs[-1]->{score} = $weight; - } else { - $prefs[-1]->{score} = $default_weight; - $default_weight -= 0.001; - } + my $self = shift; + my $default_weight = 1; + my @prefs; + + # Parse the Accept header, and save type name, score, and position. + my @accept_types = split /,/, $self->cgi->http('accept') || ''; + my $order = 0; + for my $accept_type (@accept_types) { + my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/); + my ($name) = ($accept_type =~ m#(\S+/[^;]+)#); + next unless $name; + push @prefs, {name => $name, order => $order++}; + if (defined $weight) { + $prefs[-1]->{score} = $weight; + } + else { + $prefs[-1]->{score} = $default_weight; + $default_weight -= 0.001; } + } - # Sort the types by score, subscore by order, and pull out just the name - @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} || - $a->{order} <=> $b->{order}} @prefs; - return @prefs; + # Sort the types by score, subscore by order, and pull out just the name + @prefs = map { $_->{name} } + sort { $b->{score} <=> $a->{score} || $a->{order} <=> $b->{order} } @prefs; + return @prefs; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm index 3fa8b65cf..5cc25f432 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm @@ -15,150 +15,150 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Bug; BEGIN { - *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/bug$}, { - GET => { - method => 'search', - }, - POST => { - method => 'create', - status_code => STATUS_CREATED - } - }, - qr{^/bug/$}, { - GET => { - method => 'get' - } - }, - qr{^/bug/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - return { ids => [ $_[0] ] }; - } - } - }, - qr{^/bug/([^/]+)/comment$}, { - GET => { - method => 'comments', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - POST => { - method => 'add_comment', - params => sub { - return { id => $_[0] }; - }, - success_code => STATUS_CREATED - } - }, - qr{^/bug/comment/([^/]+)$}, { - GET => { - method => 'comments', - params => sub { - return { comment_ids => [ $_[0] ] }; - } - } - }, - qr{^/bug/comment/tags/([^/]+)$}, { - GET => { - method => 'search_comment_tags', - params => sub { - return { query => $_[0] }; - }, - }, - }, - qr{^/bug/comment/([^/]+)/tags$}, { - PUT => { - method => 'update_comment_tags', - params => sub { - return { comment_id => $_[0] }; - }, - }, - }, - qr{^/bug/([^/]+)/history$}, { - GET => { - method => 'history', - params => sub { - return { ids => [ $_[0] ] }; - }, - } - }, - qr{^/bug/([^/]+)/attachment$}, { - GET => { - method => 'attachments', - params => sub { - return { ids => [ $_[0] ] }; - } - }, - POST => { - method => 'add_attachment', - params => sub { - return { ids => [ $_[0] ] }; - }, - success_code => STATUS_CREATED - } - }, - qr{^/bug/attachment/([^/]+)$}, { - GET => { - method => 'attachments', - params => sub { - return { attachment_ids => [ $_[0] ] }; - } - }, - PUT => { - method => 'update_attachment', - params => sub { - return { ids => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/bug$}, + { + GET => {method => 'search',}, + POST => {method => 'create', status_code => STATUS_CREATED} + }, + qr{^/bug/$}, + {GET => {method => 'get'}}, + qr{^/bug/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + return {ids => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + return {ids => [$_[0]]}; + } + } + }, + qr{^/bug/([^/]+)/comment$}, + { + GET => { + method => 'comments', + params => sub { + return {ids => [$_[0]]}; + } + }, + POST => { + method => 'add_comment', + params => sub { + return {id => $_[0]}; }, - qr{^/field/bug$}, { - GET => { - method => 'fields', - } + success_code => STATUS_CREATED + } + }, + qr{^/bug/comment/([^/]+)$}, + { + GET => { + method => 'comments', + params => sub { + return {comment_ids => [$_[0]]}; + } + } + }, + qr{^/bug/comment/tags/([^/]+)$}, + { + GET => { + method => 'search_comment_tags', + params => sub { + return {query => $_[0]}; }, - qr{^/field/bug/([^/]+)$}, { - GET => { - method => 'fields', - params => sub { - my $value = $_[0]; - my $param = 'names'; - $param = 'ids' if $value =~ /^\d+$/; - return { $param => [ $_[0] ] }; - } - } + }, + }, + qr{^/bug/comment/([^/]+)/tags$}, + { + PUT => { + method => 'update_comment_tags', + params => sub { + return {comment_id => $_[0]}; }, - qr{^/field/bug/([^/]+)/values$}, { - GET => { - method => 'legal_values', - params => sub { - return { field => $_[0] }; - } - } + }, + }, + qr{^/bug/([^/]+)/history$}, + { + GET => { + method => 'history', + params => sub { + return {ids => [$_[0]]}; }, - qr{^/field/bug/([^/]+)/([^/]+)/values$}, { - GET => { - method => 'legal_values', - params => sub { - return { field => $_[0], - product_id => $_[1] }; - } - } + } + }, + qr{^/bug/([^/]+)/attachment$}, + { + GET => { + method => 'attachments', + params => sub { + return {ids => [$_[0]]}; + } + }, + POST => { + method => 'add_attachment', + params => sub { + return {ids => [$_[0]]}; }, - ]; - return $rest_resources; + success_code => STATUS_CREATED + } + }, + qr{^/bug/attachment/([^/]+)$}, + { + GET => { + method => 'attachments', + params => sub { + return {attachment_ids => [$_[0]]}; + } + }, + PUT => { + method => 'update_attachment', + params => sub { + return {ids => [$_[0]]}; + } + } + }, + qr{^/field/bug$}, + {GET => {method => 'fields',}}, + qr{^/field/bug/([^/]+)$}, + { + GET => { + method => 'fields', + params => sub { + my $value = $_[0]; + my $param = 'names'; + $param = 'ids' if $value =~ /^\d+$/; + return {$param => [$_[0]]}; + } + } + }, + qr{^/field/bug/([^/]+)/values$}, + { + GET => { + method => 'legal_values', + params => sub { + return {field => $_[0]}; + } + } + }, + qr{^/field/bug/([^/]+)/([^/]+)/values$}, + { + GET => { + method => 'legal_values', + params => sub { + return {field => $_[0], product_id => $_[1]}; + } + } + }, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm index 8502d6b3b..806c3f9c7 100644 --- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm +++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm @@ -12,27 +12,28 @@ use strict; use warnings; BEGIN { - *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; + *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources; } sub _rest_resources { - return [ - # bug-id - qr{^/bug_user_last_visit/(\d+)$}, { - GET => { - method => 'get', - params => sub { - return { ids => [$_[0]] }; - }, - }, - POST => { - method => 'update', - params => sub { - return { ids => [$_[0]] }; - }, - }, + return [ + # bug-id + qr{^/bug_user_last_visit/(\d+)$}, + { + GET => { + method => 'get', + params => sub { + return {ids => [$_[0]]}; }, - ]; + }, + POST => { + method => 'update', + params => sub { + return {ids => [$_[0]]}; + }, + }, + }, + ]; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm index a8f3f9330..072cfe2f6 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm @@ -15,43 +15,19 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Bugzilla; BEGIN { - *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/version$}, { - GET => { - method => 'version' - } - }, - qr{^/extensions$}, { - GET => { - method => 'extensions' - } - }, - qr{^/timezone$}, { - GET => { - method => 'timezone' - } - }, - qr{^/time$}, { - GET => { - method => 'time' - } - }, - qr{^/last_audit_time$}, { - GET => { - method => 'last_audit_time' - } - }, - qr{^/parameters$}, { - GET => { - method => 'parameters' - } - } - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/version$}, {GET => {method => 'version'}}, + qr{^/extensions$}, {GET => {method => 'extensions'}}, + qr{^/timezone$}, {GET => {method => 'timezone'}}, + qr{^/time$}, {GET => {method => 'time'}}, + qr{^/last_audit_time$}, {GET => {method => 'last_audit_time'}}, + qr{^/parameters$}, {GET => {method => 'parameters'}} + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm index 3f8d32a03..ed65aea5c 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm @@ -15,22 +15,23 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Classification; BEGIN { - *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/classification/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/classification/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Component.pm b/Bugzilla/WebService/Server/REST/Resources/Component.pm index 198c09332..8870a0f04 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Component.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Component.pm @@ -17,19 +17,15 @@ use Bugzilla::WebService::Component; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::Component::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Component::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/component$}, { - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/component$}, + {POST => {method => 'create', success_code => STATUS_CREATED}}, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm index 21dad0f73..438c8fb30 100644 --- a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm +++ b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm @@ -17,43 +17,40 @@ use Bugzilla::WebService::FlagType; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/flag_type$}, { - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/flag_type/([^/]+)/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { product => $_[0], - component => $_[1] }; - } - } - }, - qr{^/flag_type/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - return { product => $_[0] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } - }, - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/flag_type$}, + {POST => {method => 'create', success_code => STATUS_CREATED}}, + qr{^/flag_type/([^/]+)/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + return {product => $_[0], component => $_[1]}; + } + } + }, + qr{^/flag_type/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + return {product => $_[0]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + } + }, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm index b052e384b..7f607b7d1 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Group.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm @@ -15,31 +15,28 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::Group; BEGIN { - *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Group::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/group$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/group/([^/]+)$}, { - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/group$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/group/([^/]+)$}, + { + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm index 607b94b53..eabe19681 100644 --- a/Bugzilla/WebService/Server/REST/Resources/Product.pm +++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm @@ -17,53 +17,41 @@ use Bugzilla::WebService::Product; use Bugzilla::Error; BEGIN { - *Bugzilla::WebService::Product::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::Product::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/product_accessible$}, { - GET => { - method => 'get_accessible_products' - } - }, - qr{^/product_enterable$}, { - GET => { - method => 'get_enterable_products' - } - }, - qr{^/product_selectable$}, { - GET => { - method => 'get_selectable_products' - } - }, - qr{^/product$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/product/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } - }, - ]; - return $rest_resources; + my $rest_resources = [ + qr{^/product_accessible$}, + {GET => {method => 'get_accessible_products'}}, + qr{^/product_enterable$}, + {GET => {method => 'get_enterable_products'}}, + qr{^/product_selectable$}, + {GET => {method => 'get_selectable_products'}}, + qr{^/product$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/product/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + } + }, + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm index a83109e73..4555b4dbc 100644 --- a/Bugzilla/WebService/Server/REST/Resources/User.pm +++ b/Bugzilla/WebService/Server/REST/Resources/User.pm @@ -15,53 +15,41 @@ use Bugzilla::WebService::Constants; use Bugzilla::WebService::User; BEGIN { - *Bugzilla::WebService::User::rest_resources = \&_rest_resources; -}; + *Bugzilla::WebService::User::rest_resources = \&_rest_resources; +} sub _rest_resources { - my $rest_resources = [ - qr{^/login$}, { - GET => { - method => 'login' - } - }, - qr{^/logout$}, { - GET => { - method => 'logout' - } - }, - qr{^/valid_login$}, { - GET => { - method => 'valid_login' - } - }, - qr{^/user$}, { - GET => { - method => 'get' - }, - POST => { - method => 'create', - success_code => STATUS_CREATED - } - }, - qr{^/user/([^/]+)$}, { - GET => { - method => 'get', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - }, - PUT => { - method => 'update', - params => sub { - my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; - return { $param => [ $_[0] ] }; - } - } + my $rest_resources = [ + qr{^/login$}, + {GET => {method => 'login'}}, + qr{^/logout$}, + {GET => {method => 'logout'}}, + qr{^/valid_login$}, + {GET => {method => 'valid_login'}}, + qr{^/user$}, + { + GET => {method => 'get'}, + POST => {method => 'create', success_code => STATUS_CREATED} + }, + qr{^/user/([^/]+)$}, + { + GET => { + method => 'get', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; + } + }, + PUT => { + method => 'update', + params => sub { + my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names'; + return {$param => [$_[0]]}; } - ]; - return $rest_resources; + } + } + ]; + return $rest_resources; } 1; diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm index 8deb253ad..b0eae8e19 100644 --- a/Bugzilla/WebService/Server/XMLRPC.pm +++ b/Bugzilla/WebService/Server/XMLRPC.pm @@ -14,9 +14,10 @@ use warnings; use XMLRPC::Transport::HTTP; use Bugzilla::WebService::Server; if ($ENV{MOD_PERL}) { - our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); -} else { - our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); + our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server); +} +else { + our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server); } use Bugzilla::WebService::Constants; @@ -26,97 +27,99 @@ use Bugzilla::Util; use List::MoreUtils qw(none); BEGIN { - # Allow WebService methods to call XMLRPC::Lite's type method directly - *Bugzilla::WebService::type = sub { - my ($self, $type, $value) = @_; - if ($type eq 'dateTime') { - # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. - # Our "base" implementation is in Bugzilla::WebService::Server. - $value = Bugzilla::WebService::Server->datetime_format_outbound($value); - $value =~ s/-//g; - } - elsif ($type eq 'email') { - $type = 'string'; - if (Bugzilla->params->{'webservice_email_filter'}) { - $value = email_filter($value); - } - } - return XMLRPC::Data->type($type)->value($value); - }; - - # Add support for ETags into XMLRPC WebServices - *Bugzilla::WebService::bz_etag = sub { - return Bugzilla::WebService::Server->bz_etag($_[1]); - }; + # Allow WebService methods to call XMLRPC::Lite's type method directly + *Bugzilla::WebService::type = sub { + my ($self, $type, $value) = @_; + if ($type eq 'dateTime') { + + # This is the XML-RPC implementation, see the README in Bugzilla/WebService/. + # Our "base" implementation is in Bugzilla::WebService::Server. + $value = Bugzilla::WebService::Server->datetime_format_outbound($value); + $value =~ s/-//g; + } + elsif ($type eq 'email') { + $type = 'string'; + if (Bugzilla->params->{'webservice_email_filter'}) { + $value = email_filter($value); + } + } + return XMLRPC::Data->type($type)->value($value); + }; + + # Add support for ETags into XMLRPC WebServices + *Bugzilla::WebService::bz_etag = sub { + return Bugzilla::WebService::Server->bz_etag($_[1]); + }; } sub initialize { - my $self = shift; - my %retval = $self->SUPER::initialize(@_); - $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; - $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; - $retval{'dispatch_with'} = WS_DISPATCH; - return %retval; + my $self = shift; + my %retval = $self->SUPER::initialize(@_); + $retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new; + $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new; + $retval{'dispatch_with'} = WS_DISPATCH; + return %retval; } sub make_response { - my $self = shift; - my $cgi = Bugzilla->cgi; - - # Fix various problems with IIS. - if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { - $ENV{CONTENT_LENGTH} = 0; - binmode(STDOUT, ':bytes'); - } - - $self->SUPER::make_response(@_); - - # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around - # its cookies in Bugzilla::CGI, so we need to copy them over. - foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { - $self->response->headers->push_header('Set-Cookie', $cookie); - } - - # Copy across security related headers from Bugzilla::CGI - foreach my $header (split(/[\r\n]+/, $cgi->header)) { - my ($name, $value) = $header =~ /^([^:]+): (.*)/; - if (!$self->response->headers->header($name)) { - $self->response->headers->header($name => $value); - } - } - - # ETag support - my $etag = $self->bz_etag; - if (!$etag) { - my $data = $self->response->as_string; - $etag = $self->bz_etag($data); - } - - if ($etag && $cgi->check_etag($etag)) { - $self->response->headers->push_header('ETag', $etag); - $self->response->headers->push_header('status', '304 Not Modified'); - } - elsif ($etag) { - $self->response->headers->push_header('ETag', $etag); + my $self = shift; + my $cgi = Bugzilla->cgi; + + # Fix various problems with IIS. + if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) { + $ENV{CONTENT_LENGTH} = 0; + binmode(STDOUT, ':bytes'); + } + + $self->SUPER::make_response(@_); + + # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around + # its cookies in Bugzilla::CGI, so we need to copy them over. + foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) { + $self->response->headers->push_header('Set-Cookie', $cookie); + } + + # Copy across security related headers from Bugzilla::CGI + foreach my $header (split(/[\r\n]+/, $cgi->header)) { + my ($name, $value) = $header =~ /^([^:]+): (.*)/; + if (!$self->response->headers->header($name)) { + $self->response->headers->header($name => $value); } + } + + # ETag support + my $etag = $self->bz_etag; + if (!$etag) { + my $data = $self->response->as_string; + $etag = $self->bz_etag($data); + } + + if ($etag && $cgi->check_etag($etag)) { + $self->response->headers->push_header('ETag', $etag); + $self->response->headers->push_header('status', '304 Not Modified'); + } + elsif ($etag) { + $self->response->headers->push_header('ETag', $etag); + } } sub handle_login { - my ($self, $classes, $action, $uri, $method) = @_; - my $class = $classes->{$uri}; - my $full_method = $uri . "." . $method; - # Only allowed methods to be used from the module's whitelist - my $file = $class; - $file =~ s{::}{/}g; - $file .= ".pm"; - require $file; - if (none { $_ eq $method } $class->PUBLIC_METHODS) { - ThrowCodeError('unknown_method', { method => $full_method }); - } - - $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; - $self->SUPER::handle_login($class, $method, $full_method); - return; + my ($self, $classes, $action, $uri, $method) = @_; + my $class = $classes->{$uri}; + my $full_method = $uri . "." . $method; + + # Only allowed methods to be used from the module's whitelist + my $file = $class; + $file =~ s{::}{/}g; + $file .= ".pm"; + require $file; + if (none { $_ eq $method } $class->PUBLIC_METHODS) { + ThrowCodeError('unknown_method', {method => $full_method}); + } + + $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/; + $self->SUPER::handle_login($class, $method, $full_method); + return; } 1; @@ -140,100 +143,111 @@ use Bugzilla::WebService::Util qw(fix_credentials); use Scalar::Util qw(tainted); sub new { - my $self = shift->SUPER::new(@_); - # Initialise XML::Parser to not expand references to entities, to prevent DoS - require XML::Parser; - my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } ); - $self->{_parser}->parser($parser, $parser); - return $self; + my $self = shift->SUPER::new(@_); + + # Initialise XML::Parser to not expand references to entities, to prevent DoS + require XML::Parser; + my $parser = XML::Parser->new( + NoExpand => 1, + Handlers => { + Default => sub { } + } + ); + $self->{_parser}->parser($parser, $parser); + return $self; } sub deserialize { - my $self = shift; - - # Only allow certain content types to protect against CSRF attacks - my $content_type = lc($ENV{'CONTENT_TYPE'}); - # Remove charset, etc, if provided - $content_type =~ s/^([^;]+);.*/$1/; - if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { - ThrowUserError('xmlrpc_illegal_content_type', - { content_type => $ENV{'CONTENT_TYPE'} }); - } + my $self = shift; - my ($xml) = @_; - my $som = $self->SUPER::deserialize(@_); - if (tainted($xml)) { - $som->{_bz_do_taint} = 1; - } - bless $som, 'Bugzilla::XMLRPC::SOM'; - my $params = $som->paramsin; - # This allows positional parameters for Testopia. - $params = {} if ref $params ne 'HASH'; + # Only allow certain content types to protect against CSRF attacks + my $content_type = lc($ENV{'CONTENT_TYPE'}); + + # Remove charset, etc, if provided + $content_type =~ s/^([^;]+);.*/$1/; + if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) { + ThrowUserError('xmlrpc_illegal_content_type', + {content_type => $ENV{'CONTENT_TYPE'}}); + } - # Update the params to allow for several convenience key/values - # use for authentication - fix_credentials($params); + my ($xml) = @_; + my $som = $self->SUPER::deserialize(@_); + if (tainted($xml)) { + $som->{_bz_do_taint} = 1; + } + bless $som, 'Bugzilla::XMLRPC::SOM'; + my $params = $som->paramsin; - Bugzilla->input_params($params); + # This allows positional parameters for Testopia. + $params = {} if ref $params ne 'HASH'; - return $som; + # Update the params to allow for several convenience key/values + # use for authentication + fix_credentials($params); + + Bugzilla->input_params($params); + + return $som; } # Some method arguments need to be converted in some way, when they are input. sub decode_value { - my $self = shift; - my ($type) = @{ $_[0] }; - my $value = $self->SUPER::decode_value(@_); - - # We only validate/convert certain types here. - return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; - - # Though the XML-RPC standard doesn't allow an empty <int>, - # <double>,or <dateTime.iso8601>, we do, and we just say - # "that's undef". - if (grep($type eq $_, qw(int double dateTime))) { - return undef if $value eq ''; - } - - my $validator = $self->_validation_subs->{$type}; - if (!$validator->($value)) { - ThrowUserError('xmlrpc_invalid_value', - { type => $type, value => $value }); - } - - # We convert dateTimes to a DB-friendly date format. - if ($type eq 'dateTime.iso8601') { - if ($value !~ /T.*[\-+Z]/i) { - # The caller did not specify a timezone, so we assume UTC. - # pass 'Z' specifier to datetime_from to force it - $value = $value . 'Z'; - } - $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + my $self = shift; + my ($type) = @{$_[0]}; + my $value = $self->SUPER::decode_value(@_); + + # We only validate/convert certain types here. + return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/; + + # Though the XML-RPC standard doesn't allow an empty <int>, + # <double>,or <dateTime.iso8601>, we do, and we just say + # "that's undef". + if (grep($type eq $_, qw(int double dateTime))) { + return undef if $value eq ''; + } + + my $validator = $self->_validation_subs->{$type}; + if (!$validator->($value)) { + ThrowUserError('xmlrpc_invalid_value', {type => $type, value => $value}); + } + + # We convert dateTimes to a DB-friendly date format. + if ($type eq 'dateTime.iso8601') { + if ($value !~ /T.*[\-+Z]/i) { + + # The caller did not specify a timezone, so we assume UTC. + # pass 'Z' specifier to datetime_from to force it + $value = $value . 'Z'; } + $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value); + } - return $value; + return $value; } sub _validation_subs { - my $self = shift; - return $self->{_validation_subs} if $self->{_validation_subs}; - # The only place that XMLRPC::Lite stores any sort of validation - # regex is in XMLRPC::Serializer. We want to re-use those regexes here. - my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; - - # $lookup is a hash whose values are arrayrefs, and whose keys are the - # names of types. The second item of each arrayref is a subroutine - # that will do our validation for us. - my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); - # Add a boolean validator - $validators{'boolean'} = sub {$_[0] =~ /^[01]$/}; - # Some types have multiple names, or have a different name in - # XMLRPC::Serializer than their standard XML-RPC name. - $validators{'dateTime.iso8601'} = $validators{'dateTime'}; - $validators{'i4'} = $validators{'int'}; - - $self->{_validation_subs} = \%validators; - return \%validators; + my $self = shift; + return $self->{_validation_subs} if $self->{_validation_subs}; + + # The only place that XMLRPC::Lite stores any sort of validation + # regex is in XMLRPC::Serializer. We want to re-use those regexes here. + my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup; + + # $lookup is a hash whose values are arrayrefs, and whose keys are the + # names of types. The second item of each arrayref is a subroutine + # that will do our validation for us. + my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup); + + # Add a boolean validator + $validators{'boolean'} = sub { $_[0] =~ /^[01]$/ }; + + # Some types have multiple names, or have a different name in + # XMLRPC::Serializer than their standard XML-RPC name. + $validators{'dateTime.iso8601'} = $validators{'dateTime'}; + $validators{'i4'} = $validators{'int'}; + + $self->{_validation_subs} = \%validators; + return \%validators; } 1; @@ -249,16 +263,16 @@ our @ISA = qw(XMLRPC::SOM); use Bugzilla::WebService::Util qw(taint_data); sub paramsin { - my $self = shift; - if (!$self->{bz_params_in}) { - my @params = $self->SUPER::paramsin(@_); - if ($self->{_bz_do_taint}) { - taint_data(@params); - } - $self->{bz_params_in} = \@params; + my $self = shift; + if (!$self->{bz_params_in}) { + my @params = $self->SUPER::paramsin(@_); + if ($self->{_bz_do_taint}) { + taint_data(@params); } - my $params = $self->{bz_params_in}; - return wantarray ? @$params : $params->[0]; + $self->{bz_params_in} = \@params; + } + my $params = $self->{bz_params_in}; + return wantarray ? @$params : $params->[0]; } 1; @@ -272,43 +286,46 @@ use strict; use warnings; use Scalar::Util qw(blessed reftype); + # We can't use "use parent" because XMLRPC::Serializer doesn't return # a true value. use XMLRPC::Lite; our @ISA = qw(XMLRPC::Serializer); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - # This fixes UTF-8. - $self->{'_typelookup'}->{'base64'} = - [10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/}, - 'as_base64']; - # This makes arrays work right even though we're a subclass. - # (See http://rt.cpan.org//Ticket/Display.html?id=34514) - $self->{'_encodingStyle'} = ''; - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + + # This fixes UTF-8. + $self->{'_typelookup'}->{'base64'} = [ + 10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/ }, + 'as_base64' + ]; + + # This makes arrays work right even though we're a subclass. + # (See http://rt.cpan.org//Ticket/Display.html?id=34514) + $self->{'_encodingStyle'} = ''; + return $self; } # Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension. sub encode_object { - my $self = shift; - my @encoded = $self->SUPER::encode_object(@_); + my $self = shift; + my @encoded = $self->SUPER::encode_object(@_); - return $encoded[0]->[0] eq 'nil' - ? ['value', {}, [@encoded]] - : @encoded; + return $encoded[0]->[0] eq 'nil' ? ['value', {}, [@encoded]] : @encoded; } # Removes undefined values so they do not produce invalid XMLRPC. sub envelope { - my $self = shift; - my ($type, $method, $data) = @_; - # If the type isn't a successful response we don't want to change the values. - if ($type eq 'response') { - _strip_undefs($data); - } - return $self->SUPER::envelope($type, $method, $data); + my $self = shift; + my ($type, $method, $data) = @_; + + # If the type isn't a successful response we don't want to change the values. + if ($type eq 'response') { + _strip_undefs($data); + } + return $self->SUPER::envelope($type, $method, $data); } # In an XMLRPC response we have to handle hashes of arrays, hashes, scalars, @@ -316,58 +333,58 @@ sub envelope { # The whole XMLRPC::Data object must be removed if its value key is undefined # so it cannot be recursed like the other hash type objects. sub _strip_undefs { - my ($initial) = @_; - my $type = reftype($initial) or return; - - if ($type eq "HASH") { - while (my ($key, $value) = each(%$initial)) { - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the hash. - delete $initial->{$key}; - } - else { - _strip_undefs($value); - } - } + my ($initial) = @_; + my $type = reftype($initial) or return; + + if ($type eq "HASH") { + while (my ($key, $value) = each(%$initial)) { + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the hash. + delete $initial->{$key}; + } + else { + _strip_undefs($value); + } } - elsif ($type eq "ARRAY") { - for (my $count = 0; $count < scalar @{$initial}; $count++) { - my $value = $initial->[$count]; - if ( !defined $value - || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) ) - { - # If the value is undefined remove it from the array. - splice(@$initial, $count, 1); - $count--; - } - else { - _strip_undefs($value); - } - } + } + elsif ($type eq "ARRAY") { + for (my $count = 0; $count < scalar @{$initial}; $count++) { + my $value = $initial->[$count]; + if (!defined $value + || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value)) + { + # If the value is undefined remove it from the array. + splice(@$initial, $count, 1); + $count--; + } + else { + _strip_undefs($value); + } } + } } sub BEGIN { - no strict 'refs'; - for my $type (qw(double i4 int dateTime)) { - my $method = 'as_' . $type; - *$method = sub { - my ($self, $value) = @_; - if (!defined($value)) { - return as_nil(); - } - else { - my $super_method = "SUPER::$method"; - return $self->$super_method($value); - } - } - } + no strict 'refs'; + for my $type (qw(double i4 int dateTime)) { + my $method = 'as_' . $type; + *$method = sub { + my ($self, $value) = @_; + if (!defined($value)) { + return as_nil(); + } + else { + my $super_method = "SUPER::$method"; + return $self->$super_method($value); + } + } + } } sub as_nil { - return ['nil', {}]; + return ['nil', {}]; } 1; diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index 0ae76d70f..3fcc929ce 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -18,40 +18,35 @@ use Bugzilla::Error; use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Util qw(trim detaint_natural); -use Bugzilla::WebService::Util qw(filter filter_wants validate translate params_to_objects); +use Bugzilla::WebService::Util + qw(filter filter_wants validate translate params_to_objects); use List::Util qw(first min); # Don't need auth to login -use constant LOGIN_EXEMPT => { - login => 1, - offer_account_by_email => 1, -}; +use constant LOGIN_EXEMPT => {login => 1, offer_account_by_email => 1,}; use constant READ_ONLY => qw( - get + get ); use constant PUBLIC_METHODS => qw( - create - get - login - logout - offer_account_by_email - update - valid_login + create + get + login + logout + offer_account_by_email + update + valid_login ); -use constant MAPPED_FIELDS => { - email => 'login', - full_name => 'name', - login_denied_text => 'disabledtext', -}; +use constant MAPPED_FIELDS => + {email => 'login', full_name => 'name', login_denied_text => 'disabledtext',}; use constant MAPPED_RETURNS => { - login_name => 'email', - realname => 'full_name', - disabledtext => 'login_denied_text', + login_name => 'email', + realname => 'full_name', + disabledtext => 'login_denied_text', }; ############## @@ -59,38 +54,38 @@ use constant MAPPED_RETURNS => { ############## sub login { - my ($self, $params) = @_; + my ($self, $params) = @_; - # Check to see if we are already logged in - my $user = Bugzilla->user; - if ($user->id) { - return $self->_login_to_hash($user); - } + # Check to see if we are already logged in + my $user = Bugzilla->user; + if ($user->id) { + return $self->_login_to_hash($user); + } - # Username and password params are required - foreach my $param ("login", "password") { - (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) - || ThrowCodeError('param_required', { param => $param }); - } + # Username and password params are required + foreach my $param ("login", "password") { + (defined $params->{$param} || defined $params->{'Bugzilla_' . $param}) + || ThrowCodeError('param_required', {param => $param}); + } - $user = Bugzilla->login(); - return $self->_login_to_hash($user); + $user = Bugzilla->login(); + return $self->_login_to_hash($user); } sub logout { - my $self = shift; - Bugzilla->logout; + my $self = shift; + Bugzilla->logout; } sub valid_login { - my ($self, $params) = @_; - defined $params->{login} - || ThrowCodeError('param_required', { param => 'login' }); - Bugzilla->login(); - if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { - return $self->type('boolean', 1); - } - return $self->type('boolean', 0); + my ($self, $params) = @_; + defined $params->{login} + || ThrowCodeError('param_required', {param => 'login'}); + Bugzilla->login(); + if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) { + return $self->type('boolean', 1); + } + return $self->type('boolean', 0); } ################# @@ -98,168 +93,169 @@ sub valid_login { ################# sub offer_account_by_email { - my $self = shift; - my ($params) = @_; - my $email = trim($params->{email}) - || ThrowCodeError('param_required', { param => 'email' }); - - Bugzilla->user->check_account_creation_enabled; - Bugzilla->user->check_and_send_account_creation_confirmation($email); - return undef; + my $self = shift; + my ($params) = @_; + my $email = trim($params->{email}) + || ThrowCodeError('param_required', {param => 'email'}); + + Bugzilla->user->check_account_creation_enabled; + Bugzilla->user->check_and_send_account_creation_confirmation($email); + return undef; } sub create { - my $self = shift; - my ($params) = @_; - - Bugzilla->user->in_group('editusers') - || ThrowUserError("auth_failure", { group => "editusers", - action => "add", - object => "users"}); - - my $email = trim($params->{email}) - || ThrowCodeError('param_required', { param => 'email' }); - my $realname = trim($params->{full_name}); - my $password = trim($params->{password}) || '*'; - - my $user = Bugzilla::User->create({ - login_name => $email, - realname => $realname, - cryptpassword => $password - }); - - return { id => $self->type('int', $user->id) }; + my $self = shift; + my ($params) = @_; + + Bugzilla->user->in_group('editusers') + || ThrowUserError("auth_failure", + {group => "editusers", action => "add", object => "users"}); + + my $email = trim($params->{email}) + || ThrowCodeError('param_required', {param => 'email'}); + my $realname = trim($params->{full_name}); + my $password = trim($params->{password}) || '*'; + + my $user = Bugzilla::User->create( + {login_name => $email, realname => $realname, cryptpassword => $password}); + + return {id => $self->type('int', $user->id)}; } -# function to return user information by passing either user ids or +# function to return user information by passing either user ids or # login names or both together: -# $call = $rpc->call( 'User.get', { ids => [1,2,3], +# $call = $rpc->call( 'User.get', { ids => [1,2,3], # names => ['testusera@redhat.com', 'testuserb@redhat.com'] }); sub get { - my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); - - Bugzilla->switch_to_shadow_db(); - - defined($params->{names}) || defined($params->{ids}) - || defined($params->{match}) - || ThrowCodeError('params_required', - { function => 'User.get', params => ['ids', 'names', 'match'] }); - - my @user_objects; - @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} } - if $params->{names}; - - # start filtering to remove duplicate user ids - my %unique_users = map { $_->id => $_ } @user_objects; - @user_objects = values %unique_users; - - my @users; - - # If the user is not logged in: Return an error if they passed any user ids. - # Otherwise, return a limited amount of information based on login names. - if (!Bugzilla->user->id){ - if ($params->{ids}){ - ThrowUserError("user_access_by_id_denied"); - } - if ($params->{match}) { - ThrowUserError('user_access_by_match_denied'); - } - my $in_group = $self->_filter_users_by_group( - \@user_objects, $params); - @users = map { filter $params, { - id => $self->type('int', $_->id), - real_name => $self->type('string', $_->name), - name => $self->type('email', $_->login), - } } @$in_group; - - return { users => \@users }; - } + my ($self, $params) + = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups'); - my $obj_by_ids; - $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; - - # obj_by_ids are only visible to the user if they can see - # the otheruser, for non visible otheruser throw an error - foreach my $obj (@$obj_by_ids) { - if (Bugzilla->user->can_see_user($obj)){ - if (!$unique_users{$obj->id}) { - push (@user_objects, $obj); - $unique_users{$obj->id} = $obj; - } - } - else { - ThrowUserError('auth_failure', {reason => "not_visible", - action => "access", - object => "user", - userid => $obj->id}); - } - } + Bugzilla->switch_to_shadow_db(); - # User Matching - my $limit = Bugzilla->params->{maxusermatches}; - if ($params->{limit}) { - detaint_natural($params->{limit}) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::WebService::User::match', - param => 'limit' }); - $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; - } + defined($params->{names}) + || defined($params->{ids}) + || defined($params->{match}) + || ThrowCodeError('params_required', + {function => 'User.get', params => ['ids', 'names', 'match']}); - my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; - foreach my $match_string (@{ $params->{'match'} || [] }) { - my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); - foreach my $user (@$matched) { - if (!$unique_users{$user->id}) { - push(@user_objects, $user); - $unique_users{$user->id} = $user; - } - } - } + my @user_objects; + @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}} + if $params->{names}; + + # start filtering to remove duplicate user ids + my %unique_users = map { $_->id => $_ } @user_objects; + @user_objects = values %unique_users; + my @users; + + # If the user is not logged in: Return an error if they passed any user ids. + # Otherwise, return a limited amount of information based on login names. + if (!Bugzilla->user->id) { + if ($params->{ids}) { + ThrowUserError("user_access_by_id_denied"); + } + if ($params->{match}) { + ThrowUserError('user_access_by_match_denied'); + } my $in_group = $self->_filter_users_by_group(\@user_objects, $params); - foreach my $user (@$in_group) { - my $user_info = filter $params, { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), - email => $self->type('email', $user->email), - can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), - }; - - if (Bugzilla->user->in_group('editusers')) { - $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled); - $user_info->{login_denied_text} = $self->type('string', $user->disabledtext); + @users = map { + filter $params, + { + id => $self->type('int', $_->id), + real_name => $self->type('string', $_->name), + name => $self->type('email', $_->login), } - - if (Bugzilla->user->id == $user->id) { - if (filter_wants($params, 'saved_searches')) { - $user_info->{saved_searches} = [ - map { $self->_query_to_hash($_) } @{ $user->queries } - ]; - } - if (filter_wants($params, 'saved_reports')) { - $user_info->{saved_reports} = [ - map { $self->_report_to_hash($_) } @{ $user->reports } - ]; - } + } @$in_group; + + return {users => \@users}; + } + + my $obj_by_ids; + $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids}; + + # obj_by_ids are only visible to the user if they can see + # the otheruser, for non visible otheruser throw an error + foreach my $obj (@$obj_by_ids) { + if (Bugzilla->user->can_see_user($obj)) { + if (!$unique_users{$obj->id}) { + push(@user_objects, $obj); + $unique_users{$obj->id} = $obj; + } + } + else { + ThrowUserError( + 'auth_failure', + { + reason => "not_visible", + action => "access", + object => "user", + userid => $obj->id } + ); + } + } + + # User Matching + my $limit = Bugzilla->params->{maxusermatches}; + if ($params->{limit}) { + detaint_natural($params->{limit}) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::WebService::User::match', param => 'limit'}); + $limit = $limit ? min($params->{limit}, $limit) : $params->{limit}; + } + + my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1; + foreach my $match_string (@{$params->{'match'} || []}) { + my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled); + foreach my $user (@$matched) { + if (!$unique_users{$user->id}) { + push(@user_objects, $user); + $unique_users{$user->id} = $user; + } + } + } + + my $in_group = $self->_filter_users_by_group(\@user_objects, $params); + foreach my $user (@$in_group) { + my $user_info = filter $params, + { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + email => $self->type('email', $user->email), + can_login => $self->type('boolean', $user->is_enabled ? 1 : 0), + }; + + if (Bugzilla->user->in_group('editusers')) { + $user_info->{email_enabled} = $self->type('boolean', $user->email_enabled); + $user_info->{login_denied_text} = $self->type('string', $user->disabledtext); + } - if (filter_wants($params, 'groups')) { - if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { - $user_info->{groups} = [ - map { $self->_group_to_hash($_) } @{ $user->groups } - ]; - } - else { - $user_info->{groups} = $self->_filter_bless_groups($user->groups); - } - } + if (Bugzilla->user->id == $user->id) { + if (filter_wants($params, 'saved_searches')) { + $user_info->{saved_searches} + = [map { $self->_query_to_hash($_) } @{$user->queries}]; + } + if (filter_wants($params, 'saved_reports')) { + $user_info->{saved_reports} + = [map { $self->_report_to_hash($_) } @{$user->reports}]; + } + } - push(@users, $user_info); + if (filter_wants($params, 'groups')) { + if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) { + $user_info->{groups} = [map { $self->_group_to_hash($_) } @{$user->groups}]; + } + else { + $user_info->{groups} = $self->_filter_bless_groups($user->groups); + } } - return { users => \@users }; + push(@users, $user_info); + } + + return {users => \@users}; } ############### @@ -267,156 +263,157 @@ sub get { ############### sub update { - my ($self, $params) = @_; - - my $dbh = Bugzilla->dbh; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; - # Reject access if there is no sense in continuing. - $user->in_group('editusers') - || ThrowUserError("auth_failure", {group => "editusers", - action => "edit", - object => "users"}); + my $user = Bugzilla->login(LOGIN_REQUIRED); - defined($params->{names}) || defined($params->{ids}) - || ThrowCodeError('params_required', - { function => 'User.update', params => ['ids', 'names'] }); + # Reject access if there is no sense in continuing. + $user->in_group('editusers') + || ThrowUserError("auth_failure", + {group => "editusers", action => "edit", object => "users"}); - my $user_objects = params_to_objects($params, 'Bugzilla::User'); + defined($params->{names}) + || defined($params->{ids}) + || ThrowCodeError('params_required', + {function => 'User.update', params => ['ids', 'names']}); - my $values = translate($params, MAPPED_FIELDS); + my $user_objects = params_to_objects($params, 'Bugzilla::User'); - # We delete names and ids to keep only new values to set. - delete $values->{names}; - delete $values->{ids}; + my $values = translate($params, MAPPED_FIELDS); - $dbh->bz_start_transaction(); - foreach my $user (@$user_objects){ - $user->set_all($values); - } + # We delete names and ids to keep only new values to set. + delete $values->{names}; + delete $values->{ids}; - my %changes; - foreach my $user (@$user_objects){ - my $returned_changes = $user->update(); - $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); - } - $dbh->bz_commit_transaction(); - - my @result; - foreach my $user (@$user_objects) { - my %hash = ( - id => $user->id, - changes => {}, - ); - - foreach my $field (keys %{ $changes{$user->id} }) { - my $change = $changes{$user->id}->{$field}; - # We normalize undef to an empty string, so that the API - # stays consistent for things that can become empty. - $change->[0] = '' if !defined $change->[0]; - $change->[1] = '' if !defined $change->[1]; - # We also flatten arrays (used by groups and blessed_groups) - $change->[0] = join(',', @{$change->[0]}) if ref $change->[0]; - $change->[1] = join(',', @{$change->[1]}) if ref $change->[1]; - - $hash{changes}{$field} = { - removed => $self->type('string', $change->[0]), - added => $self->type('string', $change->[1]) - }; - } + $dbh->bz_start_transaction(); + foreach my $user (@$user_objects) { + $user->set_all($values); + } - push(@result, \%hash); - } + my %changes; + foreach my $user (@$user_objects) { + my $returned_changes = $user->update(); + $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS); + } + $dbh->bz_commit_transaction(); - return { users => \@result }; -} + my @result; + foreach my $user (@$user_objects) { + my %hash = (id => $user->id, changes => {},); -sub _filter_users_by_group { - my ($self, $users, $params) = @_; - my ($group_ids, $group_names) = @$params{qw(group_ids groups)}; + foreach my $field (keys %{$changes{$user->id}}) { + my $change = $changes{$user->id}->{$field}; - # If no groups are specified, we return all users. - return $users if (!$group_ids and !$group_names); + # We normalize undef to an empty string, so that the API + # stays consistent for things that can become empty. + $change->[0] = '' if !defined $change->[0]; + $change->[1] = '' if !defined $change->[1]; - my $user = Bugzilla->user; - my (@groups, %groups); + # We also flatten arrays (used by groups and blessed_groups) + $change->[0] = join(',', @{$change->[0]}) if ref $change->[0]; + $change->[1] = join(',', @{$change->[1]}) if ref $change->[1]; - if ($group_ids) { - @groups = map { Bugzilla::Group->check({ id => $_ }) } @$group_ids; - $groups{$_->id} = $_ foreach @groups; + $hash{changes}{$field} = { + removed => $self->type('string', $change->[0]), + added => $self->type('string', $change->[1]) + }; } - if ($group_names) { - foreach my $name (@$group_names) { - my $group = Bugzilla::Group->check({ name => $name, _error => 'invalid_group_name' }); - $user->in_group($group) || ThrowUserError('invalid_group_name', { name => $name }); - $groups{$group->id} = $group; - } + + push(@result, \%hash); + } + + return {users => \@result}; +} + +sub _filter_users_by_group { + my ($self, $users, $params) = @_; + my ($group_ids, $group_names) = @$params{qw(group_ids groups)}; + + # If no groups are specified, we return all users. + return $users if (!$group_ids and !$group_names); + + my $user = Bugzilla->user; + my (@groups, %groups); + + if ($group_ids) { + @groups = map { Bugzilla::Group->check({id => $_}) } @$group_ids; + $groups{$_->id} = $_ foreach @groups; + } + if ($group_names) { + foreach my $name (@$group_names) { + my $group + = Bugzilla::Group->check({name => $name, _error => 'invalid_group_name'}); + $user->in_group($group) + || ThrowUserError('invalid_group_name', {name => $name}); + $groups{$group->id} = $group; } - @groups = values %groups; + } + @groups = values %groups; - my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users; - return \@in_group; + my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users; + return \@in_group; } sub _user_in_any_group { - my ($self, $user, $groups) = @_; - foreach my $group (@$groups) { - return 1 if $user->in_group($group); - } - return 0; + my ($self, $user, $groups) = @_; + foreach my $group (@$groups) { + return 1 if $user->in_group($group); + } + return 0; } sub _filter_bless_groups { - my ($self, $groups) = @_; - my $user = Bugzilla->user; + my ($self, $groups) = @_; + my $user = Bugzilla->user; - my @filtered_groups; - foreach my $group (@$groups) { - next unless $user->can_bless($group->id); - push(@filtered_groups, $self->_group_to_hash($group)); - } + my @filtered_groups; + foreach my $group (@$groups) { + next unless $user->can_bless($group->id); + push(@filtered_groups, $self->_group_to_hash($group)); + } - return \@filtered_groups; + return \@filtered_groups; } sub _group_to_hash { - my ($self, $group) = @_; - my $item = { - id => $self->type('int', $group->id), - name => $self->type('string', $group->name), - description => $self->type('string', $group->description), - }; - return $item; + my ($self, $group) = @_; + my $item = { + id => $self->type('int', $group->id), + name => $self->type('string', $group->name), + description => $self->type('string', $group->description), + }; + return $item; } sub _query_to_hash { - my ($self, $query) = @_; - my $item = { - id => $self->type('int', $query->id), - name => $self->type('string', $query->name), - query => $self->type('string', $query->url), - }; - return $item; + my ($self, $query) = @_; + my $item = { + id => $self->type('int', $query->id), + name => $self->type('string', $query->name), + query => $self->type('string', $query->url), + }; + return $item; } sub _report_to_hash { - my ($self, $report) = @_; - my $item = { - id => $self->type('int', $report->id), - name => $self->type('string', $report->name), - query => $self->type('string', $report->query), - }; - return $item; + my ($self, $report) = @_; + my $item = { + id => $self->type('int', $report->id), + name => $self->type('string', $report->name), + query => $self->type('string', $report->query), + }; + return $item; } sub _login_to_hash { - my ($self, $user) = @_; - my $item = { id => $self->type('int', $user->id) }; - if ($user->{_login_token}) { - $item->{'token'} = $user->id . "-" . $user->{_login_token}; - } - return $item; + my ($self, $user) = @_; + my $item = {id => $self->type('int', $user->id)}; + if ($user->{_login_token}) { + $item->{'token'} = $user->id . "-" . $user->{_login_token}; + } + return $item; } 1; diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index a879c0e0d..3e70921b3 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -25,269 +25,277 @@ use parent qw(Exporter); require Test::Taint; our @EXPORT_OK = qw( - extract_flags - filter - filter_wants - taint_data - validate - translate - params_to_objects - fix_credentials + extract_flags + filter + filter_wants + taint_data + validate + translate + params_to_objects + fix_credentials ); sub extract_flags { - my ($flags, $bug, $attachment) = @_; - my (@new_flags, @old_flags); + my ($flags, $bug, $attachment) = @_; + my (@new_flags, @old_flags); - my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types; - my $current_flags = $attachment ? $attachment->flags : $bug->flags; + my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types; + my $current_flags = $attachment ? $attachment->flags : $bug->flags; - # Copy the user provided $flags as we may call extract_flags more than - # once when editing multiple bugs or attachments. - my $flags_copy = dclone($flags); + # Copy the user provided $flags as we may call extract_flags more than + # once when editing multiple bugs or attachments. + my $flags_copy = dclone($flags); - foreach my $flag (@$flags_copy) { - my $id = $flag->{id}; - my $type_id = $flag->{type_id}; + foreach my $flag (@$flags_copy) { + my $id = $flag->{id}; + my $type_id = $flag->{type_id}; - my $new = delete $flag->{new}; - my $name = delete $flag->{name}; + my $new = delete $flag->{new}; + my $name = delete $flag->{name}; - if ($id) { - my $flag_obj = grep($id == $_->id, @$current_flags); - $flag_obj || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::Flag', id => $id }); - } - elsif ($type_id) { - my $type_obj = grep($type_id == $_->id, @$flag_types); - $type_obj || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::FlagType', id => $type_id }); - if (!$new) { - my @flag_matches = grep($type_id == $_->type->id, @$current_flags); - @flag_matches > 1 && ThrowUserError('flag_not_unique', - { value => $type_id }); - if (!@flag_matches) { - delete $flag->{id}; - } - else { - delete $flag->{type_id}; - $flag->{id} = $flag_matches[0]->id; - } - } + if ($id) { + my $flag_obj = grep($id == $_->id, @$current_flags); + $flag_obj + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::Flag', id => $id}); + } + elsif ($type_id) { + my $type_obj = grep($type_id == $_->id, @$flag_types); + $type_obj + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::FlagType', id => $type_id}); + if (!$new) { + my @flag_matches = grep($type_id == $_->type->id, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $type_id}); + if (!@flag_matches) { + delete $flag->{id}; } - elsif ($name) { - my @type_matches = grep($name eq $_->name, @$flag_types); - @type_matches > 1 && ThrowUserError('flag_type_not_unique', - { value => $name }); - @type_matches || ThrowUserError('object_does_not_exist', - { class => 'Bugzilla::FlagType', name => $name }); - if ($new) { - delete $flag->{id}; - $flag->{type_id} = $type_matches[0]->id; - } - else { - my @flag_matches = grep($name eq $_->type->name, @$current_flags); - @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name }); - if (@flag_matches) { - $flag->{id} = $flag_matches[0]->id; - } - else { - delete $flag->{id}; - $flag->{type_id} = $type_matches[0]->id; - } - } + else { + delete $flag->{type_id}; + $flag->{id} = $flag_matches[0]->id; } - - if ($flag->{id}) { - push(@old_flags, $flag); + } + } + elsif ($name) { + my @type_matches = grep($name eq $_->name, @$flag_types); + @type_matches > 1 && ThrowUserError('flag_type_not_unique', {value => $name}); + @type_matches + || ThrowUserError('object_does_not_exist', + {class => 'Bugzilla::FlagType', name => $name}); + if ($new) { + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; + } + else { + my @flag_matches = grep($name eq $_->type->name, @$current_flags); + @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $name}); + if (@flag_matches) { + $flag->{id} = $flag_matches[0]->id; } else { - push(@new_flags, $flag); + delete $flag->{id}; + $flag->{type_id} = $type_matches[0]->id; } + } } - return (\@old_flags, \@new_flags); + if ($flag->{id}) { + push(@old_flags, $flag); + } + else { + push(@new_flags, $flag); + } + } + + return (\@old_flags, \@new_flags); } sub filter($$;$$) { - my ($params, $hash, $types, $prefix) = @_; - my %newhash = %$hash; + my ($params, $hash, $types, $prefix) = @_; + my %newhash = %$hash; - foreach my $key (keys %$hash) { - delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); - } + foreach my $key (keys %$hash) { + delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix); + } - return \%newhash; + return \%newhash; } sub filter_wants($$;$$) { - my ($params, $field, $types, $prefix) = @_; - - # Since this is operation is resource intensive, we will cache the results - # This assumes that $params->{*_fields} doesn't change between calls - my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; - $field = "${prefix}.${field}" if $prefix; - - if (exists $cache->{$field}) { - return $cache->{$field}; - } - - # Mimic old behavior if no types provided - my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); - - my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] }; - my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] }; - - my %include_types; - my %exclude_types; - - # Only return default fields if nothing is specified - $include_types{default} = 1 if !%include; - - # Look for any field types requested - foreach my $key (keys %include) { - next if $key !~ /^_(.*)$/; - $include_types{$1} = 1; - delete $include{$key}; - } - foreach my $key (keys %exclude) { - next if $key !~ /^_(.*)$/; - $exclude_types{$1} = 1; - delete $exclude{$key}; - } - - # Explicit inclusion/exclusion - return $cache->{$field} = 0 if $exclude{$field}; - return $cache->{$field} = 1 if $include{$field}; - - # If the user has asked to include all or exclude all - return $cache->{$field} = 0 if $exclude_types{'all'}; - return $cache->{$field} = 1 if $include_types{'all'}; - - # If the user has not asked for any fields specifically or if the user has asked - # for one or more of the field's types (and not excluded them) - foreach my $type (keys %field_types) { - return $cache->{$field} = 0 if $exclude_types{$type}; - return $cache->{$field} = 1 if $include_types{$type}; - } - - my $wants = 0; - if ($prefix) { - # Include the field if the parent is include (and this one is not excluded) - $wants = 1 if $include{$prefix}; - } - else { - # We want to include this if one of the sub keys is included - my $key = $field . '.'; - my $len = length($key); - $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; - } - - return $cache->{$field} = $wants; + my ($params, $field, $types, $prefix) = @_; + + # Since this is operation is resource intensive, we will cache the results + # This assumes that $params->{*_fields} doesn't change between calls + my $cache = Bugzilla->request_cache->{filter_wants} ||= {}; + $field = "${prefix}.${field}" if $prefix; + + if (exists $cache->{$field}) { + return $cache->{$field}; + } + + # Mimic old behavior if no types provided + my %field_types + = map { $_ => 1 } (ref $types ? @$types : ($types || 'default')); + + my %include = map { $_ => 1 } @{$params->{'include_fields'} || []}; + my %exclude = map { $_ => 1 } @{$params->{'exclude_fields'} || []}; + + my %include_types; + my %exclude_types; + + # Only return default fields if nothing is specified + $include_types{default} = 1 if !%include; + + # Look for any field types requested + foreach my $key (keys %include) { + next if $key !~ /^_(.*)$/; + $include_types{$1} = 1; + delete $include{$key}; + } + foreach my $key (keys %exclude) { + next if $key !~ /^_(.*)$/; + $exclude_types{$1} = 1; + delete $exclude{$key}; + } + + # Explicit inclusion/exclusion + return $cache->{$field} = 0 if $exclude{$field}; + return $cache->{$field} = 1 if $include{$field}; + + # If the user has asked to include all or exclude all + return $cache->{$field} = 0 if $exclude_types{'all'}; + return $cache->{$field} = 1 if $include_types{'all'}; + + # If the user has not asked for any fields specifically or if the user has asked + # for one or more of the field's types (and not excluded them) + foreach my $type (keys %field_types) { + return $cache->{$field} = 0 if $exclude_types{$type}; + return $cache->{$field} = 1 if $include_types{$type}; + } + + my $wants = 0; + if ($prefix) { + + # Include the field if the parent is include (and this one is not excluded) + $wants = 1 if $include{$prefix}; + } + else { + # We want to include this if one of the sub keys is included + my $key = $field . '.'; + my $len = length($key); + $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include; + } + + return $cache->{$field} = $wants; } sub taint_data { - my @params = @_; - return if !@params; - # Though this is a private function, it hasn't changed since 2004 and - # should be safe to use, and prevents us from having to write it ourselves - # or require another module to do it. - Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); - Test::Taint::taint_deeply(\@params); + my @params = @_; + return if !@params; + + # Though this is a private function, it hasn't changed since 2004 and + # should be safe to use, and prevents us from having to write it ourselves + # or require another module to do it. + Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params); + Test::Taint::taint_deeply(\@params); } sub _delete_bad_keys { - foreach my $item (@_) { - next if ref $item ne 'HASH'; - foreach my $key (keys %$item) { - # Making something a hash key always untaints it, in Perl. - # However, we need to validate our argument names in some way. - # We know that all hash keys passed in to the WebService will - # match \w+, contain '.' or '-', so we delete any key that - # doesn't match that. - if ($key !~ /^[\w\.\-]+$/) { - delete $item->{$key}; - } - } + foreach my $item (@_) { + next if ref $item ne 'HASH'; + foreach my $key (keys %$item) { + + # Making something a hash key always untaints it, in Perl. + # However, we need to validate our argument names in some way. + # We know that all hash keys passed in to the WebService will + # match \w+, contain '.' or '-', so we delete any key that + # doesn't match that. + if ($key !~ /^[\w\.\-]+$/) { + delete $item->{$key}; + } } - return @_; + } + return @_; } -sub validate { - my ($self, $params, @keys) = @_; - - # If $params is defined but not a reference, then we weren't - # sent any parameters at all, and we're getting @keys where - # $params should be. - return ($self, undef) if (defined $params and !ref $params); - - my @id_params = qw(ids comment_ids); - # If @keys is not empty then we convert any named - # parameters that have scalar values to arrayrefs - # that match. - foreach my $key (@keys) { - if (exists $params->{$key}) { - $params->{$key} = [ $params->{$key} ] unless ref $params->{$key}; - - if (any { $key eq $_ } @id_params) { - my $ids = $params->{$key}; - ThrowCodeError('param_scalar_array_required', { param => $key }) - unless ref($ids) eq 'ARRAY' && none { ref $_ } @$ids; - } - } +sub validate { + my ($self, $params, @keys) = @_; + + # If $params is defined but not a reference, then we weren't + # sent any parameters at all, and we're getting @keys where + # $params should be. + return ($self, undef) if (defined $params and !ref $params); + + my @id_params = qw(ids comment_ids); + + # If @keys is not empty then we convert any named + # parameters that have scalar values to arrayrefs + # that match. + foreach my $key (@keys) { + if (exists $params->{$key}) { + $params->{$key} = [$params->{$key}] unless ref $params->{$key}; + + if (any { $key eq $_ } @id_params) { + my $ids = $params->{$key}; + ThrowCodeError('param_scalar_array_required', {param => $key}) + unless ref($ids) eq 'ARRAY' && none { ref $_ } @$ids; + } } + } - return ($self, $params); + return ($self, $params); } sub translate { - my ($params, $mapped) = @_; - my %changes; - while (my ($key,$value) = each (%$params)) { - my $new_field = $mapped->{$key} || $key; - $changes{$new_field} = $value; - } - return \%changes; + my ($params, $mapped) = @_; + my %changes; + while (my ($key, $value) = each(%$params)) { + my $new_field = $mapped->{$key} || $key; + $changes{$new_field} = $value; + } + return \%changes; } sub params_to_objects { - my ($params, $class) = @_; - my (@objects, @objects_by_ids); + my ($params, $class) = @_; + my (@objects, @objects_by_ids); - @objects = map { $class->check($_) } - @{ $params->{names} } if $params->{names}; + @objects = map { $class->check($_) } @{$params->{names}} if $params->{names}; - @objects_by_ids = map { $class->check({ id => $_ }) } - @{ $params->{ids} } if $params->{ids}; + @objects_by_ids = map { $class->check({id => $_}) } @{$params->{ids}} + if $params->{ids}; - push(@objects, @objects_by_ids); - my %seen; - @objects = grep { !$seen{$_->id}++ } @objects; - return \@objects; + push(@objects, @objects_by_ids); + my %seen; + @objects = grep { !$seen{$_->id}++ } @objects; + return \@objects; } sub fix_credentials { - my ($params) = @_; - # Allow user to pass in login=foo&password=bar as a convenience - # even if not calling GET /login. We also do not delete them as - # GET /login requires "login" and "password". - if (exists $params->{'login'} && exists $params->{'password'}) { - $params->{'Bugzilla_login'} = delete $params->{'login'}; - $params->{'Bugzilla_password'} = delete $params->{'password'}; - } - # Allow user to pass api_key=12345678 as a convenience which becomes - # "Bugzilla_api_key" which is what the auth code looks for. - if (exists $params->{api_key}) { - $params->{Bugzilla_api_key} = delete $params->{api_key}; - } - # Allow user to pass token=12345678 as a convenience which becomes - # "Bugzilla_token" which is what the auth code looks for. - if (exists $params->{'token'}) { - $params->{'Bugzilla_token'} = delete $params->{'token'}; - } - - # Allow extensions to modify the credential data before login - Bugzilla::Hook::process('webservice_fix_credentials', { params => $params }); + my ($params) = @_; + + # Allow user to pass in login=foo&password=bar as a convenience + # even if not calling GET /login. We also do not delete them as + # GET /login requires "login" and "password". + if (exists $params->{'login'} && exists $params->{'password'}) { + $params->{'Bugzilla_login'} = delete $params->{'login'}; + $params->{'Bugzilla_password'} = delete $params->{'password'}; + } + + # Allow user to pass api_key=12345678 as a convenience which becomes + # "Bugzilla_api_key" which is what the auth code looks for. + if (exists $params->{api_key}) { + $params->{Bugzilla_api_key} = delete $params->{api_key}; + } + + # Allow user to pass token=12345678 as a convenience which becomes + # "Bugzilla_token" which is what the auth code looks for. + if (exists $params->{'token'}) { + $params->{'Bugzilla_token'} = delete $params->{'token'}; + } + + # Allow extensions to modify the credential data before login + Bugzilla::Hook::process('webservice_fix_credentials', {params => $params}); } __END__ diff --git a/Bugzilla/Whine.pm b/Bugzilla/Whine.pm index eeaea6da4..081933cba 100644 --- a/Bugzilla/Whine.pm +++ b/Bugzilla/Whine.pm @@ -27,11 +27,11 @@ use Bugzilla::Whine::Query; use constant DB_TABLE => 'whine_events'; use constant DB_COLUMNS => qw( - id - owner_userid - subject - body - mailifnobugs + id + owner_userid + subject + body + mailifnobugs ); use constant LIST_ORDER => 'id'; @@ -39,15 +39,15 @@ use constant LIST_ORDER => 'id'; #################### # Simple Accessors # #################### -sub subject { return $_[0]->{'subject'}; } -sub body { return $_[0]->{'body'}; } +sub subject { return $_[0]->{'subject'}; } +sub body { return $_[0]->{'body'}; } sub mail_if_no_bugs { return $_[0]->{'mailifnobugs'}; } sub user { - my ($self) = @_; - return $self->{user} if defined $self->{user}; - $self->{user} = new Bugzilla::User($self->{'owner_userid'}); - return $self->{user}; + my ($self) = @_; + return $self->{user} if defined $self->{user}; + $self->{user} = new Bugzilla::User($self->{'owner_userid'}); + return $self->{user}; } 1; diff --git a/Bugzilla/Whine/Query.pm b/Bugzilla/Whine/Query.pm index b2a2c9e07..6648eab66 100644 --- a/Bugzilla/Whine/Query.pm +++ b/Bugzilla/Whine/Query.pm @@ -23,12 +23,12 @@ use Bugzilla::Search::Saved; use constant DB_TABLE => 'whine_queries'; use constant DB_COLUMNS => qw( - id - eventid - query_name - sortkey - onemailperbug - title + id + eventid + query_name + sortkey + onemailperbug + title ); use constant NAME_FIELD => 'id'; @@ -37,11 +37,11 @@ use constant LIST_ORDER => 'sortkey'; #################### # Simple Accessors # #################### -sub eventid { return $_[0]->{'eventid'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub eventid { return $_[0]->{'eventid'}; } +sub sortkey { return $_[0]->{'sortkey'}; } sub one_email_per_bug { return $_[0]->{'onemailperbug'}; } -sub title { return $_[0]->{'title'}; } -sub name { return $_[0]->{'query_name'}; } +sub title { return $_[0]->{'title'}; } +sub name { return $_[0]->{'query_name'}; } 1; diff --git a/Bugzilla/Whine/Schedule.pm b/Bugzilla/Whine/Schedule.pm index 11f0bf16f..7517a3f26 100644 --- a/Bugzilla/Whine/Schedule.pm +++ b/Bugzilla/Whine/Schedule.pm @@ -22,22 +22,22 @@ use Bugzilla::Constants; use constant DB_TABLE => 'whine_schedules'; use constant DB_COLUMNS => qw( - id - eventid - run_day - run_time - run_next - mailto - mailto_type + id + eventid + run_day + run_time + run_next + mailto + mailto_type ); use constant UPDATE_COLUMNS => qw( - eventid - run_day - run_time - run_next - mailto - mailto_type + eventid + run_day + run_time + run_next + mailto + mailto_type ); use constant NAME_FIELD => 'id'; use constant LIST_ORDER => 'id'; @@ -45,36 +45,38 @@ use constant LIST_ORDER => 'id'; #################### # Simple Accessors # #################### -sub eventid { return $_[0]->{'eventid'}; } -sub run_day { return $_[0]->{'run_day'}; } -sub run_time { return $_[0]->{'run_time'}; } +sub eventid { return $_[0]->{'eventid'}; } +sub run_day { return $_[0]->{'run_day'}; } +sub run_time { return $_[0]->{'run_time'}; } sub mailto_is_group { return $_[0]->{'mailto_type'}; } sub mailto { - my $self = shift; - - return $self->{mailto_object} if exists $self->{mailto_object}; - my $id = $self->{'mailto'}; - - if ($self->mailto_is_group) { - $self->{mailto_object} = Bugzilla::Group->new($id); - } else { - $self->{mailto_object} = Bugzilla::User->new($id); - } - return $self->{mailto_object}; + my $self = shift; + + return $self->{mailto_object} if exists $self->{mailto_object}; + my $id = $self->{'mailto'}; + + if ($self->mailto_is_group) { + $self->{mailto_object} = Bugzilla::Group->new($id); + } + else { + $self->{mailto_object} = Bugzilla::User->new($id); + } + return $self->{mailto_object}; } -sub mailto_users { - my $self = shift; - return $self->{mailto_users} if exists $self->{mailto_users}; - my $object = $self->mailto; - - if ($self->mailto_is_group) { - $self->{mailto_users} = $object->members_non_inherited if $object->is_active; - } else { - $self->{mailto_users} = $object; - } - return $self->{mailto_users}; +sub mailto_users { + my $self = shift; + return $self->{mailto_users} if exists $self->{mailto_users}; + my $object = $self->mailto; + + if ($self->mailto_is_group) { + $self->{mailto_users} = $object->members_non_inherited if $object->is_active; + } + else { + $self->{mailto_users} = $object; + } + return $self->{mailto_users}; } 1; |