~ubuntu-branches/debian/sid/bugzilla/sid

« back to all changes in this revision

Viewing changes to Bugzilla/Bug.pm

  • Committer: Bazaar Package Importer
  • Author(s): Raphael Bossek
  • Date: 2008-06-27 22:34:34 UTC
  • mfrom: (1.1.7 upstream)
  • Revision ID: james.westby@ubuntu.com-20080627223434-0ib57vstn43bb4a3
Tags: 3.0.4.1-1
* Update of French, Russian and German translations. (closes: #488251)
* Added Bulgarian and Belarusian translations.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- Mode: perl; indent-tabs-mode: nil -*-
2
 
#
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/
7
 
#
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.
12
 
#
13
 
# The Original Code is the Bugzilla Bug Tracking System.
14
 
#
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
18
 
# Rights Reserved.
19
 
#
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>
28
 
 
29
 
package Bugzilla::Bug;
30
 
 
31
 
use strict;
32
 
 
33
 
use Bugzilla::Attachment;
34
 
use Bugzilla::Constants;
35
 
use Bugzilla::Field;
36
 
use Bugzilla::Flag;
37
 
use Bugzilla::FlagType;
38
 
use Bugzilla::Keyword;
39
 
use Bugzilla::User;
40
 
use Bugzilla::Util;
41
 
use Bugzilla::Error;
42
 
use Bugzilla::Product;
43
 
use Bugzilla::Component;
44
 
use Bugzilla::Group;
45
 
 
46
 
use List::Util qw(min);
47
 
 
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
53
 
    LogActivityEntry
54
 
    is_open_state
55
 
    editable_bug_fields
56
 
);
57
 
 
58
 
#####################################################################
59
 
# Constants
60
 
#####################################################################
61
 
 
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;
66
 
 
67
 
# This is a sub because it needs to call other subroutines.
68
 
sub DB_COLUMNS {
69
 
    my $dbh = Bugzilla->dbh;
70
 
    return qw(
71
 
        alias
72
 
        bug_file_loc
73
 
        bug_id
74
 
        bug_severity
75
 
        bug_status
76
 
        cclist_accessible
77
 
        component_id
78
 
        delta_ts
79
 
        estimated_time
80
 
        everconfirmed
81
 
        op_sys
82
 
        priority
83
 
        product_id
84
 
        remaining_time
85
 
        rep_platform
86
 
        reporter_accessible
87
 
        resolution
88
 
        short_desc
89
 
        status_whiteboard
90
 
        target_milestone
91
 
        version
92
 
    ),
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;
99
 
}
100
 
 
101
 
use constant REQUIRED_CREATE_FIELDS => qw(
102
 
    component
103
 
    product
104
 
    short_desc
105
 
    version
106
 
);
107
 
 
108
 
# There are also other, more complex validators that are called
109
 
# from run_create_validators.
110
 
sub VALIDATORS {
111
 
    my $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,
126
 
    };
127
 
 
128
 
    my @custom_fields = Bugzilla->get_fields({custom => 1, obsolete => 0});
129
 
 
130
 
    foreach my $field (@custom_fields) {
131
 
        my $validator;
132
 
        if ($field->type == FIELD_TYPE_SINGLE_SELECT) {
133
 
            $validator = \&_check_select_field;
134
 
        }
135
 
        elsif ($field->type == FIELD_TYPE_FREETEXT) {
136
 
            $validator = \&_check_freetext_field;
137
 
        }
138
 
        $validators->{$field->name} = $validator if $validator;
139
 
    }
140
 
    return $validators;
141
 
};
142
 
 
143
 
# Used in LogActivityEntry(). Gives the max length of lines in the
144
 
# activity table.
145
 
use constant MAX_LINE_LENGTH => 254;
146
 
 
147
 
# Used in ValidateComment(). Gives the max length allowed for a comment.
148
 
use constant MAX_COMMENT_LENGTH => 65535;
149
 
 
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(
153
 
    UNCONFIRMED
154
 
    NEW
155
 
    ASSIGNED
156
 
);
157
 
 
158
 
#####################################################################
159
 
 
160
 
sub new {
161
 
    my $invocant = shift;
162
 
    my $class = ref($invocant) || $invocant;
163
 
    my $param = shift;
164
 
 
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 };
171
 
        }
172
 
        else {
173
 
            # Aliases are off, and we got something that's not a number.
174
 
            my $error_self = {};
175
 
            bless $error_self, $class;
176
 
            $error_self->{'bug_id'} = $param;
177
 
            $error_self->{'error'}  = 'InvalidBugId';
178
 
            return $error_self;
179
 
        }
180
 
    }
181
 
 
182
 
    unshift @_, $param;
183
 
    my $self = $class->SUPER::new(@_);
184
 
 
185
 
    # Bugzilla::Bug->new always returns something, but sets $self->{error}
186
 
    # if the bug wasn't found in the database.
187
 
    if (!$self) {
188
 
        my $error_self = {};
189
 
        bless $error_self, $class;
190
 
        $error_self->{'bug_id'} = ref($param) ? $param->{name} : $param;
191
 
        $error_self->{'error'}  = 'NotFound';
192
 
        return $error_self;
193
 
    }
194
 
 
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});
200
 
 
201
 
    return $self;
202
 
}
203
 
 
204
 
# Docs for create() (there's no POD in this file yet, but we very
205
 
# much need this documented right now):
206
 
#
207
 
# The same as Bugzilla::Object->create. Parameters are only required
208
 
# if they say so below.
209
 
#
210
 
# Params:
211
 
#
212
 
# C<product>     - B<Required> The name of the product this bug is being
213
 
#                  filed against.
214
 
# C<component>   - B<Required> The name of the component this bug is being
215
 
#                  filed against.
216
 
#
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.
224
 
#
225
 
# C<alias>        - An alias for this bug. Will be ignored if C<usebugaliases>
226
 
#                   is off.
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.
231
 
#
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.
236
 
#
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>.
242
 
sub create {
243
 
    my ($class, $params) = @_;
244
 
    my $dbh = Bugzilla->dbh;
245
 
 
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};
257
 
 
258
 
    $class->check_required_create_fields($params);
259
 
    $params = $class->run_create_validators($params);
260
 
 
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};
274
 
 
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));
279
 
 
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};
284
 
 
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');
288
 
 
289
 
    my $bug = $class->insert_create_data($params);
290
 
 
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);
296
 
    }
297
 
 
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;
302
 
 
303
 
    # Add the CCs
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);
307
 
    }
308
 
 
309
 
    # Add in keywords
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);
314
 
    }
315
 
 
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 = ?');
320
 
 
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);
327
 
    }
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);
334
 
    }
335
 
 
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);
345
 
    }
346
 
    my $qmarks = "?," x @columns;
347
 
    chop($qmarks);
348
 
    $dbh->do('INSERT INTO longdescs (' . join(',', @columns)  . ")
349
 
                   VALUES ($qmarks)", undef, @values);
350
 
 
351
 
    $dbh->bz_unlock_tables();
352
 
 
353
 
    return $bug;
354
 
}
355
 
 
356
 
 
357
 
sub run_create_validators {
358
 
    my $class  = shift;
359
 
    my $params = $class->SUPER::run_create_validators(@_);
360
 
 
361
 
    my $product = $params->{product};
362
 
    $params->{product_id} = $product->id;
363
 
    delete $params->{product};
364
 
 
365
 
    ($params->{bug_status}, $params->{everconfirmed})
366
 
        = $class->_check_bug_status($product, $params->{bug_status});
367
 
 
368
 
    $params->{target_milestone} = $class->_check_target_milestone($product,
369
 
        $params->{target_milestone});
370
 
 
371
 
    $params->{version} = $class->_check_version($product, $params->{version});
372
 
 
373
 
    $params->{keywords} = $class->_check_keywords($product, $params->{keywords});
374
 
 
375
 
    $params->{groups} = $class->_check_groups($product,
376
 
        $params->{groups});
377
 
 
378
 
    my $component = $class->_check_component($product, $params->{component});
379
 
    $params->{component_id} = $component->id;
380
 
    delete $params->{component};
381
 
 
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});
387
 
 
388
 
    # Callers cannot set Reporter, currently.
389
 
    $params->{reporter} = Bugzilla->user->id;
390
 
 
391
 
    $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
392
 
    $params->{delta_ts} = $params->{creation_ts};
393
 
 
394
 
    if ($params->{estimated_time}) {
395
 
        $params->{remaining_time} = $params->{estimated_time};
396
 
    }
397
 
 
398
 
    $class->_check_strict_isolation($product, $params->{cc},
399
 
                                    $params->{assigned_to}, $params->{qa_contact});
400
 
 
401
 
    ($params->{dependson}, $params->{blocked}) = 
402
 
        $class->_check_dependencies($product, $params->{dependson}, $params->{blocked});
403
 
 
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};
409
 
 
410
 
    return $params;
411
 
}
412
 
 
413
 
# This is the correct way to delete bugs from the DB.
414
 
# No bug should be deleted from anywhere else except from here.
415
 
#
416
 
sub remove_from_db {
417
 
    my ($self) = @_;
418
 
    my $dbh = Bugzilla->dbh;
419
 
 
420
 
    if ($self->{'error'}) {
421
 
        ThrowCodeError("bug_error", { bug => $self });
422
 
    }
423
 
 
424
 
    my $bug_id = $self->{'bug_id'};
425
 
 
426
 
    # tables having 'bugs.bug_id' as a foreign key:
427
 
    # - attachments
428
 
    # - bug_group_map
429
 
    # - bugs
430
 
    # - bugs_activity
431
 
    # - cc
432
 
    # - dependencies
433
 
    # - duplicates
434
 
    # - flags
435
 
    # - keywords
436
 
    # - longdescs
437
 
    # - votes
438
 
 
439
 
    # Also, the attach_data table uses attachments.attach_id as a foreign
440
 
    # key, and so indirectly depends on a bug deletion too.
441
 
 
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');
448
 
 
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);
460
 
 
461
 
    # The attach_data table doesn't depend on bugs.bug_id directly.
462
 
    my $attach_ids =
463
 
        $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
464
 
                                  WHERE bug_id = ?", undef, $bug_id);
465
 
 
466
 
    if (scalar(@$attach_ids)) {
467
 
        $dbh->do("DELETE FROM attach_data WHERE id IN (" .
468
 
                 join(",", @$attach_ids) . ")");
469
 
    }
470
 
 
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);
474
 
 
475
 
    $dbh->bz_unlock_tables();
476
 
 
477
 
    # Now this bug no longer exists
478
 
    $self->DESTROY;
479
 
    return $self;
480
 
}
481
 
 
482
 
#####################################################################
483
 
# Validators
484
 
#####################################################################
485
 
 
486
 
sub _check_alias {
487
 
   my ($invocant, $alias) = @_;
488
 
   $alias = trim($alias);
489
 
   return undef if (!Bugzilla->params->{'usebugaliases'} || !$alias);
490
 
   ValidateBugAlias($alias);
491
 
   return $alias;
492
 
}
493
 
 
494
 
sub _check_assigned_to {
495
 
    my ($invocant, $component, $name) = @_;
496
 
    my $user = Bugzilla->user;
497
 
 
498
 
    $name = trim($name);
499
 
    # Default assignee is the component owner.
500
 
    my $id;
501
 
    if (!$user->in_group('editbugs', $component->product_id) || !$name) {
502
 
        $id = $component->default_assignee->id;
503
 
    } else {
504
 
        $id = login_to_id($name, THROW_ERROR);
505
 
    }
506
 
    return $id;
507
 
}
508
 
 
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://');
513
 
    return $url;
514
 
}
515
 
 
516
 
sub _check_bug_severity {
517
 
    my ($invocant, $severity) = @_;
518
 
    $severity = trim($severity);
519
 
    check_field('bug_severity', $severity);
520
 
    return $severity;
521
 
}
522
 
 
523
 
sub _check_bug_status {
524
 
    my ($invocant, $product, $status) = @_;
525
 
    my $user = Bugzilla->user;
526
 
 
527
 
    my @valid_statuses = VALID_ENTRY_STATUS;
528
 
 
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.
532
 
       $status ||= 'NEW';
533
 
    }
534
 
    elsif (!$product->votes_to_confirm) {
535
 
        # Without privs, products that don't support UNCONFIRMED default to
536
 
        # NEW.
537
 
        $status = 'NEW';
538
 
    }
539
 
    else {
540
 
        $status = 'UNCONFIRMED';
541
 
    }
542
 
 
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;
546
 
 
547
 
    check_field('bug_status', $status, \@valid_statuses);
548
 
    return ($status, $status eq 'UNCONFIRMED' ? 0 : 1);
549
 
}
550
 
 
551
 
sub _check_cc {
552
 
    my ($invocant, $component, $ccs) = @_;
553
 
    return [map {$_->id} @{$component->initial_cc}] unless $ccs;
554
 
 
555
 
    my %cc_ids;
556
 
    foreach my $person (@$ccs) {
557
 
        next unless $person;
558
 
        my $id = login_to_id($person, THROW_ERROR);
559
 
        $cc_ids{$id} = 1;
560
 
    }
561
 
 
562
 
    # Enforce Default CC
563
 
    $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc});
564
 
 
565
 
    return [keys %cc_ids];
566
 
}
567
 
 
568
 
sub _check_comment {
569
 
    my ($invocant, $comment) = @_;
570
 
 
571
 
    $comment = '' unless defined $comment;
572
 
 
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.
577
 
 
578
 
    ValidateComment($comment);
579
 
 
580
 
    if (Bugzilla->params->{"commentoncreate"} && !$comment) {
581
 
        ThrowUserError("description_required");
582
 
    }
583
 
 
584
 
    # On creation only, there must be a single-space comment, or
585
 
    # email will be supressed.
586
 
    $comment = ' ' if $comment eq '' && !ref($invocant);
587
 
 
588
 
    return $comment;
589
 
}
590
 
 
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;
596
 
}
597
 
 
598
 
sub _check_component {
599
 
    my ($invocant, $product, $name) = @_;
600
 
    $name = trim($name);
601
 
    $name || ThrowUserError("require_component");
602
 
    my $obj = Bugzilla::Component::check_component($product, $name);
603
 
    return $obj;
604
 
}
605
 
 
606
 
sub _check_deadline {
607
 
    my ($invocant, $date) = @_;
608
 
    $date = trim($date);
609
 
    my $tt_group = Bugzilla->params->{"timetrackinggroup"};
610
 
    return undef unless $date && $tt_group 
611
 
                        && Bugzilla->user->in_group($tt_group);
612
 
    validate_date($date)
613
 
        || ThrowUserError('illegal_date', { date   => $date,
614
 
                                            format => 'YYYY-MM-DD' });
615
 
    return $date;
616
 
}
617
 
 
618
 
# Takes two comma/space-separated strings and returns arrayrefs
619
 
# of valid bug IDs.
620
 
sub _check_dependencies {
621
 
    my ($invocant, $product, $depends_on, $blocks) = @_;
622
 
 
623
 
    # Only editbugs users can set dependencies on bug entry.
624
 
    return ([], []) unless Bugzilla->user->in_group('editbugs', $product->id);
625
 
 
626
 
    $depends_on ||= '';
627
 
    $blocks     ||= '';
628
 
 
629
 
    # Make sure all the bug_ids are valid.
630
 
    my @results;
631
 
    foreach my $string ($depends_on, $blocks) {
632
 
        my @array = split(/[\s,]+/, $string);
633
 
        # Eliminate nulls
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);
639
 
    }
640
 
 
641
 
    #                               dependson    blocks
642
 
    my %deps = ValidateDependencies($results[0], $results[1]);
643
 
 
644
 
    return ($deps{'dependson'}, $deps{'blocked'});
645
 
}
646
 
 
647
 
sub _check_estimated_time {
648
 
    return $_[0]->_check_time($_[1], 'estimated_time');
649
 
}
650
 
 
651
 
sub _check_groups {
652
 
    my ($invocant, $product, $group_ids) = @_;
653
 
 
654
 
    my $user = Bugzilla->user;
655
 
 
656
 
    my %add_groups;
657
 
    my $controls = $product->group_controls;
658
 
 
659
 
    foreach my $id (@$group_ids) {
660
 
        my $group = new Bugzilla::Group($id)
661
 
            || ThrowUserError("invalid_group_ID");
662
 
 
663
 
        # This can only happen if somebody hacked the enter_bug form.
664
 
        ThrowCodeError("inactive_group", { name => $group->name })
665
 
            unless $group->is_active;
666
 
 
667
 
        my $membercontrol = $controls->{$id}
668
 
                            && $controls->{$id}->{membercontrol};
669
 
        my $othercontrol  = $controls->{$id} 
670
 
                            && $controls->{$id}->{othercontrol};
671
 
        
672
 
        my $permit = ($membercontrol && $user->in_group($group->name))
673
 
                     || $othercontrol;
674
 
 
675
 
        $add_groups{$id} = 1 if $permit;
676
 
    }
677
 
 
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;
682
 
 
683
 
        # Add groups required
684
 
        if ($membercontrol == CONTROLMAPMANDATORY
685
 
            || ($othercontrol == CONTROLMAPMANDATORY
686
 
                && !$user->in_group_id($id))) 
687
 
        {
688
 
            # User had no option, bug needs to be in this group.
689
 
            $add_groups{$id} = 1;
690
 
        }
691
 
    }
692
 
 
693
 
    my @add_groups = keys %add_groups;
694
 
    return \@add_groups;
695
 
}
696
 
 
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));
702
 
 
703
 
    my %keywords;
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;
709
 
    }
710
 
    return [values %keywords];
711
 
}
712
 
 
713
 
sub _check_product {
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 });
721
 
    return $obj;
722
 
}
723
 
 
724
 
sub _check_op_sys {
725
 
    my ($invocant, $op_sys) = @_;
726
 
    $op_sys = trim($op_sys);
727
 
    check_field('op_sys', $op_sys);
728
 
    return $op_sys;
729
 
}
730
 
 
731
 
sub _check_priority {
732
 
    my ($invocant, $priority) = @_;
733
 
    if (!Bugzilla->params->{'letsubmitterchoosepriority'}) {
734
 
        $priority = Bugzilla->params->{'defaultpriority'};
735
 
    }
736
 
    $priority = trim($priority);
737
 
    check_field('priority', $priority);
738
 
 
739
 
    return $priority;
740
 
}
741
 
 
742
 
sub _check_remaining_time {
743
 
    return $_[0]->_check_time($_[1], 'remaining_time');
744
 
}
745
 
 
746
 
sub _check_rep_platform {
747
 
    my ($invocant, $platform) = @_;
748
 
    $platform = trim($platform);
749
 
    check_field('rep_platform', $platform);
750
 
    return $platform;
751
 
}
752
 
 
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;
757
 
 
758
 
    if (!defined $short_desc || $short_desc eq '') {
759
 
        ThrowUserError("require_summary");
760
 
    }
761
 
    return $short_desc;
762
 
}
763
 
 
764
 
sub _check_status_whiteboard { return defined $_[1] ? $_[1] : ''; }
765
 
 
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) = @_;
769
 
 
770
 
    return unless Bugzilla->params->{'strict_isolation'};
771
 
 
772
 
    my @related_users = @$cc_ids;
773
 
    push(@related_users, $assignee_id);
774
 
 
775
 
    if (Bugzilla->params->{'useqacontact'} && $qa_contact_id) {
776
 
        push(@related_users, $qa_contact_id);
777
 
    }
778
 
 
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;
782
 
    my @blocked_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);
787
 
        }
788
 
    }
789
 
    if (scalar(@blocked_users)) {
790
 
        ThrowUserError("invalid_user_group",
791
 
            {'users' => \@blocked_users,
792
 
             'new' => 1,
793
 
             'product' => $product->name});
794
 
    }
795
 
}
796
 
 
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})]);
803
 
    return $target;
804
 
}
805
 
 
806
 
sub _check_time {
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);
812
 
    return $time;
813
 
}
814
 
 
815
 
sub _check_qa_contact {
816
 
    my ($invocant, $component, $name) = @_;
817
 
    my $user = Bugzilla->user;
818
 
 
819
 
    return undef unless Bugzilla->params->{'useqacontact'};
820
 
 
821
 
    $name = trim($name);
822
 
 
823
 
    my $id;
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;
827
 
    } else {
828
 
        $id = login_to_id($name, THROW_ERROR);
829
 
    }
830
 
 
831
 
    return $id;
832
 
}
833
 
 
834
 
sub _check_version {
835
 
    my ($invocant, $product, $version) = @_;
836
 
    $version = trim($version);
837
 
    check_field('version', $version, [map($_->name, @{$product->versions})]);
838
 
    return $version;
839
 
}
840
 
 
841
 
# Custom Field Validators
842
 
 
843
 
sub _check_freetext_field {
844
 
    my ($invocant, $text) = @_;
845
 
 
846
 
    $text = (defined $text) ? trim($text) : '';
847
 
    if (length($text) > MAX_FREETEXT_LENGTH) {
848
 
        ThrowUserError('freetext_too_long', { text => $text });
849
 
    }
850
 
    return $text;
851
 
}
852
 
 
853
 
sub _check_select_field {
854
 
    my ($invocant, $value, $field) = @_;
855
 
    $value = trim($value);
856
 
    check_field($field, $value);
857
 
    return $value;
858
 
}
859
 
 
860
 
#####################################################################
861
 
# Class Accessors
862
 
#####################################################################
863
 
 
864
 
sub fields {
865
 
    my $class = shift;
866
 
 
867
 
    return (
868
 
        # Standard Fields
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),
879
 
    
880
 
        # Conditional Fields
881
 
        Bugzilla->params->{'useqacontact'} ? "qa_contact" : (),
882
 
        Bugzilla->params->{'timetrackinggroup'} ? 
883
 
            qw(estimated_time remaining_time actual_time deadline) : (),
884
 
    
885
 
        # Custom Fields
886
 
        Bugzilla->custom_field_names
887
 
    );
888
 
}
889
 
 
890
 
 
891
 
#####################################################################
892
 
# Instance Accessors
893
 
#####################################################################
894
 
 
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.
898
 
 
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
902
 
# security holes.
903
 
 
904
 
sub dup_id {
905
 
    my ($self) = @_;
906
 
    return $self->{'dup_id'} if exists $self->{'dup_id'};
907
 
 
908
 
    $self->{'dup_id'} = undef;
909
 
    return if $self->{'error'};
910
 
 
911
 
    if ($self->{'resolution'} eq 'DUPLICATE') { 
912
 
        my $dbh = Bugzilla->dbh;
913
 
        $self->{'dup_id'} =
914
 
          $dbh->selectrow_array(q{SELECT dupe_of 
915
 
                                  FROM duplicates
916
 
                                  WHERE dupe = ?},
917
 
                                undef,
918
 
                                $self->{'bug_id'});
919
 
    }
920
 
    return $self->{'dup_id'};
921
 
}
922
 
 
923
 
sub actual_time {
924
 
    my ($self) = @_;
925
 
    return $self->{'actual_time'} if exists $self->{'actual_time'};
926
 
 
927
 
    if ( $self->{'error'} || 
928
 
         !Bugzilla->user->in_group(Bugzilla->params->{"timetrackinggroup"}) ) {
929
 
        $self->{'actual_time'} = undef;
930
 
        return $self->{'actual_time'};
931
 
    }
932
 
 
933
 
    my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time)
934
 
                                      FROM longdescs 
935
 
                                      WHERE longdescs.bug_id=?");
936
 
    $sth->execute($self->{bug_id});
937
 
    $self->{'actual_time'} = $sth->fetchrow_array();
938
 
    return $self->{'actual_time'};
939
 
}
940
 
 
941
 
sub any_flags_requesteeble {
942
 
    my ($self) = @_;
943
 
    return $self->{'any_flags_requesteeble'} 
944
 
        if exists $self->{'any_flags_requesteeble'};
945
 
    return 0 if $self->{'error'};
946
 
 
947
 
    $self->{'any_flags_requesteeble'} = 
948
 
        grep($_->{'is_requesteeble'}, @{$self->flag_types});
949
 
 
950
 
    return $self->{'any_flags_requesteeble'};
951
 
}
952
 
 
953
 
sub attachments {
954
 
    my ($self) = @_;
955
 
    return $self->{'attachments'} if exists $self->{'attachments'};
956
 
    return [] if $self->{'error'};
957
 
 
958
 
    $self->{'attachments'} =
959
 
        Bugzilla::Attachment->get_attachments_by_bug($self->bug_id);
960
 
    return $self->{'attachments'};
961
 
}
962
 
 
963
 
sub assigned_to {
964
 
    my ($self) = @_;
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'};
969
 
}
970
 
 
971
 
sub blocked {
972
 
    my ($self) = @_;
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'};
977
 
}
978
 
 
979
 
# Even bugs in an error state always have a bug_id.
980
 
sub bug_id { $_[0]->{'bug_id'}; }
981
 
 
982
 
sub cc {
983
 
    my ($self) = @_;
984
 
    return $self->{'cc'} if exists $self->{'cc'};
985
 
    return [] if $self->{'error'};
986
 
 
987
 
    my $dbh = Bugzilla->dbh;
988
 
    $self->{'cc'} = $dbh->selectcol_arrayref(
989
 
        q{SELECT profiles.login_name FROM cc, profiles
990
 
           WHERE bug_id = ?
991
 
             AND cc.who = profiles.userid
992
 
        ORDER BY profiles.login_name},
993
 
      undef, $self->bug_id);
994
 
 
995
 
    $self->{'cc'} = undef if !scalar(@{$self->{'cc'}});
996
 
 
997
 
    return $self->{'cc'};
998
 
}
999
 
 
1000
 
sub component {
1001
 
    my ($self) = @_;
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};
1008
 
}
1009
 
 
1010
 
sub classification_id {
1011
 
    my ($self) = @_;
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};
1018
 
}
1019
 
 
1020
 
sub classification {
1021
 
    my ($self) = @_;
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};
1028
 
}
1029
 
 
1030
 
sub dependson {
1031
 
    my ($self) = @_;
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'};
1037
 
}
1038
 
 
1039
 
sub flag_types {
1040
 
    my ($self) = @_;
1041
 
    return $self->{'flag_types'} if exists $self->{'flag_types'};
1042
 
    return [] if $self->{'error'};
1043
 
 
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'} });
1050
 
 
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' });
1056
 
    }
1057
 
 
1058
 
    $self->{'flag_types'} = $flag_types;
1059
 
 
1060
 
    return $self->{'flag_types'};
1061
 
}
1062
 
 
1063
 
sub keywords {
1064
 
    my ($self) = @_;
1065
 
    return $self->{'keywords'} if exists $self->{'keywords'};
1066
 
    return () if $self->{'error'};
1067
 
 
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));
1076
 
 
1077
 
    $self->{'keywords'} = join(', ', @$list_ref);
1078
 
    return $self->{'keywords'};
1079
 
}
1080
 
 
1081
 
sub longdescs {
1082
 
    my ($self) = @_;
1083
 
    return $self->{'longdescs'} if exists $self->{'longdescs'};
1084
 
    return [] if $self->{'error'};
1085
 
    $self->{'longdescs'} = GetComments($self->{bug_id});
1086
 
    return $self->{'longdescs'};
1087
 
}
1088
 
 
1089
 
sub milestoneurl {
1090
 
    my ($self) = @_;
1091
 
    return $self->{'milestoneurl'} if exists $self->{'milestoneurl'};
1092
 
    return '' if $self->{'error'};
1093
 
 
1094
 
    $self->{'prod_obj'} ||= new Bugzilla::Product({name => $self->product});
1095
 
    $self->{'milestoneurl'} = $self->{'prod_obj'}->milestone_url;
1096
 
    return $self->{'milestoneurl'};
1097
 
}
1098
 
 
1099
 
sub product {
1100
 
    my ($self) = @_;
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};
1107
 
}
1108
 
 
1109
 
sub qa_contact {
1110
 
    my ($self) = @_;
1111
 
    return $self->{'qa_contact'} if exists $self->{'qa_contact'};
1112
 
    return undef if $self->{'error'};
1113
 
 
1114
 
    if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact_id'}) {
1115
 
        $self->{'qa_contact'} = new Bugzilla::User($self->{'qa_contact_id'});
1116
 
    } else {
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;
1121
 
    }
1122
 
    return $self->{'qa_contact'};
1123
 
}
1124
 
 
1125
 
sub reporter {
1126
 
    my ($self) = @_;
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'};
1131
 
}
1132
 
 
1133
 
 
1134
 
sub show_attachment_flags {
1135
 
    my ($self) = @_;
1136
 
    return $self->{'show_attachment_flags'} 
1137
 
        if exists $self->{'show_attachment_flags'};
1138
 
    return 0 if $self->{'error'};
1139
 
 
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
1143
 
    # of attachments.
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 });
1151
 
 
1152
 
    $self->{'show_attachment_flags'} =
1153
 
        ($num_attachment_flag_types || $num_attachment_flags);
1154
 
 
1155
 
    return $self->{'show_attachment_flags'};
1156
 
}
1157
 
 
1158
 
sub use_votes {
1159
 
    my ($self) = @_;
1160
 
    return 0 if $self->{'error'};
1161
 
 
1162
 
    $self->{'prod_obj'} ||= new Bugzilla::Product({name => $self->product});
1163
 
 
1164
 
    return Bugzilla->params->{'usevotes'} 
1165
 
           && $self->{'prod_obj'}->votes_per_user > 0;
1166
 
}
1167
 
 
1168
 
sub groups {
1169
 
    my $self = shift;
1170
 
    return $self->{'groups'} if exists $self->{'groups'};
1171
 
    return [] if $self->{'error'};
1172
 
 
1173
 
    my $dbh = Bugzilla->dbh;
1174
 
    my @groups;
1175
 
 
1176
 
    # Some of this stuff needs to go into Bugzilla::User
1177
 
 
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.
1182
 
    #
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" .
1190
 
             " FROM groups" . 
1191
 
             " LEFT JOIN bug_group_map" .
1192
 
             " ON bug_group_map.group_id = groups.id" .
1193
 
             " AND bug_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'});
1201
 
 
1202
 
    while (my ($groupid, $name, $description, $ison, $ingroup, $isactive,
1203
 
            $membercontrol, $othercontrol) = $sth->fetchrow_array()) {
1204
 
 
1205
 
        $membercontrol ||= 0;
1206
 
 
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.
1211
 
        if ($ison ||
1212
 
            ($isactive && $ingroup
1213
 
                       && (($membercontrol == CONTROLMAPDEFAULT)
1214
 
                           || ($membercontrol == CONTROLMAPSHOWN))
1215
 
            ))
1216
 
        {
1217
 
            my $ismandatory = $isactive
1218
 
              && ($membercontrol == CONTROLMAPMANDATORY);
1219
 
 
1220
 
            push (@groups, { "bit" => $groupid,
1221
 
                             "name" => $name,
1222
 
                             "ison" => $ison,
1223
 
                             "ingroup" => $ingroup,
1224
 
                             "mandatory" => $ismandatory,
1225
 
                             "description" => $description });
1226
 
        }
1227
 
    }
1228
 
 
1229
 
    $self->{'groups'} = \@groups;
1230
 
 
1231
 
    return $self->{'groups'};
1232
 
}
1233
 
 
1234
 
sub user {
1235
 
    my $self = shift;
1236
 
    return $self->{'user'} if exists $self->{'user'};
1237
 
    return {} if $self->{'error'};
1238
 
 
1239
 
    my $user = Bugzilla->user;
1240
 
    my $canmove = Bugzilla->params->{'move-enabled'} && $user->is_mover;
1241
 
 
1242
 
    my $prod_id = $self->{'product_id'};
1243
 
 
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};
1254
 
 
1255
 
    $self->{'user'} = {canmove    => $canmove,
1256
 
                       canconfirm => $canconfirm,
1257
 
                       canedit    => $canedit,
1258
 
                       isreporter => $isreporter};
1259
 
    return $self->{'user'};
1260
 
}
1261
 
 
1262
 
sub choices {
1263
 
    my $self = shift;
1264
 
    return $self->{'choices'} if exists $self->{'choices'};
1265
 
    return {} if $self->{'error'};
1266
 
 
1267
 
    $self->{'choices'} = {};
1268
 
    $self->{prod_obj} ||= new Bugzilla::Product({name => $self->product});
1269
 
 
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;
1276
 
    }
1277
 
 
1278
 
    # Hack - this array contains "". See bug 106589.
1279
 
    my @res = grep ($_, @{settable_resolutions()});
1280
 
 
1281
 
    $self->{'choices'} =
1282
 
      {
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})],
1293
 
      };
1294
 
 
1295
 
    return $self->{'choices'};
1296
 
}
1297
 
 
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');
1304
 
    if ($pos >= 0) {
1305
 
        splice(@$resolutions, $pos, 1);
1306
 
    }
1307
 
    $pos = lsearch($resolutions, 'MOVED');
1308
 
    if ($pos >= 0) {
1309
 
        splice(@$resolutions, $pos, 1);
1310
 
    }
1311
 
    return $resolutions;
1312
 
}
1313
 
 
1314
 
sub votes {
1315
 
    my ($self) = @_;
1316
 
    return 0 if $self->{error};
1317
 
    return $self->{votes} if defined $self->{votes};
1318
 
 
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};
1326
 
}
1327
 
 
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
1330
 
# the alias.
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 {
1334
 
    my ($alias) = @_;
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);
1340
 
}
1341
 
 
1342
 
#####################################################################
1343
 
# Subroutines
1344
 
#####################################################################
1345
 
 
1346
 
sub AppendComment {
1347
 
    my ($bugid, $whoid, $comment, $isprivate, $timestamp, $work_time,
1348
 
        $type, $extra_data) = @_;
1349
 
    $work_time ||= 0;
1350
 
    $type ||= CMT_NORMAL;
1351
 
    my $dbh = Bugzilla->dbh;
1352
 
 
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'});
1357
 
 
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()');
1361
 
 
1362
 
    $comment =~ s/\r\n/\n/g;     # Handle Windows-style line endings.
1363
 
    $comment =~ s/\r/\n/g;       # Handle Mac-style line endings.
1364
 
 
1365
 
    if ($comment =~ /^\s*$/ && !$type) {  # Nothin' but whitespace
1366
 
        return;
1367
 
    }
1368
 
 
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,
1375
 
                       type, extra_data)
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);
1381
 
}
1382
 
 
1383
 
sub update_comment {
1384
 
    my ($self, $comment_id, $new_comment) = @_;
1385
 
 
1386
 
    # Some validation checks.
1387
 
    if ($self->{'error'}) {
1388
 
        ThrowCodeError("bug_error", { bug => $self });
1389
 
    }
1390
 
    detaint_natural($comment_id)
1391
 
      || ThrowCodeError('bad_arg', {argument => 'comment_id', function => 'update_comment'});
1392
 
 
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'});
1397
 
 
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);
1404
 
 
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));
1408
 
 
1409
 
    # Update the comment object with this new text.
1410
 
    $current_comment_obj[0]->{'body'} = $new_comment;
1411
 
}
1412
 
 
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);
1422
 
    }
1423
 
    # Sorted because the old @::log_columns variable, which this replaces,
1424
 
    # was sorted.
1425
 
    return sort(@fields);
1426
 
}
1427
 
 
1428
 
# XXX - When Bug::update() will be implemented, we should make this routine
1429
 
#       a private method.
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",
1436
 
            undef, $bug_id);
1437
 
    return $list_ref;
1438
 
}
1439
 
 
1440
 
# Tells you whether or not the argument is a valid "open" state.
1441
 
sub is_open_state {
1442
 
    my ($state) = @_;
1443
 
    return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0);
1444
 
}
1445
 
 
1446
 
sub ValidateTime {
1447
 
    my ($time, $field) = @_;
1448
 
 
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"});
1455
 
    }
1456
 
 
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"});
1461
 
    }
1462
 
 
1463
 
    if ($time > 99999.99) {
1464
 
        ThrowUserError("number_too_large",
1465
 
                       {field => "$field", num => "$time", max_num => "99999.99"});
1466
 
    }
1467
 
}
1468
 
 
1469
 
sub GetComments {
1470
 
    my ($id, $comment_sort_order, $start, $end, $raw) = @_;
1471
 
    my $dbh = Bugzilla->dbh;
1472
 
 
1473
 
    $comment_sort_order = $comment_sort_order ||
1474
 
        Bugzilla->user->settings->{'comment_sort_order'}->{'value'};
1475
 
 
1476
 
    my $sort_order = ($comment_sort_order eq "oldest_to_newest") ? 'asc' : 'desc';
1477
 
 
1478
 
    my @comments;
1479
 
    my @args = ($id);
1480
 
 
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
1486
 
                   FROM longdescs
1487
 
             INNER JOIN profiles
1488
 
                     ON profiles.userid = longdescs.who
1489
 
                  WHERE longdescs.bug_id = ?';
1490
 
    if ($start) {
1491
 
        $query .= ' AND longdescs.bug_when > ?
1492
 
                    AND longdescs.bug_when <= ?';
1493
 
        push(@args, ($start, $end));
1494
 
    }
1495
 
    $query .= " ORDER BY longdescs.bug_when $sort_order";
1496
 
    my $sth = $dbh->prepare($query);
1497
 
    $sth->execute(@args);
1498
 
 
1499
 
    while (my $comment_ref = $sth->fetchrow_hashref()) {
1500
 
        my %comment = %$comment_ref;
1501
 
 
1502
 
        $comment{'email'} .= Bugzilla->params->{'emailsuffix'};
1503
 
        $comment{'name'} = $comment{'name'} || $comment{'email'};
1504
 
 
1505
 
        # If raw data is requested, do not format 'special' comments.
1506
 
        $comment{'body'} = format_comment(\%comment) unless $raw;
1507
 
 
1508
 
        push (@comments, \%comment);
1509
 
    }
1510
 
   
1511
 
    if ($comment_sort_order eq "newest_to_oldest_desc_first") {
1512
 
        unshift(@comments, pop @comments);
1513
 
    }
1514
 
 
1515
 
    return \@comments;
1516
 
}
1517
 
 
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;
1522
 
    my $body;
1523
 
 
1524
 
    if ($comment->{'type'} == CMT_DUPE_OF) {
1525
 
        $body = $comment->{'body'} . "\n\n" .
1526
 
                get_text('bug_duplicate_of', { dupe_of => $comment->{'extra_data'} });
1527
 
    }
1528
 
    elsif ($comment->{'type'} == CMT_HAS_DUPE) {
1529
 
        $body = get_text('bug_has_duplicate', { dupe => $comment->{'extra_data'} });
1530
 
    }
1531
 
    elsif ($comment->{'type'} == CMT_POPULAR_VOTES) {
1532
 
        $body = get_text('bug_confirmed_by_votes');
1533
 
    }
1534
 
    elsif ($comment->{'type'} == CMT_MOVED_TO) {
1535
 
        $body = $comment->{'body'} . "\n\n" .
1536
 
                get_text('bug_moved_to', { login => $comment->{'extra_data'} });
1537
 
    }
1538
 
    else {
1539
 
        $body = $comment->{'body'};
1540
 
    }
1541
 
    return $body;
1542
 
}
1543
 
 
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;
1549
 
 
1550
 
    # Arguments passed to the SQL query.
1551
 
    my @args = ($id);
1552
 
 
1553
 
    # Only consider changes since $starttime, if given.
1554
 
    my $datepart = "";
1555
 
    if (defined $starttime) {
1556
 
        trick_taint($starttime);
1557
 
        push (@args, $starttime);
1558
 
        $datepart = "AND bugs_activity.bug_when > ?";
1559
 
    }
1560
 
 
1561
 
    # Only includes attachments the user is allowed to see.
1562
 
    my $suppjoins = "";
1563
 
    my $suppwhere = "";
1564
 
    if (Bugzilla->params->{"insidergroup"} 
1565
 
        && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'})) 
1566
 
    {
1567
 
        $suppjoins = "LEFT JOIN attachments 
1568
 
                   ON attachments.attach_id = bugs_activity.attach_id";
1569
 
        $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0";
1570
 
    }
1571
 
 
1572
 
    my $query = "
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
1582
 
          FROM bugs_activity
1583
 
               $suppjoins
1584
 
     LEFT JOIN fielddefs
1585
 
            ON bugs_activity.fieldid = fielddefs.id
1586
 
    INNER JOIN profiles
1587
 
            ON profiles.userid = bugs_activity.who
1588
 
         WHERE bugs_activity.bug_id = ?
1589
 
               $datepart
1590
 
               $suppwhere
1591
 
      ORDER BY bugs_activity.bug_when";
1592
 
 
1593
 
    my $list = $dbh->selectall_arrayref($query, undef, @args);
1594
 
 
1595
 
    my @operations;
1596
 
    my $operation = {};
1597
 
    my $changes = [];
1598
 
    my $incomplete_data = 0;
1599
 
 
1600
 
    foreach my $entry (@$list) {
1601
 
        my ($field, $fieldname, $attachid, $when, $removed, $added, $who) = @$entry;
1602
 
        my %change;
1603
 
        my $activity_visible = 1;
1604
 
 
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')
1610
 
        {
1611
 
            $activity_visible = 
1612
 
                Bugzilla->user->in_group(Bugzilla->params->{'timetrackinggroup'}) ? 1 : 0;
1613
 
        } else {
1614
 
            $activity_visible = 1;
1615
 
        }
1616
 
 
1617
 
        if ($activity_visible) {
1618
 
            # This gets replaced with a hyperlink in the template.
1619
 
            $field =~ s/^Attachment\s*// if $attachid;
1620
 
 
1621
 
            # Check for the results of an old Bugzilla data corruption bug
1622
 
            $incomplete_data = 1 if ($added =~ /^\?/ || $removed =~ /^\?/);
1623
 
 
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'}))
1631
 
            {
1632
 
                $operation->{'changes'} = $changes;
1633
 
                push (@operations, $operation);
1634
 
 
1635
 
                # Create new empty anonymous data structures.
1636
 
                $operation = {};
1637
 
                $changes = [];
1638
 
            }
1639
 
 
1640
 
            $operation->{'who'} = $who;
1641
 
            $operation->{'when'} = $when;
1642
 
 
1643
 
            $change{'field'} = $field;
1644
 
            $change{'fieldname'} = $fieldname;
1645
 
            $change{'attachid'} = $attachid;
1646
 
            $change{'removed'} = $removed;
1647
 
            $change{'added'} = $added;
1648
 
            push (@$changes, \%change);
1649
 
        }
1650
 
    }
1651
 
 
1652
 
    if ($operation->{'who'}) {
1653
 
        $operation->{'changes'} = $changes;
1654
 
        push (@operations, $operation);
1655
 
    }
1656
 
 
1657
 
    return(\@operations, $incomplete_data);
1658
 
}
1659
 
 
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
1675
 
        } else {
1676
 
            $removed = ""; # no more entries
1677
 
        }
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
1683
 
        } else {
1684
 
            $added = ""; # no more entries
1685
 
        }
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));
1693
 
    }
1694
 
}
1695
 
 
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) = @_;
1702
 
    my @dependencies;
1703
 
    my $dbh = Bugzilla->dbh;
1704
 
 
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'));
1712
 
    $sth->execute();
1713
 
 
1714
 
    while (my ($bug_id, $dependencies) = $sth->fetchrow_array()) {
1715
 
        push(@dependencies, { bug_id       => $bug_id,
1716
 
                              dependencies => $dependencies });
1717
 
    }
1718
 
 
1719
 
    return @dependencies;
1720
 
}
1721
 
 
1722
 
sub ValidateComment {
1723
 
    my ($comment) = @_;
1724
 
 
1725
 
    if (defined($comment) && length($comment) > MAX_COMMENT_LENGTH) {
1726
 
        ThrowUserError("comment_too_long");
1727
 
    }
1728
 
}
1729
 
 
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.
1732
 
sub RemoveVotes {
1733
 
    my ($id, $who, $reason) = (@_);
1734
 
    my $dbh = Bugzilla->dbh;
1735
 
 
1736
 
    my $whopart = ($who) ? " AND votes.who = $who" : "";
1737
 
 
1738
 
    my $sth = $dbh->prepare("SELECT profiles.login_name, " .
1739
 
                            "profiles.userid, votes.vote_count, " .
1740
 
                            "products.votesperuser, products.maxvotesperbug " .
1741
 
                            "FROM profiles " . 
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);
1746
 
    $sth->execute($id);
1747
 
    my @list;
1748
 
    while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) {
1749
 
        push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]);
1750
 
    }
1751
 
 
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.
1754
 
    my @messages = ();
1755
 
 
1756
 
    if (scalar(@list)) {
1757
 
        foreach my $ref (@list) {
1758
 
            my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref);
1759
 
 
1760
 
            $maxvotesperbug = min($votesperuser, $maxvotesperbug);
1761
 
 
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;
1765
 
 
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;
1769
 
 
1770
 
            my $removedvotes = $oldvotes - $newvotes;
1771
 
 
1772
 
            if ($newvotes) {
1773
 
                $dbh->do("UPDATE votes SET vote_count = ? " .
1774
 
                         "WHERE bug_id = ? AND who = ?",
1775
 
                         undef, ($newvotes, $id, $userid));
1776
 
            } else {
1777
 
                $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?",
1778
 
                         undef, ($id, $userid));
1779
 
            }
1780
 
 
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.
1785
 
 
1786
 
            # Now lets send the e-mail to alert the user to the fact that their votes have
1787
 
            # been reduced or removed.
1788
 
            my $vars = {
1789
 
                'to' => $name . Bugzilla->params->{'emailsuffix'},
1790
 
                'bugid' => $id,
1791
 
                'reason' => $reason,
1792
 
 
1793
 
                'votesremoved' => $removedvotes,
1794
 
                'votesold' => $oldvotes,
1795
 
                'votesnew' => $newvotes,
1796
 
            };
1797
 
 
1798
 
            my $voter = new Bugzilla::User($userid);
1799
 
            my $template = Bugzilla->template_inner($voter->settings->{'lang'}->{'value'});
1800
 
 
1801
 
            my $msg;
1802
 
            $template->process("email/votes-removed.txt.tmpl", $vars, \$msg);
1803
 
            push(@messages, $msg);
1804
 
        }
1805
 
        Bugzilla->template_inner("");
1806
 
 
1807
 
        my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " .
1808
 
                                          "FROM votes WHERE bug_id = ?",
1809
 
                                          undef, $id) || 0;
1810
 
        $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?",
1811
 
                 undef, ($votes, $id));
1812
 
    }
1813
 
    # Now return the array containing emails to be sent.
1814
 
    return \@messages;
1815
 
}
1816
 
 
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;
1822
 
 
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 = ?",
1829
 
                              undef, $id);
1830
 
 
1831
 
    my $ret = 0;
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'));
1842
 
        }
1843
 
        else {
1844
 
            $dbh->do("UPDATE bugs SET everconfirmed = 1, delta_ts = ? " .
1845
 
                     "WHERE bug_id = ?", undef, ($timestamp, $id));
1846
 
        }
1847
 
 
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'));
1853
 
 
1854
 
        AppendComment($id, $who, "", 0, $timestamp, 0, CMT_POPULAR_VOTES);
1855
 
 
1856
 
        $ret = 1;
1857
 
    }
1858
 
    return $ret;
1859
 
}
1860
 
 
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.
1867
 
#
1868
 
# check_can_change_field() returns true if the user is allowed to change this
1869
 
# field, and false if they are not.
1870
 
#
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 {
1879
 
    my $self = shift;
1880
 
    my ($field, $oldvalue, $newvalue, $PrivilegesRequired, $data) = (@_);
1881
 
    my $user = Bugzilla->user;
1882
 
 
1883
 
    $oldvalue = defined($oldvalue) ? $oldvalue : '';
1884
 
    $newvalue = defined($newvalue) ? $newvalue : '';
1885
 
 
1886
 
    # Return true if they haven't changed this field at all.
1887
 
    if ($oldvalue eq $newvalue) {
1888
 
        return 1;
1889
 
    } elsif (trim($oldvalue) eq trim($newvalue)) {
1890
 
        return 1;
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)
1895
 
    {
1896
 
        return 1;
1897
 
    }
1898
 
 
1899
 
    # Allow anyone to change comments.
1900
 
    if ($field =~ /^longdesc/) {
1901
 
        return 1;
1902
 
    }
1903
 
 
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')
1908
 
    {
1909
 
        return 1;
1910
 
    }
1911
 
 
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.
1915
 
    #
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.
1920
 
 
1921
 
    # Allow anyone with (product-specific) "editbugs" privs to change anything.
1922
 
    if ($user->in_group('editbugs', $self->{'product_id'})) {
1923
 
        return 1;
1924
 
    }
1925
 
 
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)))
1931
 
    {
1932
 
        $$PrivilegesRequired = 3;
1933
 
        return $user->in_group('canconfirm', $self->{'product_id'});
1934
 
    }
1935
 
 
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) {
1940
 
            return 1;
1941
 
        }
1942
 
 
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))
1947
 
        {
1948
 
            return 1;
1949
 
        }
1950
 
    }
1951
 
 
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.
1955
 
 
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;
1962
 
        return 0;
1963
 
    }
1964
 
    # - change the QA contact
1965
 
    if ($field eq 'qa_contact') {
1966
 
        $$PrivilegesRequired = 2;
1967
 
        return 0;
1968
 
    }
1969
 
    # - change the target milestone
1970
 
    if ($field eq 'target_milestone') {
1971
 
        $$PrivilegesRequired = 2;
1972
 
        return 0;
1973
 
    }
1974
 
    # - change the priority (unless he could have set it originally)
1975
 
    if ($field eq 'priority'
1976
 
        && !Bugzilla->params->{'letsubmitterchoosepriority'})
1977
 
    {
1978
 
        $$PrivilegesRequired = 2;
1979
 
        return 0;
1980
 
    }
1981
 
 
1982
 
    # The reporter is allowed to change anything else.
1983
 
    if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) {
1984
 
        return 1;
1985
 
    }
1986
 
 
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;
1990
 
    return 0;
1991
 
}
1992
 
 
1993
 
#
1994
 
# Field Validation
1995
 
#
1996
 
 
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.
2001
 
sub ValidateBugID {
2002
 
    my ($id, $field) = @_;
2003
 
    my $dbh = Bugzilla->dbh;
2004
 
    my $user = Bugzilla->user;
2005
 
 
2006
 
    # Get rid of leading '#' (number) mark, if present.
2007
 
    $id =~ s/^\s*#//;
2008
 
    # Remove whitespace
2009
 
    $id = trim($id);
2010
 
 
2011
 
    # If the ID isn't a number, it might be an alias, so try to convert it.
2012
 
    my $alias = $id;
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 });
2018
 
    }
2019
 
    
2020
 
    # Modify the calling code's original variable to contain the trimmed,
2021
 
    # converted-from-alias ID.
2022
 
    $_[0] = $id;
2023
 
    
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});
2027
 
 
2028
 
    return if (defined $field && ($field eq "dependson" || $field eq "blocked"));
2029
 
    
2030
 
    return if $user->can_see_bug($id);
2031
 
 
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).
2036
 
    if ($user->id) {
2037
 
        ThrowUserError("bug_access_denied", {'bug_id' => $id});
2038
 
    } else {
2039
 
        ThrowUserError("bug_access_query", {'bug_id' => $id});
2040
 
    }
2041
 
}
2042
 
 
2043
 
# ValidateBugAlias:
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
2046
 
#   bug id.  
2047
 
sub ValidateBugAlias {
2048
 
    my ($alias, $curr_id) = @_;
2049
 
    my $dbh = Bugzilla->dbh;
2050
 
 
2051
 
    $alias = trim($alias || "");
2052
 
    trick_taint($alias);
2053
 
 
2054
 
    if ($alias eq "") {
2055
 
        ThrowUserError("alias_not_defined");
2056
 
    }
2057
 
 
2058
 
    # Make sure the alias isn't too long.
2059
 
    if (length($alias) > 20) {
2060
 
        ThrowUserError("alias_too_long");
2061
 
    }
2062
 
 
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";
2067
 
    }
2068
 
    my $id = $dbh->selectrow_array($query, undef, $alias); 
2069
 
 
2070
 
    my $vars = {};
2071
 
    $vars->{'alias'} = $alias;
2072
 
    if ($id) {
2073
 
        $vars->{'bug_id'} = $id;
2074
 
        ThrowUserError("alias_in_use", $vars);
2075
 
    }
2076
 
 
2077
 
    # Make sure the alias isn't just a number.
2078
 
    if ($alias =~ /^\d+$/) {
2079
 
        ThrowUserError("alias_is_numeric", $vars);
2080
 
    }
2081
 
 
2082
 
    # Make sure the alias has no commas or spaces.
2083
 
    if ($alias =~ /[, ]/) {
2084
 
        ThrowUserError("alias_has_comma_or_space", $vars);
2085
 
    }
2086
 
 
2087
 
    $_[0] = $alias;
2088
 
}
2089
 
 
2090
 
# Validate and return a hash of dependencies
2091
 
sub ValidateDependencies {
2092
 
    my $fields = {};
2093
 
    # These can be arrayrefs or they can be strings.
2094
 
    $fields->{'dependson'} = shift;
2095
 
    $fields->{'blocked'} = shift;
2096
 
    my $id = shift || 0;
2097
 
 
2098
 
    unless (defined($fields->{'dependson'})
2099
 
            || defined($fields->{'blocked'}))
2100
 
    {
2101
 
        return;
2102
 
    }
2103
 
 
2104
 
    my $dbh = Bugzilla->dbh;
2105
 
    my %deps;
2106
 
    my %deptree;
2107
 
    foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) {
2108
 
        my ($me, $target) = @{$pair};
2109
 
        $deptree{$target} = [];
2110
 
        $deps{$target} = [];
2111
 
        next unless $fields->{$target};
2112
 
 
2113
 
        my %seen;
2114
 
        my $target_array = ref($fields->{$target}) ? $fields->{$target}
2115
 
                           : [split(/[\s,]+/, $fields->{$target})];
2116
 
        foreach my $i (@$target_array) {
2117
 
            if ($id == $i) {
2118
 
                ThrowUserError("dependency_loop_single");
2119
 
            }
2120
 
            if (!exists $seen{$i}) {
2121
 
                push(@{$deptree{$target}}, $i);
2122
 
                $seen{$i} = 1;
2123
 
            }
2124
 
        }
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}};
2129
 
        while (@stack) {
2130
 
            my $i = shift @stack;
2131
 
            my $dep_list =
2132
 
                $dbh->selectcol_arrayref("SELECT $target
2133
 
                                          FROM dependencies
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);
2140
 
                    push @stack, $t;
2141
 
                    $seen{$t} = 1;
2142
 
                }
2143
 
            }
2144
 
        }
2145
 
    }
2146
 
 
2147
 
    my @deps   = @{$deptree{'dependson'}};
2148
 
    my @blocks = @{$deptree{'blocked'}};
2149
 
    my %union = ();
2150
 
    my %isect = ();
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});
2155
 
    }
2156
 
    return %deps;
2157
 
}
2158
 
 
2159
 
 
2160
 
#####################################################################
2161
 
# Autoloaded Accessors
2162
 
#####################################################################
2163
 
 
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.
2168
 
#
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.
2173
 
#
2174
 
sub _validate_attribute {
2175
 
    my ($attribute) = @_;
2176
 
 
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),
2184
 
 
2185
 
        # Bug fields.
2186
 
        Bugzilla::Bug->fields
2187
 
    );
2188
 
 
2189
 
    return grep($attribute eq $_, @valid_attributes) ? 1 : 0;
2190
 
}
2191
 
 
2192
 
sub AUTOLOAD {
2193
 
  use vars qw($AUTOLOAD);
2194
 
  my $attr = $AUTOLOAD;
2195
 
 
2196
 
  $attr =~ s/.*:://;
2197
 
  return unless $attr=~ /[^A-Z]/;
2198
 
  if (!_validate_attribute($attr)) {
2199
 
      require Carp;
2200
 
      Carp::confess("invalid bug attribute $attr");
2201
 
  }
2202
 
 
2203
 
  no strict 'refs';
2204
 
  *$AUTOLOAD = sub {
2205
 
      my $self = shift;
2206
 
      if (defined $self->{$attr}) {
2207
 
          return $self->{$attr};
2208
 
      } else {
2209
 
          return '';
2210
 
      }
2211
 
  };
2212
 
 
2213
 
  goto &$AUTOLOAD;
2214
 
}
2215
 
 
2216
 
1;