Upgrade to 4.2.8
[usit-rt.git] / lib / RT / Shredder / Plugin / Users.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
3ffc5f4f 5# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
84fb5b46
MKG
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
49package RT::Shredder::Plugin::Users;
50
51use strict;
52use warnings FATAL => 'all';
53use base qw(RT::Shredder::Plugin::Base::Search);
54
55=head1 NAME
56
57RT::Shredder::Plugin::Users - search plugin for wiping users.
58
59=head1 ARGUMENTS
60
61=head2 status - string
62
63Status argument allow you to limit result set to C<disabled>,
64C<enabled> or C<any> users.
65B<< Default value is C<disabled>. >>
66
67=head2 name - mask
68
69User name mask.
70
71=head2 email - mask
72
73Email address mask.
74
75=head2 member_of - group identifier
76
77Using this option users that are members of a particular group can
78be selected for deletion. Identifier is name of user defined group
79or id of a group, as well C<Privileged> or <unprivileged> can used
80to select people from system groups.
81
3ffc5f4f
MKG
82=head2 not_member_of - group identifier
83
84Like member_of, but selects users who are not members of the provided
85group.
86
84fb5b46
MKG
87=head2 replace_relations - user identifier
88
3ffc5f4f
MKG
89When you delete an user there could be minor links to them in the RT database.
90This option allow you to replace these links with links to the new user.
91The replaceable links are Creator and LastUpdatedBy, but NOT any watcher roles.
92This means that if the user is a watcher(Requestor, Owner,
93Cc or AdminCc) of the ticket or queue then the link would be deleted.
84fb5b46 94
3ffc5f4f 95This argument could be an user id or name.
84fb5b46
MKG
96
97=head2 no_tickets - boolean
98
99If true then plugin looks for users who are not watchers (Owners,
100Requestors, Ccs or AdminCcs) of any ticket.
101
102Before RT 3.8.5, users who were watchers of deleted tickets B<will be deleted>
103when this option was enabled. Decision has been made that it's not correct
104and you should either shred these deleted tickets, change watchers or
105explicitly delete user by name or email.
106
107Note that found users still B<may have relations> with other objects,
108for example via Creator or LastUpdatedBy fields, and you most probably
109want to use C<replace_relations> option.
110
111=cut
112
113sub SupportArgs
114{
115 return $_[0]->SUPER::SupportArgs,
3ffc5f4f 116 qw(status name email member_of not_member_of replace_relations no_tickets);
84fb5b46
MKG
117}
118
119sub 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 }
3ffc5f4f
MKG
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};
84fb5b46 139
3ffc5f4f
MKG
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 }
84fb5b46
MKG
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
166sub 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'},
3ffc5f4f 194 CASESENSITIVE => 0,
84fb5b46
MKG
195 );
196 }
197 if( $self->{'opt'}{'member_of'} ) {
198 $objs->MemberOfGroup( $self->{'opt'}{'member_of'} );
199 }
3ffc5f4f
MKG
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 }
84fb5b46 207 if( $self->{'opt'}{'no_tickets'} ) {
3ffc5f4f 208 push @filter, $self->FilterWithoutTickets(
84fb5b46 209 Shredder => $args{'Shredder'},
84fb5b46 210 );
3ffc5f4f
MKG
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'};
84fb5b46 222 }
3ffc5f4f
MKG
223 $objs = \@res;
224 } elsif ( $self->{'opt'}{'limit'} ) {
225 $objs->RowsPerPage( $self->{'opt'}{'limit'} );
84fb5b46
MKG
226 }
227 return (1, $objs);
228}
229
230sub 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
3ffc5f4f
MKG
250sub 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
84fb5b46
MKG
267sub FilterWithoutTickets {
268 my $self = shift;
269 my %args = (
270 Shredder => undef,
271 Objects => undef,
272 @_,
273 );
84fb5b46 274
3ffc5f4f
MKG
275 return sub {
276 my $user = shift;
277 $self->_WithoutTickets( $user )
278 };
84fb5b46
MKG
279}
280
281sub _WithoutTickets {
282 my ($self, $user) = @_;
3ffc5f4f 283 return unless $user and $user->Id;
84fb5b46
MKG
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
2941;