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