Merge branch 'master' of https://github.com/jgoodman/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-2014 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 replace_relations - user identifier
83
84 When you delete user there are could be minor links to him in RT DB.
85 This option allow you to replace this links with link to other user.
86 This links are Creator and LastUpdatedBy, but NOT any watcher roles,
87 this means that if user is watcher(Requestor, Owner,
88 Cc or AdminCc) of the ticket or queue then link would be deleted.
89
90 This argument could be user id or name.
91
92 =head2 no_tickets - boolean
93
94 If true then plugin looks for users who are not watchers (Owners,
95 Requestors, Ccs or AdminCcs) of any ticket.
96
97 Before RT 3.8.5, users who were watchers of deleted tickets B<will be deleted>
98 when this option was enabled. Decision has been made that it's not correct
99 and you should either shred these deleted tickets, change watchers or
100 explicitly delete user by name or email.
101
102 Note that found users still B<may have relations> with other objects,
103 for example via Creator or LastUpdatedBy fields, and you most probably
104 want to use C<replace_relations> option.
105
106 =cut
107
108 sub SupportArgs
109 {
110     return $_[0]->SUPER::SupportArgs,
111            qw(status name email member_of replace_relations no_tickets);
112 }
113
114 sub TestArgs
115 {
116     my $self = shift;
117     my %args = @_;
118     if( $args{'status'} ) {
119         unless( $args{'status'} =~ /^(disabled|enabled|any)$/i ) {
120             return (0, "Status '$args{'status'}' is unsupported.");
121         }
122     } else {
123         $args{'status'} = 'disabled';
124     }
125     if( $args{'email'} ) {
126         $args{'email'} = $self->ConvertMaskToSQL( $args{'email'} );
127     }
128     if( $args{'name'} ) {
129         $args{'name'} = $self->ConvertMaskToSQL( $args{'name'} );
130     }
131     if( $args{'member_of'} ) {
132         my $group = RT::Group->new( RT->SystemUser );
133         if ( $args{'member_of'} =~ /^(Everyone|Privileged|Unprivileged)$/i ) {
134             $group->LoadSystemInternalGroup( $args{'member_of'} );
135         }
136         else {
137             $group->LoadUserDefinedGroup( $args{'member_of'} );
138         }
139         unless ( $group->id ) {
140             return (0, "Couldn't load group '$args{'member_of'}'" );
141         }
142         $args{'member_of'} = $group->id;
143
144     }
145     if( $args{'replace_relations'} ) {
146         my $uid = $args{'replace_relations'};
147         # XXX: it's possible that SystemUser is not available
148         my $user = RT::User->new( RT->SystemUser );
149         $user->Load( $uid );
150         unless( $user->id ) {
151             return (0, "Couldn't load user '$uid'" );
152         }
153         $args{'replace_relations'} = $user->id;
154     }
155     return $self->SUPER::TestArgs( %args );
156 }
157
158 sub Run
159 {
160     my $self = shift;
161     my %args = ( Shredder => undef, @_ );
162     my $objs = RT::Users->new( RT->SystemUser );
163     # XXX: we want preload only things we need, but later while
164     # logging we need all data, TODO envestigate this
165     # $objs->Columns(qw(id Name EmailAddress Lang Timezone
166     #                   Creator Created LastUpdated LastUpdatedBy));
167     if( my $s = $self->{'opt'}{'status'} ) {
168         if( $s eq 'any' ) {
169             $objs->FindAllRows;
170         } elsif( $s eq 'disabled' ) {
171             $objs->LimitToDeleted;
172         } else {
173             $objs->LimitToEnabled;
174         }
175     }
176     if( $self->{'opt'}{'email'} ) {
177         $objs->Limit( FIELD => 'EmailAddress',
178                   OPERATOR => 'MATCHES',
179                   VALUE => $self->{'opt'}{'email'},
180                 );
181     }
182     if( $self->{'opt'}{'name'} ) {
183         $objs->Limit( FIELD => 'Name',
184                   OPERATOR => 'MATCHES',
185                   VALUE => $self->{'opt'}{'name'},
186                 );
187     }
188     if( $self->{'opt'}{'member_of'} ) {
189         $objs->MemberOfGroup( $self->{'opt'}{'member_of'} );
190     }
191     if( $self->{'opt'}{'no_tickets'} ) {
192         return $self->FilterWithoutTickets(
193             Shredder => $args{'Shredder'},
194             Objects  => $objs,
195         );
196     } else {
197         if( $self->{'opt'}{'limit'} ) {
198             $objs->RowsPerPage( $self->{'opt'}{'limit'} );
199         }
200     }
201     return (1, $objs);
202 }
203
204 sub SetResolvers
205 {
206     my $self = shift;
207     my %args = ( Shredder => undef, @_ );
208
209     if( $self->{'opt'}{'replace_relations'} ) {
210         my $uid = $self->{'opt'}{'replace_relations'};
211         my $resolver = sub {
212             my %args = (@_);
213             my $t =    $args{'TargetObject'};
214             foreach my $method ( qw(Creator LastUpdatedBy) ) {
215                 next unless $t->_Accessible( $method => 'read' );
216                 $t->__Set( Field => $method, Value => $uid );
217             }
218         };
219         $args{'Shredder'}->PutResolver( BaseClass => 'RT::User', Code => $resolver );
220     }
221     return (1);
222 }
223
224 sub FilterWithoutTickets {
225     my $self = shift;
226     my %args = (
227         Shredder => undef,
228         Objects  => undef,
229         @_,
230     );
231     my $users = $args{Objects};
232     $self->FetchNext( $users, 'init' );
233
234     my @res;
235     while ( my $user = $self->FetchNext( $users ) ) {
236         push @res, $user if $self->_WithoutTickets( $user );
237         return (1, \@res) if $self->{'opt'}{'limit'} && @res >= $self->{'opt'}{'limit'};
238     }
239     return (1, \@res);
240 }
241
242 sub _WithoutTickets {
243     my ($self, $user) = @_;
244     my $tickets = RT::Tickets->new( RT->SystemUser );
245     $tickets->{'allow_deleted_search'} = 1;
246     $tickets->FromSQL( 'Watcher.id = '. $user->id );
247     # HACK: we may use Count method which counts all records
248     # that match condtion, but we really want to know only that
249     # at least one record exist, so we fetch first row only
250     $tickets->RowsPerPage(1);
251     return !$tickets->First;
252 }
253
254 1;