RT 4.0.22
[freeside.git] / rt / lib / RT / Shredder.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;
50
51 use strict;
52 use warnings;
53
54
55
56 =head1 NAME
57
58 RT::Shredder - Permanently wipeout data from RT
59
60
61 =head1 SYNOPSIS
62
63 =head2 CLI
64
65   rt-shredder --force --plugin 'Tickets=query,Queue="General" and Status="deleted"'
66
67 =head1 DESCRIPTION
68
69 RT::Shredder is extension to RT which allows you to permanently wipeout
70 data from the RT database.  Shredder supports the wiping of almost
71 all RT objects (Tickets, Transactions, Attachments, Users...).
72
73
74 =head2 "Delete" vs "Wipeout"
75
76 RT uses the term "delete" to mean "deactivate".  To avoid confusion,
77 RT::Shredder uses the term "Wipeout" to mean "permanently erase" (or
78 what most people would think of as "delete").
79
80
81 =head2 Why do you want this?
82
83 Normally in RT, "deleting" an item simply deactivates it and makes it
84 invisible from view.  This is done to retain full history and
85 auditability of your tickets.  For most RT users this is fine and they
86 have no need of RT::Shredder.
87
88 But in some large and heavily used RT instances the database can get
89 clogged up with junk, particularly spam.  This can slow down searches
90 and bloat the size of the database.  For these users, RT::Shredder
91 allows them to completely clear the database of this unwanted junk.
92
93 An additional use of Shredder is to obliterate sensitive information
94 (passwords, credit card numbers, ...) which might have made their way
95 into RT.
96
97
98 =head2 Command line tools (CLI)
99
100 L<rt-shredder> is a program which allows you to wipe objects from
101 command line or with system tasks scheduler (cron, for example).
102 See also 'rt-shredder --help'.
103
104
105 =head2 Web based interface (WebUI)
106
107 Shredder's WebUI integrates into RT's WebUI.  You can find it in the
108 Configuration->Tools->Shredder tab.  The interface is similar to the
109 CLI and gives you the same functionality. You can find 'Shredder' link
110 at the bottom of tickets search results, so you could wipeout tickets
111 in the way similar to the bulk update.
112
113
114 =head1 DATA STORAGE AND BACKUPS
115
116 Shredder allows you to store data you wiped in files as scripts with SQL
117 commands.
118
119 =head3 Restoring from backup
120
121 Should you wipeout something you did not intend to the objects can be
122 restored by using the storage files.  These files are a simple set of
123 SQL commands to re-insert your objects into the RT database.
124
125 1) Locate the appropriate shredder SQL dump file.  In the WebUI, when
126    you use shredder, the path to the dump file is displayed.  It also
127    gives the option to download the dump file after each wipeout.  Or
128    it can be found in your C<$ShredderStoragePath>.
129
130 2) Load the shredder SQL dump into your RT database.  The details will
131    be different for each database and RT configuration, consult your
132    database manual and RT config.  For example, in MySQL...
133
134     mysql -u your_rt_user -p your_rt_database < /path/to/rt/var/data/shredder/dump.sql
135
136 That's it.i This will restore everything you'd deleted during a
137 shredding session when the file had been created.
138
139 =head1 CONFIGURATION
140
141 =head2 $DependenciesLimit
142
143 Shredder stops with an error if the object has more than
144 C<$DependenciesLimit> dependencies. For example: a ticket has 1000
145 transactions or a transaction has 1000 attachments. This is protection
146 from bugs in shredder from wiping out your whole database, but
147 sometimes when you have big mail loops you may hit it.
148
149 Defaults to 1000.  To change this (for example, to 10000) add the
150 following to your F<RT_SiteConfig.pm>:
151
152     Set( $DependenciesLimit, 10_000 );>
153
154
155 =head2 $ShredderStoragePath
156
157 Directory containing Shredder backup dumps; defaults to
158 F</opt/rt4/var/data/RT-Shredder> (assuming an /opt/rt4 installation).
159
160 To change this (for example, to /some/backup/path) add the following to
161 your F<RT_SiteConfig.pm>:
162
163     Set( $ShredderStoragePath, "/some/backup/path" );>
164
165 Be sure to specify an absolute path.
166
167 =head1 Database Indexes
168
169 We have found that the following indexes significantly speed up
170 shredding on most databases.
171
172     CREATE INDEX SHREDDER_CGM1 ON CachedGroupMembers(MemberId, GroupId, Disabled);
173     CREATE INDEX SHREDDER_CGM2 ON CachedGroupMembers(ImmediateParentId,MemberId);
174     CREATE INDEX SHREDDER_CGM3 on CachedGroupMembers (Via, Id);
175
176     CREATE UNIQUE INDEX SHREDDER_GM1 ON GroupMembers(MemberId, GroupId);
177
178     CREATE INDEX SHREDDER_TXN1 ON Transactions(ReferenceType, OldReference);
179     CREATE INDEX SHREDDER_TXN2 ON Transactions(ReferenceType, NewReference);
180     CREATE INDEX SHREDDER_TXN3 ON Transactions(Type, OldValue);
181     CREATE INDEX SHREDDER_TXN4 ON Transactions(Type, NewValue)
182
183     CREATE INDEX SHREDDER_ATTACHMENTS1 ON Attachments(Creator);
184
185 =head1 INFORMATION FOR DEVELOPERS
186
187 =head2 General API
188
189 L<RT::Shredder> is an extension to RT which adds shredder methods to
190 RT objects and classes.  The API is not well documented yet, but you
191 can find usage examples in L<rt-shredder> and the
192 F<lib/t/regression/shredder/*.t> test files.
193
194 However, here is a small example that do the same action as in CLI
195 example from L</SYNOPSIS>:
196
197   use RT::Shredder;
198   RT::Shredder::Init( force => 1 );
199   my $deleted = RT::Tickets->new( RT->SystemUser );
200   $deleted->{'allow_deleted_search'} = 1;
201   $deleted->LimitQueue( VALUE => 'general' );
202   $deleted->LimitStatus( VALUE => 'deleted' );
203   while( my $t = $deleted->Next ) {
204       $t->Wipeout;
205   }
206
207
208 =head2 RT::Shredder class' API
209
210 L<RT::Shredder> implements interfaces to objects cache, actions on the
211 objects in the cache and backups storage.
212
213 =cut
214
215 our $VERSION = '0.04';
216 use File::Spec ();
217
218
219 BEGIN {
220 # I can't use 'use lib' here since it breakes tests
221 # because test suite uses old RT::Shredder setup from
222 # RT lib path
223
224 ### after:     push @INC, qw(@RT_LIB_PATH@);
225     use RT::Shredder::Constants;
226     use RT::Shredder::Exceptions;
227
228     require RT;
229
230     require RT::Shredder::Record;
231
232     require RT::Shredder::ACE;
233     require RT::Shredder::Attachment;
234     require RT::Shredder::CachedGroupMember;
235     require RT::Shredder::CustomField;
236     require RT::Shredder::CustomFieldValue;
237     require RT::Shredder::GroupMember;
238     require RT::Shredder::Group;
239     require RT::Shredder::Link;
240     require RT::Shredder::Principal;
241     require RT::Shredder::Queue;
242     require RT::Shredder::Scrip;
243     require RT::Shredder::ScripAction;
244     require RT::Shredder::ScripCondition;
245     require RT::Shredder::Template;
246     require RT::Shredder::ObjectCustomFieldValue;
247     require RT::Shredder::Ticket;
248     require RT::Shredder::Transaction;
249     require RT::Shredder::User;
250 }
251
252 our @SUPPORTED_OBJECTS = qw(
253     ACE
254     Attachment
255     CachedGroupMember
256     CustomField
257     CustomFieldValue
258     GroupMember
259     Group
260     Link
261     Principal
262     Queue
263     Scrip
264     ScripAction
265     ScripCondition
266     Template
267     ObjectCustomFieldValue
268     Ticket
269     Transaction
270     User
271 );
272
273 =head3 GENERIC
274
275 =head4 Init
276
277     RT::Shredder::Init( %default_options );
278
279 C<RT::Shredder::Init()> should be called before creating an
280 RT::Shredder object.  It iniitalizes RT and loads the RT
281 configuration.
282
283 %default_options are passed to every C<<RT::Shredder->new>> call.
284
285 =cut
286
287 our %opt = ();
288
289 sub Init
290 {
291     %opt = @_;
292     RT::LoadConfig();
293     RT::Init();
294 }
295
296 =head4 new
297
298   my $shredder = RT::Shredder->new(%options);
299
300 Construct a new RT::Shredder object.
301
302 There currently are no %options.
303
304 =cut
305
306 sub new
307 {
308     my $proto = shift;
309     my $self = bless( {}, ref $proto || $proto );
310     $self->_Init( @_ );
311     return $self;
312 }
313
314 sub _Init
315 {
316     my $self = shift;
317     $self->{'opt'}          = { %opt, @_ };
318     $self->{'cache'}        = {};
319     $self->{'resolver'}     = {};
320     $self->{'dump_plugins'} = [];
321 }
322
323 =head4 CastObjectsToRecords( Objects => undef )
324
325 Cast objects to the C<RT::Record> objects or its ancesstors.
326 Objects can be passed as SCALAR (format C<< <class>-<id> >>),
327 ARRAY, C<RT::Record> ancesstors or C<RT::SearchBuilder> ancesstor.
328
329 Most methods that takes C<Objects> argument use this method to
330 cast argument value to list of records.
331
332 Returns an array of records.
333
334 For example:
335
336     my @objs = $shredder->CastObjectsToRecords(
337         Objects => [             # ARRAY reference
338             'RT::Attachment-10', # SCALAR or SCALAR reference
339             $tickets,            # RT::Tickets object (isa RT::SearchBuilder)
340             $user,               # RT::User object (isa RT::Record)
341         ],
342     );
343
344 =cut
345
346 sub CastObjectsToRecords
347 {
348     my $self = shift;
349     my %args = ( Objects => undef, @_ );
350
351     my @res;
352     my $targets = delete $args{'Objects'};
353     unless( $targets ) {
354         RT::Shredder::Exception->throw( "Undefined Objects argument" );
355     }
356
357     if( UNIVERSAL::isa( $targets, 'RT::SearchBuilder' ) ) {
358         #XXX: try to use ->_DoSearch + ->ItemsArrayRef in feature
359         #     like we do in Record with links, but change only when
360         #     more tests would be available
361         while( my $tmp = $targets->Next ) { push @res, $tmp };
362     } elsif ( UNIVERSAL::isa( $targets, 'RT::Record' ) ) {
363         push @res, $targets;
364     } elsif ( UNIVERSAL::isa( $targets, 'ARRAY' ) ) {
365         foreach( @$targets ) {
366             push @res, $self->CastObjectsToRecords( Objects => $_ );
367         }
368     } elsif ( UNIVERSAL::isa( $targets, 'SCALAR' ) || !ref $targets ) {
369         $targets = $$targets if ref $targets;
370         my ($class, $id) = split /-/, $targets;
371         RT::Shredder::Exception->throw( "Unsupported class $class" )
372               unless $class =~ /^\w+(::\w+)*$/;
373         $class = 'RT::'. $class unless $class =~ /^RTx?::/i;
374         eval "require $class";
375         die "Couldn't load '$class' module" if $@;
376         my $obj = $class->new( RT->SystemUser );
377         die "Couldn't construct new '$class' object" unless $obj;
378         $obj->Load( $id );
379         unless ( $obj->id ) {
380             $RT::Logger->error( "Couldn't load '$class' object with id '$id'" );
381             RT::Shredder::Exception::Info->throw( 'CouldntLoadObject' );
382         }
383         die "Loaded object has different id" unless( $id eq $obj->id );
384         push @res, $obj;
385     } else {
386         RT::Shredder::Exception->throw( "Unsupported type ". ref $targets );
387     }
388     return @res;
389 }
390
391 =head3 OBJECTS CACHE
392
393 =head4 PutObjects( Objects => undef )
394
395 Puts objects into cache.
396
397 Returns array of the cache entries.
398
399 See C<CastObjectsToRecords> method for supported types of the C<Objects>
400 argument.
401
402 =cut
403
404 sub PutObjects
405 {
406     my $self = shift;
407     my %args = ( Objects => undef, @_ );
408
409     my @res;
410     for( $self->CastObjectsToRecords( Objects => delete $args{'Objects'} ) ) {
411         push @res, $self->PutObject( %args, Object => $_ )
412     }
413
414     return @res;
415 }
416
417 =head4 PutObject( Object => undef )
418
419 Puts record object into cache and returns its cache entry.
420
421 B<NOTE> that this method support B<only C<RT::Record> object or its ancesstor
422 objects>, if you want put mutliple objects or objects represented by different
423 classes then use C<PutObjects> method instead.
424
425 =cut
426
427 sub PutObject
428 {
429     my $self = shift;
430     my %args = ( Object => undef, @_ );
431
432     my $obj = $args{'Object'};
433     unless( UNIVERSAL::isa( $obj, 'RT::Record' ) ) {
434         RT::Shredder::Exception->throw( "Unsupported type '". (ref $obj || $obj || '(undef)')."'" );
435     }
436
437     my $str = $obj->_AsString;
438     return ($self->{'cache'}->{ $str } ||= { State => ON_STACK, Object => $obj } );
439 }
440
441 =head4 GetObject, GetState, GetRecord( String => ''| Object => '' )
442
443 Returns record object from cache, cache entry state or cache entry accordingly.
444
445 All three methods takes C<String> (format C<< <class>-<id> >>) or C<Object> argument.
446 C<String> argument has more priority than C<Object> so if it's not empty then methods
447 leave C<Object> argument unchecked.
448
449 You can read about possible states and their meanings in L<RT::Shredder::Constants> docs.
450
451 =cut
452
453 sub _ParseRefStrArgs
454 {
455     my $self = shift;
456     my %args = (
457         String => '',
458         Object => undef,
459         @_
460     );
461     if( $args{'String'} && $args{'Object'} ) {
462         require Carp;
463         Carp::croak( "both String and Object args passed" );
464     }
465     return $args{'String'} if $args{'String'};
466     return $args{'Object'}->_AsString if UNIVERSAL::can($args{'Object'}, '_AsString' );
467     return '';
468 }
469
470 sub GetObject { return (shift)->GetRecord( @_ )->{'Object'} }
471 sub GetState { return (shift)->GetRecord( @_ )->{'State'} }
472 sub GetRecord
473 {
474     my $self = shift;
475     my $str = $self->_ParseRefStrArgs( @_ );
476     return $self->{'cache'}->{ $str };
477 }
478
479 =head3 Dependencies resolvers
480
481 =head4 PutResolver, GetResolvers and ApplyResolvers
482
483 TODO: These methods have no documentation.
484
485 =cut
486
487 sub PutResolver
488 {
489     my $self = shift;
490     my %args = (
491         BaseClass => '',
492         TargetClass => '',
493         Code => undef,
494         @_,
495     );
496     unless( UNIVERSAL::isa( $args{'Code'} => 'CODE' ) ) {
497         die "Resolver '$args{Code}' is not code reference";
498     }
499
500     my $resolvers = (
501         (
502             $self->{'resolver'}->{ $args{'BaseClass'} } ||= {}
503         )->{  $args{'TargetClass'} || '' } ||= []
504     );
505     unshift @$resolvers, $args{'Code'};
506     return;
507 }
508
509 sub GetResolvers
510 {
511     my $self = shift;
512     my %args = (
513         BaseClass => '',
514         TargetClass => '',
515         @_,
516     );
517
518     my @res;
519     if( $args{'TargetClass'} && exists $self->{'resolver'}->{ $args{'BaseClass'} }->{ $args{'TargetClass'} } ) {
520         push @res, @{ $self->{'resolver'}->{ $args{'BaseClass'} }->{ $args{'TargetClass'} || '' } };
521     }
522     if( exists $self->{'resolver'}->{ $args{'BaseClass'} }->{ '' } ) {
523         push @res, @{ $self->{'resolver'}->{ $args{'BaseClass'} }->{''} };
524     }
525
526     return @res;
527 }
528
529 sub ApplyResolvers
530 {
531     my $self = shift;
532     my %args = ( Dependency => undef, @_ );
533     my $dep = $args{'Dependency'};
534
535     my @resolvers = $self->GetResolvers(
536         BaseClass   => $dep->BaseClass,
537         TargetClass => $dep->TargetClass,
538     );
539
540     unless( @resolvers ) {
541         RT::Shredder::Exception::Info->throw(
542             tag   => 'NoResolver',
543             error => "Couldn't find resolver for dependency '". $dep->AsString ."'",
544         );
545     }
546     $_->(
547         Shredder     => $self,
548         BaseObject   => $dep->BaseObject,
549         TargetObject => $dep->TargetObject,
550     ) foreach @resolvers;
551
552     return;
553 }
554
555 sub WipeoutAll
556 {
557     my $self = $_[0];
558
559     foreach my $cache_val ( values %{ $self->{'cache'} } ) {
560         next if $cache_val->{'State'} & (WIPED | IN_WIPING);
561         $self->Wipeout( Object => $cache_val->{'Object'} );
562     }
563 }
564
565 sub Wipeout
566 {
567     my $self = shift;
568     my $mark;
569     eval {
570         die "Couldn't begin transaction" unless $RT::Handle->BeginTransaction;
571         $mark = $self->PushDumpMark or die "Couldn't get dump mark";
572         $self->_Wipeout( @_ );
573         $self->PopDumpMark( Mark => $mark );
574         die "Couldn't commit transaction" unless $RT::Handle->Commit;
575     };
576     if( $@ ) {
577         my $error = $@;
578         $RT::Handle->Rollback('force');
579         $self->RollbackDumpTo( Mark => $mark ) if $mark;
580         die $error if RT::Shredder::Exception::Info->caught;
581         die "Couldn't wipeout object: $error";
582     }
583 }
584
585 sub _Wipeout
586 {
587     my $self = shift;
588     my %args = ( CacheRecord => undef, Object => undef, @_ );
589
590     my $record = $args{'CacheRecord'};
591     $record = $self->PutObject( Object => $args{'Object'} ) unless $record;
592     return if $record->{'State'} & (WIPED | IN_WIPING);
593
594     $record->{'State'} |= IN_WIPING;
595     my $object = $record->{'Object'};
596
597     $self->DumpObject( Object => $object, State => 'before any action' );
598
599     unless( $object->BeforeWipeout ) {
600         RT::Shredder::Exception->throw( "BeforeWipeout check returned error" );
601     }
602
603     my $deps = $object->Dependencies( Shredder => $self );
604     $deps->List(
605         WithFlags => DEPENDS_ON | VARIABLE,
606         Callback  => sub { $self->ApplyResolvers( Dependency => $_[0] ) },
607     );
608     $self->DumpObject( Object => $object, State => 'after resolvers' );
609
610     $deps->List(
611         WithFlags    => DEPENDS_ON,
612         WithoutFlags => WIPE_AFTER | VARIABLE,
613         Callback     => sub { $self->_Wipeout( Object => $_[0]->TargetObject ) },
614     );
615     $self->DumpObject( Object => $object, State => 'after wiping dependencies' );
616
617     $object->__Wipeout;
618     $record->{'State'} |= WIPED; delete $record->{'Object'};
619     $self->DumpObject( Object => $object, State => 'after wipeout' );
620
621     $deps->List(
622         WithFlags => DEPENDS_ON | WIPE_AFTER,
623         WithoutFlags => VARIABLE,
624         Callback => sub { $self->_Wipeout( Object => $_[0]->TargetObject ) },
625     );
626     $self->DumpObject( Object => $object, State => 'after late dependencies' );
627
628     return;
629 }
630
631 sub ValidateRelations
632 {
633     my $self = shift;
634     my %args = ( @_ );
635
636     foreach my $record( values %{ $self->{'cache'} } ) {
637         next if( $record->{'State'} & VALID );
638         $record->{'Object'}->ValidateRelations( Shredder => $self );
639     }
640 }
641
642 =head3 Data storage and backups
643
644 =head4 GetFileName( FileName => '<ISO DATETIME>-XXXX.sql', FromStorage => 1 )
645
646 Takes desired C<FileName> and flag C<FromStorage> then translate file name to absolute
647 path by next rules:
648
649 * Default value of the C<FileName> option is C<< <ISO DATETIME>-XXXX.sql >>;
650
651 * if C<FileName> has C<XXXX> (exactly four uppercase C<X> letters) then it would be changed with digits from 0000 to 9999 range, with first one free value;
652
653 * if C<FileName> has C<%T> then it would be replaced with the current date and time in the C<YYYY-MM-DDTHH:MM:SS> format. Note that using C<%t> may still generate not unique names, using C<XXXX> recomended.
654
655 * if C<FromStorage> argument is true (default behaviour) then result path would always be relative to C<StoragePath>;
656
657 * if C<FromStorage> argument is false then result would be relative to the current dir unless it's already absolute path.
658
659 Returns an absolute path of the file.
660
661 Examples:
662     # file from storage with default name format
663     my $fname = $shredder->GetFileName;
664
665     # file from storage with custom name format
666     my $fname = $shredder->GetFileName( FileName => 'shredder-XXXX.backup' );
667
668     # file with path relative to the current dir
669     my $fname = $shredder->GetFileName(
670         FromStorage => 0,
671         FileName => 'backups/shredder.sql',
672     );
673
674     # file with absolute path
675     my $fname = $shredder->GetFileName(
676         FromStorage => 0,
677         FileName => '/var/backups/shredder-XXXX.sql'
678     );
679
680 =cut
681
682 sub GetFileName
683 {
684     my $self = shift;
685     my %args = ( FileName => '', FromStorage => 1, @_ );
686
687     # default value
688     my $file = $args{'FileName'} || '%t-XXXX.sql';
689     if( $file =~ /\%t/i ) {
690         require POSIX;
691         my $date_time = POSIX::strftime( "%Y%m%dT%H%M%S", gmtime );
692         $file =~ s/\%t/$date_time/gi;
693     }
694
695     # convert to absolute path
696     if( $args{'FromStorage'} ) {
697         $file = File::Spec->catfile( $self->StoragePath, $file );
698     } elsif( !File::Spec->file_name_is_absolute( $file ) ) {
699         $file = File::Spec->rel2abs( $file );
700     }
701
702     # check mask
703     if( $file =~ /XXXX[^\/\\]*$/ ) {
704         my( $tmp, $i ) = ( $file, 0 );
705         do {
706             $i++;
707             $tmp = $file;
708             $tmp =~ s/XXXX([^\/\\]*)$/sprintf("%04d", $i).$1/e;
709         } while( -e $tmp && $i < 9999 );
710         $file = $tmp;
711     }
712
713     if( -f $file ) {
714         unless( -w _ ) {
715             die "File '$file' exists, but is read-only";
716         }
717     } elsif( !-e _ ) {
718         unless( File::Spec->file_name_is_absolute( $file ) ) {
719             $file = File::Spec->rel2abs( $file );
720         }
721
722         # check base dir
723         my $dir = File::Spec->join( (File::Spec->splitpath( $file ))[0,1] );
724         unless( -e $dir && -d _) {
725             die "Base directory '$dir' for file '$file' doesn't exist";
726         }
727         unless( -w $dir ) {
728             die "Base directory '$dir' is not writable";
729         }
730     } else {
731         die "'$file' is not regular file";
732     }
733
734     return $file;
735 }
736
737 =head4 StoragePath
738
739 Returns an absolute path to the storage dir.  See
740 L</$ShredderStoragePath>.
741
742 See also description of the L</GetFileName> method.
743
744 =cut
745
746 sub StoragePath
747 {
748     return scalar( RT->Config->Get('ShredderStoragePath') )
749         || File::Spec->catdir( $RT::VarPath, qw(data RT-Shredder) );
750 }
751
752 my %active_dump_state = ();
753 sub AddDumpPlugin {
754     my $self = shift;
755     my %args = ( Object => undef, Name => 'SQLDump', Arguments => undef, @_ );
756
757     my $plugin = $args{'Object'};
758     unless ( $plugin ) {
759         require RT::Shredder::Plugin;
760         $plugin = RT::Shredder::Plugin->new;
761         my( $status, $msg ) = $plugin->LoadByName( $args{'Name'} );
762         die "Couldn't load dump plugin: $msg\n" unless $status;
763     }
764     die "Plugin is not of correct type" unless lc $plugin->Type eq 'dump';
765
766     if ( my $pargs = $args{'Arguments'} ) {
767         my ($status, $msg) = $plugin->TestArgs( %$pargs );
768         die "Couldn't set plugin args: $msg\n" unless $status;
769     }
770
771     my @applies_to = $plugin->AppliesToStates;
772     die "Plugin doesn't apply to any state" unless @applies_to;
773     $active_dump_state{ lc $_ } = 1 foreach @applies_to;
774
775     push @{ $self->{'dump_plugins'} }, $plugin;
776
777     return $plugin;
778 }
779
780 sub DumpObject {
781     my $self = shift;
782     my %args = (Object => undef, State => undef, @_);
783     die "No state passed" unless $args{'State'};
784     return unless $active_dump_state{ lc $args{'State'} };
785
786     foreach (@{ $self->{'dump_plugins'} }) {
787         next unless grep lc $args{'State'} eq lc $_, $_->AppliesToStates;
788         my ($state, $msg) = $_->Run( %args );
789         die "Couldn't run plugin: $msg" unless $state;
790     }
791 }
792
793 { my $mark = 1; # XXX: integer overflows?
794 sub PushDumpMark {
795     my $self = shift;
796     $mark++;
797     foreach (@{ $self->{'dump_plugins'} }) {
798         my ($state, $msg) = $_->PushMark( Mark => $mark );
799         die "Couldn't push mark: $msg" unless $state;
800     }
801     return $mark;
802 }
803 sub PopDumpMark {
804     my $self = shift;
805     foreach (@{ $self->{'dump_plugins'} }) {
806         my ($state, $msg) = $_->PushMark( @_ );
807         die "Couldn't pop mark: $msg" unless $state;
808     }
809 }
810 sub RollbackDumpTo {
811     my $self = shift;
812     foreach (@{ $self->{'dump_plugins'} }) {
813         my ($state, $msg) = $_->RollbackTo( @_ );
814         die "Couldn't rollback to mark: $msg" unless $state;
815     }
816 }
817 }
818
819 1;
820 __END__
821
822 =head1 NOTES
823
824 =head2 Database transactions support
825
826 Since 0.03_01 RT::Shredder uses database transactions and should be
827 much safer to run on production servers.
828
829 =head2 Foreign keys
830
831 Mainstream RT doesn't use FKs, but at least I posted DDL script that creates them
832 in mysql DB, note that if you use FKs then this two valid keys don't allow delete
833 Tickets because of bug in MySQL:
834
835   ALTER TABLE Tickets ADD FOREIGN KEY (EffectiveId) REFERENCES Tickets(id);
836   ALTER TABLE CachedGroupMembers ADD FOREIGN KEY (Via) REFERENCES CachedGroupMembers(id);
837
838 L<http://bugs.mysql.com/bug.php?id=4042>
839
840 =head1 BUGS AND HOW TO CONTRIBUTE
841
842 We need your feedback in all cases: if you use it or not,
843 is it works for you or not.
844
845 =head2 Testing
846
847 Don't skip C<make test> step while install and send me reports if it's fails.
848 Add your own tests, it's easy enough if you've writen at list one perl script
849 that works with RT. Read more about testing in F<t/utils.pl>.
850
851 =head2 Reporting
852
853 Send reports to L</AUTHOR> or to the RT mailing lists.
854
855 =head2 Documentation
856
857 Many bugs in the docs: insanity, spelling, gramar and so on.
858 Patches are wellcome.
859
860 =head2 Todo
861
862 Please, see Todo file, it has some technical notes
863 about what I plan to do, when I'll do it, also it
864 describes some problems code has.
865
866 =head2 Repository
867
868 Since RT-3.7 shredder is a part of the RT distribution.
869 Versions of the RTx::Shredder extension could
870 be downloaded from the CPAN. Those work with older
871 RT versions or you can find repository at
872 L<https://opensvn.csie.org/rtx_shredder>
873
874 =head1 AUTHOR
875
876     Ruslan U. Zakirov <Ruslan.Zakirov@gmail.com>
877
878 =head1 COPYRIGHT
879
880 This program is free software; you can redistribute
881 it and/or modify it under the same terms as Perl itself.
882
883 The full text of the license can be found in the
884 Perl distribution.
885
886 =head1 SEE ALSO
887
888 L<rt-shredder>, L<rt-validator>
889
890 =cut