Commit 640e2f5e authored by Per Cederqvist's avatar Per Cederqvist

Imported Bugzilla 4.4.

parent f297f575
This is a Bazaar control directory.
Do not change any files in this directory.
See http://bazaar.canonical.com/ for more information about Bazaar.
See http://bazaar-vcs.org/ for more information about Bazaar.
bzr://bzr.mozilla.org/bugzilla/trunk/
\ No newline at end of file
bzr://bzr.mozilla.org/bugzilla/4.4/
\ No newline at end of file
No preview for this file type
......@@ -27,7 +27,7 @@ use Bugzilla::Extension;
use Bugzilla::DB;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
use Bugzilla::Install::Util qw(init_console);
use Bugzilla::Install::Util qw(init_console include_languages);
use Bugzilla::Template;
use Bugzilla::User;
use Bugzilla::Error;
......@@ -268,10 +268,8 @@ sub input_params {
return $cache->{input_params};
}
our $_localconfig;
sub localconfig {
$_localconfig ||= read_localconfig();
return $_localconfig;
return $_[0]->process_cache->{localconfig} ||= read_localconfig();
}
sub params {
......@@ -440,6 +438,10 @@ sub languages {
return Bugzilla::Install::Util::supported_languages();
}
sub current_language {
return $_[0]->request_cache->{current_language} ||= (include_languages())[0];
}
sub error_mode {
my ($class, $newval) = @_;
if (defined $newval) {
......@@ -593,7 +595,8 @@ sub fields {
}
}
return $do_by_name ? \%requested : [values %requested];
return $do_by_name ? \%requested
: [sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } values %requested];
}
sub active_custom_fields {
......@@ -617,11 +620,11 @@ sub has_flags {
sub local_timezone {
my $class = shift;
if (!defined $class->request_cache->{local_timezone}) {
$class->request_cache->{local_timezone} =
if (!defined $class->process_cache->{local_timezone}) {
$class->process_cache->{local_timezone} =
DateTime::TimeZone->new(name => 'local');
}
return $class->request_cache->{local_timezone};
return $class->process_cache->{local_timezone};
}
# This creates the request cache for non-mod_perl installations.
......@@ -642,6 +645,27 @@ sub request_cache {
return $_request_cache;
}
sub clear_request_cache {
$_request_cache = {};
if ($ENV{MOD_PERL}) {
require Apache2::RequestUtil;
my $request = eval { Apache2::RequestUtil->request };
if ($request) {
my $pnotes = $request->pnotes;
delete @$pnotes{(keys %$pnotes)};
}
}
}
# This is a per-process cache. Under mod_cgi it's identical to the
# request_cache. When using mod_perl, items in this cache live until the
# worker process is terminated.
our $_process_cache = {};
sub process_cache {
return $_process_cache;
}
# Private methods
# Per-process cleanup. Note that this is a plain subroutine, not a method,
......@@ -654,7 +678,7 @@ sub _cleanup {
$dbh->bz_rollback_transaction() if $dbh->bz_in_transaction;
$dbh->disconnect;
}
undef $_request_cache;
clear_request_cache();
# These are both set by CGI.pm but need to be undone so that
# Apache can actually shut down its children if it needs to.
......@@ -848,7 +872,7 @@ in a hashref:
=item C<by_name>
If false (or not specified), this method will return an arrayref of
the requested fields. The order of the returned fields is random.
the requested fields.
If true, this method will return a hashref of fields, where the keys
are field names and the valules are L<Bugzilla::Field> objects.
......@@ -915,6 +939,10 @@ The main database handle. See L<DBI>.
Currently installed languages.
Returns a reference to a list of RFC 1766 language tags of installed languages.
=item C<current_language>
The currently active language.
=item C<switch_to_shadow_db>
Switch from using the main database to using the shadow database.
......
......@@ -546,6 +546,7 @@ sub _check_content_type {
}
my $mimetype = mimetype($fh);
$fh->seek(0, 0);
$content_type = $mimetype if $mimetype;
}
......@@ -755,7 +756,7 @@ sub validate_obsolete {
$vars->{'attach_id'} = $attachid;
detaint_natural($attachid)
|| ThrowCodeError('invalid_attach_id_to_obsolete', $vars);
|| ThrowUserError('invalid_attach_id', $vars);
# Make sure the attachment exists in the database.
my $attachment = new Bugzilla::Attachment($attachid)
......@@ -765,12 +766,9 @@ sub validate_obsolete {
$attachment->validate_can_edit($bug->product_id)
|| ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
$vars->{'description'} = $attachment->description;
if ($attachment->bug_id != $bug->bug_id) {
$vars->{'my_bug_id'} = $bug->bug_id;
$vars->{'attach_bug_id'} = $attachment->bug_id;
ThrowCodeError('mismatched_bug_ids_on_obsolete', $vars);
ThrowUserError('mismatched_bug_ids_on_obsolete', $vars);
}
next if $attachment->isobsolete;
......@@ -878,7 +876,7 @@ sub run_create_validators {
$params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
$params->{modification_time} = $params->{creation_ts};
$params->{submitter_id} = Bugzilla->user->id || ThrowCodeError('invalid_user');
$params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user');
return $params;
}
......@@ -961,10 +959,18 @@ sub get_content_type {
return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text'));
my $content_type;
if (!defined $cgi->param('contenttypemethod')) {
ThrowUserError("missing_content_type_method");
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 ($cgi->param('contenttypemethod') eq 'autodetect') {
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.
......@@ -978,19 +984,6 @@ sub get_content_type {
$content_type = 'image/png';
}
}
elsif ($cgi->param('contenttypemethod') eq 'list') {
# The user selected a content type from the list, so use their
# selection.
$content_type = $cgi->param('contenttypeselection');
}
elsif ($cgi->param('contenttypemethod') eq 'manual') {
# The user entered a content type manually, so use their entry.
$content_type = $cgi->param('contenttypeentry');
}
else {
ThrowCodeError("illegal_content_type_method",
{ contenttypemethod => $cgi->param('contenttypemethod') });
}
return $content_type;
}
......
......@@ -151,6 +151,9 @@ sub VALIDATORS {
elsif ($field->type == FIELD_TYPE_BUG_ID) {
$validator = \&_check_bugid_field;
}
elsif ($field->type == FIELD_TYPE_TEXTAREA) {
$validator = \&_check_textarea_field;
}
else {
$validator = \&_check_default_field;
}
......@@ -519,17 +522,14 @@ sub possible_duplicates {
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, 1);
$dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms);
$relevance_sql ||= $where_sql;
}
else {
my (@where, @relevance);
my $count = 0;
foreach my $word (@words) {
$count++;
my ($term, $rel_term) = $dbh->sql_fulltext_search(
'bugs_fulltext.short_desc', $word, $count);
'bugs_fulltext.short_desc', $word);
push(@where, $term);
push(@relevance, $rel_term || $term);
}
......@@ -719,7 +719,7 @@ sub create {
# Because MySQL doesn't support transactions on the fulltext table,
# we do this after we've committed the transaction. That way we're
# sure we're inserting a good Bug ID.
$bug->_sync_fulltext('new bug');
$bug->_sync_fulltext( new_bug => 1 );
return $bug;
}
......@@ -728,6 +728,17 @@ 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};
......@@ -747,18 +758,11 @@ sub run_create_validators {
# You can't set these fields.
delete $params->{lastdiffed};
delete $params->{bug_id};
delete $params->{classification};
Bugzilla::Hook::process('bug_end_of_create_validators',
{ params => $params });
my @mandatory_fields = @{ Bugzilla->fields({ is_mandatory => 1,
enter_bug => 1,
obsolete => 0 }) };
foreach my $field (@mandatory_fields) {
$class->_check_field_is_mandatory($params->{$field->name}, $field,
$params);
}
# And this is not a valid DB field, it's just used as part of
# _check_dependencies to avoid running it twice for both blocked
# and dependson.
......@@ -779,6 +783,10 @@ sub update {
my ($changes, $old_bug) = $self->SUPER::update(@_);
Bugzilla::Hook::process('bug_start_of_update',
{ 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)) {
......@@ -1010,9 +1018,10 @@ sub update {
# in the middle of a transaction, and if that transaction is rolled
# back, this change will *not* be rolled back. As we expect rollbacks
# to be extremely rare, that is OK for us.
$self->_sync_fulltext()
if $self->{added_comments} || $changes->{short_desc}
|| $self->{comment_isprivate};
$self->_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'};
......@@ -1046,25 +1055,43 @@ sub _extract_multi_selects {
# Should be called any time you update short_desc or change a comment.
sub _sync_fulltext {
my ($self, $new_bug) = @_;
my ($self, %options) = @_;
my $dbh = Bugzilla->dbh;
if ($new_bug) {
$dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc)
SELECT bug_id, short_desc FROM bugs WHERE bug_id = ?',
undef, $self->id);
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);
}
else {
$dbh->do('UPDATE bugs_fulltext SET short_desc = ? WHERE bug_id = ?',
undef, $self->short_desc, $self->id);
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);
}
}
my $comments = $dbh->selectall_arrayref(
'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?',
undef, $self->id);
my $all = join("\n", map { $_->[0] } @$comments);
my @no_private = grep { !$_->[1] } @$comments;
my $nopriv_string = join("\n", map { $_->[0] } @no_private);
$dbh->do('UPDATE bugs_fulltext SET comments = ?, comments_noprivate = ?
WHERE bug_id = ?', undef, $all, $nopriv_string, $self->id);
}
sub remove_from_db {
......@@ -1311,11 +1338,12 @@ sub _check_bug_status {
}
# Check if a comment is required for this change.
if ($new_status->comment_required_on_change_from($old_status) && !$comment)
if ($new_status->comment_required_on_change_from($old_status)
&& !$comment->{'thetext'})
{
ThrowUserError('comment_required',
{ old => $old_status->name, new => $new_status->name,
field => 'bug_status' });
{ old => $old_status ? $old_status->name : undef,
new => $new_status->name, field => 'bug_status' });
}
if (ref $invocant
......@@ -1422,8 +1450,12 @@ sub _check_component {
$name || ThrowUserError("require_component");
my $product = blessed($invocant) ? $invocant->product_obj
: $params->{product};
my $obj = Bugzilla::Component->check({ product => $product, name => $name });
return $obj;
my $old_comp = blessed($invocant) ? $invocant->component : '';
my $object = Bugzilla::Component->check({ product => $product, name => $name });
if ($object->name ne $old_comp && !$object->is_active) {
ThrowUserError('value_inactive', { class => ref($object), value => $name });
}
return $object;
}
sub _check_creation_ts {
......@@ -1685,7 +1717,6 @@ sub _check_qa_contact {
$qa_contact = trim($qa_contact) if !ref $qa_contact;
my $component = blessed($invocant) ? $invocant->component_obj
: $params->{component};
my $id;
if (!ref $invocant) {
# Bugs get no QA Contact on creation if useqacontact is off.
return undef if !Bugzilla->params->{useqacontact};
......@@ -1694,13 +1725,14 @@ sub _check_qa_contact {
if (!Bugzilla->user->in_group('editbugs', $component->product_id)
|| !$qa_contact)
{
$id = $component->default_qa_contact->id;
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.
if (!defined $id && $qa_contact) {
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
......@@ -1881,10 +1913,14 @@ sub _check_target_milestone {
my ($invocant, $target, undef, $params) = @_;
my $product = blessed($invocant) ? $invocant->product_obj
: $params->{product};
my $old_target = blessed($invocant) ? $invocant->target_milestone : '';
$target = trim($target);
$target = $product->default_milestone if !defined $target;
my $object = Bugzilla::Milestone->check(
{ product => $product, name => $target });
if ($old_target && $object->name ne $old_target && !$object->is_active) {
ThrowUserError('value_inactive', { class => ref($object), value => $target });
}
return $object->name;
}
......@@ -1907,8 +1943,11 @@ sub _check_version {
$version = trim($version);
my $product = blessed($invocant) ? $invocant->product_obj
: $params->{product};
my $object =
Bugzilla::Version->check({ product => $product, name => $version });
my $old_vers = blessed($invocant) ? $invocant->version : '';
my $object = Bugzilla::Version->check({ product => $product, name => $version });
if ($object->name ne $old_vers && !$object->is_active) {
ThrowUserError('value_inactive', { class => ref($object), value => $version });
}
return $object->name;
}
......@@ -2024,6 +2063,19 @@ sub _check_bugid_field {
return $checked_id;
}
sub _check_textarea_field {
my ($invocant, $text, $field) = @_;
$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;
return $text;
}
sub _check_relationship_loop {
# Generates a dependency tree for a given bug. Calls itself recursively
# to generate sub-trees for the bug's dependencies.
......@@ -2445,9 +2497,9 @@ sub _set_product {
milestone => $milestone_ok ? $self->target_milestone
: $product->default_milestone
};
$vars{components} = [map { $_->name } @{$product->components}];
$vars{milestones} = [map { $_->name } @{$product->milestones}];
$vars{versions} = [map { $_->name } @{$product->versions}];
$vars{components} = [map { $_->name } grep($_->is_active, @{$product->components})];
$vars{milestones} = [map { $_->name } grep($_->is_active, @{$product->milestones})];
$vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})];
}
if (!$verified) {
......@@ -2853,7 +2905,8 @@ sub add_see_also {
# 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 !$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;
......@@ -2885,12 +2938,15 @@ sub remove_see_also {
# 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->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)) {
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;
......@@ -3165,8 +3221,6 @@ sub cc {
ORDER BY profiles.login_name},
undef, $self->bug_id);
$self->{'cc'} = undef if !scalar(@{$self->{'cc'}});
return $self->{'cc'};
}
......@@ -3418,9 +3472,6 @@ sub qa_contact {
if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) {
$self->{'qa_contact_obj'} = new Bugzilla::User($self->{'qa_contact'});
} else {
# XXX - This is somewhat inconsistent with the assignee/reporter
# methods, which will return an empty User if they get a 0.
# However, we're keeping it this way now, for backwards-compatibility.
$self->{'qa_contact_obj'} = undef;
}
return $self->{'qa_contact_obj'};
......@@ -3693,9 +3744,13 @@ sub bug_alias_to_id {
# Subroutines
#####################################################################
# Represents which fields from the bugs table are handled by process_bug.cgi.
# Returns a list of currently active and editable bug fields,
# including multi-select fields.
sub editable_bug_fields {