#d 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::Admin; use strict; use warnings; use Bugzilla; use Bugzilla::Component; use Bugzilla::Error; use Bugzilla::Group; use Bugzilla::Product; use Bugzilla::Token qw(check_hash_token delete_token); use Bugzilla::Util qw(trim detaint_natural); use Bugzilla::Extension::TrackingFlags::Constants; use Bugzilla::Extension::TrackingFlags::Flag; use Bugzilla::Extension::TrackingFlags::Flag::Bug; use Bugzilla::Extension::TrackingFlags::Flag::Value; use Bugzilla::Extension::TrackingFlags::Flag::Visibility; use JSON; use Scalar::Util qw(blessed); use base qw(Exporter); our @EXPORT = qw( admin_list admin_edit ); # # page loading # sub admin_list { my ($vars) = @_; $vars->{show_bug_counts} = Bugzilla->input_params->{show_bug_counts}; $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; } sub admin_edit { my ($vars, $page) = @_; my $input = Bugzilla->input_params; $vars->{groups} = _groups_to_json(); $vars->{mode} = $input->{mode} || 'new'; $vars->{flag_id} = $input->{flag_id} || 0; $vars->{tracking_flag_types} = FLAG_TYPES; if ($input->{delete}) { my $token = $input->{token}; check_hash_token($token, ['tracking_flags_edit']); delete_token($token); my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); $flag->remove_from_db(); $vars->{message} = 'tracking_flag_deleted'; $vars->{flag} = $flag; $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; print Bugzilla->cgi->header; my $template = Bugzilla->template; $template->process('pages/tracking_flags_admin_list.html.tmpl', $vars) || ThrowTemplateError($template->error()); exit; } elsif ($input->{save}) { my $token = $input->{token}; check_hash_token($token, ['tracking_flags_edit']); delete_token($token); my ($flag, $values, $visibilities) = _load_from_input($input, $vars); _validate($flag, $values, $visibilities); my $flag_obj = _update_db($flag, $values, $visibilities); $vars->{flag} = $flag_obj; $vars->{values} = _flag_values_to_json($values); $vars->{visibility} = _flag_visibility_to_json($visibilities); if ($vars->{mode} eq 'new') { $vars->{message} = 'tracking_flag_created'; } else { $vars->{message} = 'tracking_flag_updated'; } $vars->{mode} = 'edit'; } else { # initial load if ($vars->{mode} eq 'edit') { # edit - straight load my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); $vars->{flag} = $flag; $vars->{values} = _flag_values_to_json($flag->values); $vars->{visibility} = _flag_visibility_to_json($flag->visibility); $vars->{can_delete} = !$flag->bug_count; } elsif ($vars->{mode} eq 'copy') { # copy - load the source flag $vars->{mode} = 'new'; my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from}) || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{copy_from} }); # increment the number at the end of the name and description if ($flag->name =~ /^(\D+)(\d+)$/) { $flag->set_name("$1" . ($2 + 1)); } if ($flag->description =~ /^(\D+)([\d\.]+)$/) { my $description = $1; my $version = $2; if ($version =~ /\./) { my ($major, $minor) = split(/\./, $version); $minor++; $version = "$major.$minor"; } else { $version++; } $flag->set_description($description . $version); } $flag->set_sortkey(_next_unique_sortkey($flag->sortkey)); $flag->set_type($flag->flag_type); $flag->set_enter_bug($flag->enter_bug); # always default new flags as active, even when copying an inactive one $flag->set_is_active(1); $vars->{flag} = $flag; $vars->{values} = _flag_values_to_json($flag->values, 1); $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1); $vars->{can_delete} = 0; } else { $vars->{mode} = 'new'; $vars->{flag} = { sortkey => 0, enter_bug => 1, is_active => 1, }; $vars->{values} = _flag_values_to_json([ { id => 0, value => '---', setter_group_id => '', is_active => 1, comment => '', }, ]); $vars->{visibility} = ''; $vars->{can_delete} = 0; } } } sub _load_from_input { my ($input, $vars) = @_; # flag my $flag = { id => ($input->{mode} eq 'edit' ? $input->{flag_id} : 0), name => trim($input->{flag_name} || ''), description => trim($input->{flag_desc} || ''), sortkey => $input->{flag_sort} || 0, type => trim($input->{flag_type} || ''), enter_bug => $input->{flag_enter_bug} ? 1 : 0, is_active => $input->{flag_active} ? 1 : 0, }; detaint_natural($flag->{id}); detaint_natural($flag->{sortkey}); detaint_natural($flag->{enter_bug}); detaint_natural($flag->{is_active}); # values my $values = decode_json($input->{values} || '[]'); foreach my $value (@$values) { $value->{value} = '' unless exists $value->{value} && defined $value->{value}; $value->{setter_group_id} = '' unless $value->{setter_group_id}; $value->{is_active} = $value->{is_active} ? 1 : 0; } # vibility my $visibilities = decode_json($input->{visibility} || '[]'); foreach my $visibility (@$visibilities) { $visibility->{product} = '' unless exists $visibility->{product} && defined $visibility->{product}; $visibility->{component} = '' unless exists $visibility->{component} && defined $visibility->{component}; } return ($flag, $values, $visibilities); } sub _next_unique_sortkey { my ($sortkey) = @_; my %current; foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) { $current{$flag->sortkey} = 1; } $sortkey += 5; $sortkey += 5 while exists $current{$sortkey}; return $sortkey; } # # validation # sub _validate { my ($flag, $values, $visibilities) = @_; # flag my @missing; push @missing, 'Field Name' if $flag->{name} eq ''; push @missing, 'Field Description' if $flag->{description} eq ''; push @missing, 'Field Sort Key' if $flag->{sortkey} eq ''; scalar(@missing) && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); $flag->{name} =~ /^cf_/ || ThrowUserError('tracking_flags_cf_prefix'); if ($flag->{id}) { my $old_flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); if ($flag->{name} ne $old_flag->name) { Bugzilla::Field->new({ name => $flag->{name} }) && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); } } else { Bugzilla::Field->new({ name => $flag->{name} }) && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); } # values scalar(@$values) || ThrowUserError('tracking_flags_missing_values'); my %seen; foreach my $value (@$values) { my $v = $value->{value}; $v eq '' && ThrowUserError('tracking_flags_missing_value'); exists $seen{$v} && ThrowUserError('tracking_flags_duplicate_value', { value => $v }); $seen{$v} = 1; push @missing, "Setter for $v" if !$value->{setter_group_id}; } scalar(@missing) && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); # visibility scalar(@$visibilities) || ThrowUserError('tracking_flags_missing_visibility'); %seen = (); foreach my $visibility (@$visibilities) { my $name = $visibility->{product} . ':' . $visibility->{component}; exists $seen{$name} && ThrowUserError('tracking_flags_duplicate_visibility', { name => $name }); $visibility->{product_obj} = Bugzilla::Product->new({ name => $visibility->{product} }) || ThrowCodeError('tracking_flags_invalid_product', { product => $visibility->{product} }); if ($visibility->{component} ne '') { $visibility->{component_obj} = Bugzilla::Component->new({ product => $visibility->{product_obj}, name => $visibility->{component} }) || ThrowCodeError('tracking_flags_invalid_component', { product => $visibility->{product}, component_name => $visibility->{component}, }); } } } # # database updating # sub _update_db { my ($flag, $values, $visibilities) = @_; my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); my $flag_obj = _update_db_flag($flag); _update_db_values($flag_obj, $flag, $values); _update_db_visibility($flag_obj, $flag, $visibilities); $dbh->bz_commit_transaction(); return $flag_obj; } sub _update_db_flag { my ($flag) = @_; my $object_set = { name => $flag->{name}, description => $flag->{description}, sortkey => $flag->{sortkey}, type => $flag->{type}, enter_bug => $flag->{enter_bug}, is_active => $flag->{is_active}, }; my $flag_obj; if ($flag->{id}) { # update existing flag $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); $flag_obj->set_all($object_set); $flag_obj->update(); } else { # create new flag $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set); } return $flag_obj; } sub _update_db_values { my ($flag_obj, $flag, $values) = @_; # delete foreach my $current_value (@{ $flag_obj->values }) { if (!grep { $_->{id} == $current_value->id } @$values) { $current_value->remove_from_db(); } } # add/update my $sortkey = 0; foreach my $value (@{ $values }) { $sortkey += 10; my $object_set = { value => $value->{value}, setter_group_id => $value->{setter_group_id}, is_active => $value->{is_active}, sortkey => $sortkey, comment => $value->{comment}, }; if ($value->{id}) { my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id}) || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} }); my $old_value = $value_obj->value; $value_obj->set_all($object_set); $value_obj->update(); Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({ value_obj => $value_obj, old_value => $old_value, new_value => $value_obj->value, }); } else { $object_set->{tracking_flag_id} = $flag_obj->flag_id; Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set); } } } sub _update_db_visibility { my ($flag_obj, $flag, $visibilities) = @_; # delete foreach my $current_visibility (@{ $flag_obj->visibility }) { if (!grep { $_->{id} == $current_visibility->id } @$visibilities) { $current_visibility->remove_from_db(); } } # add foreach my $visibility (@{ $visibilities }) { next if $visibility->{id}; Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ tracking_flag_id => $flag_obj->flag_id, product_id => $visibility->{product_obj}->id, component_id => $visibility->{component} ? $visibility->{component_obj}->id : undef, }); } } # # serialisation # sub _groups_to_json { my @data; foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) { push @data, { id => $group->id, name => $group->name, }; } return encode_json(\@data); } sub _flag_values_to_json { my ($values, $is_copy) = @_; # setting is_copy will set the id's to zero, to force new values rather # than editing existing ones my @data; foreach my $value (@$values) { push @data, { id => $is_copy ? 0 : $value->{id}, value => $value->{value}, setter_group_id => $value->{setter_group_id}, is_active => $value->{is_active} ? JSON::true : JSON::false, comment => $value->{comment} // '', }; } return encode_json(\@data); } sub _flag_visibility_to_json { my ($visibilities, $is_copy) = @_; # setting is_copy will set the id's to zero, to force new visibilites # rather than editing existing ones my @data; foreach my $visibility (@$visibilities) { my $product = exists $visibility->{product_id} ? $visibility->product->name : $visibility->{product}; my $component; if (exists $visibility->{component_id} && $visibility->{component_id}) { $component = $visibility->component->name; } elsif (exists $visibility->{component}) { $component = $visibility->{component}; } else { $component = undef; } push @data, { id => $is_copy ? 0 : $visibility->{id}, product => $product, component => $component, }; } @data = sort { lc($a->{product}) cmp lc($b->{product}) || lc($a->{component}) cmp lc($b->{component}) } @data; return encode_json(\@data); } 1;