Bug 25429: Require command line switch to enable, control DAYS via syspref
[koha.git] / misc / cronjobs / cleanup_database.pl
1 #!/usr/bin/perl
2
3 # Copyright 2009 PTFS, Inc.
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use constant DEFAULT_ZEBRAQ_PURGEDAYS             => 30;
23 use constant DEFAULT_MAIL_PURGEDAYS               => 30;
24 use constant DEFAULT_IMPORT_PURGEDAYS             => 60;
25 use constant DEFAULT_LOGS_PURGEDAYS               => 180;
26 use constant DEFAULT_MESSAGES_PURGEDAYS           => 365;
27 use constant DEFAULT_SEARCHHISTORY_PURGEDAYS      => 30;
28 use constant DEFAULT_SHARE_INVITATION_EXPIRY_DAYS => 14;
29 use constant DEFAULT_DEBARMENTS_PURGEDAYS         => 30;
30
31 BEGIN {
32     # find Koha's Perl modules
33     # test carefully before changing this
34     use FindBin ();
35     eval { require "$FindBin::Bin/../kohalib.pl" };
36 }
37
38 use Koha::Script -cron;
39 use C4::Context;
40 use C4::Search;
41 use C4::Search::History;
42 use Getopt::Long qw( GetOptions );
43 use C4::Log qw( cronlogaction );
44 use C4::Accounts qw( purge_zero_balance_fees );
45 use Koha::UploadedFiles;
46 use Koha::Old::Biblios;
47 use Koha::Old::Items;
48 use Koha::Old::Biblioitems;
49 use Koha::Old::Checkouts;
50 use Koha::Old::Holds;
51 use Koha::Old::Patrons;
52 use Koha::Item::Transfers;
53 use Koha::PseudonymizedTransactions;
54 use Koha::Patron::Messages;
55
56 sub usage {
57     print STDERR <<USAGE;
58 Usage: $0 [-h|--help] [--confirm] [--sessions] [--sessdays DAYS] [-v|--verbose] [--zebraqueue DAYS] [-m|--mail] [--merged] [--import DAYS] [--logs DAYS] [--searchhistory DAYS] [--restrictions DAYS] [--all-restrictions] [--fees DAYS] [--temp-uploads] [--temp-uploads-days DAYS] [--uploads-missing 0|1 ] [--statistics DAYS] [--deleted-catalog DAYS] [--deleted-patrons DAYS] [--old-issues DAYS] [--old-reserves DAYS] [--transfers DAYS] [--labels DAYS] [--cards DAYS]
59
60    -h --help          prints this help message, and exits, ignoring all
61                       other options
62    --confirm          Confirmation flag, the script will be running in dry-run mode is not set.
63    --sessions         purge the sessions table.  If you use this while users 
64                       are logged into Koha, they will have to reconnect.
65    --sessdays DAYS    purge only sessions older than DAYS days.
66    -v --verbose       will cause the script to give you a bit more information
67                       about the run.
68    --zebraqueue DAYS  purge completed zebraqueue entries older than DAYS days.
69                       Defaults to 30 days if no days specified.
70    -m --mail DAYS     purge items from the mail queue that are older than DAYS days.
71                       Defaults to 30 days if no days specified.
72    --merged           purged completed entries from need_merge_authorities.
73    --messages DAYS    purge entries from messages table older than DAYS days.
74                       Defaults to 365 days if no days specified.
75    --import DAYS      purge records from import tables older than DAYS days.
76                       Defaults to 60 days if no days specified.
77    --z3950            purge records from import tables that are the result
78                       of Z39.50 searches
79    --fees DAYS        purge entries accountlines older than DAYS days, where
80                       amountoutstanding is 0 or NULL.
81                       In the case of --fees, DAYS must be greater than
82                       or equal to 1.
83    --logs DAYS        purge entries from action_logs older than DAYS days.
84                       Defaults to 180 days if no days specified.
85    --searchhistory DAYS  purge entries from search_history older than DAYS days.
86                          Defaults to 30 days if no days specified
87    --list-invites  DAYS  purge (unaccepted) list share invites older than DAYS
88                          days.  Defaults to 14 days if no days specified.
89    --restrictions DAYS   purge patrons restrictions expired since more than DAYS days.
90                          Defaults to 30 days if no days specified.
91    --all-restrictions   purge all expired patrons restrictions.
92    --del-exp-selfreg  Delete expired self registration accounts
93    --del-unv-selfreg  DAYS  Delete unverified self registrations older than DAYS
94    --unique-holidays DAYS  Delete all unique holidays older than DAYS
95    --temp-uploads     Delete temporary uploads.
96    --temp-uploads-days DAYS Override the corresponding preference value.
97    --uploads-missing FLAG Delete upload records for missing files when FLAG is true, count them otherwise
98    --oauth-tokens     Delete expired OAuth2 tokens
99    --statistics DAYS       Purge statistics entries more than DAYS days old.
100                            This table is used to build reports, make sure you are aware of the consequences of this before using it!
101    --deleted-catalog  DAYS Purge catalog records deleted more then DAYS days ago
102                            (from tables deleteditems, deletedbiblioitems, deletedbiblio_metadata and deletedbiblio).
103    --deleted-patrons DAYS  Purge patrons deleted more than DAYS days ago.
104    --old-issues DAYS       Purge checkouts (old_issues) returned more than DAYS days ago.
105    --old-reserves DAYS     Purge reserves (old_reserves) more than DAYS old.
106    --transfers DAYS        Purge transfers completed more than DAYS day ago.
107    --pseudo-transactions DAYS   Purge the pseudonymized transactions that have been originally created more than DAYS days ago
108                                 DAYS is optional and can be replaced by:
109                                     --pseudo-transactions-from YYYY-MM-DD and/or --pseudo-transactions-to YYYY-MM-DD
110    --labels DAYS           Purge item label batches last added to more than DAYS days ago.
111    --cards DAY             Purge card creator batches last added to more than DAYS days ago.
112    --return-claims         Purge all resolved return claims older than the number of days specified in
113                            the system preference CleanUpDatabaseReturnClaims.
114 USAGE
115     exit $_[0];
116 }
117
118 my $help;
119 my $confirm;
120 my $sessions;
121 my $sess_days;
122 my $verbose;
123 my $zebraqueue_days;
124 my $mail;
125 my $purge_merged;
126 my $pImport;
127 my $pLogs;
128 my $pSearchhistory;
129 my $pZ3950;
130 my $pListShareInvites;
131 my $pDebarments;
132 my $allDebarments;
133 my $return_claims;
134 my $pExpSelfReg;
135 my $pUnvSelfReg;
136 my $fees_days;
137 my $special_holidays_days;
138 my $temp_uploads;
139 my $temp_uploads_days;
140 my $uploads_missing;
141 my $oauth_tokens;
142 my $pStatistics;
143 my $pDeletedCatalog;
144 my $pDeletedPatrons;
145 my $pOldIssues;
146 my $pOldReserves;
147 my $pTransfers;
148 my ( $pPseudoTransactions, $pPseudoTransactionsFrom, $pPseudoTransactionsTo );
149 my $pMessages;
150 my $lock_days = C4::Context->preference('LockExpiredDelay');
151 my $labels;
152 my $cards;
153
154 GetOptions(
155     'h|help'            => \$help,
156     'confirm'           => \$confirm,
157     'sessions'          => \$sessions,
158     'sessdays:i'        => \$sess_days,
159     'v|verbose'         => \$verbose,
160     'm|mail:i'          => \$mail,
161     'zebraqueue:i'      => \$zebraqueue_days,
162     'merged'            => \$purge_merged,
163     'import:i'          => \$pImport,
164     'z3950'             => \$pZ3950,
165     'logs:i'            => \$pLogs,
166     'messages:i'        => \$pMessages,
167     'fees:i'            => \$fees_days,
168     'searchhistory:i'   => \$pSearchhistory,
169     'list-invites:i'    => \$pListShareInvites,
170     'restrictions:i'    => \$pDebarments,
171     'all-restrictions'  => \$allDebarments,
172     'del-exp-selfreg'   => \$pExpSelfReg,
173     'del-unv-selfreg'   => \$pUnvSelfReg,
174     'unique-holidays:i' => \$special_holidays_days,
175     'temp-uploads'      => \$temp_uploads,
176     'temp-uploads-days:i' => \$temp_uploads_days,
177     'uploads-missing:i' => \$uploads_missing,
178     'oauth-tokens'      => \$oauth_tokens,
179     'statistics:i'      => \$pStatistics,
180     'deleted-catalog:i' => \$pDeletedCatalog,
181     'deleted-patrons:i' => \$pDeletedPatrons,
182     'old-issues:i'      => \$pOldIssues,
183     'old-reserves:i'    => \$pOldReserves,
184     'transfers:i'       => \$pTransfers,
185     'pseudo-transactions:i'      => \$pPseudoTransactions,
186     'pseudo-transactions-from:s' => \$pPseudoTransactionsFrom,
187     'pseudo-transactions-to:s'   => \$pPseudoTransactionsTo,
188     'labels'            => \$labels,
189     'cards'             => \$cards,
190     'return-claims'     => \$return_claims,
191 ) || usage(1);
192
193 # Use default values
194 $sessions          = 1                                    if $sess_days                  && $sess_days > 0;
195 $pImport           = DEFAULT_IMPORT_PURGEDAYS             if defined($pImport)           && $pImport == 0;
196 $pLogs             = DEFAULT_LOGS_PURGEDAYS               if defined($pLogs)             && $pLogs == 0;
197 $zebraqueue_days   = DEFAULT_ZEBRAQ_PURGEDAYS             if defined($zebraqueue_days)   && $zebraqueue_days == 0;
198 $mail              = DEFAULT_MAIL_PURGEDAYS               if defined($mail)              && $mail == 0;
199 $pSearchhistory    = DEFAULT_SEARCHHISTORY_PURGEDAYS      if defined($pSearchhistory)    && $pSearchhistory == 0;
200 $pListShareInvites = DEFAULT_SHARE_INVITATION_EXPIRY_DAYS if defined($pListShareInvites) && $pListShareInvites == 0;
201 $pDebarments       = DEFAULT_DEBARMENTS_PURGEDAYS         if defined($pDebarments)       && $pDebarments == 0;
202 $pMessages         = DEFAULT_MESSAGES_PURGEDAYS           if defined($pMessages)         && $pMessages == 0;
203
204 if ($help) {
205     usage(0);
206 }
207
208 unless ( $sessions
209     || $zebraqueue_days
210     || $mail
211     || $purge_merged
212     || $pImport
213     || $pLogs
214     || $fees_days
215     || $pSearchhistory
216     || $pZ3950
217     || $pListShareInvites
218     || $pDebarments
219     || $allDebarments
220     || $pExpSelfReg
221     || $pUnvSelfReg
222     || $special_holidays_days
223     || $temp_uploads
224     || defined $uploads_missing
225     || $oauth_tokens
226     || $pStatistics
227     || $pDeletedCatalog
228     || $pDeletedPatrons
229     || $pOldIssues
230     || $pOldReserves
231     || $pTransfers
232     || defined $pPseudoTransactions
233     || $pPseudoTransactionsFrom
234     || $pPseudoTransactionsTo
235     || $pMessages
236     || defined $lock_days && $lock_days ne q{}
237     || $labels
238     || $cards
239     || $return_claims
240 ) {
241     print "You did not specify any cleanup work for the script to do.\n\n";
242     usage(1);
243 }
244
245 if ($pDebarments && $allDebarments) {
246     print "You can not specify both --restrictions and --all-restrictions.\n\n";
247     usage(1);
248 }
249
250 say "Confirm flag not passed, running in dry-run mode..." unless $confirm;
251
252 cronlogaction() unless $confirm;
253
254 my $dbh = C4::Context->dbh();
255 my $sth;
256 my $sth2;
257
258 if ( $sessions && !$sess_days ) {
259     if ($verbose) {
260         say "Session purge triggered.";
261         $sth = $dbh->prepare(q{ SELECT COUNT(*) FROM sessions });
262         $sth->execute() or die $dbh->errstr;
263         my @count_arr = $sth->fetchrow_array;
264         say $confirm ? "$count_arr[0] entries will be deleted." : "$count_arr[0] entries would be deleted.";
265     }
266     if ( $confirm ) {
267         $sth = $dbh->prepare(q{ TRUNCATE sessions });
268         $sth->execute() or die $dbh->errstr;
269     }
270     if ($verbose) {
271         print "Done with session purge.\n";
272     }
273 }
274 elsif ( $sessions && $sess_days > 0 ) {
275     print "Session purge triggered with days>$sess_days.\n" if $verbose;
276     RemoveOldSessions() if $confirm;
277     print "Done with session purge with days>$sess_days.\n" if $verbose;
278 }
279
280 if ($zebraqueue_days) {
281     my $count = 0;
282     print "Zebraqueue purge triggered for $zebraqueue_days days.\n" if $verbose;
283     $sth = $dbh->prepare(
284         q{
285             SELECT id,biblio_auth_number,server,time
286             FROM zebraqueue
287             WHERE done=1 AND time < date_sub(curdate(), INTERVAL ? DAY)
288         }
289     );
290     $sth->execute($zebraqueue_days) or die $dbh->errstr;
291     $sth2 = $dbh->prepare(q{ DELETE FROM zebraqueue WHERE id=? });
292     while ( my $record = $sth->fetchrow_hashref ) {
293         if ( $confirm ) {
294             $sth2->execute( $record->{id} ) or die $dbh->errstr;
295         }
296         $count++;
297     }
298     if ( $verbose ) {
299         say $confirm ? "$count records were deleted." : "$count records would have been deleted.";
300         say "Done with zebraqueue purge.";
301     }
302 }
303
304 if ($mail) {
305     my $count = 0;
306     print "Mail queue purge triggered for $mail days.\n" if $verbose;
307     $sth = $dbh->prepare(
308         q{
309             DELETE FROM message_queue
310             WHERE time_queued < date_sub(curdate(), INTERVAL ? DAY)
311         }
312     );
313     if ( $confirm ) {
314         $sth->execute($mail) or die $dbh->errstr;
315         $count = $sth->rows;
316     }
317     if ( $verbose ) {
318         say $confirm ? "$count messages were deleted from the mail queue." : "Message from message_queue would have been deleted";
319         say "Done with message_queue purge.";
320     }
321 }
322
323 if ($purge_merged) {
324     print "Purging completed entries from need_merge_authorities.\n" if $verbose;
325     if ( $confirm ) {
326         $sth = $dbh->prepare(q{ DELETE FROM need_merge_authorities WHERE done=1 });
327         $sth->execute() or die $dbh->errstr;
328     }
329     print "Done with purging need_merge_authorities.\n" if $verbose;
330 }
331
332 if ($pImport) {
333     print "Purging records from import tables.\n" if $verbose;
334     PurgeImportTables() if $confirm;
335     print "Done with purging import tables.\n" if $verbose;
336 }
337
338 if ($pZ3950) {
339     print "Purging Z39.50 records from import tables.\n" if $verbose;
340     PurgeZ3950() if $confirm;
341     print "Done with purging Z39.50 records from import tables.\n" if $verbose;
342 }
343
344 if ($pLogs) {
345     print "Purging records from action_logs.\n" if $verbose;
346     $sth = $dbh->prepare(
347         q{
348             DELETE FROM action_logs
349             WHERE timestamp < date_sub(curdate(), INTERVAL ? DAY)
350         }
351     );
352     if ( $confirm ) {
353         $sth->execute($pLogs) or die $dbh->errstr;
354     }
355     print "Done with purging action_logs.\n" if $verbose;
356 }
357
358 if ($pMessages) {
359     print "Purging messages older than $pMessages days.\n" if $verbose;
360     my $messages = Koha::Patron::Messages->filter_by_last_update(
361         { timestamp_column_name => 'message_date', days => $pMessages } );
362     my $count = $messages->count;
363     $messages->delete if $confirm;
364     if ( $verbose ) {
365         say $confirm
366           ? sprintf( "Done with purging %d messages", $count )
367           : sprintf( "%d messages would have been removed", $count );
368     }
369 }
370
371 if ($fees_days) {
372     print "Purging records from accountlines.\n" if $verbose;
373     purge_zero_balance_fees( $fees_days ) if $confirm;
374     print "Done purging records from accountlines.\n" if $verbose;
375 }
376
377 if ($pSearchhistory) {
378     print "Purging records older than $pSearchhistory from search_history.\n" if $verbose;
379     C4::Search::History::delete({ interval => $pSearchhistory }) if $confirm;
380     print "Done with purging search_history.\n" if $verbose;
381 }
382
383 if ($pListShareInvites) {
384     print "Purging unaccepted list share invites older than $pListShareInvites days.\n" if $verbose;
385     $sth = $dbh->prepare(
386         q{
387             DELETE FROM virtualshelfshares
388             WHERE invitekey IS NOT NULL
389             AND (sharedate + INTERVAL ? DAY) < NOW()
390         }
391     );
392     if ( $confirm ) {
393         $sth->execute($pListShareInvites);
394     }
395     print "Done with purging unaccepted list share invites.\n" if $verbose;
396 }
397
398 if ($pDebarments) {
399     print "Expired patrons restrictions purge triggered for $pDebarments days.\n" if $verbose;
400     my $count = PurgeDebarments($pDebarments, $confirm);
401     if ( $verbose ) {
402         say $confirm ? "$count restrictions were deleted." : "$count restrictions would have been deleted";
403         say "Done with restrictions purge.";
404     }
405 }
406
407 if($allDebarments) {
408     print "All expired patrons restrictions purge triggered.\n" if $verbose;
409     my $count = PurgeDebarments(0, $confirm);
410     if ( $verbose ) {
411         say $confirm ? "$count restrictions were deleted." : "$count restrictions would have been deleted";
412         say "Done with all restrictions purge.";
413     }
414 }
415
416 # Lock expired patrons?
417 if( defined $lock_days && $lock_days ne q{} ) {
418     say "Start locking expired patrons" if $verbose;
419     my $expired_patrons = Koha::Patrons->filter_by_expiration_date({ days => $lock_days })->search({ login_attempts => { '!=' => -1 } });
420     my $count = $expired_patrons->count;
421     $expired_patrons->lock({ remove => 1 }) if $confirm;
422     if( $verbose ) {
423         say $confirm ? sprintf("Locked %d patrons", $count) : sprintf("Found %d patrons", $count);
424     }
425 }
426
427 # Handle unsubscribe requests from GDPR consent form, depends on UnsubscribeReflectionDelay preference
428 say "Start lock unsubscribed, anonymize and delete" if $verbose;
429 my $unsubscribed_patrons = Koha::Patrons->search_unsubscribed;
430 my $count = $unsubscribed_patrons->count;
431 $unsubscribed_patrons->lock( { expire => 1, remove => 1 } ) if $confirm;
432 say $confirm ? sprintf("Locked %d patrons", $count) : sprintf("%d patrons would have been locked", $count) if $verbose;
433
434 # Anonymize patron data, depending on PatronAnonymizeDelay
435 my $anonymize_candidates = Koha::Patrons->search_anonymize_candidates( { locked => 1 } );
436 $count = $anonymize_candidates->count;
437 $anonymize_candidates->anonymize if $confirm;
438 say $confirm ? sprintf("Anonymized %d patrons", $count) : sprintf("%d patrons would have been anonymized", $count) if $verbose;
439
440 # Remove patron data, depending on PatronRemovalDelay (will raise an exception if problem encountered
441 my $anonymized_patrons = Koha::Patrons->search_anonymized;
442 $count = $anonymized_patrons->count;
443 if ( $confirm ) {
444     $anonymized_patrons->delete( { move => 1 } );
445     if ($@) {
446         warn $@;
447     }
448 }
449 if ($verbose) {
450     say $confirm ? sprintf("Deleted %d patrons", $count) : sprintf("%d patrons would have been deleted", $count);
451 }
452
453 # FIXME The output for dry-run mode needs to be improved
454 # But non trivial changes to C4::Members need to be done before.
455 if( $pExpSelfReg ) {
456     if ( $confirm ) {
457         DeleteExpiredSelfRegs();
458     } elsif ( $verbose ) {
459         say "self-registered borrowers may be deleted";
460     }
461 }
462 if( $pUnvSelfReg ) {
463     if ( $confirm ) {
464         DeleteUnverifiedSelfRegs( $pUnvSelfReg );
465     } elsif ( $verbose ) {
466         say "unverified self-registrations may be deleted";
467     }
468 }
469
470 if ($special_holidays_days) {
471     if ( $confirm ) {
472         DeleteSpecialHolidays( abs($special_holidays_days) );
473     } elsif ( $verbose ) {
474         say "self-registered borrowers may be deleted";
475     }
476 }
477
478 if( $temp_uploads ) {
479     # Delete temporary uploads, governed by a pref (unless you override)
480     print "Purging temporary uploads.\n" if $verbose;
481     if ( $confirm ) {
482         Koha::UploadedFiles->delete_temporary({
483             defined($temp_uploads_days)
484                 ? ( override_pref => $temp_uploads_days )
485                 : ()
486         });
487     }
488     print "Done purging temporary uploads.\n" if $verbose;
489 }
490
491 if( defined $uploads_missing ) {
492     print "Looking for missing uploads\n" if $verbose;
493     if ( $confirm ) {
494         my $keep = $uploads_missing == 1 ? 0 : 1;
495         my $count = Koha::UploadedFiles->delete_missing({ keep_record => $keep });
496         if( $keep ) {
497             print "Counted $count missing uploaded files\n";
498         } else {
499             print "Removed $count records for missing uploads\n";
500         }
501     } else {
502         # FIXME need to create a filter_by_missing method (then call ->delete) instead of delete_missing
503         say "Dry-run mode cannot guess how many uploads would have been deleted";
504     }
505 }
506
507 if ($oauth_tokens) {
508     require Koha::OAuthAccessTokens;
509
510     my $tokens = Koha::OAuthAccessTokens->search({ expires => { '<=', time } });
511     my $count = $tokens->count;
512     $tokens->delete if $confirm;
513     if ( $verbose ) {
514         say $confirm
515           ? sprintf( "Removed %d expired OAuth2 tokens", $count )
516           : sprintf( "%d expired OAuth tokens would have been removed", $count );
517     }
518 }
519
520 if ($pStatistics) {
521     print "Purging statistics older than $pStatistics days.\n" if $verbose;
522     my $statistics = Koha::Statistics->filter_by_last_update(
523         { timestamp_column_name => 'datetime', days => $pStatistics } );
524     my $count = $statistics->count;
525     $statistics->delete if $confirm;
526     if ( $verbose ) {
527         say $confirm
528           ? sprintf( "Done with purging %d statistics", $count )
529           : sprintf( "%d statistics would have been removed", $count );
530     }
531 }
532
533 $return_claims = C4::Context->preference('CleanUpDatabaseReturnClaims') if $return_claims;
534 if ($return_claims) {
535     print "Purging return claims older than $return_claims days.\n" if $verbose;
536
537     $return_claims = Koha::Checkouts::ReturnClaims->filter_by_last_update(
538         {
539             timestamp_column_name => 'resolved_on',
540             days => $return_claims
541         }
542     );
543
544     my $count = $return_claims->count;
545     $return_claims->delete if $confirm;
546
547     if ($verbose) {
548         say $confirm
549             ? sprintf "Done with purging %d resolved return claims.", $count
550             : sprintf "%d resolved return claims would have been purged.", $count;
551     }
552 }
553
554 if ($pDeletedCatalog) {
555     print "Purging deleted catalog older than $pDeletedCatalog days.\n"
556       if $verbose;
557     my $old_items = Koha::Old::Items->filter_by_last_update( { days => $pDeletedCatalog } );
558     my $old_biblioitems = Koha::Old::Biblioitems->filter_by_last_update( { days => $pDeletedCatalog } );
559     my $old_biblios = Koha::Old::Biblios->filter_by_last_update( { days => $pDeletedCatalog } );
560     my ( $c_i, $c_bi, $c_b ) =
561       ( $old_items->count, $old_biblioitems->count, $old_biblios->count );
562     if ($confirm) {
563         $old_items->delete;
564         $old_biblioitems->delete;
565         $old_biblios->delete;
566     }
567     if ($verbose) {
568         say sprintf(
569             $confirm
570             ? "Done with purging deleted catalog (%d items, %d biblioitems, %d biblios)."
571             : "Deleted catalog would have been removed (%d items, %d biblioitems, %d biblios).",
572         $c_i, $c_bi, $c_b);
573     }
574 }
575
576 if ($pDeletedPatrons) {
577     print "Purging deleted patrons older than $pDeletedPatrons days.\n" if $verbose;
578     my $old_patrons = Koha::Old::Patrons->filter_by_last_update(
579         { timestamp_column_name => 'updated_on', days => $pDeletedPatrons } );
580     my $count = $old_patrons->count;
581     $old_patrons->delete if $confirm;
582     if ($verbose) {
583         say $confirm
584           ? sprintf "Done with purging %d deleted patrons.", $count
585           : sprintf "%d deleted patrons would have been purged.", $count;
586     }
587 }
588
589 if ($pOldIssues) {
590     print "Purging old checkouts older than $pOldIssues days.\n" if $verbose;
591     my $old_checkouts = Koha::Old::Checkouts->filter_by_last_update( { days => $pOldIssues } );
592     my $count = $old_checkouts->count;
593     $old_checkouts->delete if $confirm;
594     if ($verbose) {
595         say $confirm
596           ? sprintf "Done with purging %d old checkouts.", $count
597           : sprintf "%d old checkouts would have been purged.", $count;
598     }
599 }
600
601 if ($pOldReserves) {
602     print "Purging old reserves older than $pOldReserves days.\n" if $verbose;
603     my $old_reserves = Koha::Old::Holds->filter_by_last_update( { days => $pOldReserves } );
604     my $count = $old_reserves->count;
605     $old_reserves->delete if $verbose;
606     if ($verbose) {
607         say $confirm
608           ? sprintf "Done with purging %d old reserves.", $count
609           : sprintf "%d old reserves would have been purged.", $count;
610     }
611 }
612
613 if ($pTransfers) {
614     print "Purging arrived item transfers older than $pTransfers days.\n" if $verbose;
615     my $transfers = Koha::Item::Transfers->filter_by_last_update(
616         {
617             timestamp_column_name => 'datearrived',
618             days => $pTransfers,
619         }
620     );
621     my $count = $transfers->count;
622     $transfers->delete if $verbose;
623     if ($verbose) {
624         say $confirm
625           ? sprintf "Done with purging %d transfers.", $count
626           : sprintf "%d transfers would have been purged.", $count;
627     }
628 }
629
630 if (defined $pPseudoTransactions or $pPseudoTransactionsFrom or $pPseudoTransactionsTo ) {
631     print "Purging pseudonymized transactions\n" if $verbose;
632     my $anonymized_transactions = Koha::PseudonymizedTransactions->filter_by_last_update(
633         {
634             timestamp_column_name => 'datetime',
635             ( defined $pPseudoTransactions  ? ( days => $pPseudoTransactions     ) : () ),
636             ( $pPseudoTransactionsFrom      ? ( from => $pPseudoTransactionsFrom ) : () ),
637             ( $pPseudoTransactionsTo        ? ( to   => $pPseudoTransactionsTo   ) : () ),
638         }
639     );
640     my $count = $anonymized_transactions->count;
641     $anonymized_transactions->delete if $confirm;
642     if ($verbose) {
643         say $confirm
644           ? sprintf "Done with purging %d pseudonymized transactions.", $count
645           : sprintf "%d pseudonymized transactions would have been purged.", $count;
646     }
647 }
648
649 if ($labels) {
650     print "Purging item label batches last added to more than $labels days ago.\n" if $verbose;
651     my $count = PurgeCreatorBatches($labels, 'labels', $confirm);
652     if ($verbose) {
653         say $confirm
654           ? sprintf "Done with purging %d item label batches last added to more than %d days ago.\n", $count, $labels
655           : sprintf "%d item label batches would have been purged.", $count;
656     }
657 }
658
659 if ($cards) {
660     print "Purging card creator batches last added to more than $cards days ago.\n" if $verbose;
661     my $count = PurgeCreatorBatches($labels, 'patroncards', $confirm);
662     if ($verbose) {
663         say $confirm
664           ? sprintf "Done with purging %d card creator batches last added to more than %d days ago.\n", $count, $labels
665           : sprintf "%d card creator batches would have been purged.", $count;
666     }
667 }
668
669 exit(0);
670
671 sub RemoveOldSessions {
672     my ( $id, $a_session, $limit, $lasttime );
673     $limit = time() - 24 * 3600 * $sess_days;
674
675     $sth = $dbh->prepare(q{ SELECT id, a_session FROM sessions });
676     $sth->execute or die $dbh->errstr;
677     $sth->bind_columns( \$id, \$a_session );
678     $sth2  = $dbh->prepare(q{ DELETE FROM sessions WHERE id=? });
679     my $count = 0;
680
681     while ( $sth->fetch ) {
682         $lasttime = 0;
683         if ( $a_session =~ /lasttime:\s+'?(\d+)/ ) {
684             $lasttime = $1;
685         }
686         elsif ( $a_session =~ /(ATIME|CTIME):\s+'?(\d+)/ ) {
687             $lasttime = $2;
688         }
689         if ( $lasttime && $lasttime < $limit ) {
690             $sth2->execute($id) or die $dbh->errstr;
691             $count++;
692         }
693     }
694     if ($verbose) {
695         print "$count sessions were deleted.\n";
696     }
697 }
698
699 sub PurgeImportTables {
700
701     #First purge import_records
702     #Delete cascades to import_biblios, import_items and import_record_matches
703     $sth = $dbh->prepare(
704         q{
705             DELETE FROM import_records
706             WHERE upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
707         }
708     );
709     $sth->execute($pImport) or die $dbh->errstr;
710
711     # Now purge import_batches
712     # Timestamp cannot be used here without care, because records are added
713     # continuously to batches without updating timestamp (Z39.50 search).
714     # So we only delete older empty batches.
715     # This delete will therefore not have a cascading effect.
716     $sth = $dbh->prepare(
717         q{
718             DELETE ba
719             FROM import_batches ba
720             LEFT JOIN import_records re ON re.import_batch_id=ba.import_batch_id
721             WHERE re.import_record_id IS NULL AND
722             ba.upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
723         }
724     );
725     $sth->execute($pImport) or die $dbh->errstr;
726 }
727
728 sub PurgeZ3950 {
729     $sth = $dbh->prepare(
730         q{
731             DELETE FROM import_batches
732             WHERE batch_type = 'z3950'
733         }
734     );
735     $sth->execute() or die $dbh->errstr;
736 }
737
738 sub PurgeDebarments {
739     require Koha::Patron::Debarments;
740     my ( $days, $doit ) = @_;
741     my $count = 0;
742     $sth   = $dbh->prepare(
743         q{
744             SELECT borrower_debarment_id
745             FROM borrower_debarments
746             WHERE expiration < date_sub(curdate(), INTERVAL ? DAY)
747         }
748     );
749     $sth->execute($days) or die $dbh->errstr;
750     while ( my ($borrower_debarment_id) = $sth->fetchrow_array ) {
751         Koha::Patron::Debarments::DelDebarment($borrower_debarment_id) if $doit;
752         $count++;
753     }
754     return $count;
755 }
756
757 sub PurgeCreatorBatches {
758     require C4::Labels::Batch;
759     my ( $days, $creator, $doit ) = @_;
760     my $count = 0;
761     $sth = $dbh->prepare(
762         q{
763             SELECT batch_id, branch_code FROM creator_batches
764             WHERE batch_id in
765                 (SELECT batch_id
766                 FROM (SELECT batch_id
767                         FROM creator_batches
768                         WHERE creator=?
769                         GROUP BY batch_id
770                         HAVING max(timestamp) <= date_sub(curdate(),interval ? day)) a)
771         }
772     );
773     $sth->execute( $creator, $days ) or die $dbh->errstr;
774     while ( my ( $batch_id, $branch_code ) = $sth->fetchrow_array ) {
775         C4::Labels::Batch::delete(
776             batch_id    => $batch_id,
777             branch_code => $branch_code
778         ) if $doit;
779         $count++;
780     }
781     return $count;
782 }
783
784 sub DeleteExpiredSelfRegs {
785     my $cnt= C4::Members::DeleteExpiredOpacRegistrations();
786     print "Removed $cnt expired self-registered borrowers\n" if $verbose;
787 }
788
789 sub DeleteUnverifiedSelfRegs {
790     my $cnt= C4::Members::DeleteUnverifiedOpacRegistrations( $_[0] );
791     print "Removed $cnt unverified self-registrations\n" if $verbose;
792 }
793
794 sub DeleteSpecialHolidays {
795     my ( $days ) = @_;
796
797     my $sth = $dbh->prepare(q{
798         DELETE FROM special_holidays
799         WHERE DATE( CONCAT( year, '-', month, '-', day ) ) < DATE_SUB( CAST(NOW() AS DATE), INTERVAL ? DAY );
800     });
801     my $count = $sth->execute( $days ) + 0;
802     print "Removed $count unique holidays\n" if $verbose;
803 }