]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 529974: Let users with local editcomponents privs manage flags for products they...
authorFrédéric Buclin <LpSolit@gmail.com>
Sat, 4 Dec 2010 01:22:49 +0000 (02:22 +0100)
committerFrédéric Buclin <LpSolit@gmail.com>
Sat, 4 Dec 2010 01:22:49 +0000 (02:22 +0100)
a=LpSolit (module owner)

Bugzilla/FlagType.pm
Bugzilla/User.pm
editflagtypes.cgi
skins/standard/attachment.css
skins/standard/global.css
template/en/default/admin/admin.html.tmpl
template/en/default/admin/flag-type/edit.html.tmpl
template/en/default/global/user-error.html.tmpl

index d6a122609440f59b54d9acdc9da68dccf63cdf3c..bd3f7b05439510390cab3db7d48af7ba1ccfb06a 100644 (file)
@@ -112,6 +112,9 @@ use constant UPDATE_VALIDATORS => {
 
 sub create {
     my $class = shift;
+    my $dbh = Bugzilla->dbh;
+
+    $dbh->bz_start_transaction();
 
     $class->check_required_create_fields(@_);
     my $params = $class->run_create_validators(@_);
@@ -126,6 +129,9 @@ sub create {
 
     $flagtype->set_clusions({ inclusions => $inclusions,
                               exclusions => $exclusions });
+    $flagtype->update();
+
+    $dbh->bz_commit_transaction();
     return $flagtype;
 }
 
@@ -161,7 +167,7 @@ sub update {
                                                FROM flags
                                          INNER JOIN bugs
                                                  ON flags.bug_id = bugs.bug_id
-                                    LEFT OUTER JOIN flaginclusions AS i
+                                          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)
@@ -351,7 +357,6 @@ sub set_request_group    { $_[0]->set('request_group_id', $_[1]); }
 
 sub set_clusions {
     my ($self, $list) = @_;
-    my $dbh = Bugzilla->dbh;
     my %products;
 
     foreach my $category (keys %$list) {
@@ -360,22 +365,33 @@ sub set_clusions {
 
         foreach my $prod_comp (@{$list->{$category} || []}) {
             my ($prod_id, $comp_id) = split(':', $prod_comp);
-            my $component;
+            my $prod_name = '__Any__';
+            my $comp_name = '__Any__';
             # Does the product exist?
-            if ($prod_id && detaint_natural($prod_id)) {
-                $products{$prod_id} ||= new Bugzilla::Product($prod_id);
-                next unless defined $products{$prod_id};
+            if ($prod_id) {
+                $products{$prod_id} ||= Bugzilla::Product->check({ id => $prod_id });
+                detaint_natural($prod_id);
+                $prod_name = $products{$prod_id}->name;
 
                 # Does the component belong to this product?
-                if ($comp_id && detaint_natural($comp_id)) {
-                    ($component) = grep { $_->id == $comp_id } @{$products{$prod_id}->components};
-                    next unless $component;
+                if ($comp_id) {
+                    detaint_natural($comp_id)
+                      || ThrowCodeError('param_must_be_numeric',
+                                        { function => 'Bugzilla::FlagType::set_clusions' });
+
+                    my ($component) = grep { $_->id == $comp_id } @{$products{$prod_id}->components}
+                                        or ThrowUserError('product_unknown_component',
+                                                          { product => $prod_name, comp_id => $comp_id });
+                    $comp_name = $component->name;
                 }
+                else {
+                    $comp_id = 0;
+                }
+            }
+            else {
+                $prod_id = 0;
+                $comp_id = 0;
             }
-            $prod_id ||= 0;
-            $comp_id ||= 0;
-            my $prod_name = $prod_id ? $products{$prod_id}->name : '__Any__';
-            my $comp_name = $comp_id ? $component->name : '__Any__';
             $clusions{"$prod_name:$comp_name"} = "$prod_id:$comp_id";
             $clusions_as_hash{$prod_id}->{$comp_id} = 1;
         }
@@ -520,15 +536,16 @@ sub get_clusions {
     my $dbh = Bugzilla->dbh;
 
     my $list =
-        $dbh->selectall_arrayref("SELECT products.id, products.name, " .
-                                 "       components.id, components.name " . 
-                                 "FROM flagtypes, flag${type}clusions " . 
-                                 "LEFT OUTER JOIN products " .
-                                 "  ON flag${type}clusions.product_id = products.id " . 
-                                 "LEFT OUTER JOIN components " .
-                                 "  ON flag${type}clusions.component_id = components.id " . 
-                                 "WHERE flagtypes.id = ? " .
-                                 " AND flag${type}clusions.type_id = flagtypes.id",
+        $dbh->selectall_arrayref("SELECT products.id, products.name,
+                                         components.id, components.name
+                                    FROM flagtypes
+                              INNER JOIN flag${type}clusions
+                                      ON flag${type}clusions.type_id = flagtypes.id
+                               LEFT JOIN products
+                                      ON flag${type}clusions.product_id = products.id
+                               LEFT JOIN components
+                                      ON flag${type}clusions.component_id = components.id
+                                   WHERE flagtypes.id = ?",
                                  undef, $id);
     my (%clusions, %clusions_as_hash);
     foreach my $data (@$list) {
@@ -667,7 +684,7 @@ sub sqlify_criteria {
             $join_clause .= "AND (e.component_id = $component_id OR e.component_id IS NULL) ";
         }
         else {
-            $addl_join_clause = "AND e.component_id IS NULL OR (i.component_id != e.component_id) ";
+            $addl_join_clause = "AND e.component_id IS NULL OR (i.component_id = e.component_id) ";
         }
         $join_clause .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_id IS NULL)";
 
index de2d0dcc746724d219fe7e8ee2e60a4e1f71f74f..47f923f20d9541443d75d5acb21a5aa88ddf2965 100644 (file)
@@ -1006,6 +1006,49 @@ sub check_can_admin_product {
     return $product;
 }
 
+sub check_can_admin_flagtype {
+    my ($self, $flagtype_id) = @_;
+
+    my $flagtype = Bugzilla::FlagType->check({ id => $flagtype_id });
+    my $can_fully_edit = 1;
+
+    if (!$self->in_group('editcomponents')) {
+        my $products = $self->get_products_by_permission('editcomponents');
+        # You need editcomponents privs for at least one product to have
+        # a chance to edit the flagtype.
+        scalar(@$products)
+          || ThrowUserError('auth_failure', {group  => 'editcomponents',
+                                             action => 'edit',
+                                             object => 'flagtypes'});
+        my $can_admin = 0;
+        my $i = $flagtype->inclusions_as_hash;
+        my $e = $flagtype->exclusions_as_hash;
+
+        # If there is at least one product for which the user doesn't have
+        # editcomponents privs, then don't allow him to do everything with
+        # this flagtype, independently of whether this product is in the
+        # exclusion list or not.
+        my %product_ids;
+        map { $product_ids{$_->id} = 1 } @$products;
+        $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i;
+
+        unless ($e->{0}->{0}) {
+            foreach my $product (@$products) {
+                my $id = $product->id;
+                next if $e->{$id}->{0};
+                # If we are here, the product has not been explicitly excluded.
+                # Check whether it's explicitly included, or at least one of
+                # its components.
+                $can_admin = ($i->{0}->{0} || $i->{$id}->{0}
+                              || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}}));
+                last if $can_admin;
+            }
+        }
+        $can_admin || ThrowUserError('flag_type_not_editable', { flagtype => $flagtype });
+    }
+    return wantarray ? ($flagtype, $can_fully_edit) : $flagtype;
+}
+
 sub can_request_flag {
     my ($self, $flag_type) = @_;
 
@@ -2261,6 +2304,21 @@ not be aware of the existence of the product.
 
  Returns:     On success, a product object. On failure, an error is thrown.
 
+=item C<check_can_admin_flagtype($flagtype_id)>
+
+ Description: Checks whether the user is allowed to edit properties of the flag type.
+              If the flag type is also used by some products for which the user
+              hasn't editcomponents privs, then the user is only allowed to edit
+              the inclusion and exclusion lists for products he can administrate.
+
+ Params:      $flagtype_id - a flag type ID.
+
+ Returns:     On success, a flag type object. On failure, an error is thrown.
+              In list context, a boolean indicating whether the user can edit
+              all properties of the flag type is also returned. The boolean
+              is false if the user can only edit the inclusion and exclusions
+              lists.
+
 =item C<can_request_flag($flag_type)>
 
  Description: Checks whether the user can request flags of the given type.
index ecfa32ca8f4b0ab0e75086e08890763c4355ad74..4d11eecdb43f52381fa20d4013d33786f283d299 100755 (executable)
@@ -38,7 +38,6 @@ use Bugzilla::Group;
 use Bugzilla::Util;
 use Bugzilla::Error;
 use Bugzilla::Product;
-use Bugzilla::Component;
 use Bugzilla::Token;
 
 # Make sure the user is logged in and has the right privileges.
@@ -46,16 +45,18 @@ my $user = Bugzilla->login(LOGIN_REQUIRED);
 my $cgi = Bugzilla->cgi;
 my $template = Bugzilla->template;
 
-# We need this everywhere.
-my $vars = get_products_and_components();
-
 print $cgi->header();
 
 $user->in_group('editcomponents')
+  || scalar(@{$user->get_products_by_permission('editcomponents')})
   || ThrowUserError("auth_failure", {group  => "editcomponents",
                                      action => "edit",
                                      object => "flagtypes"});
 
+# We need this everywhere.
+my $vars = get_products_and_components();
+my @products = @{$vars->{products}};
+
 my $action = $cgi->param('action') || 'list';
 my $token  = $cgi->param('token');
 my $product = $cgi->param('product');
@@ -63,13 +64,18 @@ my $component = $cgi->param('component');
 my $flag_id = $cgi->param('id');
 
 if ($product) {
-    $product = Bugzilla::Product->check({ name => $product, allow_inaccessible => 1 });
+    # Make sure the user is allowed to view this product name.
+    # Users with global editcomponents privs can see all product names.
+    ($product) = grep { lc($_->name) eq lc($product) } @products;
+    $product || ThrowUserError('product_access_denied', { name => $cgi->param('product') });
 }
 
 if ($component) {
     ($product && $product->id)
       || ThrowUserError('flag_type_component_without_product');
-    $component = Bugzilla::Component->check({ product => $product, name => $component });
+    ($component) = grep { lc($_->name) eq lc($component) } @{$product->components};
+    $component || ThrowUserError('product_unknown_component', { product => $product->name,
+                                                                comp => $cgi->param('component') });
 }
 
 # If 'categoryAction' is set, it has priority over 'action'.
@@ -78,15 +84,30 @@ if (my ($category_action) = grep { $_ =~ /^categoryAction-(?:\w+)$/ } $cgi->para
 
     my @inclusions = $cgi->param('inclusions');
     my @exclusions = $cgi->param('exclusions');
+    my @categories;
+    if ($category_action =~ /^(in|ex)clude$/) {
+        if (!$user->in_group('editcomponents') && !$product) {
+            # The user can only add the flag type to products he can administrate.
+            foreach my $prod (@products) {
+                push(@categories, $prod->id . ':0')
+            }
+        }
+        else {
+            my $category = ($product ? $product->id : 0) . ':' .
+                           ($component ? $component->id : 0);
+            push(@categories, $category);
+        }
+    }
+
     if ($category_action eq 'include') {
-        my $category = ($product ? $product->id : 0) . ":" .
-                       ($component ? $component->id : 0);
-        push(@inclusions, $category) unless grep($_ eq $category, @inclusions);
+        foreach my $category (@categories) {
+            push(@inclusions, $category) unless grep($_ eq $category, @inclusions);
+        }
     }
     elsif ($category_action eq 'exclude') {
-        my $category = ($product ? $product->id : 0) . ":" .
-                       ($component ? $component->id : 0);
-        push(@exclusions, $category) unless grep($_ eq $category, @exclusions);
+        foreach my $category (@categories) {
+            push(@exclusions, $category) unless grep($_ eq $category, @exclusions);
+        }
     }
     elsif ($category_action eq 'removeInclusion') {
         my @inclusion_to_remove = $cgi->param('inclusion_to_remove');
@@ -101,11 +122,6 @@ if (my ($category_action) = grep { $_ =~ /^categoryAction-(?:\w+)$/ } $cgi->para
         }
     }
 
-    # Convert the array @clusions('prod_ID:comp_ID') back to a hash of
-    # the form %clusions{'prod_name:comp_name'} = 'prod_ID:comp_ID'
-    my %inclusions = clusion_array_to_hash(\@inclusions);
-    my %exclusions = clusion_array_to_hash(\@exclusions);
-
     $vars->{'groups'} = [Bugzilla::Group->get_all];
     $vars->{'action'} = $action;
 
@@ -122,11 +138,13 @@ if (my ($category_action) = grep { $_ =~ /^categoryAction-(?:\w+)$/ } $cgi->para
     $type->{'request_group'} = {};
     $type->{'request_group'}->{'name'} = $cgi->param('request_group');
 
-    $type->{'inclusions'} = \%inclusions;
-    $type->{'exclusions'} = \%exclusions;
+    $vars->{'inclusions'} = clusion_array_to_hash(\@inclusions, \@products);
+    $vars->{'exclusions'} = clusion_array_to_hash(\@exclusions, \@products);
+
     $vars->{'type'} = $type;
     $vars->{'token'} = $token;
     $vars->{'check_clusions'} = 1;
+    $vars->{'can_fully_edit'} = $cgi->param('can_fully_edit');
 
     $template->process("admin/flag-type/edit.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
@@ -165,8 +183,7 @@ if ($action eq 'list') {
     }
     # If no product is given, then show all flag types available.
     else {
-        my $flagtypes = Bugzilla::FlagType::match({ group => $group_id });
-
+        my $flagtypes = get_editable_flagtypes(\@products, $group_id);
         $bug_flagtypes = [grep { $_->target_type eq 'bug' } @$flagtypes];
         $attach_flagtypes = [grep { $_->target_type eq 'attachment' } @$flagtypes];
     }
@@ -202,8 +219,12 @@ if ($action eq 'enter') {
 
     $vars->{'action'} = 'insert';
     $vars->{'token'} = issue_session_token('add_flagtype');
-    $vars->{'type'} = { 'target_type' => $type,
-                        'inclusions'  => { '__Any__:__Any__' => '0:0' } };
+    $vars->{'type'} = { 'target_type' => $type };
+    # Only users with global editcomponents privs can add a flagtype
+    # to all products.
+    $vars->{'inclusions'} = { '__Any__:__Any__' => '0:0' }
+      if $user->in_group('editcomponents');
+    $vars->{'can_fully_edit'} = 1;
     # Get a list of groups available to restrict this flag type against.
     $vars->{'groups'} = [Bugzilla::Group->get_all];
 
@@ -213,7 +234,19 @@ if ($action eq 'enter') {
 }
 
 if ($action eq 'edit' || $action eq 'copy') {
-    $vars->{'type'} = Bugzilla::FlagType->check({ id => $flag_id });
+    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+    $vars->{'type'} = $flagtype;
+    $vars->{'can_fully_edit'} = $can_fully_edit;
+
+    if ($user->in_group('editcomponents')) {
+        $vars->{'inclusions'} = $flagtype->inclusions;
+        $vars->{'exclusions'} = $flagtype->exclusions;
+    }
+    else {
+        # Filter products the user shouldn't know about.
+        $vars->{'inclusions'} = clusion_array_to_hash([values %{$flagtype->inclusions}], \@products);
+        $vars->{'exclusions'} = clusion_array_to_hash([values %{$flagtype->exclusions}], \@products);
+    }
 
     if ($action eq 'copy') {
         $vars->{'action'} = "insert";
@@ -249,6 +282,12 @@ if ($action eq 'insert') {
     my @inclusions       = $cgi->param('inclusions');
     my @exclusions       = $cgi->param('exclusions');
 
+    # Filter inclusion and exclusion lists to products the user can see.
+    unless ($user->in_group('editcomponents')) {
+        @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)};
+        @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)};
+    }
+
     my $flagtype = Bugzilla::FlagType->create({
         name        => $name,
         description => $description,
@@ -270,9 +309,9 @@ if ($action eq 'insert') {
     $vars->{'name'} = $flagtype->name;
     $vars->{'message'} = "flag_type_created";
 
-    my @flagtypes = Bugzilla::FlagType->get_all;
-    $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes];
-    $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @flagtypes];
+    my $flagtypes = get_editable_flagtypes(\@products);
+    $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes];
+    $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @$flagtypes];
 
     $template->process("admin/flag-type/list.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
@@ -295,17 +334,34 @@ if ($action eq 'update') {
     my @inclusions       = $cgi->param('inclusions');
     my @exclusions       = $cgi->param('exclusions');
 
-    my $flagtype = Bugzilla::FlagType->check({ id => $flag_id });
-    $flagtype->set_name($name);
-    $flagtype->set_description($description);
-    $flagtype->set_cc_list($cc_list);
-    $flagtype->set_sortkey($sortkey);
-    $flagtype->set_is_active($is_active);
-    $flagtype->set_is_requestable($is_requestable);
-    $flagtype->set_is_specifically_requestable($is_specifically);
-    $flagtype->set_is_multiplicable($is_multiplicable);
-    $flagtype->set_grant_group($grant_group);
-    $flagtype->set_request_group($request_group);
+    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+    if ($cgi->param('check_clusions') && !$user->in_group('editcomponents')) {
+        # Filter inclusion and exclusion lists to products the user can edit.
+        @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)};
+        @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)};
+        # Bring back the products the user cannot edit.
+        foreach my $item (values %{$flagtype->inclusions}) {
+            my ($prod_id, $comp_id) = split(':', $item);
+            push(@inclusions, $item) unless grep { $_->id == $prod_id } @products;
+        }
+        foreach my $item (values %{$flagtype->exclusions}) {
+            my ($prod_id, $comp_id) = split(':', $item);
+            push(@exclusions, $item) unless grep { $_->id == $prod_id } @products;
+        }
+    }
+
+    if ($can_fully_edit) {
+        $flagtype->set_name($name);
+        $flagtype->set_description($description);
+        $flagtype->set_cc_list($cc_list);
+        $flagtype->set_sortkey($sortkey);
+        $flagtype->set_is_active($is_active);
+        $flagtype->set_is_requestable($is_requestable);
+        $flagtype->set_is_specifically_requestable($is_specifically);
+        $flagtype->set_is_multiplicable($is_multiplicable);
+        $flagtype->set_grant_group($grant_group);
+        $flagtype->set_request_group($request_group);
+    }
     $flagtype->set_clusions({ inclusions => \@inclusions, exclusions => \@exclusions})
       if $cgi->param('check_clusions');
     my $changes = $flagtype->update();
@@ -316,9 +372,9 @@ if ($action eq 'update') {
     $vars->{'changes'} = $changes;
     $vars->{'message'} = 'flag_type_updated';
 
-    my @flagtypes = Bugzilla::FlagType->get_all;
-    $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes];
-    $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @flagtypes];
+    my $flagtypes = get_editable_flagtypes(\@products);
+    $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes];
+    $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @$flagtypes];
 
     $template->process("admin/flag-type/list.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
@@ -326,7 +382,10 @@ if ($action eq 'update') {
 }
 
 if ($action eq 'confirmdelete') {
-    $vars->{'flag_type'} = Bugzilla::FlagType->check({ id => $flag_id });
+    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+    ThrowUserError('flag_type_cannot_delete', { flagtype => $flagtype }) unless $can_fully_edit;
+
+    $vars->{'flag_type'} = $flagtype;
     $vars->{'token'} = issue_session_token('delete_flagtype');
 
     $template->process("admin/flag-type/confirm-delete.html.tmpl", $vars)
@@ -337,7 +396,9 @@ if ($action eq 'confirmdelete') {
 if ($action eq 'delete') {
     check_token_data($token, 'delete_flagtype');
 
-    my $flagtype = Bugzilla::FlagType->check({ id => $flag_id });
+    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+    ThrowUserError('flag_type_cannot_delete', { flagtype => $flagtype }) unless $can_fully_edit;
+
     $flagtype->remove_from_db();
 
     delete_token($token);
@@ -357,7 +418,9 @@ if ($action eq 'delete') {
 if ($action eq 'deactivate') {
     check_token_data($token, 'delete_flagtype');
 
-    my $flagtype = Bugzilla::FlagType->check({ id => $flag_id });
+    my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
+    ThrowUserError('flag_type_cannot_deactivate', { flagtype => $flagtype }) unless $can_fully_edit;
+
     $flagtype->set_is_active(0);
     $flagtype->update();
 
@@ -383,8 +446,15 @@ ThrowUserError('unknown_action', {action => $action});
 
 sub get_products_and_components {
     my $vars = {};
+    my $user = Bugzilla->user;
 
-    my @products = Bugzilla::Product->get_all;
+    my @products;
+    if ($user->in_group('editcomponents')) {
+        @products = Bugzilla::Product->get_all;
+    }
+    else {
+        @products = @{$user->get_products_by_permission('editcomponents')};
+    }
     # We require all unique component names.
     my %components;
     foreach my $product (@products) {
@@ -397,6 +467,29 @@ sub get_products_and_components {
     return $vars;
 }
 
+sub get_editable_flagtypes {
+    my ($products, $group_id) = @_;
+    my $flagtypes;
+
+    if (Bugzilla->user->in_group('editcomponents')) {
+        $flagtypes = Bugzilla::FlagType::match({ group => $group_id });
+        return $flagtypes;
+    }
+
+    my %visible_flagtypes;
+    foreach my $product (@$products) {
+        foreach my $target ('bug', 'attachment') {
+            my $prod_flagtypes = $product->flag_types->{$target};
+            $visible_flagtypes{$_->id} ||= $_ foreach @$prod_flagtypes;
+        }
+    }
+    @$flagtypes = sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name }
+                    values %visible_flagtypes;
+    # Filter flag types if a group ID is given.
+    $flagtypes = filter_group($flagtypes, $group_id);
+    return $flagtypes;
+}
+
 sub filter_group {
     my ($flag_types, $gid) = @_;
     return $flag_types unless $gid;
@@ -410,24 +503,38 @@ sub filter_group {
 # Convert the array @clusions('prod_ID:comp_ID') back to a hash of
 # the form %clusions{'prod_name:comp_name'} = 'prod_ID:comp_ID'
 sub clusion_array_to_hash {
-    my $array = shift;
+    my ($array, $visible_products) = @_;
+    my $user = Bugzilla->user;
+    my $has_privs = $user->in_group('editcomponents');
+
     my %hash;
     my %products;
     my %components;
+
     foreach my $ids (@$array) {
-        trick_taint($ids);
         my ($product_id, $component_id) = split(":", $ids);
         my $product_name = "__Any__";
+        my $component_name = "__Any__";
+
         if ($product_id) {
-            $products{$product_id} ||= new Bugzilla::Product($product_id);
-            $product_name = $products{$product_id}->name if $products{$product_id};
+            ($products{$product_id}) = grep { $_->id == $product_id } @$visible_products;
+            next unless $products{$product_id};
+            $product_name = $products{$product_id}->name;
+
+            if ($component_id) {
+                ($components{$component_id}) =
+                  grep { $_->id == $component_id } @{$products{$product_id}->components};
+                next unless $components{$component_id};
+                $component_name = $components{$component_id}->name;
+            }
         }
-        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};
+        else {
+            # Users with local editcomponents privs cannot use __Any__:__Any__.
+            next unless $has_privs;
+            # It's illegal to select a component without a product.
+            next if $component_id;
         }
         $hash{"$product_name:$component_name"} = $ids;
     }
-    return %hash;
+    return \%hash;
 }
index b42b2224cfc31ef0a30208cc71edb55cc53a0955..3c93ec6fb2eb3c1745bd7cd1aa486b89b3fe6bcb 100644 (file)
@@ -99,10 +99,6 @@ tbody.file pre:empty {
   width: 3em;
 }
 
-.warning {
-  color: red
-}
-
 table.attachment_info th {
     text-align: right;
     vertical-align: top;
index 0deb4b94a1a4fe08cc8fbee0b778ad8a181db3f3..50f5d442b6e4b52e0ee54444aa88db49f5629079 100644 (file)
@@ -385,6 +385,10 @@ input.requestee {
     font-size: x-large;
 }
 
+.warning {
+  color: red;
+}
+
 .throw_error {
     background-color: #ff0000;
     color: black;
index 145360bfa147bbe837488c1bd3373db6030b04b2..98f729b0275efdce93501096fa7021889992bfef 100644 (file)
@@ -76,7 +76,8 @@
         <a href="editcomponents.cgi">components</a>, <a href="editversions.cgi">versions</a>
         and <a href="editmilestones.cgi">milestones</a> directly.</dd>
 
-        [% class = user.in_group('editcomponents') ? "" : "forbidden" %]
+        [% class = (user.in_group('editcomponents')
+                   || user.get_products_by_permission('editcomponents').size) ? "" : "forbidden" %]
         <dt id="flags" class="[% class %]"><a href="editflagtypes.cgi">Flags</a></dt>
         <dd class="[% class %]">A flag is a custom 4-states attribute of [% terms.bugs %]
         and/or attachments. These states are: granted, denied, requested and undefined.
index eb02113774664a7d8f3eb043cbefb0d1e67bdb7f..13db9fab1869f170e5a42f85e13634b398524ec7 100644 (file)
@@ -17,6 +17,7 @@
   #
   # Contributor(s): Myk Melez <myk@mozilla.org>
   #                 Mark Bickford <markhb@maine.rr.com>
+  #                 Frédéric Buclin <LpSolit@gmail.com>
   #%]
 
 [% PROCESS global/variables.none.tmpl %]
     table#form th { text-align: right; vertical-align: baseline; white-space: nowrap; }
     table#form td { text-align: left; vertical-align: baseline; }
   "
-  onload="var f = document.forms[0]; selectProduct(f.product, f.component, null, null, '__Any__');"
+  onload="var f = document.forms['flagtype_properties'];
+          selectProduct(f.product, f.component, null, null, '__Any__');"
   javascript_urls=["js/productform.js"]
   doc_section = doc_section
 %]
 
-<form method="post" action="editflagtypes.cgi">
+<form id="flagtype_properties" method="post" action="editflagtypes.cgi">
   <input type="hidden" name="action" value="[% action FILTER html %]">
+  <input type="hidden" name="can_fully_edit" value="[% can_fully_edit FILTER html %]">
   <input type="hidden" name="id" value="[% type.id %]">
   <input type="hidden" name="token" value="[% token FILTER html %]">
   <input type="hidden" name="target_type" value="[% type.target_type FILTER html %]">
   <input type="hidden" name="check_clusions" value="[% check_clusions FILTER none %]">
-  [% FOREACH category = type.inclusions %]
-    <input type="hidden" name="inclusions" value="[% category.value FILTER html %]">
+  [% FOREACH category = inclusions.values %]
+    <input type="hidden" name="inclusions" value="[% category FILTER html %]">
   [% END %]
-  [% FOREACH category = type.exclusions %]
-    <input type="hidden" name="exclusions" value="[% category.value FILTER html %]">
+  [% FOREACH category = exclusions.values %]
+    <input type="hidden" name="exclusions" value="[% category FILTER html %]">
   [% END %]
 
   [%# Add a hidden button at the top of the form so that the user pressing "return"
@@ -69,8 +72,8 @@
       <th>Name:</th>
       <td>
         a short name identifying this type.<br>
-        <input type="text" name="name" value="[% type.name FILTER html %]"
-               size="50" maxlength="50">
+        <input type="text" name="name" value="[% type.name FILTER html %]" size="50"
+               maxlength="50" [%- ' disabled="disabled"' UNLESS can_fully_edit %]>
       </td>
     </tr>
 
@@ -83,6 +86,7 @@
           minrows        = 4
           cols           = 80
           defaultcontent = type.description
+          disabled       = !can_fully_edit
         %]
       </td>
     </tr>
         the products/components to which [% type.target_type == "bug" ? terms.bugs : "attachments" %]
         must (inclusions) or must not (exclusions) belong in order for users
         to be able to set flags of this type for them.
+        [% UNLESS can_fully_edit %]
+          <p class="warning">This flagtype also applies to some products you are not allowed
+          to edit (and so which are not displayed in the lists below). Your limited privileges
+          means you are only allowed to add and remove this flagtype to/from products you can
+          edit, but not to edit other properties of the flagtype.</p>
+        [% END %]
         <table>
           <tr>
             <td style="vertical-align: top;">
               <select name="product" onchange="selectProduct(this, this.form.component, null, null, '__Any__');">
                 <option value="">__Any__</option>
                 [% FOREACH prod = products %]
-                  <option value="[% prod.name FILTER html %]" 
-                          [% "selected" IF type.product.name == prod.name %]>
-                          [% prod.name FILTER html %]</option>
+                  <option value="[% prod.name FILTER html %]">[% prod.name FILTER html %]</option>
                 [% END %]
               </select><br>
               <select name="component">
                 <option value="">__Any__</option>
                 [% FOREACH comp = components %]
-                  <option value="[% comp FILTER html %]" 
-                          [% "selected" IF type.component.name == comp %]>
-                          [% comp FILTER html %]</option>
+                  <option value="[% comp FILTER html %]">[% comp FILTER html %]</option>
                 [% END %]
               </select><br>
               <input type="submit" name="categoryAction-include" value="Include">
             </td>
             <td style="vertical-align: top;">
               <b>Inclusions:</b><br>
-              [% PROCESS "global/select-menu.html.tmpl" name="inclusion_to_remove" multiple="1" size="7" options=type.inclusions %]<br>
+              [% PROCESS category_select name="inclusion_to_remove" categories = inclusions %]<br>
               <input type="submit" name="categoryAction-removeInclusion" value="Remove Inclusion">
             </td>
             <td style="vertical-align: top;">
               <b>Exclusions:</b><br>
-              [% PROCESS "global/select-menu.html.tmpl" name="exclusion_to_remove" multiple="1" size="7" options=type.exclusions %]<br>
+              [% PROCESS category_select name="exclusion_to_remove" categories = exclusions %]<br>
               <input type="submit" name="categoryAction-removeExclusion" value="Remove Exclusion">
             </td>
           </tr>
         this type will be sorted when displayed to users in a list; ignore if you
         don't care what order the types appear in or if you want them to appear
         in alphabetical order.<br>
-        <input type="text" name="sortkey" value="[% type.sortkey || 1 %]" size="5" maxlength="5">
+        <input type="text" name="sortkey" value="[% type.sortkey || 1 %]" size="5" maxlength="5"
+               [%- ' disabled="disabled"' UNLESS can_fully_edit %]>
       </td>
     </tr>
 
       <th>&nbsp;</th>
       <td>
         <input type="checkbox" id="is_active" name="is_active"
+               [%- ' disabled="disabled"' UNLESS can_fully_edit %]
                [% " checked" IF type.is_active || !type.is_active.defined %]>
         <label for="is_active">active (flags of this type appear in the UI and can be set)</label>
       </td>
       <th>&nbsp;</th>
       <td>
         <input type="checkbox" id="is_requestable" name="is_requestable"
+               [%- ' disabled="disabled"' UNLESS can_fully_edit %]
                [% " checked" IF type.is_requestable || !type.is_requestable.defined %]>
         <label for="is_requestable">requestable (users can ask for flags of this type to be set)</label>
       </td>
           <kbd>[% Param('emailsuffix') %]</kbd> will <em>not</em> be appended
           to these addresses, so you should add it explicitly if so desired.
         [% END %]<br>
-        <input type="text" name="cc_list" value="[% type.cc_list FILTER html %]" size="80" maxlength="200">
+        <input type="text" name="cc_list" value="[% type.cc_list FILTER html %]" size="80"
+               maxlength="200" [%- ' disabled="disabled"' UNLESS can_fully_edit %]>
       </td>
     </tr>
 
       <th>&nbsp;</th>
       <td>
         <input type="checkbox" id="is_requesteeble" name="is_requesteeble"
+               [%- ' disabled="disabled"' UNLESS can_fully_edit %]
                [% " checked" IF type.is_requesteeble || !type.is_requesteeble.defined %]>
         <label for="is_requesteeble">specifically requestable (users can ask specific other users
           to set flags of this type as opposed to just asking the wind)</label>
       <th>&nbsp;</th>
       <td>
         <input type="checkbox" id="is_multiplicable" name="is_multiplicable"
+               [%- ' disabled="disabled"' UNLESS can_fully_edit %]
                [% " checked" IF type.is_multiplicable || !type.is_multiplicable.defined %]>
         <label for="is_multiplicable">multiplicable (multiple flags of this type can be set on
           the same [% type.target_type == "bug" ? terms.bug : "attachment" %])</label>
       <td>
         the group allowed to grant/deny flags of this type
         (to allow all users to grant/deny these flags, select no group).<br>
-        [% PROCESS select selname = "grant_group" %]
+        [% PROCESS group_select selname = "grant_group" %]
       </td>
     </tr>
 
         if flags of this type are requestable, the group allowed to request them
         (to allow all users to request these flags, select no group).<br>
         Note that the request group alone has no effect if the grant group is not defined!<br>
-        [% PROCESS select selname = "request_group" %]
+        [% PROCESS group_select selname = "request_group" %]
       </td>
     </tr>
 
 [%# Block for SELECT fields                                                  #%]
 [%############################################################################%]
 
-[% BLOCK select %]
-  <select name="[% selname %]" id="[% selname %]">
+[% BLOCK group_select %]
+  <select name="[% selname %]" id="[% selname %]" [%- ' disabled="disabled"' UNLESS can_fully_edit %]>
     <option value="">(no group)</option>
     [% FOREACH group = groups %]
       <option value="[% group.name FILTER html %]"
     [% END %]
   </select>
 [% END %]
+
+[% BLOCK category_select %]
+  <select name="[% name FILTER html %]" multiple="multiple" size="7">
+    [% FOREACH option = categories.keys.sort %]
+      <option value="[% categories.$option FILTER html %]">
+        [% option FILTER html %]
+      </option>
+    [% END %]
+  </select>
+[% END %]
\ No newline at end of file
index 5aa8955fd1449727560880d2311bee5c330ffa96..410636c604fd438de2c3fa0df9cbd8c47a91d077 100644 (file)
     [% END %]
     is invalid.
 
+  [% ELSIF error == "flag_type_cannot_deactivate" %]
+    [% title = "Cannot Deactivate Flag Type" %]
+    Sorry, but the flag type '[% flagtype.name FILTER html %]' also applies to some
+    products you cannot see, and so you are not allowed to deactivate it.
+
+  [% ELSIF error == "flag_type_cannot_delete" %]
+    [% title = "Flag Type Deletion Not Allowed" %]
+    Sorry, but the flag type '[% flagtype.name FILTER html %]' also applies to some
+    products you cannot see, and so you are not allowed to delete it.
+
   [% ELSIF error == "flag_type_cc_list_invalid" %]
     [% title = "Flag Type CC List Invalid" %]
     [% admindocslinks = {'flags-overview.html#flags-admin'  => 'Administering Flags'} %]
     The name <em>[% name FILTER html %]</em> must be 1-50 characters long
     and must not contain any spaces or commas.
 
+  [% ELSIF error == "flag_type_not_editable" %]
+    [% title = "Flag Type Not Editable" %]
+    You are not allowed to edit properties of the '[% flagtype.name FILTER html %]'
+    flag type, because this flag type is not available for the products you can administer.
+
   [% ELSIF error == "flag_type_not_multiplicable" %]
     [% docslinks = {'flags-overview.html' => 'An overview on Flags',
                     'flags.html' => 'Using Flags'} %]
     [%+ constants.USER_PASSWORD_MIN_LENGTH FILTER html %] characters long.
 
   [% ELSIF error == "product_access_denied" %]
+    [% title = "Product Access Denied" %]
     Either the product 
     [%+ IF id.defined %]
       with the id [% id FILTER html %]
                          'versions.html' => 'Administering versions'} %]
     You must enter a valid version to create a new product.
 
+  [% ELSIF error == "product_unknown_component" %]
+    [% title = "Unknown Component" %]
+    Product '[% product FILTER html %]' has no component
+    [% IF comp_id %]
+      with ID [% comp_id FILTER html %].
+    [% ELSE %]
+      named '[% comp FILTER html %]'.
+    [% END %]
+
   [% ELSIF error == "query_name_exists" %]
     [% title = "Search Name Already In Use" %]
     The name <em>[% name FILTER html %]</em> is already used by another