rt 4.2.16
[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-2019 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 =head2 no_ticket_transactions - boolean
112
113 If true then plugin looks for users who have created no ticket transactions.
114 This is especially useful after wiping out tickets.
115
116 Note that found users still B<may have relations> with other objects,
117 for example via Creator or LastUpdatedBy fields, and you most probably
118 want to use C<replace_relations> option.
119
120 =cut
121
122 sub SupportArgs
123 {
124     return $_[0]->SUPER::SupportArgs,
125            qw(status name email member_of not_member_of replace_relations no_tickets no_ticket_transactions);
126 }
127
128 sub TestArgs
129 {
130     my $self = shift;
131     my %args = @_;
132     if( $args{'status'} ) {
133         unless( $args{'status'} =~ /^(disabled|enabled|any)$/i ) {
134             return (0, "Status '$args{'status'}' is unsupported.");
135         }
136     } else {
137         $args{'status'} = 'disabled';
138     }
139     if( $args{'email'} ) {
140         $args{'email'} = $self->ConvertMaskToSQL( $args{'email'} );
141     }
142     if( $args{'name'} ) {
143         $args{'name'} = $self->ConvertMaskToSQL( $args{'name'} );
144     }
145     if( $args{'member_of'} or $args{'not_member_of'} ) {
146         foreach my $group_option ( qw(member_of not_member_of) ){
147             next unless $args{$group_option};
148
149             my $group = RT::Group->new( RT->SystemUser );
150             if ( $args{$group_option} =~ /^(Everyone|Privileged|Unprivileged)$/i ) {
151                 $group->LoadSystemInternalGroup( $args{$group_option} );
152             }
153             else {
154                 $group->LoadUserDefinedGroup( $args{$group_option} );
155             }
156             unless ( $group->id ) {
157                 return (0, "Couldn't load group '$args{$group_option}'" );
158             }
159             $args{$group_option} = $group->id;
160         }
161     }
162     if( $args{'replace_relations'} ) {
163         my $uid = $args{'replace_relations'};
164         # XXX: it's possible that SystemUser is not available
165         my $user = RT::User->new( RT->SystemUser );
166         $user->Load( $uid );
167         unless( $user->id ) {
168             return (0, "Couldn't load user '$uid'" );
169         }
170         $args{'replace_relations'} = $user->id;
171     }
172     return $self->SUPER::TestArgs( %args );
173 }
174
175 sub Run
176 {
177     my $self = shift;
178     my %args = ( Shredder => undef, @_ );
179     my $objs = RT::Users->new( RT->SystemUser );
180     # XXX: we want preload only things we need, but later while
181     # logging we need all data, TODO envestigate this
182     # $objs->Columns(qw(id Name EmailAddress Lang Timezone
183     #                   Creator Created LastUpdated LastUpdatedBy));
184     if( my $s = $self->{'opt'}{'status'} ) {
185         if( $s eq 'any' ) {
186             $objs->FindAllRows;
187         } elsif( $s eq 'disabled' ) {
188             $objs->LimitToDeleted;
189         } else {
190             $objs->LimitToEnabled;
191         }
192     }
193     if( $self->{'opt'}{'email'} ) {
194         $objs->Limit( FIELD => 'EmailAddress',
195                   OPERATOR => 'MATCHES',
196                   VALUE => $self->{'opt'}{'email'},
197                 );
198     }
199     if( $self->{'opt'}{'name'} ) {
200         $objs->Limit( FIELD => 'Name',
201                   OPERATOR => 'MATCHES',
202                   VALUE => $self->{'opt'}{'name'},
203                   CASESENSITIVE => 0,
204                 );
205     }
206     if( $self->{'opt'}{'member_of'} ) {
207         $objs->MemberOfGroup( $self->{'opt'}{'member_of'} );
208     }
209     my @filter;
210     if( $self->{'opt'}{'not_member_of'} ) {
211         push @filter, $self->FilterNotMemberOfGroup(
212             Shredder => $args{'Shredder'},
213             GroupId  => $self->{'opt'}{'not_member_of'},
214         );
215     }
216     if( $self->{'opt'}{'no_tickets'} ) {
217         push @filter, $self->FilterWithoutTickets(
218             Shredder => $args{'Shredder'},
219         );
220     }
221     if( $self->{'opt'}{'no_ticket_transactions'} ) {
222         push @filter, $self->FilterWithoutTicketTransactions(
223             Shredder => $args{'Shredder'},
224         );
225     }
226
227     if (@filter) {
228         $self->FetchNext( $objs, 'init' );
229         my @res;
230         USER: while ( my $user = $self->FetchNext( $objs ) ) {
231             for my $filter (@filter) {
232                 next USER unless $filter->($user);
233             }
234             push @res, $user;
235             last if $self->{'opt'}{'limit'} && @res >= $self->{'opt'}{'limit'};
236         }
237         $objs = \@res;
238     } elsif ( $self->{'opt'}{'limit'} ) {
239         $objs->RowsPerPage( $self->{'opt'}{'limit'} );
240     }
241     return (1, $objs);
242 }
243
244 sub SetResolvers
245 {
246     my $self = shift;
247     my %args = ( Shredder => undef, @_ );
248
249     if( $self->{'opt'}{'replace_relations'} ) {
250         my $uid = $self->{'opt'}{'replace_relations'};
251         my $resolver = sub {
252             my %args = (@_);
253             my $t =    $args{'TargetObject'};
254             foreach my $method ( qw(Creator LastUpdatedBy) ) {
255                 next unless $t->_Accessible( $method => 'read' );
256                 $t->__Set( Field => $method, Value => $uid );
257             }
258         };
259         $args{'Shredder'}->PutResolver( BaseClass => 'RT::User', Code => $resolver );
260     }
261     return (1);
262 }
263
264 sub FilterNotMemberOfGroup {
265     my $self = shift;
266     my %args = (
267         Shredder => undef,
268         GroupId  => undef,
269         @_,
270     );
271
272     my $group = RT::Group->new(RT->SystemUser);
273     $group->Load($args{'GroupId'});
274
275     return sub {
276         my $user = shift;
277         not $group->HasMemberRecursively($user->id);
278     };
279 }
280
281 sub FilterWithoutTickets {
282     my $self = shift;
283     my %args = (
284         Shredder => undef,
285         Objects  => undef,
286         @_,
287     );
288
289     return sub {
290         my $user = shift;
291         $self->_WithoutTickets( $user )
292     };
293 }
294
295 sub _WithoutTickets {
296     my ($self, $user) = @_;
297     return unless $user and $user->Id;
298     my $tickets = RT::Tickets->new( RT->SystemUser );
299     $tickets->{'allow_deleted_search'} = 1;
300     $tickets->FromSQL( 'Watcher.id = '. $user->id );
301
302     # we could use the Count method which counts all records
303     # that match, but we really want to know only that
304     # at least one record exists, so this is faster
305     $tickets->RowsPerPage(1);
306     return !$tickets->First;
307 }
308
309 sub FilterWithoutTicketTransactions {
310     my $self = shift;
311     my %args = (
312         Shredder => undef,
313         Objects  => undef,
314         @_,
315     );
316
317     return sub {
318         my $user = shift;
319         $self->_WithoutTicketTransactions( $user )
320     };
321 }
322
323 sub _WithoutTicketTransactions {
324     my ($self, $user) = @_;
325     return unless $user and $user->Id;
326     my $txns = RT::Transactions->new( RT->SystemUser );
327     $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
328     $txns->Limit(FIELD => 'Creator', VALUE => $user->Id);
329
330     # we could use the Count method which counts all records
331     # that match, but we really want to know only that
332     # at least one record exists, so this is faster
333     $txns->RowsPerPage(1);
334     return !$txns->First;
335 }
336
337 1;