1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4#
5# This Source Code Form is "Incompatible With Secondary Licenses", as
6# defined by the Mozilla Public License, v. 2.0.
7
8=head1 NAME
9
10Bugzilla::Field - a particular piece of information about bugs
11                  and useful routines for form field manipulation
12
13=head1 SYNOPSIS
14
15  use Bugzilla;
16  use Data::Dumper;
17
18  # Display information about all fields.
19  print Dumper(Bugzilla->fields());
20
21  # Display information about non-obsolete custom fields.
22  print Dumper(Bugzilla->active_custom_fields);
23
24  use Bugzilla::Field;
25
26  # Display information about non-obsolete custom fields.
27  # Bugzilla->fields() is a wrapper around Bugzilla::Field->get_all(),
28  # with arguments which filter the fields before returning them.
29  print Dumper(Bugzilla->fields({ obsolete => 0, custom => 1 }));
30
31  # Create or update a custom field or field definition.
32  my $field = Bugzilla::Field->create(
33    {name => 'cf_silly', description => 'Silly', custom => 1});
34
35  # Instantiate a Field object for an existing field.
36  my $field = new Bugzilla::Field({name => 'qacontact_accessible'});
37  if ($field->obsolete) {
38      say $field->description . " is obsolete";
39  }
40
41  # Validation Routines
42  check_field($name, $value, \@legal_values, $no_warn);
43  $fieldid = get_field_id($fieldname);
44
45=head1 DESCRIPTION
46
47Field.pm defines field objects, which represent the particular pieces
48of information that Bugzilla stores about bugs.
49
50This package also provides functions for dealing with CGI form fields.
51
52C<Bugzilla::Field> is an implementation of L<Bugzilla::Object>, and
53so provides all of the methods available in L<Bugzilla::Object>,
54in addition to what is documented here.
55
56=cut
57
58package Bugzilla::Field;
59
60use 5.10.1;
61use strict;
62use warnings;
63
64use parent qw(Exporter Bugzilla::Object);
65@Bugzilla::Field::EXPORT = qw(check_field get_field_id get_legal_field_values);
66
67use Bugzilla::Constants;
68use Bugzilla::Error;
69use Bugzilla::Util;
70use List::MoreUtils qw(any);
71
72use Scalar::Util qw(blessed);
73
74###############################
75####    Initialization     ####
76###############################
77
78use constant IS_CONFIG => 1;
79
80use constant DB_TABLE   => 'fielddefs';
81use constant LIST_ORDER => 'sortkey, name';
82
83use constant DB_COLUMNS => qw(
84    id
85    name
86    description
87    long_desc
88    type
89    custom
90    mailhead
91    sortkey
92    obsolete
93    enter_bug
94    buglist
95    visibility_field_id
96    value_field_id
97    reverse_desc
98    is_mandatory
99    is_numeric
100);
101
102use constant VALIDATORS => {
103    custom       => \&_check_custom,
104    description  => \&_check_description,
105    long_desc    => \&_check_long_desc,
106    enter_bug    => \&_check_enter_bug,
107    buglist      => \&Bugzilla::Object::check_boolean,
108    mailhead     => \&_check_mailhead,
109    name         => \&_check_name,
110    obsolete     => \&_check_obsolete,
111    reverse_desc => \&_check_reverse_desc,
112    sortkey      => \&_check_sortkey,
113    type         => \&_check_type,
114    value_field_id      => \&_check_value_field_id,
115    visibility_field_id => \&_check_visibility_field_id,
116    visibility_values => \&_check_visibility_values,
117    is_mandatory => \&Bugzilla::Object::check_boolean,
118    is_numeric   => \&_check_is_numeric,
119};
120
121use constant VALIDATOR_DEPENDENCIES => {
122    is_numeric => ['type'],
123    name => ['custom'],
124    type => ['custom'],
125    reverse_desc => ['type'],
126    value_field_id => ['type'],
127    visibility_values => ['visibility_field_id'],
128};
129
130use constant UPDATE_COLUMNS => qw(
131    description
132    long_desc
133    mailhead
134    sortkey
135    obsolete
136    enter_bug
137    buglist
138    visibility_field_id
139    value_field_id
140    reverse_desc
141    is_mandatory
142    is_numeric
143    type
144);
145
146# How various field types translate into SQL data definitions.
147use constant SQL_DEFINITIONS => {
148    # Using commas because these are constants and they shouldn't
149    # be auto-quoted by the "=>" operator.
150    FIELD_TYPE_FREETEXT,      { TYPE => 'varchar(255)',
151                                NOTNULL => 1, DEFAULT => "''"},
152    FIELD_TYPE_SINGLE_SELECT, { TYPE => 'varchar(64)', NOTNULL => 1,
153                                DEFAULT => "'---'" },
154    FIELD_TYPE_TEXTAREA,      { TYPE => 'MEDIUMTEXT',
155                                NOTNULL => 1, DEFAULT => "''"},
156    FIELD_TYPE_DATETIME,      { TYPE => 'DATETIME'   },
157    FIELD_TYPE_DATE,          { TYPE => 'DATE'       },
158    FIELD_TYPE_BUG_ID,        { TYPE => 'INT3'       },
159    FIELD_TYPE_INTEGER,       { TYPE => 'INT4',  NOTNULL => 1, DEFAULT => 0 },
160};
161
162# Field definitions for the fields that ship with Bugzilla.
163# These are used by populate_field_definitions to populate
164# the fielddefs table.
165# 'days_elapsed' is set in populate_field_definitions() itself.
166use constant DEFAULT_FIELDS => (
167    {name => 'bug_id',       desc => 'Bug #',      in_new_bugmail => 1,
168     buglist => 1, is_numeric => 1},
169    {name => 'short_desc',   desc => 'Summary',    in_new_bugmail => 1,
170     is_mandatory => 1, buglist => 1},
171    {name => 'classification', desc => 'Classification', in_new_bugmail => 1,
172     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
173    {name => 'product',      desc => 'Product',    in_new_bugmail => 1,
174     is_mandatory => 1,
175     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
176    {name => 'version',      desc => 'Version',    in_new_bugmail => 1,
177     is_mandatory => 1, buglist => 1},
178    {name => 'rep_platform', desc => 'Platform',   in_new_bugmail => 1,
179     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
180    {name => 'bug_file_loc', desc => 'URL',        in_new_bugmail => 1,
181     buglist => 1},
182    {name => 'op_sys',       desc => 'OS/Version', in_new_bugmail => 1,
183     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
184    {name => 'bug_status',   desc => 'Status',     in_new_bugmail => 1,
185     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
186    {name => 'status_whiteboard', desc => 'Status Whiteboard',
187     in_new_bugmail => 1, buglist => 1},
188    {name => 'keywords',     desc => 'Keywords',   in_new_bugmail => 1,
189     type => FIELD_TYPE_KEYWORDS, buglist => 1},
190    {name => 'resolution',   desc => 'Resolution',
191     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
192    {name => 'bug_severity', desc => 'Severity',   in_new_bugmail => 1,
193     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
194    {name => 'priority',     desc => 'Priority',   in_new_bugmail => 1,
195     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
196    {name => 'component',    desc => 'Component',  in_new_bugmail => 1,
197     is_mandatory => 1,
198     type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
199    {name => 'assigned_to',  desc => 'AssignedTo', in_new_bugmail => 1,
200     buglist => 1},
201    {name => 'reporter',     desc => 'ReportedBy', in_new_bugmail => 1,
202     buglist => 1},
203    {name => 'qa_contact',   desc => 'QAContact',  in_new_bugmail => 1,
204     buglist => 1},
205    {name => 'assigned_to_realname',  desc => 'AssignedToName',
206     in_new_bugmail => 0, buglist => 1},
207    {name => 'reporter_realname',     desc => 'ReportedByName',
208     in_new_bugmail => 0, buglist => 1},
209    {name => 'qa_contact_realname',   desc => 'QAContactName',
210     in_new_bugmail => 0, buglist => 1},
211    {name => 'cc',           desc => 'CC',         in_new_bugmail => 1},
212    {name => 'dependson',    desc => 'Depends on', in_new_bugmail => 1,
213     is_numeric => 1, buglist => 1},
214    {name => 'blocked',      desc => 'Blocks',     in_new_bugmail => 1,
215     is_numeric => 1, buglist => 1},
216
217    {name => 'attachments.description', desc => 'Attachment description'},
218    {name => 'attachments.filename',    desc => 'Attachment filename'},
219    {name => 'attachments.mimetype',    desc => 'Attachment mime type'},
220    {name => 'attachments.ispatch',     desc => 'Attachment is patch',
221     is_numeric => 1},
222    {name => 'attachments.isobsolete',  desc => 'Attachment is obsolete',
223     is_numeric => 1},
224    {name => 'attachments.isprivate',   desc => 'Attachment is private',
225     is_numeric => 1},
226    {name => 'attachments.submitter',   desc => 'Attachment creator'},
227
228    {name => 'target_milestone',      desc => 'Target Milestone',
229     in_new_bugmail => 1, buglist => 1},
230    {name => 'creation_ts',           desc => 'Creation date',
231     buglist => 1},
232    {name => 'delta_ts',              desc => 'Last changed date',
233     buglist => 1},
234    {name => 'longdesc',              desc => 'Comment'},
235    {name => 'longdescs.isprivate',   desc => 'Comment is private',
236     is_numeric => 1},
237    {name => 'longdescs.count',       desc => 'Number of Comments',
238     buglist => 1, is_numeric => 1},
239    {name => 'alias',                 desc => 'Alias', buglist => 1},
240    {name => 'everconfirmed',         desc => 'Ever Confirmed',
241     is_numeric => 1},
242    {name => 'reporter_accessible',   desc => 'Reporter Accessible',
243     is_numeric => 1},
244    {name => 'cclist_accessible',     desc => 'CC Accessible',
245     is_numeric => 1},
246    {name => 'bug_group',             desc => 'Group', in_new_bugmail => 1},
247    {name => 'estimated_time',        desc => 'Estimated Hours',
248     in_new_bugmail => 1, buglist => 1, is_numeric => 1},
249    {name => 'remaining_time',        desc => 'Remaining Hours', buglist => 1,
250     is_numeric => 1},
251    {name => 'deadline',              desc => 'Deadline',
252     type => FIELD_TYPE_DATETIME, in_new_bugmail => 1, buglist => 1},
253    {name => 'commenter',             desc => 'Commenter'},
254    {name => 'flagtypes.name',        desc => 'Flags', buglist => 1},
255    {name => 'requestees.login_name', desc => 'Flag Requestee'},
256    {name => 'setters.login_name',    desc => 'Flag Setter'},
257    {name => 'work_time',             desc => 'Hours Worked', buglist => 1,
258     is_numeric => 1},
259    {name => 'percentage_complete',   desc => 'Percentage Complete',
260     buglist => 1, is_numeric => 1},
261    {name => 'content',               desc => 'Content'},
262    {name => 'attach_data.thedata',   desc => 'Attachment data'},
263    {name => "owner_idle_time",       desc => "Time Since Assignee Touched"},
264    {name => 'see_also',              desc => "See Also",
265     type => FIELD_TYPE_BUG_URLS},
266    {name => 'tag',                   desc => 'Personal Tags', buglist => 1,
267     type => FIELD_TYPE_KEYWORDS},
268    {name => 'last_visit_ts',         desc => 'Last Visit', buglist => 1,
269     type => FIELD_TYPE_DATETIME},
270    {name => 'comment_tag',           desc => 'Comment Tag'},
271);
272
273################
274# Constructors #
275################
276
277# Override match to add is_select.
278sub match {
279    my $self = shift;
280    my ($params) = @_;
281    if (delete $params->{is_select}) {
282        $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT];
283    }
284    return $self->SUPER::match(@_);
285}
286
287##############
288# Validators #
289##############
290
291sub _check_custom { return $_[1] ? 1 : 0; }
292
293sub _check_description {
294    my ($invocant, $desc) = @_;
295    $desc = clean_text($desc);
296    $desc || ThrowUserError('field_missing_description');
297    return $desc;
298}
299
300sub _check_long_desc {
301    my ($invocant, $long_desc) = @_;
302    $long_desc = clean_text($long_desc || '');
303    if (length($long_desc) > MAX_FIELD_LONG_DESC_LENGTH) {
304        ThrowUserError('field_long_desc_too_long');
305    }
306    return $long_desc;
307}
308
309sub _check_enter_bug { return $_[1] ? 1 : 0; }
310
311sub _check_is_numeric {
312    my ($invocant, $value, undef, $params) = @_;
313    my $type = blessed($invocant) ? $invocant->type : $params->{type};
314    return 1 if $type == FIELD_TYPE_BUG_ID;
315    return $value ? 1 : 0;
316}
317
318sub _check_mailhead { return $_[1] ? 1 : 0; }
319
320sub _check_name {
321    my ($class, $name, undef, $params) = @_;
322    $name = lc(clean_text($name));
323    $name || ThrowUserError('field_missing_name');
324
325    # Don't want to allow a name that might mess up SQL.
326    my $name_regex = qr/^[\w\.]+$/;
327    # Custom fields have more restrictive name requirements than
328    # standard fields.
329    $name_regex = qr/^[a-zA-Z0-9_]+$/ if $params->{custom};
330    # Custom fields can't be named just "cf_", and there is no normal
331    # field named just "cf_".
332    ($name =~ $name_regex && $name ne "cf_")
333         || ThrowUserError('field_invalid_name', { name => $name });
334
335    # If it's custom, prepend cf_ to the custom field name to distinguish
336    # it from standard fields.
337    if ($name !~ /^cf_/ && $params->{custom}) {
338        $name = 'cf_' . $name;
339    }
340
341    # Assure the name is unique. Names can't be changed, so we don't have
342    # to worry about what to do on updates.
343    my $field = new Bugzilla::Field({ name => $name });
344    ThrowUserError('field_already_exists', {'field' => $field }) if $field;
345
346    return $name;
347}
348
349sub _check_obsolete { return $_[1] ? 1 : 0; }
350
351sub _check_sortkey {
352    my ($invocant, $sortkey) = @_;
353    my $skey = $sortkey;
354    if (!defined $skey || $skey eq '') {
355        ($sortkey) = Bugzilla->dbh->selectrow_array(
356            'SELECT MAX(sortkey) + 100 FROM fielddefs') || 100;
357    }
358    detaint_natural($sortkey)
359        || ThrowUserError('field_invalid_sortkey', { sortkey => $skey });
360    return $sortkey;
361}
362
363sub _check_type {
364    my ($invocant, $type, undef, $params) = @_;
365    my $saved_type = $type;
366    (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE)
367      || ThrowCodeError('invalid_customfield_type', { type => $saved_type });
368
369    my $custom = blessed($invocant) ? $invocant->custom : $params->{custom};
370    if ($custom && !$type) {
371        ThrowCodeError('field_type_not_specified');
372    }
373
374    return $type;
375}
376
377sub _check_value_field_id {
378    my ($invocant, $field_id, undef, $params) = @_;
379    my $is_select = $invocant->is_select($params);
380    if ($field_id && !$is_select) {
381        ThrowUserError('field_value_control_select_only');
382    }
383    return $invocant->_check_visibility_field_id($field_id);
384}
385
386sub _check_visibility_field_id {
387    my ($invocant, $field_id) = @_;
388    $field_id = trim($field_id);
389    return undef if !$field_id;
390    my $field = Bugzilla::Field->check({ id => $field_id });
391    if (blessed($invocant) && $field->id == $invocant->id) {
392        ThrowUserError('field_cant_control_self', { field => $field });
393    }
394    if (!$field->is_select) {
395        ThrowUserError('field_control_must_be_select',
396                       { field => $field });
397    }
398    return $field->id;
399}
400
401sub _check_visibility_values {
402    my ($invocant, $values, undef, $params) = @_;
403    my $field;
404    if (blessed $invocant) {
405        $field = $invocant->visibility_field;
406    }
407    elsif ($params->{visibility_field_id}) {
408        $field = $invocant->new($params->{visibility_field_id});
409    }
410    # When no field is set, no values are set.
411    return [] if !$field;
412
413    if (!scalar @$values) {
414        ThrowUserError('field_visibility_values_must_be_selected',
415                       { field => $field });
416    }
417
418    my @visibility_values;
419    my $choice = Bugzilla::Field::Choice->type($field);
420    foreach my $value (@$values) {
421        if (!blessed $value) {
422            $value = $choice->check({ id => $value });
423        }
424        push(@visibility_values, $value);
425    }
426
427    return \@visibility_values;
428}
429
430sub _check_reverse_desc {
431    my ($invocant, $reverse_desc, undef, $params) = @_;
432    my $type = blessed($invocant) ? $invocant->type : $params->{type};
433    if ($type != FIELD_TYPE_BUG_ID) {
434        return undef; # store NULL for non-reversible field types
435    }
436
437    $reverse_desc = clean_text($reverse_desc);
438    return $reverse_desc;
439}
440
441sub _check_is_mandatory { return $_[1] ? 1 : 0; }
442
443=pod
444
445=head2 Instance Properties
446
447=over
448
449=item C<name>
450
451the name of the field in the database; begins with "cf_" if field
452is a custom field, but test the value of the boolean "custom" property
453to determine if a given field is a custom field;
454
455=item C<description>
456
457a short string describing the field; displayed to Bugzilla users
458in several places within Bugzilla's UI, f.e. as the form field label
459on the "show bug" page;
460
461=back
462
463=cut
464
465sub description { return $_[0]->{description} }
466
467=over
468
469=item C<long_desc>
470
471A string providing detailed info about the field;
472
473=back
474
475=cut
476
477sub long_desc { return $_[0]->{long_desc} }
478
479=over
480
481=item C<type>
482
483an integer specifying the kind of field this is; values correspond to
484the FIELD_TYPE_* constants in Constants.pm
485
486=back
487
488=cut
489
490sub type { return $_[0]->{type} }
491
492=over
493
494=item C<custom>
495
496a boolean specifying whether or not the field is a custom field;
497if true, field name should start "cf_", but use this property to determine
498which fields are custom fields;
499
500=back
501
502=cut
503
504sub custom { return $_[0]->{custom} }
505
506=over
507
508=item C<in_new_bugmail>
509
510a boolean specifying whether or not the field is displayed in bugmail
511for newly-created bugs;
512
513=back
514
515=cut
516
517sub in_new_bugmail { return $_[0]->{mailhead} }
518
519=over
520
521=item C<sortkey>
522
523an integer specifying the sortkey of the field.
524
525=back
526
527=cut
528
529sub sortkey { return $_[0]->{sortkey} }
530
531=over
532
533=item C<obsolete>
534
535a boolean specifying whether or not the field is obsolete;
536
537=back
538
539=cut
540
541sub obsolete { return $_[0]->{obsolete} }
542
543=over
544
545=item C<enter_bug>
546
547A boolean specifying whether or not this field should appear on
548enter_bug.cgi
549
550=back
551
552=cut
553
554sub enter_bug { return $_[0]->{enter_bug} }
555
556=over
557
558=item C<buglist>
559
560A boolean specifying whether or not this field is selectable
561as a display or order column in buglist.cgi
562
563=back
564
565=cut
566
567sub buglist { return $_[0]->{buglist} }
568
569=over
570
571=item C<is_select>
572
573True if this is a C<FIELD_TYPE_SINGLE_SELECT> or C<FIELD_TYPE_MULTI_SELECT>
574field. It is only safe to call L</legal_values> if this is true.
575
576=item C<legal_values>
577
578Valid values for this field, as an array of L<Bugzilla::Field::Choice>
579objects.
580
581=back
582
583=cut
584
585sub is_select {
586    my ($invocant, $params) = @_;
587    # This allows this method to be called by create() validators.
588    my $type = blessed($invocant) ? $invocant->type : $params->{type};
589    return ($type == FIELD_TYPE_SINGLE_SELECT
590            || $type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0
591}
592
593=over
594
595=item C<is_abnormal>
596
597Most fields that have a C<SELECT> L</type> have a certain schema for
598the table that stores their values, the table has the same name as the field,
599and the field's legal values can be edited via F<editvalues.cgi>.
600
601However, some fields do not follow that pattern. Those fields are
602considered "abnormal".
603
604This method returns C<1> if the field is "abnormal", C<0> otherwise.
605
606=back
607
608=cut
609
610sub is_abnormal {
611    my $self = shift;
612    return ABNORMAL_SELECTS->{$self->name} ? 1 : 0;
613}
614
615sub legal_values {
616    my $self = shift;
617
618    if (!defined $self->{'legal_values'}) {
619        require Bugzilla::Field::Choice;
620        my @values = Bugzilla::Field::Choice->type($self)->get_all();
621        $self->{'legal_values'} = \@values;
622    }
623    return $self->{'legal_values'};
624}
625
626=pod
627
628=over
629
630=item C<is_timetracking>
631
632True if this is a time-tracking field that should only be shown to users
633in the C<timetrackinggroup>.
634
635=back
636
637=cut
638
639sub is_timetracking {
640    my ($self) = @_;
641    return grep($_ eq $self->name, TIMETRACKING_FIELDS) ? 1 : 0;
642}
643
644=pod
645
646=over
647
648=item C<visibility_field>
649
650What field controls this field's visibility? Returns a C<Bugzilla::Field>
651object representing the field that controls this field's visibility.
652
653Returns undef if there is no field that controls this field's visibility.
654
655=back
656
657=cut
658
659sub visibility_field {
660    my $self = shift;
661    if ($self->{visibility_field_id}) {
662        $self->{visibility_field} ||=
663            $self->new($self->{visibility_field_id});
664    }
665    return $self->{visibility_field};
666}
667
668=pod
669
670=over
671
672=item C<visibility_values>
673
674If we have a L</visibility_field>, then what values does that field have to
675be set to in order to show this field? Returns a L<Bugzilla::Field::Choice>
676or undef if there is no C<visibility_field> set.
677
678=back
679
680=cut
681
682sub visibility_values {
683    my $self = shift;
684    my $dbh = Bugzilla->dbh;
685
686    return [] if !$self->{visibility_field_id};
687
688    if (!defined $self->{visibility_values}) {
689        my $visibility_value_ids =
690            $dbh->selectcol_arrayref("SELECT value_id FROM field_visibility
691                                      WHERE field_id = ?", undef, $self->id);
692
693        $self->{visibility_values} =
694            Bugzilla::Field::Choice->type($self->visibility_field)
695            ->new_from_list($visibility_value_ids);
696    }
697
698    return $self->{visibility_values};
699}
700
701=pod
702
703=over
704
705=item C<controls_visibility_of>
706
707An arrayref of C<Bugzilla::Field> objects, representing fields that this
708field controls the visibility of.
709
710=back
711
712=cut
713
714sub controls_visibility_of {
715    my $self = shift;
716    $self->{controls_visibility_of} ||=
717        Bugzilla::Field->match({ visibility_field_id => $self->id });
718    return $self->{controls_visibility_of};
719}
720
721=pod
722
723=over
724
725=item C<value_field>
726
727The Bugzilla::Field that controls the list of values for this field.
728
729Returns undef if there is no field that controls this field's visibility.
730
731=back
732
733=cut
734
735sub value_field {
736    my $self = shift;
737    if ($self->{value_field_id}) {
738        $self->{value_field} ||= $self->new($self->{value_field_id});
739    }
740    return $self->{value_field};
741}
742
743=pod
744
745=over
746
747=item C<controls_values_of>
748
749An arrayref of C<Bugzilla::Field> objects, representing fields that this
750field controls the values of.
751
752=back
753
754=cut
755
756sub controls_values_of {
757    my $self = shift;
758    $self->{controls_values_of} ||=
759        Bugzilla::Field->match({ value_field_id => $self->id });
760    return $self->{controls_values_of};
761}
762
763=over
764
765=item C<is_visible_on_bug>
766
767See L<Bugzilla::Field::ChoiceInterface>.
768
769=back
770
771=cut
772
773sub is_visible_on_bug {
774    my ($self, $bug) = @_;
775
776    # Always return visible, if this field is not
777    # visibility controlled.
778    return 1 if !$self->{visibility_field_id};
779
780    my $visibility_values = $self->visibility_values;
781
782    return (any { $_->is_set_on_bug($bug) } @$visibility_values) ? 1 : 0;
783}
784
785=over
786
787=item C<is_relationship>
788
789Applies only to fields of type FIELD_TYPE_BUG_ID.
790Checks to see if a reverse relationship description has been set.
791This is the canonical condition to enable reverse link display,
792dependency tree display, and similar functionality.
793
794=back
795
796=cut
797
798sub is_relationship  {
799    my $self = shift;
800    my $desc = $self->reverse_desc;
801    if (defined $desc && $desc ne "") {
802        return 1;
803    }
804    return 0;
805}
806
807=over
808
809=item C<reverse_desc>
810
811Applies only to fields of type FIELD_TYPE_BUG_ID.
812Describes the reverse relationship of this field.
813For example, if a BUG_ID field is called "Is a duplicate of",
814the reverse description would be "Duplicates of this bug".
815
816=back
817
818=cut
819
820sub reverse_desc { return $_[0]->{reverse_desc} }
821
822=over
823
824=item C<is_mandatory>
825
826a boolean specifying whether or not the field is mandatory;
827
828=back
829
830=cut
831
832sub is_mandatory { return $_[0]->{is_mandatory} }
833
834=over
835
836=item C<is_numeric>
837
838A boolean specifying whether or not this field logically contains
839numeric (integer, decimal, or boolean) values. By "logically contains" we
840mean that the user inputs numbers into the value of the field in the UI.
841This is mostly used by L<Bugzilla::Search>.
842
843=back
844
845=cut
846
847sub is_numeric { return $_[0]->{is_numeric} }
848
849
850=pod
851
852=head2 Instance Mutators
853
854These set the particular field that they are named after.
855
856They take a single value--the new value for that field.
857
858They will throw an error if you try to set the values to something invalid.
859
860=over
861
862=item C<set_description>
863
864=item C<set_long_desc>
865
866=item C<set_enter_bug>
867
868=item C<set_obsolete>
869
870=item C<set_sortkey>
871
872=item C<set_in_new_bugmail>
873
874=item C<set_buglist>
875
876=item C<set_reverse_desc>
877
878=item C<set_visibility_field>
879
880=item C<set_visibility_values>
881
882=item C<set_value_field>
883
884=item C<set_is_mandatory>
885
886
887=back
888
889=cut
890
891sub set_description    { $_[0]->set('description', $_[1]); }
892sub set_long_desc      { $_[0]->set('long_desc',   $_[1]); }
893sub set_enter_bug      { $_[0]->set('enter_bug',   $_[1]); }
894sub set_is_numeric     { $_[0]->set('is_numeric',  $_[1]); }
895sub set_obsolete       { $_[0]->set('obsolete',    $_[1]); }
896sub set_sortkey        { $_[0]->set('sortkey',     $_[1]); }
897sub set_in_new_bugmail { $_[0]->set('mailhead',    $_[1]); }
898sub set_buglist        { $_[0]->set('buglist',     $_[1]); }
899sub set_reverse_desc    { $_[0]->set('reverse_desc', $_[1]); }
900sub set_visibility_field {
901    my ($self, $value) = @_;
902    $self->set('visibility_field_id', $value);
903    delete $self->{visibility_field};
904    delete $self->{visibility_values};
905}
906sub set_visibility_values {
907    my ($self, $value_ids) = @_;
908    $self->set('visibility_values', $value_ids);
909}
910sub set_value_field {
911    my ($self, $value) = @_;
912    $self->set('value_field_id', $value);
913    delete $self->{value_field};
914}
915sub set_is_mandatory { $_[0]->set('is_mandatory', $_[1]); }
916
917# This is only used internally by upgrade code in Bugzilla::Field.
918sub _set_type { $_[0]->set('type', $_[1]); }
919
920=pod
921
922=head2 Instance Method
923
924=over
925
926=item C<remove_from_db>
927
928Attempts to remove the passed in field from the database.
929Deleting a field is only successful if the field is obsolete and
930there are no values specified (or EVER specified) for the field.
931
932=back
933
934=cut
935
936sub remove_from_db {
937    my $self = shift;
938    my $dbh = Bugzilla->dbh;
939
940    my $name = $self->name;
941
942    if (!$self->custom) {
943        ThrowCodeError('field_not_custom', {'name' => $name });
944    }
945
946    if (!$self->obsolete) {
947        ThrowUserError('customfield_not_obsolete', {'name' => $self->name });
948    }
949
950    $dbh->bz_start_transaction();
951
952    # Check to see if bug activity table has records (should be fast with index)
953    my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity
954                                      WHERE fieldid = ?", undef, $self->id);
955    if ($has_activity) {
956        ThrowUserError('customfield_has_activity', {'name' => $name });
957    }
958
959    # Check to see if bugs table has records (slow)
960    my $bugs_query = "";
961
962    if ($self->type == FIELD_TYPE_MULTI_SELECT) {
963        $bugs_query = "SELECT COUNT(*) FROM bug_$name";
964    }
965    else {
966        $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
967        if ($self->type != FIELD_TYPE_BUG_ID
968            && $self->type != FIELD_TYPE_DATE
969            && $self->type != FIELD_TYPE_DATETIME)
970        {
971            $bugs_query .= " AND $name != ''";
972        }
973        # Ignore the default single select value
974        if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
975            $bugs_query .= " AND $name != '---'";
976        }
977    }
978
979    my $has_bugs = $dbh->selectrow_array($bugs_query);
980    if ($has_bugs) {
981        ThrowUserError('customfield_has_contents', {'name' => $name });
982    }
983
984    # Once we reach here, we should be OK to delete.
985    $self->SUPER::remove_from_db();
986
987    my $type = $self->type;
988
989    # the values for multi-select are stored in a seperate table
990    if ($type != FIELD_TYPE_MULTI_SELECT) {
991        $dbh->bz_drop_column('bugs', $name);
992    }
993
994    if ($self->is_select) {
995        # Delete the table that holds the legal values for this field.
996        $dbh->bz_drop_field_tables($self);
997    }
998
999    $dbh->bz_commit_transaction()
1000}
1001
1002=pod
1003
1004=head2 Class Methods
1005
1006=over
1007
1008=item C<create>
1009
1010Just like L<Bugzilla::Object/create>. Takes the following parameters:
1011
1012=over
1013
1014=item C<name> B<Required> - The name of the field.
1015
1016=item C<description> B<Required> - The field label to display in the UI.
1017
1018=item C<long_desc> - A longer description of the field.
1019
1020=item C<mailhead> - boolean - Whether this field appears at the
1021top of the bugmail for a newly-filed bug. Defaults to 0.
1022
1023=item C<custom> - boolean - True if this is a Custom Field. The field
1024will be added to the C<bugs> table if it does not exist. Defaults to 0.
1025
1026=item C<sortkey> - integer - The sortkey of the field. Defaults to 0.
1027
1028=item C<enter_bug> - boolean - Whether this field is
1029editable on the bug creation form. Defaults to 0.
1030
1031=item C<buglist> - boolean - Whether this field is
1032selectable as a display or order column in bug lists. Defaults to 0.
1033
1034C<obsolete> - boolean - Whether this field is obsolete. Defaults to 0.
1035
1036C<is_mandatory> - boolean - Whether this field is mandatory. Defaults to 0.
1037
1038=back
1039
1040=back
1041
1042=cut
1043
1044sub create {
1045    my $class = shift;
1046    my ($params) = @_;
1047    my $dbh = Bugzilla->dbh;
1048
1049    # This makes sure the "sortkey" validator runs, even if
1050    # the parameter isn't sent to create().
1051    $params->{sortkey} = undef if !exists $params->{sortkey};
1052    $params->{type} ||= 0;
1053    # We mark the custom field as obsolete till it has been fully created,
1054    # to avoid race conditions when viewing bugs at the same time.
1055    my $is_obsolete = $params->{obsolete};
1056    $params->{obsolete} = 1 if $params->{custom};
1057
1058    $dbh->bz_start_transaction();
1059    $class->check_required_create_fields(@_);
1060    my $field_values      = $class->run_create_validators($params);
1061    my $visibility_values = delete $field_values->{visibility_values};
1062    my $field             = $class->insert_create_data($field_values);
1063
1064    $field->set_visibility_values($visibility_values);
1065    $field->_update_visibility_values();
1066
1067    $dbh->bz_commit_transaction();
1068    Bugzilla->memcached->clear_config();
1069
1070    if ($field->custom) {
1071        my $name = $field->name;
1072        my $type = $field->type;
1073        if (SQL_DEFINITIONS->{$type}) {
1074            # Create the database column that stores the data for this field.
1075            $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
1076        }
1077
1078        if ($field->is_select) {
1079            # Create the table that holds the legal values for this field.
1080            $dbh->bz_add_field_tables($field);
1081        }
1082
1083        if ($type == FIELD_TYPE_SINGLE_SELECT) {
1084            # Insert a default value of "---" into the legal values table.
1085            $dbh->do("INSERT INTO $name (value) VALUES ('---')");
1086        }
1087
1088        # Restore the original obsolete state of the custom field.
1089        $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id)
1090          unless $is_obsolete;
1091
1092        Bugzilla->memcached->clear({ table => 'fielddefs', id => $field->id });
1093        Bugzilla->memcached->clear_config();
1094    }
1095
1096    return $field;
1097}
1098
1099sub update {
1100    my $self = shift;
1101    my $changes = $self->SUPER::update(@_);
1102    my $dbh = Bugzilla->dbh;
1103    if ($changes->{value_field_id} && $self->is_select) {
1104        $dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL");
1105    }
1106    $self->_update_visibility_values();
1107    Bugzilla->memcached->clear_config();
1108    return $changes;
1109}
1110
1111sub _update_visibility_values {
1112    my $self = shift;
1113    my $dbh = Bugzilla->dbh;
1114
1115    my @visibility_value_ids = map($_->id, @{$self->visibility_values});
1116    $self->_delete_visibility_values();
1117    for my $value_id (@visibility_value_ids) {
1118        $dbh->do("INSERT INTO field_visibility (field_id, value_id)
1119                  VALUES (?, ?)", undef, $self->id, $value_id);
1120    }
1121}
1122
1123sub _delete_visibility_values {
1124    my ($self) = @_;
1125    my $dbh = Bugzilla->dbh;
1126    $dbh->do("DELETE FROM field_visibility WHERE field_id = ?",
1127        undef, $self->id);
1128    delete $self->{visibility_values};
1129}
1130
1131=pod
1132
1133=over
1134
1135=item C<get_legal_field_values($field)>
1136
1137Description: returns all the legal values for a field that has a
1138             list of legal values, like rep_platform or resolution.
1139             The table where these values are stored must at least have
1140             the following columns: value, isactive, sortkey.
1141
1142Params:    C<$field> - Name of the table where valid values are.
1143
1144Returns:   a reference to a list of valid values.
1145
1146=back
1147
1148=cut
1149
1150sub get_legal_field_values {
1151    my ($field) = @_;
1152    my $dbh = Bugzilla->dbh;
1153    my $result_ref = $dbh->selectcol_arrayref(
1154         "SELECT value FROM $field
1155           WHERE isactive = ?
1156        ORDER BY sortkey, value", undef, (1));
1157    return $result_ref;
1158}
1159
1160=over
1161
1162=item C<populate_field_definitions()>
1163
1164Description: Populates the fielddefs table during an installation
1165             or upgrade.
1166
1167Params:      none
1168
1169Returns:     nothing
1170
1171=back
1172
1173=cut
1174
1175sub populate_field_definitions {
1176    my $dbh = Bugzilla->dbh;
1177
1178    # ADD and UPDATE field definitions
1179    foreach my $def (DEFAULT_FIELDS) {
1180        my $field = new Bugzilla::Field({ name => $def->{name} });
1181        if ($field) {
1182            $field->set_description($def->{desc});
1183            $field->set_in_new_bugmail($def->{in_new_bugmail});
1184            $field->set_buglist($def->{buglist});
1185            $field->_set_type($def->{type}) if $def->{type};
1186            $field->set_is_mandatory($def->{is_mandatory});
1187            $field->set_is_numeric($def->{is_numeric});
1188            $field->update();
1189        }
1190        else {
1191            if (exists $def->{in_new_bugmail}) {
1192                $def->{mailhead} = $def->{in_new_bugmail};
1193                delete $def->{in_new_bugmail};
1194            }
1195            $def->{description} = delete $def->{desc};
1196            Bugzilla::Field->create($def);
1197        }
1198    }
1199
1200    # DELETE fields which were added only accidentally, or which
1201    # were never tracked in bugs_activity. Note that you can never
1202    # delete fields which are used by bugs_activity.
1203
1204    # Oops. Bug 163299
1205    $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'");
1206    # Oops. Bug 215319
1207    $dbh->do("DELETE FROM fielddefs WHERE name='requesters.login_name'");
1208    # This field was never tracked in bugs_activity, so it's safe to delete.
1209    $dbh->do("DELETE FROM fielddefs WHERE name='attachments.thedata'");
1210
1211    # MODIFY old field definitions
1212
1213    # 2005-11-13 LpSolit@gmail.com - Bug 302599
1214    # One of the field names was a fragment of SQL code, which is DB dependent.
1215    # We have to rename it to a real name, which is DB independent.
1216    my $new_field_name = 'days_elapsed';
1217    my $field_description = 'Days since bug changed';
1218
1219    my ($old_field_id, $old_field_name) =
1220        $dbh->selectrow_array('SELECT id, name FROM fielddefs
1221                                WHERE description = ?',
1222                              undef, $field_description);
1223
1224    if ($old_field_id && ($old_field_name ne $new_field_name)) {
1225        say "SQL fragment found in the 'fielddefs' table...";
1226        say "Old field name: $old_field_name";
1227        # We have to fix saved searches first. Queries have been escaped
1228        # before being saved. We have to do the same here to find them.
1229        $old_field_name = url_quote($old_field_name);
1230        my $broken_named_queries =
1231            $dbh->selectall_arrayref('SELECT userid, name, query
1232                                        FROM namedqueries WHERE ' .
1233                                      $dbh->sql_istrcmp('query', '?', 'LIKE'),
1234                                      undef, "%=$old_field_name%");
1235
1236        my $sth_UpdateQueries = $dbh->prepare('UPDATE namedqueries SET query = ?
1237                                                WHERE userid = ? AND name = ?');
1238
1239        print "Fixing saved searches...\n" if scalar(@$broken_named_queries);
1240        foreach my $named_query (@$broken_named_queries) {
1241            my ($userid, $name, $query) = @$named_query;
1242            $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi;
1243            $sth_UpdateQueries->execute($query, $userid, $name);
1244        }
1245
1246        # We now do the same with saved chart series.
1247        my $broken_series =
1248            $dbh->selectall_arrayref('SELECT series_id, query
1249                                        FROM series WHERE ' .
1250                                      $dbh->sql_istrcmp('query', '?', 'LIKE'),
1251                                      undef, "%=$old_field_name%");
1252
1253        my $sth_UpdateSeries = $dbh->prepare('UPDATE series SET query = ?
1254                                               WHERE series_id = ?');
1255
1256        print "Fixing saved chart series...\n" if scalar(@$broken_series);
1257        foreach my $series (@$broken_series) {
1258            my ($series_id, $query) = @$series;
1259            $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi;
1260            $sth_UpdateSeries->execute($query, $series_id);
1261        }
1262        # Now that saved searches have been fixed, we can fix the field name.
1263        say "Fixing the 'fielddefs' table...";
1264        say "New field name: $new_field_name";
1265        $dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?',
1266                  undef, ($new_field_name, $old_field_id));
1267    }
1268
1269    # This field has to be created separately, or the above upgrade code
1270    # might not run properly.
1271    Bugzilla::Field->create({ name => $new_field_name,
1272                              description => $field_description })
1273        unless new Bugzilla::Field({ name => $new_field_name });
1274
1275}
1276
1277
1278
1279=head2 Data Validation
1280
1281=over
1282
1283=item C<check_field($name, $value, \@legal_values, $no_warn)>
1284
1285Description: Makes sure the field $name is defined and its $value
1286             is non empty. If @legal_values is defined, this routine
1287             checks whether its value is one of the legal values
1288             associated with this field, else it checks against
1289             the default valid values for this field obtained by
1290             C<get_legal_field_values($name)>. If the test is successful,
1291             the function returns 1. If the test fails, an error
1292             is thrown (by default), unless $no_warn is true, in which
1293             case the function returns 0.
1294
1295Params:      $name         - the field name
1296             $value        - the field value
1297             @legal_values - (optional) list of legal values
1298             $no_warn      - (optional) do not throw an error if true
1299
1300Returns:     1 on success; 0 on failure if $no_warn is true (else an
1301             error is thrown).
1302
1303=back
1304
1305=cut
1306
1307sub check_field {
1308    my ($name, $value, $legalsRef, $no_warn) = @_;
1309    my $dbh = Bugzilla->dbh;
1310
1311    # If $legalsRef is undefined, we use the default valid values.
1312    # Valid values for this check are all possible values.
1313    # Using get_legal_values would only return active values, but since
1314    # some bugs may have inactive values set, we want to check them too.
1315    unless (defined $legalsRef) {
1316        $legalsRef = Bugzilla::Field->new({name => $name})->legal_values;
1317        my @values = map($_->name, @$legalsRef);
1318        $legalsRef = \@values;
1319
1320    }
1321
1322    if (!defined($value)
1323        or trim($value) eq ""
1324        or !grep { $_ eq $value } @$legalsRef)
1325    {
1326        return 0 if $no_warn; # We don't want an error to be thrown; return.
1327        trick_taint($name);
1328
1329        my $field = new Bugzilla::Field({ name => $name });
1330        my $field_desc = $field ? $field->description : $name;
1331        ThrowCodeError('illegal_field', { field => $field_desc });
1332    }
1333    return 1;
1334}
1335
1336=pod
1337
1338=over
1339
1340=item C<get_field_id($fieldname)>
1341
1342Description: Returns the ID of the specified field name and throws
1343             an error if this field does not exist.
1344
1345Params:      $fieldname - a field name
1346
1347Returns:     the corresponding field ID or an error if the field name
1348             does not exist.
1349
1350=back
1351
1352=cut
1353
1354sub get_field_id {
1355    my $field = Bugzilla->fields({ by_name => 1 })->{$_[0]}
1356      or ThrowCodeError('invalid_field_name', {field => $_[0]});
1357
1358    return $field->id;
1359}
1360
13611;
1362
1363__END__
1364
1365=head1 B<Methods in need of POD>
1366
1367=over
1368
1369=item match
1370
1371=item set_is_numeric
1372
1373=item update
1374
1375=back
1376