# 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. package Bugzilla::Field::Choice; use 5.10.1; use strict; use warnings; use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object); use Bugzilla::Config qw(SetParam write_params); use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Field; use Bugzilla::Util qw(trim detaint_natural); use Scalar::Util qw(blessed); ################## # Initialization # ################## use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( id value sortkey isactive visibility_value_id ); use constant UPDATE_COLUMNS => qw( value sortkey isactive visibility_value_id ); use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'sortkey, value'; use constant VALIDATORS => { value => \&_check_value, sortkey => \&_check_sortkey, visibility_value_id => \&_check_visibility_value_id, isactive => \&_check_isactive, }; use constant CLASS_MAP => { bug_status => 'Bugzilla::Status', classification => 'Bugzilla::Classification', component => 'Bugzilla::Component', product => 'Bugzilla::Product', }; use constant DEFAULT_MAP => { op_sys => 'defaultopsys', rep_platform => 'defaultplatform', priority => 'defaultpriority', bug_severity => 'defaultseverity', }; ################# # Class Factory # ################# # Bugzilla::Field::Choice is actually an abstract base class. Every field # type has its own dynamically-generated class for its values. This allows # certain fields to have special types, like how bug_status's values # are Bugzilla::Status objects. sub type { my ($class, $field) = @_; my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field); my $field_name = $field_obj->name; if (my $package = $class->CLASS_MAP->{$field_name}) { # Callers expect the module to be already loaded. eval "require $package"; return $package; } # For generic classes, we use a lowercase class name, so as # not to interfere with any real subclasses we might make some day. my $package = "Bugzilla::Field::Choice::$field_name"; Bugzilla->request_cache->{"field_$package"} = $field_obj; # This package only needs to be created once. We check if the DB_TABLE # glob for this package already exists, which tells us whether or not # we need to create the package (this works even under mod_perl, where # this package definition will persist across requests)). if (!defined *{"${package}::DB_TABLE"}) { eval < '$field_name'; EOC } return $package; } ################ # Constructors # ################ # We just make new() enforce this, which should give developers # the understanding that you can't use Bugzilla::Field::Choice # without calling type(). sub new { my $class = shift; if ($class eq 'Bugzilla::Field::Choice') { ThrowCodeError('field_choice_must_use_type'); } $class->SUPER::new(@_); } ######################### # Database Manipulation # ######################### # Our subclasses can take more arguments than we normally accept. # So, we override create() to remove arguments that aren't valid # columns. (Normally Bugzilla::Object dies if you pass arguments # that aren't valid columns.) sub create { my $class = shift; my ($params) = @_; foreach my $key (keys %$params) { if (!grep {$_ eq $key} $class->_get_db_columns) { delete $params->{$key}; } } return $class->SUPER::create(@_); } sub update { my $self = shift; my $dbh = Bugzilla->dbh; my $fname = $self->field->name; $dbh->bz_start_transaction(); my ($changes, $old_self) = $self->SUPER::update(@_); if (exists $changes->{value}) { my ($old, $new) = @{ $changes->{value} }; if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { $dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?", undef, $new, $old); } else { $dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?", undef, $new, $old); } if ($old_self->is_default) { my $param = $self->DEFAULT_MAP->{$self->field->name}; SetParam($param, $self->name); write_params(); } } $dbh->bz_commit_transaction(); return wantarray ? ($changes, $old_self) : $changes; } sub remove_from_db { my $self = shift; if ($self->is_default) { ThrowUserError('fieldvalue_is_default', { field => $self->field, value => $self, param_name => $self->DEFAULT_MAP->{$self->field->name}, }); } if ($self->is_static) { ThrowUserError('fieldvalue_not_deletable', { field => $self->field, value => $self }); } if ($self->bug_count) { ThrowUserError("fieldvalue_still_has_bugs", { field => $self->field, value => $self }); } $self->_check_if_controller(); # From ChoiceInterface. $self->SUPER::remove_from_db(); } ############ # Mutators # ############ sub set_is_active { $_[0]->set('isactive', $_[1]); } sub set_name { $_[0]->set('value', $_[1]); } sub set_sortkey { $_[0]->set('sortkey', $_[1]); } sub set_visibility_value { my ($self, $value) = @_; $self->set('visibility_value_id', $value); delete $self->{visibility_value}; } ############## # Validators # ############## sub _check_isactive { my ($invocant, $value) = @_; $value = Bugzilla::Object::check_boolean($invocant, $value); if (!$value and ref $invocant) { if ($invocant->is_default) { my $field = $invocant->field; ThrowUserError('fieldvalue_is_default', { value => $invocant, field => $field, param_name => $invocant->DEFAULT_MAP->{$field->name} }); } if ($invocant->is_static) { ThrowUserError('fieldvalue_not_deletable', { value => $invocant, field => $invocant->field }); } } return $value; } sub _check_value { my ($invocant, $value) = @_; my $field = $invocant->field; $value = trim($value); # Make sure people don't rename static values if (blessed($invocant) && $value ne $invocant->name && $invocant->is_static) { ThrowUserError('fieldvalue_not_editable', { field => $field, old_value => $invocant }); } ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq ""; ThrowUserError('fieldvalue_name_too_long', { value => $value }) if length($value) > MAX_FIELD_VALUE_SIZE; my $exists = $invocant->type($field)->new({ name => $value }); if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) { ThrowUserError('fieldvalue_already_exists', { field => $field, value => $exists }); } return $value; } sub _check_sortkey { my ($invocant, $value) = @_; $value = trim($value); return 0 if !$value; # Store for the error message in case detaint_natural clears it. my $orig_value = $value; (detaint_natural($value) && $value <= MAX_SMALLINT) || ThrowUserError('fieldvalue_sortkey_invalid', { sortkey => $orig_value, field => $invocant->field }); return $value; } sub _check_visibility_value_id { my ($invocant, $value_id) = @_; $value_id = trim($value_id); my $field = $invocant->field->value_field; return undef if !$field || !$value_id; my $value_obj = Bugzilla::Field::Choice->type($field) ->check({ id => $value_id }); return $value_obj->id; } 1; __END__ =head1 NAME Bugzilla::Field::Choice - A legal value for a