1
# -*- Mode: perl; indent-tabs-mode: nil -*-
3
# The contents of this file are subject to the Mozilla Public
4
# License Version 1.1 (the "License"); you may not use this file
5
# except in compliance with the License. You may obtain a copy of
6
# the License at http://www.mozilla.org/MPL/
8
# Software distributed under the License is distributed on an "AS
9
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
10
# implied. See the License for the specific language governing
11
# rights and limitations under the License.
13
# The Original Code is the Bugzilla Bug Tracking System.
15
# The Initial Developer of the Original Code is Netscape Communications
16
# Corporation. Portions created by Netscape are
17
# Copyright (C) 1998 Netscape Communications Corporation. All
20
# Contributor(s): Dawn Endico <endico@mozilla.org>
21
# Terry Weissman <terry@mozilla.org>
22
# Chris Yeh <cyeh@bluemartini.com>
23
# Bradley Baetz <bbaetz@acm.org>
24
# Dave Miller <justdave@bugzilla.org>
25
# Max Kanat-Alexander <mkanat@bugzilla.org>
26
# Frédéric Buclin <LpSolit@gmail.com>
27
# Lance Larsh <lance.larsh@oracle.com>
29
package Bugzilla::Bug;
33
use Bugzilla::Attachment;
34
use Bugzilla::Constants;
37
use Bugzilla::FlagType;
38
use Bugzilla::Keyword;
42
use Bugzilla::Product;
43
use Bugzilla::Component;
46
use List::Util qw(min);
48
use base qw(Bugzilla::Object Exporter);
49
@Bugzilla::Bug::EXPORT = qw(
50
AppendComment ValidateComment
51
bug_alias_to_id ValidateBugAlias ValidateBugID
52
RemoveVotes CheckIfVotedConfirmed
58
#####################################################################
60
#####################################################################
62
use constant DB_TABLE => 'bugs';
63
use constant ID_FIELD => 'bug_id';
64
use constant NAME_FIELD => 'alias';
65
use constant LIST_ORDER => ID_FIELD;
67
# This is a sub because it needs to call other subroutines.
69
my $dbh = Bugzilla->dbh;
93
'assigned_to AS assigned_to_id',
94
'reporter AS reporter_id',
95
'qa_contact AS qa_contact_id',
96
$dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts',
97
$dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
98
Bugzilla->custom_field_names;
101
use constant REQUIRED_CREATE_FIELDS => qw(
108
# There are also other, more complex validators that are called
109
# from run_create_validators.
112
alias => \&_check_alias,
113
bug_file_loc => \&_check_bug_file_loc,
114
bug_severity => \&_check_bug_severity,
115
comment => \&_check_comment,
116
commentprivacy => \&_check_commentprivacy,
117
deadline => \&_check_deadline,
118
estimated_time => \&_check_estimated_time,
119
op_sys => \&_check_op_sys,
120
priority => \&_check_priority,
121
product => \&_check_product,
122
remaining_time => \&_check_remaining_time,
123
rep_platform => \&_check_rep_platform,
124
short_desc => \&_check_short_desc,
125
status_whiteboard => \&_check_status_whiteboard,
128
my @custom_fields = Bugzilla->get_fields({custom => 1, obsolete => 0});
130
foreach my $field (@custom_fields) {
132
if ($field->type == FIELD_TYPE_SINGLE_SELECT) {
133
$validator = \&_check_select_field;
135
elsif ($field->type == FIELD_TYPE_FREETEXT) {
136
$validator = \&_check_freetext_field;
138
$validators->{$field->name} = $validator if $validator;
143
# Used in LogActivityEntry(). Gives the max length of lines in the
145
use constant MAX_LINE_LENGTH => 254;
147
# Used in ValidateComment(). Gives the max length allowed for a comment.
148
use constant MAX_COMMENT_LENGTH => 65535;
150
# The statuses that are valid on enter_bug.cgi and post_bug.cgi.
151
# The order is important--see _check_bug_status
152
use constant VALID_ENTRY_STATUS => qw(
158
#####################################################################
161
my $invocant = shift;
162
my $class = ref($invocant) || $invocant;
165
# If we get something that looks like a word (not a number),
166
# make it the "name" param.
167
if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) {
168
# But only if aliases are enabled.
169
if (Bugzilla->params->{'usebugaliases'} && $param) {
170
$param = { name => $param };
173
# Aliases are off, and we got something that's not a number.
175
bless $error_self, $class;
176
$error_self->{'bug_id'} = $param;
177
$error_self->{'error'} = 'InvalidBugId';
183
my $self = $class->SUPER::new(@_);
185
# Bugzilla::Bug->new always returns something, but sets $self->{error}
186
# if the bug wasn't found in the database.
189
bless $error_self, $class;
190
$error_self->{'bug_id'} = ref($param) ? $param->{name} : $param;
191
$error_self->{'error'} = 'NotFound';
195
# XXX At some point these should be moved into accessors.
196
# They only are here because this is how Bugzilla::Bug
197
# originally did things, before it was a Bugzilla::Object.
198
$self->{'isunconfirmed'} = ($self->{bug_status} eq 'UNCONFIRMED');
199
$self->{'isopened'} = is_open_state($self->{bug_status});
204
# Docs for create() (there's no POD in this file yet, but we very
205
# much need this documented right now):
207
# The same as Bugzilla::Object->create. Parameters are only required
208
# if they say so below.
212
# C<product> - B<Required> The name of the product this bug is being
214
# C<component> - B<Required> The name of the component this bug is being
217
# C<bug_severity> - B<Required> The severity for the bug, a string.
218
# C<creation_ts> - B<Required> A SQL timestamp for when the bug was created.
219
# C<short_desc> - B<Required> A summary for the bug.
220
# C<op_sys> - B<Required> The OS the bug was found against.
221
# C<priority> - B<Required> The initial priority for the bug.
222
# C<rep_platform> - B<Required> The platform the bug was found against.
223
# C<version> - B<Required> The version of the product the bug was found in.
225
# C<alias> - An alias for this bug. Will be ignored if C<usebugaliases>
227
# C<target_milestone> - When this bug is expected to be fixed.
228
# C<status_whiteboard> - A string.
229
# C<bug_status> - The initial status of the bug, a string.
230
# C<bug_file_loc> - The URL field.
232
# C<assigned_to> - The full login name of the user who the bug is
233
# initially assigned to.
234
# C<qa_contact> - The full login name of the QA Contact for this bug.
235
# Will be ignored if C<useqacontact> is off.
237
# C<estimated_time> - For time-tracking. Will be ignored if
238
# C<timetrackinggroup> is not set, or if the current
239
# user is not a member of the timetrackinggroup.
240
# C<deadline> - For time-tracking. Will be ignored for the same
241
# reasons as C<estimated_time>.
243
my ($class, $params) = @_;
244
my $dbh = Bugzilla->dbh;
246
# These fields have default values which we can use if they are undefined.
247
$params->{bug_severity} = Bugzilla->params->{defaultseverity}
248
unless defined $params->{bug_severity};
249
$params->{priority} = Bugzilla->params->{defaultpriority}
250
unless defined $params->{priority};
251
$params->{op_sys} = Bugzilla->params->{defaultopsys}
252
unless defined $params->{op_sys};
253
$params->{rep_platform} = Bugzilla->params->{defaultplatform}
254
unless defined $params->{rep_platform};
255
# Make sure a comment is always defined.
256
$params->{comment} = '' unless defined $params->{comment};
258
$class->check_required_create_fields($params);
259
$params = $class->run_create_validators($params);
261
# These are not a fields in the bugs table, so we don't pass them to
262
# insert_create_data.
263
my $cc_ids = $params->{cc};
264
delete $params->{cc};
265
my $groups = $params->{groups};
266
delete $params->{groups};
267
my $depends_on = $params->{dependson};
268
delete $params->{dependson};
269
my $blocked = $params->{blocked};
270
delete $params->{blocked};
271
my ($comment, $privacy) = ($params->{comment}, $params->{commentprivacy});
272
delete $params->{comment};
273
delete $params->{commentprivacy};
275
# Set up the keyword cache for bug creation.
276
my $keywords = $params->{keywords};
277
$params->{keywords} = join(', ', sort {lc($a) cmp lc($b)}
278
map($_->name, @$keywords));
280
# We don't want the bug to appear in the system until it's correctly
281
# protected by groups.
282
my $timestamp = $params->{creation_ts};
283
delete $params->{creation_ts};
285
$dbh->bz_lock_tables('bugs WRITE', 'bug_group_map WRITE',
286
'longdescs WRITE', 'cc WRITE', 'keywords WRITE', 'dependencies WRITE',
287
'bugs_activity WRITE', 'fielddefs READ');
289
my $bug = $class->insert_create_data($params);
291
# Add the group restrictions
292
my $sth_group = $dbh->prepare(
293
'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)');
294
foreach my $group_id (@$groups) {
295
$sth_group->execute($bug->bug_id, $group_id);
298
$dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', undef,
299
$timestamp, $bug->bug_id);
300
# Update the bug instance as well
301
$bug->{creation_ts} = $timestamp;
304
my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)');
305
foreach my $user_id (@$cc_ids) {
306
$sth_cc->execute($bug->bug_id, $user_id);
310
my $sth_keyword = $dbh->prepare(
311
'INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)');
312
foreach my $keyword_id (map($_->id, @$keywords)) {
313
$sth_keyword->execute($bug->bug_id, $keyword_id);
316
# Set up dependencies (blocked/dependson)
317
my $sth_deps = $dbh->prepare(
318
'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)');
319
my $sth_bug_time = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
321
foreach my $depends_on_id (@$depends_on) {
322
$sth_deps->execute($bug->bug_id, $depends_on_id);
323
# Log the reverse action on the other bug.
324
LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id,
325
$bug->{reporter_id}, $timestamp);
326
$sth_bug_time->execute($timestamp, $depends_on_id);
328
foreach my $blocked_id (@$blocked) {
329
$sth_deps->execute($blocked_id, $bug->bug_id);
330
# Log the reverse action on the other bug.
331
LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id,
332
$bug->{reporter_id}, $timestamp);
333
$sth_bug_time->execute($timestamp, $blocked_id);
336
# And insert the comment. We always insert a comment on bug creation,
337
# but sometimes it's blank.
338
my @columns = qw(bug_id who bug_when thetext);
339
my @values = ($bug->bug_id, $bug->{reporter_id}, $timestamp, $comment);
340
# We don't include the "isprivate" column unless it was specified.
341
# This allows it to fall back to its database default.
342
if (defined $privacy) {
343
push(@columns, 'isprivate');
344
push(@values, $privacy);
346
my $qmarks = "?," x @columns;
348
$dbh->do('INSERT INTO longdescs (' . join(',', @columns) . ")
349
VALUES ($qmarks)", undef, @values);
351
$dbh->bz_unlock_tables();
357
sub run_create_validators {
359
my $params = $class->SUPER::run_create_validators(@_);
361
my $product = $params->{product};
362
$params->{product_id} = $product->id;
363
delete $params->{product};
365
($params->{bug_status}, $params->{everconfirmed})
366
= $class->_check_bug_status($product, $params->{bug_status});
368
$params->{target_milestone} = $class->_check_target_milestone($product,
369
$params->{target_milestone});
371
$params->{version} = $class->_check_version($product, $params->{version});
373
$params->{keywords} = $class->_check_keywords($product, $params->{keywords});
375
$params->{groups} = $class->_check_groups($product,
378
my $component = $class->_check_component($product, $params->{component});
379
$params->{component_id} = $component->id;
380
delete $params->{component};
382
$params->{assigned_to} =
383
$class->_check_assigned_to($component, $params->{assigned_to});
384
$params->{qa_contact} =
385
$class->_check_qa_contact($component, $params->{qa_contact});
386
$params->{cc} = $class->_check_cc($component, $params->{cc});
388
# Callers cannot set Reporter, currently.
389
$params->{reporter} = Bugzilla->user->id;
391
$params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
392
$params->{delta_ts} = $params->{creation_ts};
394
if ($params->{estimated_time}) {
395
$params->{remaining_time} = $params->{estimated_time};
398
$class->_check_strict_isolation($product, $params->{cc},
399
$params->{assigned_to}, $params->{qa_contact});
401
($params->{dependson}, $params->{blocked}) =
402
$class->_check_dependencies($product, $params->{dependson}, $params->{blocked});
404
# You can't set these fields on bug creation (or sometimes ever).
405
delete $params->{resolution};
406
delete $params->{votes};
407
delete $params->{lastdiffed};
408
delete $params->{bug_id};
413
# This is the correct way to delete bugs from the DB.
414
# No bug should be deleted from anywhere else except from here.
418
my $dbh = Bugzilla->dbh;
420
if ($self->{'error'}) {
421
ThrowCodeError("bug_error", { bug => $self });
424
my $bug_id = $self->{'bug_id'};
426
# tables having 'bugs.bug_id' as a foreign key:
439
# Also, the attach_data table uses attachments.attach_id as a foreign
440
# key, and so indirectly depends on a bug deletion too.
442
$dbh->bz_lock_tables('attachments WRITE', 'bug_group_map WRITE',
443
'bugs WRITE', 'bugs_activity WRITE', 'cc WRITE',
444
'dependencies WRITE', 'duplicates WRITE',
445
'flags WRITE', 'keywords WRITE',
446
'longdescs WRITE', 'votes WRITE',
447
'attach_data WRITE');
449
$dbh->do("DELETE FROM bug_group_map WHERE bug_id = ?", undef, $bug_id);
450
$dbh->do("DELETE FROM bugs_activity WHERE bug_id = ?", undef, $bug_id);
451
$dbh->do("DELETE FROM cc WHERE bug_id = ?", undef, $bug_id);
452
$dbh->do("DELETE FROM dependencies WHERE blocked = ? OR dependson = ?",
453
undef, ($bug_id, $bug_id));
454
$dbh->do("DELETE FROM duplicates WHERE dupe = ? OR dupe_of = ?",
455
undef, ($bug_id, $bug_id));
456
$dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id);
457
$dbh->do("DELETE FROM keywords WHERE bug_id = ?", undef, $bug_id);
458
$dbh->do("DELETE FROM longdescs WHERE bug_id = ?", undef, $bug_id);
459
$dbh->do("DELETE FROM votes WHERE bug_id = ?", undef, $bug_id);
461
# The attach_data table doesn't depend on bugs.bug_id directly.
463
$dbh->selectcol_arrayref("SELECT attach_id FROM attachments
464
WHERE bug_id = ?", undef, $bug_id);
466
if (scalar(@$attach_ids)) {
467
$dbh->do("DELETE FROM attach_data WHERE id IN (" .
468
join(",", @$attach_ids) . ")");
471
# Several of the previous tables also depend on attach_id.
472
$dbh->do("DELETE FROM attachments WHERE bug_id = ?", undef, $bug_id);
473
$dbh->do("DELETE FROM bugs WHERE bug_id = ?", undef, $bug_id);
475
$dbh->bz_unlock_tables();
477
# Now this bug no longer exists
482
#####################################################################
484
#####################################################################
487
my ($invocant, $alias) = @_;
488
$alias = trim($alias);
489
return undef if (!Bugzilla->params->{'usebugaliases'} || !$alias);
490
ValidateBugAlias($alias);
494
sub _check_assigned_to {
495
my ($invocant, $component, $name) = @_;
496
my $user = Bugzilla->user;
499
# Default assignee is the component owner.
501
if (!$user->in_group('editbugs', $component->product_id) || !$name) {
502
$id = $component->default_assignee->id;
504
$id = login_to_id($name, THROW_ERROR);
509
sub _check_bug_file_loc {
510
my ($invocant, $url) = @_;
511
# If bug_file_loc is "http://", the default, use an empty value instead.
512
$url = '' if (!defined($url) || $url eq 'http://');
516
sub _check_bug_severity {
517
my ($invocant, $severity) = @_;
518
$severity = trim($severity);
519
check_field('bug_severity', $severity);
523
sub _check_bug_status {
524
my ($invocant, $product, $status) = @_;
525
my $user = Bugzilla->user;
527
my @valid_statuses = VALID_ENTRY_STATUS;
529
if ($user->in_group('editbugs', $product->id)
530
|| $user->in_group('canconfirm', $product->id)) {
531
# Default to NEW if the user with privs hasn't selected another status.
534
elsif (!$product->votes_to_confirm) {
535
# Without privs, products that don't support UNCONFIRMED default to
540
$status = 'UNCONFIRMED';
543
# UNCONFIRMED becomes an invalid status if votes_to_confirm is 0,
544
# even if you are in editbugs.
545
shift @valid_statuses if !$product->votes_to_confirm;
547
check_field('bug_status', $status, \@valid_statuses);
548
return ($status, $status eq 'UNCONFIRMED' ? 0 : 1);
552
my ($invocant, $component, $ccs) = @_;
553
return [map {$_->id} @{$component->initial_cc}] unless $ccs;
556
foreach my $person (@$ccs) {
558
my $id = login_to_id($person, THROW_ERROR);
563
$cc_ids{$_->id} = 1 foreach (@{$component->initial_cc});
565
return [keys %cc_ids];
569
my ($invocant, $comment) = @_;
571
$comment = '' unless defined $comment;
573
# Remove any trailing whitespace. Leading whitespace could be
574
# a valid part of the comment.
575
$comment =~ s/\s*$//s;
576
$comment =~ s/\r\n?/\n/g; # Get rid of \r.
578
ValidateComment($comment);
580
if (Bugzilla->params->{"commentoncreate"} && !$comment) {
581
ThrowUserError("description_required");
584
# On creation only, there must be a single-space comment, or
585
# email will be supressed.
586
$comment = ' ' if $comment eq '' && !ref($invocant);
591
sub _check_commentprivacy {
592
my ($invocant, $comment_privacy) = @_;
593
my $insider_group = Bugzilla->params->{"insidergroup"};
594
return ($insider_group && Bugzilla->user->in_group($insider_group)
595
&& $comment_privacy) ? 1 : 0;
598
sub _check_component {
599
my ($invocant, $product, $name) = @_;
601
$name || ThrowUserError("require_component");
602
my $obj = Bugzilla::Component::check_component($product, $name);
606
sub _check_deadline {
607
my ($invocant, $date) = @_;
609
my $tt_group = Bugzilla->params->{"timetrackinggroup"};
610
return undef unless $date && $tt_group
611
&& Bugzilla->user->in_group($tt_group);
613
|| ThrowUserError('illegal_date', { date => $date,
614
format => 'YYYY-MM-DD' });
618
# Takes two comma/space-separated strings and returns arrayrefs
620
sub _check_dependencies {
621
my ($invocant, $product, $depends_on, $blocks) = @_;
623
# Only editbugs users can set dependencies on bug entry.
624
return ([], []) unless Bugzilla->user->in_group('editbugs', $product->id);
629
# Make sure all the bug_ids are valid.
631
foreach my $string ($depends_on, $blocks) {
632
my @array = split(/[\s,]+/, $string);
634
@array = grep($_, @array);
635
# $field is not passed to ValidateBugID to prevent adding new
636
# dependencies on inaccessible bugs.
637
ValidateBugID($_) foreach (@array);
638
push(@results, \@array);
642
my %deps = ValidateDependencies($results[0], $results[1]);
644
return ($deps{'dependson'}, $deps{'blocked'});
647
sub _check_estimated_time {
648
return $_[0]->_check_time($_[1], 'estimated_time');
652
my ($invocant, $product, $group_ids) = @_;
654
my $user = Bugzilla->user;
657
my $controls = $product->group_controls;
659
foreach my $id (@$group_ids) {
660
my $group = new Bugzilla::Group($id)
661
|| ThrowUserError("invalid_group_ID");
663
# This can only happen if somebody hacked the enter_bug form.
664
ThrowCodeError("inactive_group", { name => $group->name })
665
unless $group->is_active;
667
my $membercontrol = $controls->{$id}
668
&& $controls->{$id}->{membercontrol};
669
my $othercontrol = $controls->{$id}
670
&& $controls->{$id}->{othercontrol};
672
my $permit = ($membercontrol && $user->in_group($group->name))
675
$add_groups{$id} = 1 if $permit;
678
foreach my $id (keys %$controls) {
679
next unless $controls->{$id}->{'group'}->is_active;
680
my $membercontrol = $controls->{$id}->{membercontrol} || 0;
681
my $othercontrol = $controls->{$id}->{othercontrol} || 0;
683
# Add groups required
684
if ($membercontrol == CONTROLMAPMANDATORY
685
|| ($othercontrol == CONTROLMAPMANDATORY
686
&& !$user->in_group_id($id)))
688
# User had no option, bug needs to be in this group.
689
$add_groups{$id} = 1;
693
my @add_groups = keys %add_groups;
697
sub _check_keywords {
698
my ($invocant, $product, $keyword_string) = @_;
699
$keyword_string = trim($keyword_string);
700
return [] if (!$keyword_string
701
|| !Bugzilla->user->in_group('editbugs', $product->id));
704
foreach my $keyword (split(/[\s,]+/, $keyword_string)) {
705
next unless $keyword;
706
my $obj = new Bugzilla::Keyword({ name => $keyword });
707
ThrowUserError("unknown_keyword", { keyword => $keyword }) if !$obj;
708
$keywords{$obj->id} = $obj;
710
return [values %keywords];
714
my ($invocant, $name) = @_;
715
# Check that the product exists and that the user
716
# is allowed to enter bugs into this product.
717
Bugzilla->user->can_enter_product($name, THROW_ERROR);
718
# can_enter_product already does everything that check_product
719
# would do for us, so we don't need to use it.
720
my $obj = new Bugzilla::Product({ name => $name });
725
my ($invocant, $op_sys) = @_;
726
$op_sys = trim($op_sys);
727
check_field('op_sys', $op_sys);
731
sub _check_priority {
732
my ($invocant, $priority) = @_;
733
if (!Bugzilla->params->{'letsubmitterchoosepriority'}) {
734
$priority = Bugzilla->params->{'defaultpriority'};
736
$priority = trim($priority);
737
check_field('priority', $priority);
742
sub _check_remaining_time {
743
return $_[0]->_check_time($_[1], 'remaining_time');
746
sub _check_rep_platform {
747
my ($invocant, $platform) = @_;
748
$platform = trim($platform);
749
check_field('rep_platform', $platform);
753
sub _check_short_desc {
754
my ($invocant, $short_desc) = @_;
755
# Set the parameter to itself, but cleaned up
756
$short_desc = clean_text($short_desc) if $short_desc;
758
if (!defined $short_desc || $short_desc eq '') {
759
ThrowUserError("require_summary");
764
sub _check_status_whiteboard { return defined $_[1] ? $_[1] : ''; }
766
# Unlike other checkers, this one doesn't return anything.
767
sub _check_strict_isolation {
768
my ($invocant, $product, $cc_ids, $assignee_id, $qa_contact_id) = @_;
770
return unless Bugzilla->params->{'strict_isolation'};
772
my @related_users = @$cc_ids;
773
push(@related_users, $assignee_id);
775
if (Bugzilla->params->{'useqacontact'} && $qa_contact_id) {
776
push(@related_users, $qa_contact_id);
779
# For each unique user in @related_users...(assignee and qa_contact
780
# could be duplicates of users in the CC list)
781
my %unique_users = map {$_ => 1} @related_users;
783
foreach my $pid (keys %unique_users) {
784
my $related_user = Bugzilla::User->new($pid);
785
if (!$related_user->can_edit_product($product->id)) {
786
push (@blocked_users, $related_user->login);
789
if (scalar(@blocked_users)) {
790
ThrowUserError("invalid_user_group",
791
{'users' => \@blocked_users,
793
'product' => $product->name});
797
sub _check_target_milestone {
798
my ($invocant, $product, $target) = @_;
799
$target = trim($target);
800
$target = $product->default_milestone if !defined $target;
801
check_field('target_milestone', $target,
802
[map($_->name, @{$product->milestones})]);
807
my ($invocant, $time, $field) = @_;
808
my $tt_group = Bugzilla->params->{"timetrackinggroup"};
809
return 0 unless $tt_group && Bugzilla->user->in_group($tt_group);
810
$time = trim($time) || 0;
811
ValidateTime($time, $field);
815
sub _check_qa_contact {
816
my ($invocant, $component, $name) = @_;
817
my $user = Bugzilla->user;
819
return undef unless Bugzilla->params->{'useqacontact'};
824
if (!$user->in_group('editbugs', $component->product_id) || !$name) {
825
# We want to insert NULL into the database if we get a 0.
826
$id = $component->default_qa_contact->id || undef;
828
$id = login_to_id($name, THROW_ERROR);
835
my ($invocant, $product, $version) = @_;
836
$version = trim($version);
837
check_field('version', $version, [map($_->name, @{$product->versions})]);
841
# Custom Field Validators
843
sub _check_freetext_field {
844
my ($invocant, $text) = @_;
846
$text = (defined $text) ? trim($text) : '';
847
if (length($text) > MAX_FREETEXT_LENGTH) {
848
ThrowUserError('freetext_too_long', { text => $text });
853
sub _check_select_field {
854
my ($invocant, $value, $field) = @_;
855
$value = trim($value);
856
check_field($field, $value);
860
#####################################################################
862
#####################################################################
869
# Keep this ordering in sync with bugzilla.dtd.
870
qw(bug_id alias creation_ts short_desc delta_ts
871
reporter_accessible cclist_accessible
872
classification_id classification
873
product component version rep_platform op_sys
874
bug_status resolution dup_id
875
bug_file_loc status_whiteboard keywords
876
priority bug_severity target_milestone
877
dependson blocked votes everconfirmed
878
reporter assigned_to cc),
881
Bugzilla->params->{'useqacontact'} ? "qa_contact" : (),
882
Bugzilla->params->{'timetrackinggroup'} ?
883
qw(estimated_time remaining_time actual_time deadline) : (),
886
Bugzilla->custom_field_names
891
#####################################################################
893
#####################################################################
895
# These subs are in alphabetical order, as much as possible.
896
# If you add a new sub, please try to keep it in alphabetical order
897
# with the other ones.
899
# Note: If you add a new method, remember that you must check the error
900
# state of the bug before returning any data. If $self->{error} is
901
# defined, then return something empty. Otherwise you risk potential
906
return $self->{'dup_id'} if exists $self->{'dup_id'};
908
$self->{'dup_id'} = undef;
909
return if $self->{'error'};
911
if ($self->{'resolution'} eq 'DUPLICATE') {
912
my $dbh = Bugzilla->dbh;
914
$dbh->selectrow_array(q{SELECT dupe_of
920
return $self->{'dup_id'};
925
return $self->{'actual_time'} if exists $self->{'actual_time'};
927
if ( $self->{'error'} ||
928
!Bugzilla->user->in_group(Bugzilla->params->{"timetrackinggroup"}) ) {
929
$self->{'actual_time'} = undef;
930
return $self->{'actual_time'};
933
my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time)
935
WHERE longdescs.bug_id=?");
936
$sth->execute($self->{bug_id});
937
$self->{'actual_time'} = $sth->fetchrow_array();
938
return $self->{'actual_time'};
941
sub any_flags_requesteeble {
943
return $self->{'any_flags_requesteeble'}
944
if exists $self->{'any_flags_requesteeble'};
945
return 0 if $self->{'error'};
947
$self->{'any_flags_requesteeble'} =
948
grep($_->{'is_requesteeble'}, @{$self->flag_types});
950
return $self->{'any_flags_requesteeble'};
955
return $self->{'attachments'} if exists $self->{'attachments'};
956
return [] if $self->{'error'};
958
$self->{'attachments'} =
959
Bugzilla::Attachment->get_attachments_by_bug($self->bug_id);
960
return $self->{'attachments'};
965
return $self->{'assigned_to'} if exists $self->{'assigned_to'};
966
$self->{'assigned_to_id'} = 0 if $self->{'error'};
967
$self->{'assigned_to'} = new Bugzilla::User($self->{'assigned_to_id'});
968
return $self->{'assigned_to'};
973
return $self->{'blocked'} if exists $self->{'blocked'};
974
return [] if $self->{'error'};
975
$self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id);
976
return $self->{'blocked'};
979
# Even bugs in an error state always have a bug_id.
980
sub bug_id { $_[0]->{'bug_id'}; }
984
return $self->{'cc'} if exists $self->{'cc'};
985
return [] if $self->{'error'};
987
my $dbh = Bugzilla->dbh;
988
$self->{'cc'} = $dbh->selectcol_arrayref(
989
q{SELECT profiles.login_name FROM cc, profiles
991
AND cc.who = profiles.userid
992
ORDER BY profiles.login_name},
993
undef, $self->bug_id);
995
$self->{'cc'} = undef if !scalar(@{$self->{'cc'}});
997
return $self->{'cc'};
1002
return $self->{component} if exists $self->{component};
1003
return '' if $self->{error};
1004
($self->{component}) = Bugzilla->dbh->selectrow_array(
1005
'SELECT name FROM components WHERE id = ?',
1006
undef, $self->{component_id});
1007
return $self->{component};
1010
sub classification_id {
1012
return $self->{classification_id} if exists $self->{classification_id};
1013
return 0 if $self->{error};
1014
($self->{classification_id}) = Bugzilla->dbh->selectrow_array(
1015
'SELECT classification_id FROM products WHERE id = ?',
1016
undef, $self->{product_id});
1017
return $self->{classification_id};
1020
sub classification {
1022
return $self->{classification} if exists $self->{classification};
1023
return '' if $self->{error};
1024
($self->{classification}) = Bugzilla->dbh->selectrow_array(
1025
'SELECT name FROM classifications WHERE id = ?',
1026
undef, $self->classification_id);
1027
return $self->{classification};
1032
return $self->{'dependson'} if exists $self->{'dependson'};
1033
return [] if $self->{'error'};
1034
$self->{'dependson'} =
1035
EmitDependList("blocked", "dependson", $self->bug_id);
1036
return $self->{'dependson'};
1041
return $self->{'flag_types'} if exists $self->{'flag_types'};
1042
return [] if $self->{'error'};
1044
# The types of flags that can be set on this bug.
1045
# If none, no UI for setting flags will be displayed.
1046
my $flag_types = Bugzilla::FlagType::match(
1047
{'target_type' => 'bug',
1048
'product_id' => $self->{'product_id'},
1049
'component_id' => $self->{'component_id'} });
1051
foreach my $flag_type (@$flag_types) {
1052
$flag_type->{'flags'} = Bugzilla::Flag::match(
1053
{ 'bug_id' => $self->bug_id,
1054
'type_id' => $flag_type->{'id'},
1055
'target_type' => 'bug' });
1058
$self->{'flag_types'} = $flag_types;
1060
return $self->{'flag_types'};
1065
return $self->{'keywords'} if exists $self->{'keywords'};
1066
return () if $self->{'error'};
1068
my $dbh = Bugzilla->dbh;
1069
my $list_ref = $dbh->selectcol_arrayref(
1070
"SELECT keyworddefs.name
1071
FROM keyworddefs, keywords
1072
WHERE keywords.bug_id = ?
1073
AND keyworddefs.id = keywords.keywordid
1074
ORDER BY keyworddefs.name",
1075
undef, ($self->bug_id));
1077
$self->{'keywords'} = join(', ', @$list_ref);
1078
return $self->{'keywords'};
1083
return $self->{'longdescs'} if exists $self->{'longdescs'};
1084
return [] if $self->{'error'};
1085
$self->{'longdescs'} = GetComments($self->{bug_id});
1086
return $self->{'longdescs'};
1091
return $self->{'milestoneurl'} if exists $self->{'milestoneurl'};
1092
return '' if $self->{'error'};
1094
$self->{'prod_obj'} ||= new Bugzilla::Product({name => $self->product});
1095
$self->{'milestoneurl'} = $self->{'prod_obj'}->milestone_url;
1096
return $self->{'milestoneurl'};
1101
return $self->{product} if exists $self->{product};
1102
return '' if $self->{error};
1103
($self->{product}) = Bugzilla->dbh->selectrow_array(
1104
'SELECT name FROM products WHERE id = ?',
1105
undef, $self->{product_id});
1106
return $self->{product};
1111
return $self->{'qa_contact'} if exists $self->{'qa_contact'};
1112
return undef if $self->{'error'};
1114
if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact_id'}) {
1115
$self->{'qa_contact'} = new Bugzilla::User($self->{'qa_contact_id'});
1117
# XXX - This is somewhat inconsistent with the assignee/reporter
1118
# methods, which will return an empty User if they get a 0.
1119
# However, we're keeping it this way now, for backwards-compatibility.
1120
$self->{'qa_contact'} = undef;
1122
return $self->{'qa_contact'};
1127
return $self->{'reporter'} if exists $self->{'reporter'};
1128
$self->{'reporter_id'} = 0 if $self->{'error'};
1129
$self->{'reporter'} = new Bugzilla::User($self->{'reporter_id'});
1130
return $self->{'reporter'};
1134
sub show_attachment_flags {
1136
return $self->{'show_attachment_flags'}
1137
if exists $self->{'show_attachment_flags'};
1138
return 0 if $self->{'error'};
1140
# The number of types of flags that can be set on attachments to this bug
1141
# and the number of flags on those attachments. One of these counts must be
1142
# greater than zero in order for the "flags" column to appear in the table
1144
my $num_attachment_flag_types = Bugzilla::FlagType::count(
1145
{ 'target_type' => 'attachment',
1146
'product_id' => $self->{'product_id'},
1147
'component_id' => $self->{'component_id'} });
1148
my $num_attachment_flags = Bugzilla::Flag::count(
1149
{ 'target_type' => 'attachment',
1150
'bug_id' => $self->bug_id });
1152
$self->{'show_attachment_flags'} =
1153
($num_attachment_flag_types || $num_attachment_flags);
1155
return $self->{'show_attachment_flags'};
1160
return 0 if $self->{'error'};
1162
$self->{'prod_obj'} ||= new Bugzilla::Product({name => $self->product});
1164
return Bugzilla->params->{'usevotes'}
1165
&& $self->{'prod_obj'}->votes_per_user > 0;
1170
return $self->{'groups'} if exists $self->{'groups'};
1171
return [] if $self->{'error'};
1173
my $dbh = Bugzilla->dbh;
1176
# Some of this stuff needs to go into Bugzilla::User
1178
# For every group, we need to know if there is ANY bug_group_map
1179
# record putting the current bug in that group and if there is ANY
1180
# user_group_map record putting the user in that group.
1181
# The LEFT JOINs are checking for record existence.
1183
my $grouplist = Bugzilla->user->groups_as_string;
1184
my $sth = $dbh->prepare(
1185
"SELECT DISTINCT groups.id, name, description," .
1186
" CASE WHEN bug_group_map.group_id IS NOT NULL" .
1187
" THEN 1 ELSE 0 END," .
1188
" CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," .
1189
" isactive, membercontrol, othercontrol" .
1191
" LEFT JOIN bug_group_map" .
1192
" ON bug_group_map.group_id = groups.id" .
1194
" LEFT JOIN group_control_map" .
1195
" ON group_control_map.group_id = groups.id" .
1196
" AND group_control_map.product_id = ? " .
1197
" WHERE isbuggroup = 1" .
1198
" ORDER BY description");
1199
$sth->execute($self->{'bug_id'},
1200
$self->{'product_id'});
1202
while (my ($groupid, $name, $description, $ison, $ingroup, $isactive,
1203
$membercontrol, $othercontrol) = $sth->fetchrow_array()) {
1205
$membercontrol ||= 0;
1207
# For product groups, we only want to use the group if either
1208
# (1) The bit is set and not required, or
1209
# (2) The group is Shown or Default for members and
1210
# the user is a member of the group.
1212
($isactive && $ingroup
1213
&& (($membercontrol == CONTROLMAPDEFAULT)
1214
|| ($membercontrol == CONTROLMAPSHOWN))
1217
my $ismandatory = $isactive
1218
&& ($membercontrol == CONTROLMAPMANDATORY);
1220
push (@groups, { "bit" => $groupid,
1223
"ingroup" => $ingroup,
1224
"mandatory" => $ismandatory,
1225
"description" => $description });
1229
$self->{'groups'} = \@groups;
1231
return $self->{'groups'};
1236
return $self->{'user'} if exists $self->{'user'};
1237
return {} if $self->{'error'};
1239
my $user = Bugzilla->user;
1240
my $canmove = Bugzilla->params->{'move-enabled'} && $user->is_mover;
1242
my $prod_id = $self->{'product_id'};
1244
my $unknown_privileges = $user->in_group('editbugs', $prod_id);
1245
my $canedit = $unknown_privileges
1246
|| $user->id == $self->{assigned_to_id}
1247
|| (Bugzilla->params->{'useqacontact'}
1248
&& $self->{'qa_contact_id'}
1249
&& $user->id == $self->{qa_contact_id});
1250
my $canconfirm = $unknown_privileges
1251
|| $user->in_group('canconfirm', $prod_id);
1252
my $isreporter = $user->id
1253
&& $user->id == $self->{reporter_id};
1255
$self->{'user'} = {canmove => $canmove,
1256
canconfirm => $canconfirm,
1257
canedit => $canedit,
1258
isreporter => $isreporter};
1259
return $self->{'user'};
1264
return $self->{'choices'} if exists $self->{'choices'};
1265
return {} if $self->{'error'};
1267
$self->{'choices'} = {};
1268
$self->{prod_obj} ||= new Bugzilla::Product({name => $self->product});
1270
my @prodlist = map {$_->name} @{Bugzilla->user->get_enterable_products};
1271
# The current product is part of the popup, even if new bugs are no longer
1272
# allowed for that product
1273
if (lsearch(\@prodlist, $self->product) < 0) {
1274
push(@prodlist, $self->product);
1275
@prodlist = sort @prodlist;
1278
# Hack - this array contains "". See bug 106589.
1279
my @res = grep ($_, @{settable_resolutions()});
1281
$self->{'choices'} =
1283
'product' => \@prodlist,
1284
'rep_platform' => get_legal_field_values('rep_platform'),
1285
'priority' => get_legal_field_values('priority'),
1286
'bug_severity' => get_legal_field_values('bug_severity'),
1287
'op_sys' => get_legal_field_values('op_sys'),
1288
'bug_status' => get_legal_field_values('bug_status'),
1289
'resolution' => \@res,
1290
'component' => [map($_->name, @{$self->{prod_obj}->components})],
1291
'version' => [map($_->name, @{$self->{prod_obj}->versions})],
1292
'target_milestone' => [map($_->name, @{$self->{prod_obj}->milestones})],
1295
return $self->{'choices'};
1298
# List of resolutions that may be set directly by hand in the bug form.
1299
# 'MOVED' and 'DUPLICATE' are excluded from the list because setting
1300
# bugs to those resolutions requires a special process.
1301
sub settable_resolutions {
1302
my $resolutions = get_legal_field_values('resolution');
1303
my $pos = lsearch($resolutions, 'DUPLICATE');
1305
splice(@$resolutions, $pos, 1);
1307
$pos = lsearch($resolutions, 'MOVED');
1309
splice(@$resolutions, $pos, 1);
1311
return $resolutions;
1316
return 0 if $self->{error};
1317
return $self->{votes} if defined $self->{votes};
1319
my $dbh = Bugzilla->dbh;
1320
$self->{votes} = $dbh->selectrow_array(
1321
'SELECT SUM(vote_count) FROM votes
1322
WHERE bug_id = ? ' . $dbh->sql_group_by('bug_id'),
1323
undef, $self->bug_id);
1324
$self->{votes} ||= 0;
1325
return $self->{votes};
1328
# Convenience Function. If you need speed, use this. If you need
1329
# other Bug fields in addition to this, just create a new Bug with
1331
# Queries the database for the bug with a given alias, and returns
1332
# the ID of the bug if it exists or the undefined value if it doesn't.
1333
sub bug_alias_to_id {
1335
return undef unless Bugzilla->params->{"usebugaliases"};
1336
my $dbh = Bugzilla->dbh;
1337
trick_taint($alias);
1338
return $dbh->selectrow_array(
1339
"SELECT bug_id FROM bugs WHERE alias = ?", undef, $alias);
1342
#####################################################################
1344
#####################################################################
1347
my ($bugid, $whoid, $comment, $isprivate, $timestamp, $work_time,
1348
$type, $extra_data) = @_;
1350
$type ||= CMT_NORMAL;
1351
my $dbh = Bugzilla->dbh;
1353
ValidateTime($work_time, "work_time") if $work_time;
1354
trick_taint($work_time);
1355
detaint_natural($type)
1356
|| ThrowCodeError('bad_arg', {argument => 'type', function => 'AppendComment'});
1358
# Use the date/time we were given if possible (allowing calling code
1359
# to synchronize the comment's timestamp with those of other records).
1360
$timestamp ||= $dbh->selectrow_array('SELECT NOW()');
1362
$comment =~ s/\r\n/\n/g; # Handle Windows-style line endings.
1363
$comment =~ s/\r/\n/g; # Handle Mac-style line endings.
1365
if ($comment =~ /^\s*$/ && !$type) { # Nothin' but whitespace
1369
# Comments are always safe, because we always display their raw contents,
1370
# and we use them in a placeholder below.
1371
trick_taint($comment);
1372
my $privacyval = $isprivate ? 1 : 0 ;
1373
$dbh->do(q{INSERT INTO longdescs
1374
(bug_id, who, bug_when, thetext, isprivate, work_time,
1376
VALUES (?, ?, ?, ?, ?, ?, ?, ?)}, undef,
1377
($bugid, $whoid, $timestamp, $comment, $privacyval, $work_time,
1378
$type, $extra_data));
1379
$dbh->do("UPDATE bugs SET delta_ts = ? WHERE bug_id = ?",
1380
undef, $timestamp, $bugid);
1383
sub update_comment {
1384
my ($self, $comment_id, $new_comment) = @_;
1386
# Some validation checks.
1387
if ($self->{'error'}) {
1388
ThrowCodeError("bug_error", { bug => $self });
1390
detaint_natural($comment_id)
1391
|| ThrowCodeError('bad_arg', {argument => 'comment_id', function => 'update_comment'});
1393
# The comment ID must belong to this bug.
1394
my @current_comment_obj = grep {$_->{'id'} == $comment_id} @{$self->longdescs};
1395
scalar(@current_comment_obj)
1396
|| ThrowCodeError('bad_arg', {argument => 'comment_id', function => 'update_comment'});
1398
# If the new comment is undefined, then there is nothing to update.
1399
# To delete a comment, an empty string should be passed.
1400
return unless defined $new_comment;
1401
$new_comment =~ s/\s*$//s; # Remove trailing whitespaces.
1402
$new_comment =~ s/\r\n?/\n/g; # Handle Windows and Mac-style line endings.
1403
trick_taint($new_comment);
1405
# We assume ValidateComment() has already been called earlier.
1406
Bugzilla->dbh->do('UPDATE longdescs SET thetext = ? WHERE comment_id = ?',
1407
undef, ($new_comment, $comment_id));
1409
# Update the comment object with this new text.
1410
$current_comment_obj[0]->{'body'} = $new_comment;
1413
# Represents which fields from the bugs table are handled by process_bug.cgi.
1414
sub editable_bug_fields {
1415
my @fields = Bugzilla->dbh->bz_table_columns('bugs');
1416
# Obsolete custom fields are not editable.
1417
my @obsolete_fields = Bugzilla->get_fields({obsolete => 1, custom => 1});
1418
@obsolete_fields = map { $_->name } @obsolete_fields;
1419
foreach my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", "lastdiffed", @obsolete_fields) {
1420
my $location = lsearch(\@fields, $remove);
1421
splice(@fields, $location, 1);
1423
# Sorted because the old @::log_columns variable, which this replaces,
1425
return sort(@fields);
1428
# XXX - When Bug::update() will be implemented, we should make this routine
1430
sub EmitDependList {
1431
my ($myfield, $targetfield, $bug_id) = (@_);
1432
my $dbh = Bugzilla->dbh;
1433
my $list_ref = $dbh->selectcol_arrayref(
1434
"SELECT $targetfield FROM dependencies
1435
WHERE $myfield = ? ORDER BY $targetfield",
1440
# Tells you whether or not the argument is a valid "open" state.
1443
return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0);
1447
my ($time, $field) = @_;
1449
# regexp verifies one or more digits, optionally followed by a period and
1450
# zero or more digits, OR we have a period followed by one or more digits
1451
# (allow negatives, though, so people can back out errors in time reporting)
1452
if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) {
1453
ThrowUserError("number_not_numeric",
1454
{field => "$field", num => "$time"});
1457
# Only the "work_time" field is allowed to contain a negative value.
1458
if ( ($time < 0) && ($field ne "work_time") ) {
1459
ThrowUserError("number_too_small",
1460
{field => "$field", num => "$time", min_num => "0"});
1463
if ($time > 99999.99) {
1464
ThrowUserError("number_too_large",
1465
{field => "$field", num => "$time", max_num => "99999.99"});
1470
my ($id, $comment_sort_order, $start, $end, $raw) = @_;
1471
my $dbh = Bugzilla->dbh;
1473
$comment_sort_order = $comment_sort_order ||
1474
Bugzilla->user->settings->{'comment_sort_order'}->{'value'};
1476
my $sort_order = ($comment_sort_order eq "oldest_to_newest") ? 'asc' : 'desc';
1481
my $query = 'SELECT longdescs.comment_id AS id, profiles.realname AS name,
1482
profiles.login_name AS email, ' .
1483
$dbh->sql_date_format('longdescs.bug_when', '%Y.%m.%d %H:%i:%s') .
1484
' AS time, longdescs.thetext AS body, longdescs.work_time,
1485
isprivate, already_wrapped, type, extra_data
1488
ON profiles.userid = longdescs.who
1489
WHERE longdescs.bug_id = ?';
1491
$query .= ' AND longdescs.bug_when > ?
1492
AND longdescs.bug_when <= ?';
1493
push(@args, ($start, $end));
1495
$query .= " ORDER BY longdescs.bug_when $sort_order";
1496
my $sth = $dbh->prepare($query);
1497
$sth->execute(@args);
1499
while (my $comment_ref = $sth->fetchrow_hashref()) {
1500
my %comment = %$comment_ref;
1502
$comment{'email'} .= Bugzilla->params->{'emailsuffix'};
1503
$comment{'name'} = $comment{'name'} || $comment{'email'};
1505
# If raw data is requested, do not format 'special' comments.
1506
$comment{'body'} = format_comment(\%comment) unless $raw;
1508
push (@comments, \%comment);
1511
if ($comment_sort_order eq "newest_to_oldest_desc_first") {
1512
unshift(@comments, pop @comments);
1518
# Format language specific comments. This routine must not update
1519
# $comment{'body'} itself, see BugMail::prepare_comments().
1520
sub format_comment {
1521
my $comment = shift;
1524
if ($comment->{'type'} == CMT_DUPE_OF) {
1525
$body = $comment->{'body'} . "\n\n" .
1526
get_text('bug_duplicate_of', { dupe_of => $comment->{'extra_data'} });
1528
elsif ($comment->{'type'} == CMT_HAS_DUPE) {
1529
$body = get_text('bug_has_duplicate', { dupe => $comment->{'extra_data'} });
1531
elsif ($comment->{'type'} == CMT_POPULAR_VOTES) {
1532
$body = get_text('bug_confirmed_by_votes');
1534
elsif ($comment->{'type'} == CMT_MOVED_TO) {
1535
$body = $comment->{'body'} . "\n\n" .
1536
get_text('bug_moved_to', { login => $comment->{'extra_data'} });
1539
$body = $comment->{'body'};
1544
# Get the activity of a bug, starting from $starttime (if given).
1545
# This routine assumes ValidateBugID has been previously called.
1546
sub GetBugActivity {
1547
my ($id, $starttime) = @_;
1548
my $dbh = Bugzilla->dbh;
1550
# Arguments passed to the SQL query.
1553
# Only consider changes since $starttime, if given.
1555
if (defined $starttime) {
1556
trick_taint($starttime);
1557
push (@args, $starttime);
1558
$datepart = "AND bugs_activity.bug_when > ?";
1561
# Only includes attachments the user is allowed to see.
1564
if (Bugzilla->params->{"insidergroup"}
1565
&& !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'}))
1567
$suppjoins = "LEFT JOIN attachments
1568
ON attachments.attach_id = bugs_activity.attach_id";
1569
$suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0";
1573
SELECT COALESCE(fielddefs.description, "
1574
# This is a hack - PostgreSQL requires both COALESCE
1575
# arguments to be of the same type, and this is the only
1576
# way supported by both MySQL 3 and PostgreSQL to convert
1577
# an integer to a string. MySQL 4 supports CAST.
1578
. $dbh->sql_string_concat('bugs_activity.fieldid', q{''}) .
1579
"), fielddefs.name, bugs_activity.attach_id, " .
1580
$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
1581
", bugs_activity.removed, bugs_activity.added, profiles.login_name
1585
ON bugs_activity.fieldid = fielddefs.id
1587
ON profiles.userid = bugs_activity.who
1588
WHERE bugs_activity.bug_id = ?
1591
ORDER BY bugs_activity.bug_when";
1593
my $list = $dbh->selectall_arrayref($query, undef, @args);
1598
my $incomplete_data = 0;
1600
foreach my $entry (@$list) {
1601
my ($field, $fieldname, $attachid, $when, $removed, $added, $who) = @$entry;
1603
my $activity_visible = 1;
1605
# check if the user should see this field's activity
1606
if ($fieldname eq 'remaining_time'
1607
|| $fieldname eq 'estimated_time'
1608
|| $fieldname eq 'work_time'
1609
|| $fieldname eq 'deadline')
1612
Bugzilla->user->in_group(Bugzilla->params->{'timetrackinggroup'}) ? 1 : 0;
1614
$activity_visible = 1;
1617
if ($activity_visible) {
1618
# This gets replaced with a hyperlink in the template.
1619
$field =~ s/^Attachment\s*// if $attachid;
1621
# Check for the results of an old Bugzilla data corruption bug
1622
$incomplete_data = 1 if ($added =~ /^\?/ || $removed =~ /^\?/);
1624
# An operation, done by 'who' at time 'when', has a number of
1625
# 'changes' associated with it.
1626
# If this is the start of a new operation, store the data from the
1627
# previous one, and set up the new one.
1628
if ($operation->{'who'}
1629
&& ($who ne $operation->{'who'}
1630
|| $when ne $operation->{'when'}))
1632
$operation->{'changes'} = $changes;
1633
push (@operations, $operation);
1635
# Create new empty anonymous data structures.
1640
$operation->{'who'} = $who;
1641
$operation->{'when'} = $when;
1643
$change{'field'} = $field;
1644
$change{'fieldname'} = $fieldname;
1645
$change{'attachid'} = $attachid;
1646
$change{'removed'} = $removed;
1647
$change{'added'} = $added;
1648
push (@$changes, \%change);
1652
if ($operation->{'who'}) {
1653
$operation->{'changes'} = $changes;
1654
push (@operations, $operation);
1657
return(\@operations, $incomplete_data);
1660
# Update the bugs_activity table to reflect changes made in bugs.
1661
sub LogActivityEntry {
1662
my ($i, $col, $removed, $added, $whoid, $timestamp) = @_;
1663
my $dbh = Bugzilla->dbh;
1664
# in the case of CCs, deps, and keywords, there's a possibility that someone
1665
# might try to add or remove a lot of them at once, which might take more
1666
# space than the activity table allows. We'll solve this by splitting it
1667
# into multiple entries if it's too long.
1668
while ($removed || $added) {
1669
my ($removestr, $addstr) = ($removed, $added);
1670
if (length($removestr) > MAX_LINE_LENGTH) {
1671
my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH);
1672
$removestr = substr($removed, 0, $commaposition);
1673
$removed = substr($removed, $commaposition);
1674
$removed =~ s/^[,\s]+//; # remove any comma or space
1676
$removed = ""; # no more entries
1678
if (length($addstr) > MAX_LINE_LENGTH) {
1679
my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH);
1680
$addstr = substr($added, 0, $commaposition);
1681
$added = substr($added, $commaposition);
1682
$added =~ s/^[,\s]+//; # remove any comma or space
1684
$added = ""; # no more entries
1686
trick_taint($addstr);
1687
trick_taint($removestr);
1688
my $fieldid = get_field_id($col);
1689
$dbh->do("INSERT INTO bugs_activity
1690
(bug_id, who, bug_when, fieldid, removed, added)
1691
VALUES (?, ?, ?, ?, ?, ?)",
1692
undef, ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr));
1696
# CountOpenDependencies counts the number of open dependent bugs for a
1697
# list of bugs and returns a list of bug_id's and their dependency count
1698
# It takes one parameter:
1699
# - A list of bug numbers whose dependencies are to be checked
1700
sub CountOpenDependencies {
1701
my (@bug_list) = @_;
1703
my $dbh = Bugzilla->dbh;
1705
my $sth = $dbh->prepare(
1706
"SELECT blocked, COUNT(bug_status) " .
1707
"FROM bugs, dependencies " .
1708
"WHERE blocked IN (" . (join "," , @bug_list) . ") " .
1709
"AND bug_id = dependson " .
1710
"AND bug_status IN ('" . (join "','", BUG_STATE_OPEN) . "') " .
1711
$dbh->sql_group_by('blocked'));
1714
while (my ($bug_id, $dependencies) = $sth->fetchrow_array()) {
1715
push(@dependencies, { bug_id => $bug_id,
1716
dependencies => $dependencies });
1719
return @dependencies;
1722
sub ValidateComment {
1725
if (defined($comment) && length($comment) > MAX_COMMENT_LENGTH) {
1726
ThrowUserError("comment_too_long");
1730
# If a bug is moved to a product which allows less votes per bug
1731
# compared to the previous product, extra votes need to be removed.
1733
my ($id, $who, $reason) = (@_);
1734
my $dbh = Bugzilla->dbh;
1736
my $whopart = ($who) ? " AND votes.who = $who" : "";
1738
my $sth = $dbh->prepare("SELECT profiles.login_name, " .
1739
"profiles.userid, votes.vote_count, " .
1740
"products.votesperuser, products.maxvotesperbug " .
1742
"LEFT JOIN votes ON profiles.userid = votes.who " .
1743
"LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " .
1744
"LEFT JOIN products ON products.id = bugs.product_id " .
1745
"WHERE votes.bug_id = ? " . $whopart);
1748
while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) {
1749
push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]);
1752
# @messages stores all emails which have to be sent, if any.
1753
# This array is passed to the caller which will send these emails itself.
1756
if (scalar(@list)) {
1757
foreach my $ref (@list) {
1758
my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref);
1760
$maxvotesperbug = min($votesperuser, $maxvotesperbug);
1762
# If this product allows voting and the user's votes are in
1763
# the acceptable range, then don't do anything.
1764
next if $votesperuser && $oldvotes <= $maxvotesperbug;
1766
# If the user has more votes on this bug than this product
1767
# allows, then reduce the number of votes so it fits
1768
my $newvotes = $maxvotesperbug;
1770
my $removedvotes = $oldvotes - $newvotes;
1773
$dbh->do("UPDATE votes SET vote_count = ? " .
1774
"WHERE bug_id = ? AND who = ?",
1775
undef, ($newvotes, $id, $userid));
1777
$dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?",
1778
undef, ($id, $userid));
1781
# Notice that we did not make sure that the user fit within the $votesperuser
1782
# range. This is considered to be an acceptable alternative to losing votes
1783
# during product moves. Then next time the user attempts to change their votes,
1784
# they will be forced to fit within the $votesperuser limit.
1786
# Now lets send the e-mail to alert the user to the fact that their votes have
1787
# been reduced or removed.
1789
'to' => $name . Bugzilla->params->{'emailsuffix'},
1791
'reason' => $reason,
1793
'votesremoved' => $removedvotes,
1794
'votesold' => $oldvotes,
1795
'votesnew' => $newvotes,
1798
my $voter = new Bugzilla::User($userid);
1799
my $template = Bugzilla->template_inner($voter->settings->{'lang'}->{'value'});
1802
$template->process("email/votes-removed.txt.tmpl", $vars, \$msg);
1803
push(@messages, $msg);
1805
Bugzilla->template_inner("");
1807
my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " .
1808
"FROM votes WHERE bug_id = ?",
1810
$dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?",
1811
undef, ($votes, $id));
1813
# Now return the array containing emails to be sent.
1817
# If a user votes for a bug, or the number of votes required to
1818
# confirm a bug has been reduced, check if the bug is now confirmed.
1819
sub CheckIfVotedConfirmed {
1820
my ($id, $who) = (@_);
1821
my $dbh = Bugzilla->dbh;
1823
my ($votes, $status, $everconfirmed, $votestoconfirm, $timestamp) =
1824
$dbh->selectrow_array("SELECT votes, bug_status, everconfirmed, " .
1825
" votestoconfirm, NOW() " .
1826
"FROM bugs INNER JOIN products " .
1827
" ON products.id = bugs.product_id " .
1828
"WHERE bugs.bug_id = ?",
1832
if ($votes >= $votestoconfirm && !$everconfirmed) {
1833
if ($status eq 'UNCONFIRMED') {
1834
my $fieldid = get_field_id("bug_status");
1835
$dbh->do("UPDATE bugs SET bug_status = 'NEW', everconfirmed = 1, " .
1836
"delta_ts = ? WHERE bug_id = ?",
1837
undef, ($timestamp, $id));
1838
$dbh->do("INSERT INTO bugs_activity " .
1839
"(bug_id, who, bug_when, fieldid, removed, added) " .
1840
"VALUES (?, ?, ?, ?, ?, ?)",
1841
undef, ($id, $who, $timestamp, $fieldid, 'UNCONFIRMED', 'NEW'));
1844
$dbh->do("UPDATE bugs SET everconfirmed = 1, delta_ts = ? " .
1845
"WHERE bug_id = ?", undef, ($timestamp, $id));
1848
my $fieldid = get_field_id("everconfirmed");
1849
$dbh->do("INSERT INTO bugs_activity " .
1850
"(bug_id, who, bug_when, fieldid, removed, added) " .
1851
"VALUES (?, ?, ?, ?, ?, ?)",
1852
undef, ($id, $who, $timestamp, $fieldid, '0', '1'));
1854
AppendComment($id, $who, "", 0, $timestamp, 0, CMT_POPULAR_VOTES);
1861
################################################################################
1862
# check_can_change_field() defines what users are allowed to change. You
1863
# can add code here for site-specific policy changes, according to the
1864
# instructions given in the Bugzilla Guide and below. Note that you may also
1865
# have to update the Bugzilla::Bug::user() function to give people access to the
1866
# options that they are permitted to change.
1868
# check_can_change_field() returns true if the user is allowed to change this
1869
# field, and false if they are not.
1871
# The parameters to this method are as follows:
1872
# $field - name of the field in the bugs table the user is trying to change
1873
# $oldvalue - what they are changing it from
1874
# $newvalue - what they are changing it to
1875
# $PrivilegesRequired - return the reason of the failure, if any
1876
# $data - hash containing relevant parameters, e.g. from the CGI object
1877
################################################################################
1878
sub check_can_change_field {
1880
my ($field, $oldvalue, $newvalue, $PrivilegesRequired, $data) = (@_);
1881
my $user = Bugzilla->user;
1883
$oldvalue = defined($oldvalue) ? $oldvalue : '';
1884
$newvalue = defined($newvalue) ? $newvalue : '';
1886
# Return true if they haven't changed this field at all.
1887
if ($oldvalue eq $newvalue) {
1889
} elsif (trim($oldvalue) eq trim($newvalue)) {
1891
# numeric fields need to be compared using ==
1892
} elsif (($field eq 'estimated_time' || $field eq 'remaining_time')
1893
&& $newvalue ne $data->{'dontchange'}
1894
&& $oldvalue == $newvalue)
1899
# Allow anyone to change comments.
1900
if ($field =~ /^longdesc/) {
1904
# Ignore the assigned_to field if the bug is not being reassigned
1905
if ($field eq 'assigned_to'
1906
&& $data->{'knob'} ne 'reassignbycomponent'
1907
&& $data->{'knob'} ne 'reassign')
1912
# If the user isn't allowed to change a field, we must tell him who can.
1913
# We store the required permission set into the $PrivilegesRequired
1914
# variable which gets passed to the error template.
1916
# $PrivilegesRequired = 0 : no privileges required;
1917
# $PrivilegesRequired = 1 : the reporter, assignee or an empowered user;
1918
# $PrivilegesRequired = 2 : the assignee or an empowered user;
1919
# $PrivilegesRequired = 3 : an empowered user.
1921
# Allow anyone with (product-specific) "editbugs" privs to change anything.
1922
if ($user->in_group('editbugs', $self->{'product_id'})) {
1926
# *Only* users with (product-specific) "canconfirm" privs can confirm bugs.
1927
if ($field eq 'canconfirm'
1928
|| ($field eq 'bug_status'
1929
&& $oldvalue eq 'UNCONFIRMED'
1930
&& is_open_state($newvalue)))
1932
$$PrivilegesRequired = 3;
1933
return $user->in_group('canconfirm', $self->{'product_id'});
1936
# Make sure that a valid bug ID has been given.
1937
if (!$self->{'error'}) {
1938
# Allow the assignee to change anything else.
1939
if ($self->{'assigned_to_id'} == $user->id) {
1943
# Allow the QA contact to change anything else.
1944
if (Bugzilla->params->{'useqacontact'}
1945
&& $self->{'qa_contact_id'}
1946
&& ($self->{'qa_contact_id'} == $user->id))
1952
# At this point, the user is either the reporter or an
1953
# unprivileged user. We first check for fields the reporter
1954
# is not allowed to change.
1956
# The reporter may not:
1957
# - reassign bugs, unless the bugs are assigned to him;
1958
# in that case we will have already returned 1 above
1959
# when checking for the assignee of the bug.
1960
if ($field eq 'assigned_to') {
1961
$$PrivilegesRequired = 2;
1964
# - change the QA contact
1965
if ($field eq 'qa_contact') {
1966
$$PrivilegesRequired = 2;
1969
# - change the target milestone
1970
if ($field eq 'target_milestone') {
1971
$$PrivilegesRequired = 2;
1974
# - change the priority (unless he could have set it originally)
1975
if ($field eq 'priority'
1976
&& !Bugzilla->params->{'letsubmitterchoosepriority'})
1978
$$PrivilegesRequired = 2;
1982
# The reporter is allowed to change anything else.
1983
if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) {
1987
# If we haven't returned by this point, then the user doesn't
1988
# have the necessary permissions to change this field.
1989
$$PrivilegesRequired = 1;
1997
# Validates and verifies a bug ID, making sure the number is a
1998
# positive integer, that it represents an existing bug in the
1999
# database, and that the user is authorized to access that bug.
2000
# We detaint the number here, too.
2002
my ($id, $field) = @_;
2003
my $dbh = Bugzilla->dbh;
2004
my $user = Bugzilla->user;
2006
# Get rid of leading '#' (number) mark, if present.
2011
# If the ID isn't a number, it might be an alias, so try to convert it.
2013
if (!detaint_natural($id)) {
2014
$id = bug_alias_to_id($alias);
2015
$id || ThrowUserError("invalid_bug_id_or_alias",
2016
{'bug_id' => $alias,
2017
'field' => $field });
2020
# Modify the calling code's original variable to contain the trimmed,
2021
# converted-from-alias ID.
2024
# First check that the bug exists
2025
$dbh->selectrow_array("SELECT bug_id FROM bugs WHERE bug_id = ?", undef, $id)
2026
|| ThrowUserError("invalid_bug_id_non_existent", {'bug_id' => $id});
2028
return if (defined $field && ($field eq "dependson" || $field eq "blocked"));
2030
return if $user->can_see_bug($id);
2032
# The user did not pass any of the authorization tests, which means they
2033
# are not authorized to see the bug. Display an error and stop execution.
2034
# The error the user sees depends on whether or not they are logged in
2035
# (i.e. $user->id contains the user's positive integer ID).
2037
ThrowUserError("bug_access_denied", {'bug_id' => $id});
2039
ThrowUserError("bug_access_query", {'bug_id' => $id});
2044
# Check that the bug alias is valid and not used by another bug. If
2045
# curr_id is specified, verify the alias is not used for any other
2047
sub ValidateBugAlias {
2048
my ($alias, $curr_id) = @_;
2049
my $dbh = Bugzilla->dbh;
2051
$alias = trim($alias || "");
2052
trick_taint($alias);
2055
ThrowUserError("alias_not_defined");
2058
# Make sure the alias isn't too long.
2059
if (length($alias) > 20) {
2060
ThrowUserError("alias_too_long");
2063
# Make sure the alias is unique.
2064
my $query = "SELECT bug_id FROM bugs WHERE alias = ?";
2065
if ($curr_id && detaint_natural($curr_id)) {
2066
$query .= " AND bug_id != $curr_id";
2068
my $id = $dbh->selectrow_array($query, undef, $alias);
2071
$vars->{'alias'} = $alias;
2073
$vars->{'bug_id'} = $id;
2074
ThrowUserError("alias_in_use", $vars);
2077
# Make sure the alias isn't just a number.
2078
if ($alias =~ /^\d+$/) {
2079
ThrowUserError("alias_is_numeric", $vars);
2082
# Make sure the alias has no commas or spaces.
2083
if ($alias =~ /[, ]/) {
2084
ThrowUserError("alias_has_comma_or_space", $vars);
2090
# Validate and return a hash of dependencies
2091
sub ValidateDependencies {
2093
# These can be arrayrefs or they can be strings.
2094
$fields->{'dependson'} = shift;
2095
$fields->{'blocked'} = shift;
2096
my $id = shift || 0;
2098
unless (defined($fields->{'dependson'})
2099
|| defined($fields->{'blocked'}))
2104
my $dbh = Bugzilla->dbh;
2107
foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) {
2108
my ($me, $target) = @{$pair};
2109
$deptree{$target} = [];
2110
$deps{$target} = [];
2111
next unless $fields->{$target};
2114
my $target_array = ref($fields->{$target}) ? $fields->{$target}
2115
: [split(/[\s,]+/, $fields->{$target})];
2116
foreach my $i (@$target_array) {
2118
ThrowUserError("dependency_loop_single");
2120
if (!exists $seen{$i}) {
2121
push(@{$deptree{$target}}, $i);
2125
# populate $deps{$target} as first-level deps only.
2126
# and find remainder of dependency tree in $deptree{$target}
2127
@{$deps{$target}} = @{$deptree{$target}};
2128
my @stack = @{$deps{$target}};
2130
my $i = shift @stack;
2132
$dbh->selectcol_arrayref("SELECT $target
2134
WHERE $me = ?", undef, $i);
2135
foreach my $t (@$dep_list) {
2136
# ignore any _current_ dependencies involving this bug,
2137
# as they will be overwritten with data from the form.
2138
if ($t != $id && !exists $seen{$t}) {
2139
push(@{$deptree{$target}}, $t);
2147
my @deps = @{$deptree{'dependson'}};
2148
my @blocks = @{$deptree{'blocked'}};
2151
foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ }
2152
my @isect = keys %isect;
2153
if (scalar(@isect) > 0) {
2154
ThrowUserError("dependency_loop_multi", {'deps' => \@isect});
2160
#####################################################################
2161
# Autoloaded Accessors
2162
#####################################################################
2164
# Determines whether an attribute access trapped by the AUTOLOAD function
2165
# is for a valid bug attribute. Bug attributes are properties and methods
2166
# predefined by this module as well as bug fields for which an accessor
2167
# can be defined by AUTOLOAD at runtime when the accessor is first accessed.
2169
# XXX Strangely, some predefined attributes are on the list, but others aren't,
2170
# and the original code didn't specify why that is. Presumably the only
2171
# attributes that need to be on this list are those that aren't predefined;
2172
# we should verify that and update the list accordingly.
2174
sub _validate_attribute {
2175
my ($attribute) = @_;
2177
my @valid_attributes = (
2178
# Miscellaneous properties and methods.
2179
qw(error groups product_id component_id
2180
longdescs milestoneurl attachments
2181
isopened isunconfirmed
2182
flag_types num_attachment_flag_types
2183
show_attachment_flags any_flags_requesteeble),
2186
Bugzilla::Bug->fields
2189
return grep($attribute eq $_, @valid_attributes) ? 1 : 0;
2193
use vars qw($AUTOLOAD);
2194
my $attr = $AUTOLOAD;
2197
return unless $attr=~ /[^A-Z]/;
2198
if (!_validate_attribute($attr)) {
2200
Carp::confess("invalid bug attribute $attr");
2206
if (defined $self->{$attr}) {
2207
return $self->{$attr};