# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. =head1 NAME Bugzilla::Field - a particular piece of information about bugs and useful routines for form field manipulation =head1 SYNOPSIS use Bugzilla; use Data::Dumper; # Display information about all fields. print Dumper(Bugzilla->fields()); # Display information about non-obsolete custom fields. print Dumper(Bugzilla->active_custom_fields); use Bugzilla::Field; # Display information about non-obsolete custom fields. # Bugzilla->fields() is a wrapper around Bugzilla::Field->get_all(), # with arguments which filter the fields before returning them. print Dumper(Bugzilla->fields({ obsolete => 0, custom => 1 })); # Create or update a custom field or field definition. my $field = Bugzilla::Field->create( {name => 'cf_silly', description => 'Silly', custom => 1}); # Instantiate a Field object for an existing field. my $field = new Bugzilla::Field({name => 'qacontact_accessible'}); if ($field->obsolete) { say $field->description . " is obsolete"; } # Validation Routines check_field($name, $value, \@legal_values, $no_warn); $fieldid = get_field_id($fieldname); =head1 DESCRIPTION Field.pm defines field objects, which represent the particular pieces of information that Bugzilla stores about bugs. This package also provides functions for dealing with CGI form fields. C is an implementation of L, and so provides all of the methods available in L, in addition to what is documented here. =cut package Bugzilla::Field; use 5.10.1; use strict; use warnings; use parent qw(Exporter Bugzilla::Object); @Bugzilla::Field::EXPORT = qw(check_field get_field_id get_legal_field_values); use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Util; use List::MoreUtils qw(any); use Scalar::Util qw(blessed); ############################### #### Initialization #### ############################### use constant IS_CONFIG => 1; use constant DB_TABLE => 'fielddefs'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( id name description long_desc type custom mailhead sortkey obsolete enter_bug buglist visibility_field_id value_field_id reverse_desc is_mandatory is_numeric ); use constant VALIDATORS => { custom => \&_check_custom, description => \&_check_description, long_desc => \&_check_long_desc, enter_bug => \&_check_enter_bug, buglist => \&Bugzilla::Object::check_boolean, mailhead => \&_check_mailhead, name => \&_check_name, obsolete => \&_check_obsolete, reverse_desc => \&_check_reverse_desc, sortkey => \&_check_sortkey, type => \&_check_type, value_field_id => \&_check_value_field_id, visibility_field_id => \&_check_visibility_field_id, visibility_values => \&_check_visibility_values, is_mandatory => \&Bugzilla::Object::check_boolean, is_numeric => \&_check_is_numeric, }; use constant VALIDATOR_DEPENDENCIES => { is_numeric => ['type'], name => ['custom'], type => ['custom'], reverse_desc => ['type'], value_field_id => ['type'], visibility_values => ['visibility_field_id'], }; use constant UPDATE_COLUMNS => qw( description long_desc mailhead sortkey obsolete enter_bug buglist visibility_field_id value_field_id reverse_desc is_mandatory is_numeric type ); # How various field types translate into SQL data definitions. use constant SQL_DEFINITIONS => { # Using commas because these are constants and they shouldn't # be auto-quoted by the "=>" operator. FIELD_TYPE_FREETEXT, { TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, FIELD_TYPE_SINGLE_SELECT, { TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'" }, FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' }, FIELD_TYPE_DATE, { TYPE => 'DATE' }, FIELD_TYPE_BUG_ID, { TYPE => 'INT3' }, FIELD_TYPE_INTEGER, { TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0 }, }; # Field definitions for the fields that ship with Bugzilla. # These are used by populate_field_definitions to populate # the fielddefs table. # 'days_elapsed' is set in populate_field_definitions() itself. use constant DEFAULT_FIELDS => ( {name => 'bug_id', desc => 'Bug #', in_new_bugmail => 1, buglist => 1, is_numeric => 1}, {name => 'short_desc', desc => 'Summary', in_new_bugmail => 1, is_mandatory => 1, buglist => 1}, {name => 'classification', desc => 'Classification', in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'product', desc => 'Product', in_new_bugmail => 1, is_mandatory => 1, type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'version', desc => 'Version', in_new_bugmail => 1, is_mandatory => 1, buglist => 1}, {name => 'rep_platform', desc => 'Platform', in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'bug_file_loc', desc => 'URL', in_new_bugmail => 1, buglist => 1}, {name => 'op_sys', desc => 'OS/Version', in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'bug_status', desc => 'Status', in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'status_whiteboard', desc => 'Status Whiteboard', in_new_bugmail => 1, buglist => 1}, {name => 'keywords', desc => 'Keywords', in_new_bugmail => 1, type => FIELD_TYPE_KEYWORDS, buglist => 1}, {name => 'resolution', desc => 'Resolution', type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'bug_severity', desc => 'Severity', in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'priority', desc => 'Priority', in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'component', desc => 'Component', in_new_bugmail => 1, is_mandatory => 1, type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, {name => 'assigned_to', desc => 'AssignedTo', in_new_bugmail => 1, buglist => 1}, {name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1, buglist => 1}, {name => 'qa_contact', desc => 'QAContact', in_new_bugmail => 1, buglist => 1}, {name => 'assigned_to_realname', desc => 'AssignedToName', in_new_bugmail => 0, buglist => 1}, {name => 'reporter_realname', desc => 'ReportedByName', in_new_bugmail => 0, buglist => 1}, {name => 'qa_contact_realname', desc => 'QAContactName', in_new_bugmail => 0, buglist => 1}, {name => 'cc', desc => 'CC', in_new_bugmail => 1}, {name => 'dependson', desc => 'Depends on', in_new_bugmail => 1, is_numeric => 1, buglist => 1}, {name => 'blocked', desc => 'Blocks', in_new_bugmail => 1, is_numeric => 1, buglist => 1}, {name => 'attachments.description', desc => 'Attachment description'}, {name => 'attachments.filename', desc => 'Attachment filename'}, {name => 'attachments.mimetype', desc => 'Attachment mime type'}, {name => 'attachments.ispatch', desc => 'Attachment is patch', is_numeric => 1}, {name => 'attachments.isobsolete', desc => 'Attachment is obsolete', is_numeric => 1}, {name => 'attachments.isprivate', desc => 'Attachment is private', is_numeric => 1}, {name => 'attachments.submitter', desc => 'Attachment creator'}, {name => 'target_milestone', desc => 'Target Milestone', in_new_bugmail => 1, buglist => 1}, {name => 'creation_ts', desc => 'Creation date', buglist => 1}, {name => 'delta_ts', desc => 'Last changed date', buglist => 1}, {name => 'longdesc', desc => 'Comment'}, {name => 'longdescs.isprivate', desc => 'Comment is private', is_numeric => 1}, {name => 'longdescs.count', desc => 'Number of Comments', buglist => 1, is_numeric => 1}, {name => 'alias', desc => 'Alias', buglist => 1}, {name => 'everconfirmed', desc => 'Ever Confirmed', is_numeric => 1}, {name => 'reporter_accessible', desc => 'Reporter Accessible', is_numeric => 1}, {name => 'cclist_accessible', desc => 'CC Accessible', is_numeric => 1}, {name => 'bug_group', desc => 'Group', in_new_bugmail => 1}, {name => 'estimated_time', desc => 'Estimated Hours', in_new_bugmail => 1, buglist => 1, is_numeric => 1}, {name => 'remaining_time', desc => 'Remaining Hours', buglist => 1, is_numeric => 1}, {name => 'deadline', desc => 'Deadline', type => FIELD_TYPE_DATETIME, in_new_bugmail => 1, buglist => 1}, {name => 'commenter', desc => 'Commenter'}, {name => 'flagtypes.name', desc => 'Flags', buglist => 1}, {name => 'requestees.login_name', desc => 'Flag Requestee'}, {name => 'setters.login_name', desc => 'Flag Setter'}, {name => 'work_time', desc => 'Hours Worked', buglist => 1, is_numeric => 1}, {name => 'percentage_complete', desc => 'Percentage Complete', buglist => 1, is_numeric => 1}, {name => 'content', desc => 'Content'}, {name => 'attach_data.thedata', desc => 'Attachment data'}, {name => "owner_idle_time", desc => "Time Since Assignee Touched"}, {name => 'see_also', desc => "See Also", type => FIELD_TYPE_BUG_URLS}, {name => 'tag', desc => 'Personal Tags', buglist => 1, type => FIELD_TYPE_KEYWORDS}, {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1, type => FIELD_TYPE_DATETIME}, {name => 'comment_tag', desc => 'Comment Tag'}, {name => 'dupe_of', desc => 'Duplicate of'}, ); ################ # Constructors # ################ # Override match to add is_select. sub match { my $self = shift; my ($params) = @_; if (delete $params->{is_select}) { $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT]; } return $self->SUPER::match(@_); } ############## # Validators # ############## sub _check_custom { return $_[1] ? 1 : 0; } sub _check_description { my ($invocant, $desc) = @_; $desc = clean_text($desc); $desc || ThrowUserError('field_missing_description'); return $desc; } sub _check_long_desc { my ($invocant, $long_desc) = @_; $long_desc = clean_text($long_desc || ''); if (length($long_desc) > MAX_FIELD_LONG_DESC_LENGTH) { ThrowUserError('field_long_desc_too_long'); } return $long_desc; } sub _check_enter_bug { return $_[1] ? 1 : 0; } sub _check_is_numeric { my ($invocant, $value, undef, $params) = @_; my $type = blessed($invocant) ? $invocant->type : $params->{type}; return 1 if $type == FIELD_TYPE_BUG_ID; return $value ? 1 : 0; } sub _check_mailhead { return $_[1] ? 1 : 0; } sub _check_name { my ($class, $name, undef, $params) = @_; $name = lc(clean_text($name)); $name || ThrowUserError('field_missing_name'); # Don't want to allow a name that might mess up SQL. my $name_regex = qr/^[\w\.]+$/; # Custom fields have more restrictive name requirements than # standard fields. $name_regex = qr/^[a-zA-Z0-9_]+$/ if $params->{custom}; # Custom fields can't be named just "cf_", and there is no normal # field named just "cf_". ($name =~ $name_regex && $name ne "cf_") || ThrowUserError('field_invalid_name', { name => $name }); # If it's custom, prepend cf_ to the custom field name to distinguish # it from standard fields. if ($name !~ /^cf_/ && $params->{custom}) { $name = 'cf_' . $name; } # Assure the name is unique. Names can't be changed, so we don't have # to worry about what to do on updates. my $field = new Bugzilla::Field({ name => $name }); ThrowUserError('field_already_exists', {'field' => $field }) if $field; return $name; } sub _check_obsolete { return $_[1] ? 1 : 0; } sub _check_sortkey { my ($invocant, $sortkey) = @_; my $skey = $sortkey; if (!defined $skey || $skey eq '') { ($sortkey) = Bugzilla->dbh->selectrow_array( 'SELECT MAX(sortkey) + 100 FROM fielddefs') || 100; } detaint_natural($sortkey) || ThrowUserError('field_invalid_sortkey', { sortkey => $skey }); return $sortkey; } sub _check_type { my ($invocant, $type, undef, $params) = @_; my $saved_type = $type; (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE) || ThrowCodeError('invalid_customfield_type', { type => $saved_type }); my $custom = blessed($invocant) ? $invocant->custom : $params->{custom}; if ($custom && !$type) { ThrowCodeError('field_type_not_specified'); } return $type; } sub _check_value_field_id { my ($invocant, $field_id, undef, $params) = @_; my $is_select = $invocant->is_select($params); if ($field_id && !$is_select) { ThrowUserError('field_value_control_select_only'); } return $invocant->_check_visibility_field_id($field_id); } sub _check_visibility_field_id { my ($invocant, $field_id) = @_; $field_id = trim($field_id); return undef if !$field_id; my $field = Bugzilla::Field->check({ id => $field_id }); if (blessed($invocant) && $field->id == $invocant->id) { ThrowUserError('field_cant_control_self', { field => $field }); } if (!$field->is_select) { ThrowUserError('field_control_must_be_select', { field => $field }); } return $field->id; } sub _check_visibility_values { my ($invocant, $values, undef, $params) = @_; my $field; if (blessed $invocant) { $field = $invocant->visibility_field; } elsif ($params->{visibility_field_id}) { $field = $invocant->new($params->{visibility_field_id}); } # When no field is set, no values are set. return [] if !$field; if (!scalar @$values) { ThrowUserError('field_visibility_values_must_be_selected', { field => $field }); } my @visibility_values; my $choice = Bugzilla::Field::Choice->type($field); foreach my $value (@$values) { if (!blessed $value) { $value = $choice->check({ id => $value }); } push(@visibility_values, $value); } return \@visibility_values; } sub _check_reverse_desc { my ($invocant, $reverse_desc, undef, $params) = @_; my $type = blessed($invocant) ? $invocant->type : $params->{type}; if ($type != FIELD_TYPE_BUG_ID) { return undef; # store NULL for non-reversible field types } $reverse_desc = clean_text($reverse_desc); return $reverse_desc; } sub _check_is_mandatory { return $_[1] ? 1 : 0; } =pod =head2 Instance Properties =over =item C the name of the field in the database; begins with "cf_" if field is a custom field, but test the value of the boolean "custom" property to determine if a given field is a custom field; =item C a short string describing the field; displayed to Bugzilla users in several places within Bugzilla's UI, f.e. as the form field label on the "show bug" page; =back =cut sub description { return $_[0]->{description} } =over =item C A string providing detailed info about the field; =back =cut sub long_desc { return $_[0]->{long_desc} } =over =item C an integer specifying the kind of field this is; values correspond to the FIELD_TYPE_* constants in Constants.pm =back =cut sub type { return $_[0]->{type} } =over =item C a boolean specifying whether or not the field is a custom field; if true, field name should start "cf_", but use this property to determine which fields are custom fields; =back =cut sub custom { return $_[0]->{custom} } =over =item C a boolean specifying whether or not the field is displayed in bugmail for newly-created bugs; =back =cut sub in_new_bugmail { return $_[0]->{mailhead} } =over =item C an integer specifying the sortkey of the field. =back =cut sub sortkey { return $_[0]->{sortkey} } =over =item C a boolean specifying whether or not the field is obsolete; =back =cut sub obsolete { return $_[0]->{obsolete} } =over =item C A boolean specifying whether or not this field should appear on enter_bug.cgi =back =cut sub enter_bug { return $_[0]->{enter_bug} } =over =item C A boolean specifying whether or not this field is selectable as a display or order column in buglist.cgi =back =cut sub buglist { return $_[0]->{buglist} } =over =item C True if this is a C or C field. It is only safe to call L if this is true. =item C Valid values for this field, as an array of L objects. =back =cut sub is_select { my ($invocant, $params) = @_; # This allows this method to be called by create() validators. my $type = blessed($invocant) ? $invocant->type : $params->{type}; return ($type == FIELD_TYPE_SINGLE_SELECT || $type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0 } =over =item C Most fields that have a C