#!/usr/bin/perl -wT # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of # the License at http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or # implied. See the License for the specific language governing # rights and limitations under the License. # # The Original Code is the Bugzilla Bug Tracking System. # # The Initial Developer of the Original Code is Netscape Communications # Corporation. Portions created by Netscape are # Copyright (C) 1998 Netscape Communications Corporation. All # Rights Reserved. # # Contributor(s): Myk Melez # Frédéric Buclin ################################################################################ # Script Initialization ################################################################################ use strict; use lib qw(. lib); use Bugzilla; use Bugzilla::Constants; use Bugzilla::Flag; use Bugzilla::FlagType; use Bugzilla::Group; use Bugzilla::Util; use Bugzilla::Error; use Bugzilla::Product; use Bugzilla::Component; use Bugzilla::Bug; use Bugzilla::Attachment; use Bugzilla::Token; local our $ARGS = Bugzilla->input_params; local our $template = Bugzilla->template; local our $vars = {}; # Make sure the user is logged in and is an administrator. my $user = Bugzilla->login(LOGIN_REQUIRED); $user->in_group('editflagtypes') || ThrowUserError("auth_failure", { group => "editflagtypes", action => "edit", object => "flagtypes", }); # We need this everywhere. $vars = get_products_and_components($vars); ################################################################################ # Main Body Execution ################################################################################ # All calls to this script should contain an "action" variable whose value # determines what the user wants to do. The code below checks the value of # that variable and runs the appropriate code. # Determine whether to use the action specified by the user or the default. my $action = $ARGS->{action} || 'list'; my $token = $ARGS->{token}; my @categoryActions; if (@categoryActions = grep(/^categoryAction-.+/, keys %$ARGS)) { $categoryActions[0] =~ s/^categoryAction-//; processCategoryChange($categoryActions[0], $token); exit; } if ($action eq 'list') { ft_list(); } elsif ($action eq 'enter') { edit($action); } elsif ($action eq 'copy') { edit($action); } elsif ($action eq 'edit') { edit($action); } elsif ($action eq 'insert') { insert($token); } elsif ($action eq 'update') { update($token); } elsif ($action eq 'confirmdelete') { confirmDelete(); } elsif ($action eq 'delete') { deleteType($token); } elsif ($action eq 'deactivate') { deactivate($token); } else { ThrowCodeError("action_unrecognized", { action => $action }); } exit; ################################################################################ # Functions ################################################################################ sub get_prod_comp { my $product = $ARGS->{product} || undef; my $component; if ($product) { $product = Bugzilla::Product::check_product($product); $component = $ARGS->{component} || undef; $component = Bugzilla::Component->check({ product => $product, name => $component }) if $component; } return ($product, $component); } sub ft_list { my ($product, $component) = get_prod_comp(); my $product_id = $product ? $product->id : 0; my $component_id = $component ? $component->id : 0; my $show_flag_counts = $ARGS->{show_flag_counts} && 1; # Define the variables and functions that will be passed to the UI template. $vars->{selected_product} = $ARGS->{product}; $vars->{selected_component} = $ARGS->{component}; my $bug_flagtypes; my $attach_flagtypes; # If a component is given, restrict the list to flag types available # for this component. if ($component) { $bug_flagtypes = $component->flag_types->{bug}; $attach_flagtypes = $component->flag_types->{attachment}; # Filter flag types if a group ID is given. $bug_flagtypes = filter_group($bug_flagtypes); $attach_flagtypes = filter_group($attach_flagtypes); } # If only a product is specified but no component, then restrict the list # to flag types available in at least one component of that product. elsif ($product) { $bug_flagtypes = $product->flag_types->{bug}; $attach_flagtypes = $product->flag_types->{attachment}; # Filter flag types if a group ID is given. $bug_flagtypes = filter_group($bug_flagtypes); $attach_flagtypes = filter_group($attach_flagtypes); } # If no product is given, then show all flag types available. else { $bug_flagtypes = Bugzilla::FlagType::match({ target_type => 'bug', group => $ARGS->{group} }); $attach_flagtypes = Bugzilla::FlagType::match({ target_type => 'attachment', group => $ARGS->{group} }); } if ($show_flag_counts) { my %bug_lists; my %map = ('+' => 'granted', '-' => 'denied', '?' => 'pending'); foreach my $flagtype (@$bug_flagtypes, @$attach_flagtypes) { $bug_lists{$flagtype->id} = {}; my $flags = Bugzilla::Flag->match({type_id => $flagtype->id}); # Build lists of bugs, triaged by flag status. push @{$bug_lists{$flagtype->id}->{$map{$_->status}}}, $_->bug_id for @$flags; } $vars->{bug_lists} = \%bug_lists; $vars->{show_flag_counts} = 1; } $vars->{bug_types} = $bug_flagtypes; $vars->{attachment_types} = $attach_flagtypes; # Generate and return the UI (HTML page) from the appropriate template. $template->process("admin/flag-type/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); } sub edit { my ($action) = @_; my $flag_type; my $target_type; if ($action eq 'enter') { $target_type = $ARGS->{target_type} eq 'attachment' ? 'attachment' : 'bug'; } else { $flag_type = validateID(); } $vars->{last_action} = $ARGS->{action}; if ($ARGS->{action} eq 'enter' || $ARGS->{action} eq 'copy') { $vars->{action} = "insert"; $vars->{token} = issue_session_token('add_flagtype'); } else { $vars->{action} = "update"; $vars->{token} = issue_session_token('edit_flagtype'); } # If copying or editing an existing flag type, retrieve it. if ($ARGS->{action} eq 'copy' || $ARGS->{action} eq 'edit') { $vars->{type} = $flag_type; } # Otherwise set the target type (the minimal information about the type # that the template needs to know) from the URL parameter and default # the list of inclusions to all categories. else { my %inclusions; $inclusions{"0:0"} = "__Any__:__Any__"; $vars->{type} = { target_type => $target_type, inclusions => \%inclusions, }; } # Get a list of groups available to restrict this flag type against. my @groups = Bugzilla::Group->get_all; $vars->{groups} = \@groups; # Generate and return the UI (HTML page) from the appropriate template. $template->process("admin/flag-type/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); } sub processCategoryChange { my ($categoryAction, $token) = @_; my @inclusions = list $ARGS->{inclusions}; my @exclusions = list $ARGS->{exclusions}; if ($categoryAction eq 'include') { my ($product, $component) = get_prod_comp(); my $category = ($product ? $product->id : 0) . ":" . ($component ? $component->id : 0); push(@inclusions, $category) unless grep($_ eq $category, @inclusions); } elsif ($categoryAction eq 'exclude') { my ($product, $component) = get_prod_comp(); my $category = ($product ? $product->id : 0) . ":" . ($component ? $component->id : 0); push(@exclusions, $category) unless grep($_ eq $category, @exclusions); } elsif ($categoryAction eq 'removeInclusion') { my %rem = map { $_ => 1 } list $ARGS->{inclusion_to_remove}; @inclusions = grep { !$rem{$_} } @inclusions; } elsif ($categoryAction eq 'removeExclusion') { my %rem = map { $_ => 1 } list $ARGS->{exclusion_to_remove}; @exclusions = grep { !$rem{$_} } @exclusions; } # Convert the array @clusions('prod_ID:comp_ID') back to a hash of # the form %clusions{'prod_ID:comp_ID'} = 'prod_name:comp_name' my %inclusions = clusion_array_to_hash(\@inclusions); my %exclusions = clusion_array_to_hash(\@exclusions); my @groups = Bugzilla::Group->get_all; $vars->{groups} = \@groups; $vars->{action} = $ARGS->{action}; $vars->{last_action} = $ARGS->{action} eq 'update' ? 'edit' : $ARGS->{action}; my $type = {}; foreach my $key (keys %$ARGS) { $type->{$key} = $ARGS->{$key}; } # FIXME That's what I call a big hack. The template expects to see a flag type object. # This script needs some rewrite anyway. $type->{grant_group} = {}; $type->{grant_group}->{name} = $ARGS->{grant_group}; $type->{request_group} = {}; $type->{request_group}->{name} = $ARGS->{request_group}; $type->{inclusions} = \%inclusions; $type->{exclusions} = \%exclusions; $vars->{type} = $type; $vars->{token} = $token; # Generate and return the UI (HTML page) from the appropriate template. $template->process("admin/flag-type/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); } # Convert the array @clusions('prod_ID:comp_ID') back to a hash of # the form %clusions{'prod_ID:comp_ID'} = 'prod_name:comp_name' sub clusion_array_to_hash { my $array = shift; my %hash; my %products; my %components; foreach my $ids (@$array) { trick_taint($ids); my ($product_id, $component_id) = split(":", $ids); my $product_name = "__Any__"; if ($product_id) { $products{$product_id} ||= new Bugzilla::Product($product_id); $product_name = $products{$product_id}->name if $products{$product_id}; } my $component_name = "__Any__"; if ($component_id) { $components{$component_id} ||= new Bugzilla::Component($component_id); $component_name = $components{$component_id}->name if $components{$component_id}; } $hash{$ids} = "$product_name:$component_name"; } return %hash; } sub insert { my $token = shift; check_token_data($token, 'add_flagtype'); Bugzilla->dbh->bz_start_transaction; my $ft = Bugzilla::FlagType->create({ map { ($_ => $ARGS->{$_}) } (Bugzilla::FlagType->UPDATE_COLUMNS, 'cc_list') }); # Populate the list of inclusions/exclusions for this flag type. validateAndSubmit($ft->id); Bugzilla->dbh->bz_commit_transaction; $vars->{name} = $ft->name; $vars->{message} = "flag_type_created"; delete_token($token); $vars->{bug_types} = Bugzilla::FlagType::match({ target_type => 'bug' }); $vars->{attachment_types} = Bugzilla::FlagType::match({ target_type => 'attachment' }); $template->process("admin/flag-type/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); } sub update { my $token = shift; check_token_data($token, 'edit_flagtype'); my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); my $flag_type = validateID(); for (Bugzilla::FlagType->UPDATE_COLUMNS, 'cc_list') { $flag_type->set($_, $ARGS->{$_}); } $flag_type->update; # Update the list of inclusions/exclusions for this flag type. validateAndSubmit($flag_type->id); $dbh->bz_commit_transaction(); # Clear existing flags for bugs/attachments in categories no longer on # the list of inclusions or that have been added to the list of exclusions. my $flag_ids = $dbh->selectcol_arrayref( 'SELECT DISTINCT flags.id FROM flags'. ' INNER JOIN bugs ON flags.bug_id = bugs.bug_id'. ' LEFT JOIN flaginclusions AS i ON (flags.type_id = i.type_id'. ' AND (bugs.product_id = i.product_id OR i.product_id IS NULL)'. ' AND (bugs.component_id = i.component_id OR i.component_id IS NULL))'. ' WHERE flags.type_id = ? AND i.type_id IS NULL', undef, $flag_type->id ); Bugzilla::Flag->force_retarget($flag_ids); $flag_ids = $dbh->selectcol_arrayref( 'SELECT DISTINCT flags.id FROM flags'. ' INNER JOIN bugs ON flags.bug_id = bugs.bug_id'. ' INNER JOIN flagexclusions AS e ON flags.type_id = e.type_id'. ' WHERE flags.type_id = ?'. ' AND (bugs.product_id = e.product_id OR e.product_id IS NULL)'. ' AND (bugs.component_id = e.component_id OR e.component_id IS NULL)', undef, $flag_type->id ); Bugzilla::Flag->force_retarget($flag_ids); # Now silently remove requestees from flags which are no longer # specifically requestable. if (!$flag_type->is_requesteeble) { $dbh->do('UPDATE flags SET requestee_id = NULL WHERE type_id = ?', undef, $flag_type->id); } $vars->{name} = $flag_type->name; $vars->{message} = "flag_type_changes_saved"; delete_token($token); $vars->{bug_types} = Bugzilla::FlagType::match({ target_type => 'bug' }); $vars->{attachment_types} = Bugzilla::FlagType::match({ target_type => 'attachment' }); Bugzilla->send_mail; $template->process("admin/flag-type/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); } sub confirmDelete { my $flag_type = validateID(); $vars->{flag_type} = $flag_type; $vars->{token} = issue_session_token('delete_flagtype'); # Generate and return the UI (HTML page) from the appropriate template. $template->process("admin/flag-type/confirm-delete.html.tmpl", $vars) || ThrowTemplateError($template->error()); } sub deleteType { my $token = shift; check_token_data($token, 'delete_flagtype'); my $flag_type = validateID(); my $id = $flag_type->id; my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); # Get the name of the flag type so we can tell users # what was deleted. $vars->{name} = $flag_type->name; $dbh->do('DELETE FROM flags WHERE type_id = ?', undef, $id); $dbh->do('DELETE FROM flaginclusions WHERE type_id = ?', undef, $id); $dbh->do('DELETE FROM flagexclusions WHERE type_id = ?', undef, $id); $dbh->do('DELETE FROM flagtypes WHERE id = ?', undef, $id); $dbh->bz_commit_transaction(); $vars->{message} = "flag_type_deleted"; delete_token($token); $vars->{bug_types} = Bugzilla::FlagType::match({'target_type' => 'bug'}); $vars->{attachment_types} = Bugzilla::FlagType::match({'target_type' => 'attachment'}); $template->process("admin/flag-type/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); } sub deactivate { my $token = shift; check_token_data($token, 'delete_flagtype'); my $flag_type = validateID(); validateIsActive(); my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction(); $dbh->do('UPDATE flagtypes SET is_active = 0 WHERE id = ?', undef, $flag_type->id); $dbh->bz_commit_transaction(); $vars->{message} = "flag_type_deactivated"; $vars->{flag_type} = $flag_type; delete_token($token); $vars->{bug_types} = Bugzilla::FlagType::match({'target_type' => 'bug'}); $vars->{attachment_types} = Bugzilla::FlagType::match({'target_type' => 'attachment'}); # Generate and return the UI (HTML page) from the appropriate template. $template->process("admin/flag-type/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); } sub get_products_and_components { my $vars = shift; my @products = Bugzilla::Product->get_all; # We require all unique component names. my %components; foreach my $product (@products) { foreach my $component (@{$product->components}) { $components{$component->name} = 1; } } $vars->{products} = \@products; $vars->{components} = [sort(keys %components)]; return $vars; } ################################################################################ # Data Validation / Security Authorization ################################################################################ sub validateID { my $id = $ARGS->{id}; my $flag_type = new Bugzilla::FlagType($id) || ThrowCodeError('flag_type_nonexistent', { id => $id }); return $flag_type; } # At this point, values either come the DB itself or have been recently # added by the user and have passed all validation tests. # The only way to have invalid product/component combinations is to # hack the URL. So we silently ignore them, if any. sub validateAndSubmit { my ($id) = @_; my $dbh = Bugzilla->dbh; # Cache product objects. my %products; foreach my $category_type ("inclusions", "exclusions") { # Will be used several times below. my $sth = $dbh->prepare( "INSERT INTO flag$category_type (type_id, product_id, component_id) VALUES (?, ?, ?)" ); $dbh->do("DELETE FROM flag$category_type WHERE type_id = ?", undef, $id); foreach my $category (list $ARGS->{$category_type}) { trick_taint($category); my ($product_id, $component_id) = split(":", $category); # Does the product exist? if ($product_id) { $products{$product_id} ||= new Bugzilla::Product($product_id); next unless defined $products{$product_id}; } # A component was selected without a product being selected. next if (!$product_id && $component_id); # Does the component belong to this product? if ($component_id) { my @match = grep {$_->id == $component_id} @{$products{$product_id}->components}; next unless scalar(@match); } $product_id ||= undef; $component_id ||= undef; $sth->execute($id, $product_id, $component_id); } } } sub filter_group { my $flag_types = shift; return $flag_types unless $ARGS->{group}; my $gid = $ARGS->{group}; my @flag_types = grep { $_->grant_group && $_->grant_group->id == $gid || $_->request_group && $_->request_group->id == $gid } @$flag_types; return \@flag_types; }