# 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::Extension::TrackingFlags::Flag; use base qw(Bugzilla::Object); use strict; use warnings; use Bugzilla::Error; use Bugzilla::Constants; use Bugzilla::Util qw(detaint_natural trim); use Bugzilla::Config qw(SetParam write_params); use Bugzilla::Extension::TrackingFlags::Constants; use Bugzilla::Extension::TrackingFlags::Flag::Bug; use Bugzilla::Extension::TrackingFlags::Flag::Value; use Bugzilla::Extension::TrackingFlags::Flag::Visibility; ############################### #### Initialization #### ############################### use constant DB_TABLE => 'tracking_flags'; use constant DB_COLUMNS => qw( id field_id name description type sortkey enter_bug is_active ); use constant LIST_ORDER => 'sortkey'; use constant UPDATE_COLUMNS => qw( name description type sortkey enter_bug is_active ); use constant VALIDATORS => { name => \&_check_name, description => \&_check_description, type => \&_check_type, sortkey => \&_check_sortkey, enter_bug => \&Bugzilla::Object::check_boolean, is_active => \&Bugzilla::Object::check_boolean, }; use constant UPDATE_VALIDATORS => { name => \&_check_name, description => \&_check_description, type => \&_check_type, sortkey => \&_check_sortkey, enter_bug => \&Bugzilla::Object::check_boolean, is_active => \&Bugzilla::Object::check_boolean, }; ############################### #### Methods #### ############################### sub new { my $class = shift; my $param = shift; my $cache = Bugzilla->request_cache; if (!ref $param && exists $cache->{'tracking_flags'} && exists $cache->{'tracking_flags'}->{$param}) { return $cache->{'tracking_flags'}->{$param}; } return $class->SUPER::new($param); } sub new_from_hash { my $class = shift; my $cache = Bugzilla->request_cache->{'tracking_flags'} //= {}; my $flag = $class->SUPER::new_from_hash(@_); if ($flag) { push @Bugzilla::Extension::TrackingFlags::FLAG_CACHE, $flag; } return $flag; } sub create { my $class = shift; my $params = shift; my $dbh = Bugzilla->dbh; my $flag; # Disable bug updates temporarily to avoid conflicts. SetParam('disable_bug_updates', 1); write_params(); eval { $dbh->bz_start_transaction(); $params = $class->run_create_validators($params); # We have to create an entry for this new flag # in the fielddefs table for use elsewhere. We cannot # use Bugzilla::Field->create as it will create the # additional tables needed by custom fields which we # do not need. Also we do this so as not to add a # another column to the bugs table. # We will create the entry as a custom field with a # type of FIELD_TYPE_EXTENSION so Bugzilla will skip # these field types in certain parts of the core code. $dbh->do("INSERT INTO fielddefs (name, description, sortkey, type, custom, obsolete, buglist) VALUES (?, ?, ?, ?, ?, ?, ?)", undef, $params->{'name'}, $params->{'description'}, $params->{'sortkey'}, FIELD_TYPE_EXTENSION, 1, 0, 1); $params->{'field_id'} = $dbh->bz_last_key; $flag = $class->SUPER::create($params); $dbh->bz_commit_transaction(); }; my $error = "$@"; SetParam('disable_bug_updates', 0); write_params(); die $error if $error; # fielddefs has been changed so we need to clear global config Bugzilla->memcached->clear_config(); return $flag; } sub update { my $self = shift; my $dbh = Bugzilla->dbh; my $old_self = $self->new($self->flag_id); # HACK! Bugzilla::Object::update uses hardcoded $self->id # instead of $self->{ID_FIELD} so we need to reverse field_id # and the real id temporarily my $field_id = $self->id; $self->{'field_id'} = $self->{'id'}; my $changes = $self->SUPER::update(@_); $self->{'field_id'} = $field_id; # Update the fielddefs entry $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?", undef, $self->name, $self->description, $old_self->name); # Update request_cache my $cache = Bugzilla->request_cache; if (exists $cache->{'tracking_flags'}) { $cache->{'tracking_flags'}->{$self->flag_id} = $self; } # fielddefs has been changed so we need to clear global config Bugzilla->memcached->clear_config(); return $changes; } sub match { my $class = shift; my ($params) = @_; # Use later for preload my $bug_id = delete $params->{'bug_id'}; # Retrieve all flags relevant for the given product and component if (!exists $params->{'id'} && ($params->{'component'} || $params->{'component_id'} || $params->{'product'} || $params->{'product_id'})) { my $visible_flags = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_); my @flag_ids = map { $_->tracking_flag_id } @$visible_flags; delete $params->{'component'} if exists $params->{'component'}; delete $params->{'component_id'} if exists $params->{'component_id'}; delete $params->{'product'} if exists $params->{'product'}; delete $params->{'product_id'} if exists $params->{'product_id'}; $params->{'id'} = \@flag_ids; } # We need to return inactive flags if a value has been set my $is_active_filter = delete $params->{is_active}; my $flags = $class->SUPER::match($params); preload_all_the_things($flags, { bug_id => $bug_id }); if ($is_active_filter) { $flags = [ grep { $_->is_active || exists $_->{bug_flag} } @$flags ]; } return [ sort { $a->sortkey <=> $b->sortkey } @$flags ]; } sub get_all { my $self = shift; my $cache = Bugzilla->request_cache; if (!exists $cache->{'tracking_flags'}) { my @tracking_flags = $self->SUPER::get_all(@_); preload_all_the_things(\@tracking_flags); my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags; $cache->{'tracking_flags'} = \%tracking_flags_hash; } return sort { $a->flag_type cmp $b->flag_type || $a->sortkey <=> $b->sortkey } values %{ $cache->{'tracking_flags'} }; } # avoids the overhead of pre-loading if just the field names are required sub get_all_names { my $self = shift; my $cache = Bugzilla->request_cache; if (!exists $cache->{'tracking_flags_names'}) { $cache->{'tracking_flags_names'} = Bugzilla->dbh->selectcol_arrayref("SELECT name FROM tracking_flags ORDER BY name"); } return @{ $cache->{'tracking_flags_names'} }; } sub remove_from_db { my $self = shift; my $dbh = Bugzilla->dbh; # Check to see if tracking_flags_bugs table has records if ($self->bug_count) { ThrowUserError('tracking_flag_has_contents', { flag => $self }); } # Disable bug updates temporarily to avoid conflicts. SetParam('disable_bug_updates', 1); write_params(); eval { $dbh->bz_start_transaction(); $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ?', undef, $self->id); $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name); $dbh->bz_commit_transaction(); # Remove from request cache my $cache = Bugzilla->request_cache; if (exists $cache->{'tracking_flags'}) { delete $cache->{'tracking_flags'}->{$self->flag_id}; } }; my $error = "$@"; SetParam('disable_bug_updates', 0); write_params(); die $error if $error; } sub preload_all_the_things { my ($flags, $params) = @_; my %flag_hash = map { $_->flag_id => $_ } @$flags; my @flag_ids = keys %flag_hash; return unless @flag_ids; # Preload values my $value_objects = Bugzilla::Extension::TrackingFlags::Flag::Value->match({ tracking_flag_id => \@flag_ids }); # Now populate the tracking flags with this set of value objects. foreach my $obj (@$value_objects) { my $flag_id = $obj->tracking_flag_id; # Prepopulate the tracking flag object in the value object $obj->{'tracking_flag'} = $flag_hash{$flag_id}; # Prepopulate the current value objects for this tracking flag $flag_hash{$flag_id}->{'values'} ||= []; push(@{$flag_hash{$flag_id}->{'values'}}, $obj); } # Preload bug values if a bug_id is passed if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) { # We don't want to use @flag_ids here as we want all flags attached to this bug # even if they are inactive. my $bug_objects = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $params->{'bug_id'} }); # Now populate the tracking flags with this set of objects. # Also we add them to the flag hash since we want them to be visible even if # they are not longer applicable to this product/component. foreach my $obj (@$bug_objects) { my $flag_id = $obj->tracking_flag_id; # Load the flag object if it does not yet exist. # This can happen if the bug value tracking flag # is no longer visible for the product/component $flag_hash{$flag_id} ||= Bugzilla::Extension::TrackingFlags::Flag->new($flag_id); # Prepopulate the tracking flag object in the bug flag object $obj->{'tracking_flag'} = $flag_hash{$flag_id}; # Prepopulate the the current bug flag object for the tracking flag $flag_hash{$flag_id}->{'bug_flag'} = $obj; } } @$flags = values %flag_hash; } ############################### #### Validators #### ############################### sub _check_name { my ($invocant, $name) = @_; $name = trim($name); $name || ThrowCodeError('param_required', { param => 'name' }); return $name; } sub _check_description { my ($invocant, $description) = @_; $description = trim($description); $description || ThrowCodeError( 'param_required', { param => 'description' } ); return $description; } sub _check_type { my ($invocant, $type) = @_; $type = trim($type); $type || ThrowCodeError( 'param_required', { param => 'type' } ); grep($_->{name} eq $type, @{FLAG_TYPES()}) || ThrowUserError('tracking_flags_invalid_flag_type', { type => $type }); return $type; } sub _check_sortkey { my ($invocant, $sortkey) = @_; detaint_natural($sortkey) || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); return $sortkey; } ############################### #### Setters #### ############################### sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } sub set_type { $_[0]->set('type', $_[1]); } sub set_sortkey { $_[0]->set('sortkey', $_[1]); } sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } sub set_is_active { $_[0]->set('is_active', $_[1]); } ############################### #### Accessors #### ############################### sub flag_id { return $_[0]->{'id'}; } sub name { return $_[0]->{'name'}; } sub description { return $_[0]->{'description'}; } sub flag_type { return $_[0]->{'type'}; } sub sortkey { return $_[0]->{'sortkey'}; } sub enter_bug { return $_[0]->{'enter_bug'}; } sub is_active { return $_[0]->{'is_active'}; } sub values { return $_[0]->{'values'} ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match({ tracking_flag_id => $_[0]->flag_id }); } sub visibility { return $_[0]->{'visibility'} ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match({ tracking_flag_id => $_[0]->flag_id }); } sub can_set_value { my ($self, $new_value, $user) = @_; $user ||= Bugzilla->user; my $new_value_obj; foreach my $value (@{$self->values}) { if ($value->value eq $new_value) { $new_value_obj = $value; last; } } return $new_value_obj && $new_value_obj->setter_group && $user->in_group($new_value_obj->setter_group->name) ? 1 : 0; } sub bug_flag { my ($self, $bug_id) = @_; # Return the current bug value object if defined unless the passed bug_id does # not equal the current bug value objects id. if (defined $self->{'bug_flag'} && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id)) { return $self->{'bug_flag'}; } # Flag::Bug->new will return a default bug value object if $params undefined my $params = !$bug_id ? undef : { condition => "tracking_flag_id = ? AND bug_id = ?", values => [ $self->flag_id, $bug_id ] }; return $self->{'bug_flag'} = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params); } sub bug_count { my ($self) = @_; return $self->{'bug_count'} if defined $self->{'bug_count'}; my $dbh = Bugzilla->dbh; return $self->{'bug_count'} = scalar $dbh->selectrow_array(" SELECT COUNT(bug_id) FROM tracking_flags_bugs WHERE tracking_flag_id = ?", undef, $self->flag_id); } sub activity_count { my ($self) = @_; return $self->{'activity_count'} if defined $self->{'activity_count'}; my $dbh = Bugzilla->dbh; return $self->{'activity_count'} = scalar $dbh->selectrow_array(" SELECT COUNT(bug_id) FROM bugs_activity WHERE fieldid = ?", undef, $self->id); } ###################################### # Compatibility with Bugzilla::Field # ###################################### # Here we return 'field_id' instead of the real # id as we want other Bugzilla code to treat this # as a Bugzilla::Field object in certain places. sub id { return $_[0]->{'field_id'}; } sub type { return FIELD_TYPE_EXTENSION; } sub legal_values { return $_[0]->values; } sub custom { return 1; } sub in_new_bugmail { return 1; } sub obsolete { return $_[0]->is_active ? 0 : 1; } sub buglist { return 1; } sub is_select { return 1; } sub is_abnormal { return 1; } sub is_timetracking { return 0; } sub visibility_field { return undef; } sub visibility_values { return undef; } sub controls_visibility_of { return undef; } sub value_field { return undef; } sub controls_values_of { return undef; } sub is_visible_on_bug { return 1; } sub is_relationship { return 0; } sub reverse_desc { return ''; } sub is_mandatory { return 0; } sub is_numeric { return 0; } 1;