1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4#
5# This Source Code Form is "Incompatible With Secondary Licenses", as
6# defined by the Mozilla Public License, v. 2.0.
7
8use strict;
9
10package Bugzilla::Group;
11
12use base qw(Bugzilla::Object);
13
14use Bugzilla::Constants;
15use Bugzilla::Util;
16use Bugzilla::Error;
17use Bugzilla::Config qw(:admin);
18
19###############################
20##### Module Initialization ###
21###############################
22
23use constant DB_COLUMNS => qw(
24    groups.id
25    groups.name
26    groups.description
27    groups.isbuggroup
28    groups.userregexp
29    groups.isactive
30    groups.icon_url
31);
32
33use constant DB_TABLE => 'groups';
34
35use constant LIST_ORDER => 'isbuggroup, name';
36
37use constant VALIDATORS => {
38    name        => \&_check_name,
39    description => \&_check_description,
40    userregexp  => \&_check_user_regexp,
41    isactive    => \&_check_is_active,
42    isbuggroup  => \&_check_is_bug_group,
43    icon_url    => \&_check_icon_url,
44};
45
46use constant UPDATE_COLUMNS => qw(
47    name
48    description
49    userregexp
50    isactive
51    icon_url
52);
53
54# Parameters that are lists of groups.
55use constant GROUP_PARAMS => qw(chartgroup insidergroup timetrackinggroup
56                                querysharegroup debug_group);
57
58###############################
59####      Accessors      ######
60###############################
61
62sub description  { return $_[0]->{'description'};  }
63sub is_bug_group { return $_[0]->{'isbuggroup'};   }
64sub user_regexp  { return $_[0]->{'userregexp'};   }
65sub is_active    { return $_[0]->{'isactive'};     }
66sub icon_url     { return $_[0]->{'icon_url'};     }
67
68sub bugs {
69    my $self = shift;
70    return $self->{bugs} if exists $self->{bugs};
71    my $bug_ids = Bugzilla->dbh->selectcol_arrayref(
72        'SELECT bug_id FROM bug_group_map WHERE group_id = ?',
73        undef, $self->id);
74    require Bugzilla::Bug;
75    $self->{bugs} = Bugzilla::Bug->new_from_list($bug_ids);
76    return $self->{bugs};
77}
78
79sub members_direct {
80    my ($self) = @_;
81    $self->{members_direct} ||= $self->_get_members(GRANT_DIRECT);
82    return $self->{members_direct};
83}
84
85sub members_non_inherited {
86    my ($self) = @_;
87    $self->{members_non_inherited} ||= $self->_get_members();
88    return $self->{members_non_inherited};
89}
90
91# A helper for members_direct and members_non_inherited
92sub _get_members {
93    my ($self, $grant_type) = @_;
94    my $dbh = Bugzilla->dbh;
95    my $grant_clause = $grant_type ? "AND grant_type = $grant_type" : "";
96    my $user_ids = $dbh->selectcol_arrayref(
97        "SELECT DISTINCT user_id
98           FROM user_group_map
99          WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id);
100    require Bugzilla::User;
101    return Bugzilla::User->new_from_list($user_ids);
102}
103
104sub flag_types {
105    my $self = shift;
106    require Bugzilla::FlagType;
107    $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id });
108    return $self->{flag_types};
109}
110
111sub grant_direct {
112    my ($self, $type) = @_;
113    $self->{grant_direct} ||= {};
114    return $self->{grant_direct}->{$type}
115        if defined $self->{grant_direct}->{$type};
116    my $dbh = Bugzilla->dbh;
117
118    my $ids = $dbh->selectcol_arrayref(
119      "SELECT member_id FROM group_group_map
120        WHERE grantor_id = ? AND grant_type = $type",
121      undef, $self->id) || [];
122
123    $self->{grant_direct}->{$type} = $self->new_from_list($ids);
124    return $self->{grant_direct}->{$type};
125}
126
127sub granted_by_direct {
128    my ($self, $type) = @_;
129    $self->{granted_by_direct} ||= {};
130    return $self->{granted_by_direct}->{$type}
131         if defined $self->{granted_by_direct}->{$type};
132    my $dbh = Bugzilla->dbh;
133
134    my $ids = $dbh->selectcol_arrayref(
135      "SELECT grantor_id FROM group_group_map
136        WHERE member_id = ? AND grant_type = $type",
137      undef, $self->id) || [];
138
139    $self->{granted_by_direct}->{$type} = $self->new_from_list($ids);
140    return $self->{granted_by_direct}->{$type};
141}
142
143sub products {
144    my $self = shift;
145    return $self->{products} if exists $self->{products};
146    my $product_data = Bugzilla->dbh->selectall_arrayref(
147        'SELECT product_id, entry, membercontrol, othercontrol,
148                canedit, editcomponents, editbugs, canconfirm
149          FROM  group_control_map WHERE group_id = ?', {Slice=>{}},
150        $self->id);
151    my @ids = map { $_->{product_id} } @$product_data;
152    require Bugzilla::Product;
153    my $products = Bugzilla::Product->new_from_list(\@ids);
154    my %data_map = map { $_->{product_id} => $_ } @$product_data;
155    my @retval;
156    foreach my $product (@$products) {
157        # Data doesn't need to contain product_id--we already have
158        # the product object.
159        delete $data_map{$product->id}->{product_id};
160        push(@retval, { controls => $data_map{$product->id},
161                        product  => $product });
162    }
163    $self->{products} = \@retval;
164    return $self->{products};
165}
166
167###############################
168####        Methods        ####
169###############################
170
171sub check_members_are_visible {
172    my $self = shift;
173    my $user = Bugzilla->user;
174    return if !Bugzilla->params->{'usevisibilitygroups'};
175
176    my $group_id = $self->id;
177    my $is_visible = grep { $_ == $group_id } @{ $user->visible_groups_inherited };
178    if (!$is_visible) {
179        ThrowUserError('group_not_visible', { group => $self });
180    }
181}
182
183sub set_description { $_[0]->set('description', $_[1]); }
184sub set_is_active   { $_[0]->set('isactive', $_[1]);    }
185sub set_name        { $_[0]->set('name', $_[1]);        }
186sub set_user_regexp { $_[0]->set('userregexp', $_[1]);  }
187sub set_icon_url    { $_[0]->set('icon_url', $_[1]);    }
188
189sub update {
190    my $self = shift;
191    my $dbh = Bugzilla->dbh;
192    $dbh->bz_start_transaction();
193    my $changes = $self->SUPER::update(@_);
194
195    if (exists $changes->{name}) {
196        my ($old_name, $new_name) = @{$changes->{name}};
197        my $update_params;
198        foreach my $group (GROUP_PARAMS) {
199            if ($old_name eq Bugzilla->params->{$group}) {
200                SetParam($group, $new_name);
201                $update_params = 1;
202            }
203        }
204        write_params() if $update_params;
205    }
206
207    # If we've changed this group to be active, fix any Mandatory groups.
208    $self->_enforce_mandatory if (exists $changes->{isactive}
209                                  && $changes->{isactive}->[1]);
210
211    $self->_rederive_regexp() if exists $changes->{userregexp};
212
213    Bugzilla::Hook::process('group_end_of_update',
214                            { group => $self, changes => $changes });
215    $dbh->bz_commit_transaction();
216    return $changes;
217}
218
219sub check_remove {
220    my ($self, $params) = @_;
221
222    # System groups cannot be deleted!
223    if (!$self->is_bug_group) {
224        ThrowUserError("system_group_not_deletable", { name => $self->name });
225    }
226
227    # Groups having a special role cannot be deleted.
228    my @special_groups;
229    foreach my $special_group (GROUP_PARAMS) {
230        if ($self->name eq Bugzilla->params->{$special_group}) {
231            push(@special_groups, $special_group);
232        }
233    }
234    if (scalar(@special_groups)) {
235        ThrowUserError('group_has_special_role',
236                       { name   => $self->name,
237                         groups => \@special_groups });
238    }
239
240    return if $params->{'test_only'};
241
242    my $cantdelete = 0;
243
244    my $users = $self->members_non_inherited;
245    if (scalar(@$users) && !$params->{'remove_from_users'}) {
246        $cantdelete = 1;
247    }
248
249    my $bugs = $self->bugs;
250    if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) {
251        $cantdelete = 1;
252    }
253
254    my $products = $self->products;
255    if (scalar(@$products) && !$params->{'remove_from_products'}) {
256        $cantdelete = 1;
257    }
258
259    my $flag_types = $self->flag_types;
260    if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) {
261        $cantdelete = 1;
262    }
263
264    ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete;
265}
266
267sub remove_from_db {
268    my $self = shift;
269    my $dbh = Bugzilla->dbh;
270    $self->check_remove(@_);
271    $dbh->bz_start_transaction();
272    Bugzilla::Hook::process('group_before_delete', { group => $self });
273    $dbh->do('DELETE FROM whine_schedules
274               WHERE mailto_type = ? AND mailto = ?',
275              undef, MAILTO_GROUP, $self->id);
276    # All the other tables will be handled by foreign keys when we
277    # drop the main "groups" row.
278    $self->SUPER::remove_from_db(@_);
279    $dbh->bz_commit_transaction();
280}
281
282# Add missing entries in bug_group_map for bugs created while
283# a mandatory group was disabled and which is now enabled again.
284sub _enforce_mandatory {
285    my ($self) = @_;
286    my $dbh = Bugzilla->dbh;
287    my $gid = $self->id;
288
289    my $bug_ids =
290      $dbh->selectcol_arrayref('SELECT bugs.bug_id
291                                  FROM bugs
292                            INNER JOIN group_control_map
293                                    ON group_control_map.product_id = bugs.product_id
294                             LEFT JOIN bug_group_map
295                                    ON bug_group_map.bug_id = bugs.bug_id
296                                   AND bug_group_map.group_id = group_control_map.group_id
297                                 WHERE group_control_map.group_id = ?
298                                   AND group_control_map.membercontrol = ?
299                                   AND bug_group_map.group_id IS NULL',
300                                 undef, ($gid, CONTROLMAPMANDATORY));
301
302    my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)');
303    foreach my $bug_id (@$bug_ids) {
304        $sth->execute($bug_id, $gid);
305    }
306}
307
308sub is_active_bug_group {
309    my $self = shift;
310    return $self->is_active && $self->is_bug_group;
311}
312
313sub _rederive_regexp {
314    my ($self) = @_;
315
316    my $dbh = Bugzilla->dbh;
317    my $sth = $dbh->prepare("SELECT userid, login_name, group_id
318                               FROM profiles
319                          LEFT JOIN user_group_map
320                                 ON user_group_map.user_id = profiles.userid
321                                    AND group_id = ?
322                                    AND grant_type = ?
323                                    AND isbless = 0");
324    my $sthadd = $dbh->prepare("INSERT INTO user_group_map
325                                 (user_id, group_id, grant_type, isbless)
326                                 VALUES (?, ?, ?, 0)");
327    my $sthdel = $dbh->prepare("DELETE FROM user_group_map
328                                 WHERE user_id = ? AND group_id = ?
329                                 AND grant_type = ? and isbless = 0");
330    $sth->execute($self->id, GRANT_REGEXP);
331    my $regexp = $self->user_regexp;
332    while (my ($uid, $login, $present) = $sth->fetchrow_array) {
333        if ($regexp ne '' and $login =~ /$regexp/i) {
334            $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present;
335        } else {
336            $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present;
337        }
338    }
339}
340
341sub flatten_group_membership {
342    my ($self, @groups) = @_;
343
344    my $dbh = Bugzilla->dbh;
345    my $sth;
346    my @groupidstocheck = @groups;
347    my %groupidschecked = ();
348    $sth = $dbh->prepare("SELECT member_id FROM group_group_map
349                             WHERE grantor_id = ?
350                               AND grant_type = " . GROUP_MEMBERSHIP);
351    while (my $node = shift @groupidstocheck) {
352        $sth->execute($node);
353        my $member;
354        while (($member) = $sth->fetchrow_array) {
355            if (!$groupidschecked{$member}) {
356                $groupidschecked{$member} = 1;
357                push @groupidstocheck, $member;
358                push @groups, $member unless grep $_ == $member, @groups;
359            }
360        }
361    }
362    return \@groups;
363}
364
365
366
367
368################################
369#####  Module Subroutines    ###
370################################
371
372sub create {
373    my $class = shift;
374    my ($params) = @_;
375    my $dbh = Bugzilla->dbh;
376
377    my $silently = delete $params->{silently};
378    if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) {
379        print get_text('install_group_create', { name => $params->{name} }),
380              "\n";
381    }
382
383    $dbh->bz_start_transaction();
384
385    my $group = $class->SUPER::create(@_);
386
387    # Since we created a new group, give the "admin" group all privileges
388    # initially.
389    my $admin = new Bugzilla::Group({name => 'admin'});
390    # This function is also used to create the "admin" group itself,
391    # so there's a chance it won't exist yet.
392    if ($admin) {
393        my $sth = $dbh->prepare('INSERT INTO group_group_map
394                                 (member_id, grantor_id, grant_type)
395                                 VALUES (?, ?, ?)');
396        $sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP);
397        $sth->execute($admin->id, $group->id, GROUP_BLESS);
398        $sth->execute($admin->id, $group->id, GROUP_VISIBLE);
399    }
400
401    $group->_rederive_regexp() if $group->user_regexp;
402
403    Bugzilla::Hook::process('group_end_of_create', { group => $group });
404    $dbh->bz_commit_transaction();
405    return $group;
406}
407
408sub ValidateGroupName {
409    my ($name, @users) = (@_);
410    my $dbh = Bugzilla->dbh;
411    my $query = "SELECT id FROM groups " .
412                "WHERE name = ?";
413    if (Bugzilla->params->{'usevisibilitygroups'}) {
414        my @visible = (-1);
415        foreach my $user (@users) {
416            $user && push @visible, @{$user->visible_groups_direct};
417        }
418        my $visible = join(', ', @visible);
419        $query .= " AND id IN($visible)";
420    }
421    my $sth = $dbh->prepare($query);
422    $sth->execute($name);
423    my ($ret) = $sth->fetchrow_array();
424    return $ret;
425}
426
427sub check_no_disclose {
428    my ($class, $params) = @_;
429    my $action = delete $params->{action};
430
431    $action =~ /^(?:add|remove)$/
432      or ThrowCodeError('bad_arg', { argument => $action,
433                                     function => "${class}::check_no_disclose" });
434
435    $params->{_error} = ($action eq 'add') ? 'group_restriction_not_allowed'
436                                           : 'group_invalid_removal';
437
438    my $group = $class->check($params);
439    return $group;
440}
441
442###############################
443###       Validators        ###
444###############################
445
446sub _check_name {
447    my ($invocant, $name) = @_;
448    $name = trim($name);
449    $name || ThrowUserError("empty_group_name");
450    # If we're creating a Group or changing the name...
451    if (!ref($invocant) || lc($invocant->name) ne lc($name)) {
452        my $exists = new Bugzilla::Group({name => $name });
453        ThrowUserError("group_exists", { name => $name }) if $exists;
454    }
455    return $name;
456}
457
458sub _check_description {
459    my ($invocant, $desc) = @_;
460    $desc = trim($desc);
461    $desc || ThrowUserError("empty_group_description");
462    return $desc;
463}
464
465sub _check_user_regexp {
466    my ($invocant, $regex) = @_;
467    $regex = trim($regex) || '';
468    ThrowUserError("invalid_regexp") unless (eval {qr/$regex/});
469    return $regex;
470}
471
472sub _check_is_active { return $_[1] ? 1 : 0; }
473sub _check_is_bug_group {
474    return $_[1] ? 1 : 0;
475}
476
477sub _check_icon_url { return $_[1] ? clean_text($_[1]) : undef; }
478
4791;
480
481__END__
482
483=head1 NAME
484
485Bugzilla::Group - Bugzilla group class.
486
487=head1 SYNOPSIS
488
489    use Bugzilla::Group;
490
491    my $group = new Bugzilla::Group(1);
492    my $group = new Bugzilla::Group({name => 'AcmeGroup'});
493
494    my $id           = $group->id;
495    my $name         = $group->name;
496    my $description  = $group->description;
497    my $user_reg_exp = $group->user_reg_exp;
498    my $is_active    = $group->is_active;
499    my $icon_url     = $group->icon_url;
500    my $is_active_bug_group = $group->is_active_bug_group;
501
502    my $group_id = Bugzilla::Group::ValidateGroupName('admin', @users);
503    my @groups   = Bugzilla::Group->get_all;
504
505=head1 DESCRIPTION
506
507Group.pm represents a Bugzilla Group object. It is an implementation
508of L<Bugzilla::Object>, and thus has all the methods that L<Bugzilla::Object>
509provides, in addition to any methods documented below.
510
511=head1 SUBROUTINES
512
513=over
514
515=item C<create>
516
517Note that in addition to what L<Bugzilla::Object/create($params)>
518normally does, this function also makes the new group be inherited
519by the C<admin> group. That is, the C<admin> group will automatically
520be a member of this group.
521
522=item C<ValidateGroupName($name, @users)>
523
524Description: ValidateGroupName checks to see if ANY of the users
525             in the provided list of user objects can see the
526             named group.
527
528Params:      $name - String with the group name.
529             @users - An array with Bugzilla::User objects.
530
531Returns:     It returns the group id if successful
532             and undef otherwise.
533
534=back
535
536
537=head1 METHODS
538
539=over
540
541=item C<check_no_disclose>
542
543=over
544
545=item B<Description>
546
547Throws an error if the user cannot add or remove this group to/from a given
548bug, but doesn't specify if this is because the group doesn't exist, or the
549user is not allowed to edit this group restriction.
550
551=item B<Params>
552
553This method takes a single hashref as argument, with the following keys:
554
555=over
556
557=item C<name>
558
559C<string> The name of the group to add or remove.
560
561=item C<bug_id>
562
563C<integer> The ID of the bug to which the group change applies.
564
565=item C<product>
566
567C<string> The name of the product the bug belongs to.
568
569=item C<action>
570
571C<string> Must be either C<add> or C<remove>, depending on whether the group
572must be added or removed from the bug. Any other value will generate an error.
573
574=back
575
576=item C<Returns>
577
578A C<Bugzilla::Group> object on success, else an error is thrown.
579
580=back
581
582=item C<check_members_are_visible>
583
584Throws an error if this group is not visible (according to
585visibility groups) to the currently-logged-in user.
586
587=item C<check_remove>
588
589=over
590
591=item B<Description>
592
593Determines whether it's OK to remove this group from the database, and
594throws an error if it's not OK.
595
596=item B<Params>
597
598=over
599
600=item C<test_only>
601
602C<boolean> If you want to only check if the group can be deleted I<at all>,
603under any circumstances, specify C<test_only> to just do the most basic tests
604(the other parameters will be ignored in this situation, as those tests won't
605be run).
606
607=item C<remove_from_users>
608
609C<boolean> True if it would be OK to remove all users who are in this group
610from this group.
611
612=item C<remove_from_bugs>
613
614C<boolean> True if it would be OK to remove all bugs that are in this group
615from this group.
616
617=item C<remove_from_flags>
618
619C<boolean> True if it would be OK to stop all flagtypes that reference
620this group from referencing this group (e.g., as their grantgroup or
621requestgroup).
622
623=item C<remove_from_products>
624
625C<boolean> True if it would be OK to remove this group from all group controls
626on products.
627
628=back
629
630=item B<Returns> (nothing)
631
632=back
633
634=item C<members_non_inherited>
635
636Returns an arrayref of L<Bugzilla::User> objects representing people who are
637"directly" in this group, meaning that they're in it because they match
638the group regular expression, or they have been actually added to the
639group manually.
640
641=item C<flatten_group_membership>
642
643Accepts a list of groups and returns a list of all the groups whose members
644inherit membership in any group on the list.  So, we can determine if a user
645is in any of the groups input to flatten_group_membership by querying the
646user_group_map for any user with DIRECT or REGEXP membership IN() the list
647of groups returned.
648
649=back
650