Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / Shredder / Plugin / Users.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::Shredder::Plugin::Users;
50
51 use strict;
52 use warnings FATAL => 'all';
53 use base qw(RT::Shredder::Plugin::Base::Search);
54
55 =head1 NAME
56
57 RT::Shredder::Plugin::Users - search plugin for wiping users.
58
59 =head1 ARGUMENTS
60
61 =head2 status - string
62
63 Status argument allow you to limit result set to C<disabled>,
64 C<enabled> or C<any> users.
65 B<< Default value is C<disabled>. >>
66
67 =head2 name - mask
68
69 User name mask.
70
71 =head2 email - mask
72
73 Email address mask.
74
75 =head2 member_of - group identifier
76
77 Using this option users that are members of a particular group can
78 be selected for deletion. Identifier is name of user defined group
79 or id of a group, as well C<Privileged> or <unprivileged> can used
80 to select people from system groups.
81
82 =head2 not_member_of - group identifier
83
84 Like member_of, but selects users who are not members of the provided
85 group.
86
87 =head2 replace_relations - user identifier
88
89 When you delete a user there could be minor links to them in the RT database.
90 This option allow you to replace these links with links to the new user.
91 The replaceable links are Creator and LastUpdatedBy, but NOT any watcher roles.
92 This means that if the user is a watcher(Requestor, Owner,
93 Cc or AdminCc) of the ticket or queue then the link would be deleted.
94
95 This argument could be a user id or name.
96
97 =head2 no_tickets - boolean
98
99 If true then plugin looks for users who are not watchers (Owners,
100 Requestors, Ccs or AdminCcs) of any ticket.
101
102 Before RT 3.8.5, users who were watchers of deleted tickets B<will be deleted>
103 when this option was enabled. Decision has been made that it's not correct
104 and you should either shred these deleted tickets, change watchers or
105 explicitly delete user by name or email.
106
107 Note that found users still B<may have relations> with other objects,
108 for example via Creator or LastUpdatedBy fields, and you most probably
109 want to use C<replace_relations> option.
110
111 =cut
112
113 sub SupportArgs
114 {
115     return $_[0]->SUPER::SupportArgs,
116            qw(status name email member_of not_member_of replace_relations no_tickets);
117 }
118
119 sub TestArgs
120 {
121     my $self = shift;
122     my %args = @_;
123     if( $args{'status'} ) {
124         unless( $args{'status'} =~ /^(disabled|enabled|any)$/i ) {
125             return (0, "Status '$args{'status'}' is unsupported.");
126         }
127     } else {
128         $args{'status'} = 'disabled';
129     }
130     if( $args{'email'} ) {
131         $args{'email'} = $self->ConvertMaskToSQL( $args{'email'} );
132     }
133     if( $args{'name'} ) {
134         $args{'name'} = $self->ConvertMaskToSQL( $args{'name'} );
135     }
136     if( $args{'member_of'} or $args{'not_member_of'} ) {
137         foreach my $group_option ( qw(member_of not_member_of) ){
138             next unless $args{$group_option};
139
140             my $group = RT::Group->new( RT->SystemUser );
141             if ( $args{$group_option} =~ /^(Everyone|Privileged|Unprivileged)$/i ) {
142                 $group->LoadSystemInternalGroup( $args{$group_option} );
143             }
144             else {
145                 $group->LoadUserDefinedGroup( $args{$group_option} );
146             }
147             unless ( $group->id ) {
148                 return (0, "Couldn't load group '$args{$group_option}'" );
149             }
150             $args{$group_option} = $group->id;
151         }
152     }
153     if( $args{'replace_relations'} ) {
154         my $uid = $args{'replace_relations'};
155         # XXX: it's possible that SystemUser is not available
156         my $user = RT::User->new( RT->SystemUser );
157         $user->Load( $uid );
158         unless( $user->id ) {
159             return (0, "Couldn't load user '$uid'" );
160         }
161         $args{'replace_relations'} = $user->id;
162     }
163     return $self->SUPER::TestArgs( %args );
164 }
165
166 sub Run
167 {
168     my $self = shift;
169     my %args = ( Shredder => undef, @_ );
170     my $objs = RT::Users->new( RT->SystemUser );
171     # XXX: we want preload only things we need, but later while
172     # logging we need all data, TODO envestigate this
173     # $objs->Columns(qw(id Name EmailAddress Lang Timezone
174     #                   Creator Created LastUpdated LastUpdatedBy));
175     if( my $s = $self->{'opt'}{'status'} ) {
176         if( $s eq 'any' ) {
177             $objs->FindAllRows;
178         } elsif( $s eq 'disabled' ) {
179             $objs->LimitToDeleted;
180         } else {
181             $objs->LimitToEnabled;
182         }
183     }
184     if( $self->{'opt'}{'email'} ) {
185         $objs->Limit( FIELD => 'EmailAddress',
186                   OPERATOR => 'MATCHES',
187                   VALUE => $self->{'opt'}{'email'},
188                 );
189     }
190     if( $self->{'opt'}{'name'} ) {
191         $objs->Limit( FIELD => 'Name',
192                   OPERATOR => 'MATCHES',
193                   VALUE => $self->{'opt'}{'name'},
194                   CASESENSITIVE => 0,
195                 );
196     }
197     if( $self->{'opt'}{'member_of'} ) {
198         $objs->MemberOfGroup( $self->{'opt'}{'member_of'} );
199     }
200     my @filter;
201     if( $self->{'opt'}{'not_member_of'} ) {
202         push @filter, $self->FilterNotMemberOfGroup(
203             Shredder => $args{'Shredder'},
204             GroupId  => $self->{'opt'}{'not_member_of'},
205         );
206     }
207     if( $self->{'opt'}{'no_tickets'} ) {
208         push @filter, $self->FilterWithoutTickets(
209             Shredder => $args{'Shredder'},
210         );
211     }
212
213     if (@filter) {
214         $self->FetchNext( $objs, 'init' );
215         my @res;
216         USER: while ( my $user = $self->FetchNext( $objs ) ) {
217             for my $filter (@filter) {
218                 next USER unless $filter->($user);
219             }
220             push @res, $user;
221             last if $self->{'opt'}{'limit'} && @res >= $self->{'opt'}{'limit'};
222         }
223         $objs = \@res;
224     } elsif ( $self->{'opt'}{'limit'} ) {
225         $objs->RowsPerPage( $self->{'opt'}{'limit'} );
226     }
227     return (1, $objs);
228 }
229
230 sub SetResolvers
231 {
232     my $self = shift;
233     my %args = ( Shredder => undef, @_ );
234
235     if( $self->{'opt'}{'replace_relations'} ) {
236         my $uid = $self->{'opt'}{'replace_relations'};
237         my $resolver = sub {
238             my %args = (@_);
239             my $t =    $args{'TargetObject'};
240             foreach my $method ( qw(Creator LastUpdatedBy) ) {
241                 next unless $t->_Accessible( $method => 'read' );
242                 $t->__Set( Field => $method, Value => $uid );
243             }
244         };
245         $args{'Shredder'}->PutResolver( BaseClass => 'RT::User', Code => $resolver );
246     }
247     return (1);
248 }
249
250 sub FilterNotMemberOfGroup {
251     my $self = shift;
252     my %args = (
253         Shredder => undef,
254         GroupId  => undef,
255         @_,
256     );
257
258     my $group = RT::Group->new(RT->SystemUser);
259     $group->Load($args{'GroupId'});
260
261     return sub {
262         my $user = shift;
263         not $group->HasMemberRecursively($user->id);
264     };
265 }
266
267 sub FilterWithoutTickets {
268     my $self = shift;
269     my %args = (
270         Shredder => undef,
271         Objects  => undef,
272         @_,
273     );
274
275     return sub {
276         my $user = shift;
277         $self->_WithoutTickets( $user )
278     };
279 }
280
281 sub _WithoutTickets {
282     my ($self, $user) = @_;
283     return unless $user and $user->Id;
284     my $tickets = RT::Tickets->new( RT->SystemUser );
285     $tickets->{'allow_deleted_search'} = 1;
286     $tickets->FromSQL( 'Watcher.id = '. $user->id );
287     # HACK: we may use Count method which counts all records
288     # that match condtion, but we really want to know only that
289     # at least one record exist, so we fetch first row only
290     $tickets->RowsPerPage(1);
291     return !$tickets->First;
292 }
293
294 1;