Bug 28962: Add del-unv-selfreg to crontab
[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 use Koha::Script -cron;
32 use C4::Context;
33 use C4::Search;
34 use C4::Search::History;
35 use Getopt::Long qw( GetOptions );
36 use C4::Log qw( cronlogaction );
37 use C4::Accounts qw( purge_zero_balance_fees );
38 use Koha::UploadedFiles;
39 use Koha::Old::Biblios;
40 use Koha::Old::Items;
41 use Koha::Old::Biblioitems;
42 use Koha::Old::Checkouts;
43 use Koha::Old::Holds;
44 use Koha::Old::Patrons;
45 use Koha::Item::Transfers;
46 use Koha::PseudonymizedTransactions;
47 use Koha::Patron::Messages;
48 use Koha::Patron::Debarments qw( DelDebarment );
49
50 sub usage {
51     print STDERR <<USAGE;
52 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]
53
54    -h --help          prints this help message, and exits, ignoring all
55                       other options
56    --confirm          Confirmation flag, the script will be running in dry-run mode is not set.
57    --sessions         purge the sessions table.  If you use this while users 
58                       are logged into Koha, they will have to reconnect.
59    --sessdays DAYS    purge only sessions older than DAYS days.
60    -v --verbose       will cause the script to give you a bit more information
61                       about the run.
62    --zebraqueue DAYS  purge completed zebraqueue entries older than DAYS days.
63                       Defaults to 30 days if no days specified.
64    -m --mail DAYS     purge items from the mail queue that are older than DAYS days.
65                       Defaults to 30 days if no days specified.
66    --merged           purged completed entries from need_merge_authorities.
67    --messages DAYS    purge entries from messages table older than DAYS days.
68                       Defaults to 365 days if no days specified.
69    --import DAYS      purge records from import tables older than DAYS days.
70                       Defaults to 60 days if no days specified.
71    --z3950            purge records from import tables that are the result
72                       of Z39.50 searches
73    --fees DAYS        purge entries accountlines older than DAYS days, where
74                       amountoutstanding is 0 or NULL.
75                       In the case of --fees, DAYS must be greater than
76                       or equal to 1.
77    --log-modules      Specify which action log modules to trim. Repeatable.
78    --preserve-logs    Specify which action logs to exclude. Repeatable.
79    --logs DAYS        purge entries from action_logs older than DAYS days.
80                       Defaults to 180 days if no days specified.
81    --searchhistory DAYS  purge entries from search_history older than DAYS days.
82                          Defaults to 30 days if no days specified
83    --list-invites  DAYS  purge (unaccepted) list share invites older than DAYS
84                          days.  Defaults to 14 days if no days specified.
85    --restrictions DAYS   purge patrons restrictions expired since more than DAYS days.
86                          Defaults to 30 days if no days specified.
87    --all-restrictions   purge all expired patrons restrictions.
88    --del-exp-selfreg  Delete expired self registration accounts
89    --del-unv-selfreg  DAYS  Delete unverified self registrations older than DAYS
90    --unique-holidays DAYS  Delete all unique holidays older than DAYS
91    --temp-uploads     Delete temporary uploads.
92    --temp-uploads-days DAYS Override the corresponding preference value.
93    --uploads-missing FLAG Delete upload records for missing files when FLAG is true, count them otherwise
94    --oauth-tokens     Delete expired OAuth2 tokens
95    --statistics DAYS       Purge statistics entries more than DAYS days old.
96                            This table is used to build reports, make sure you are aware of the consequences of this before using it!
97    --deleted-catalog  DAYS Purge catalog records deleted more then DAYS days ago
98                            (from tables deleteditems, deletedbiblioitems, deletedbiblio_metadata and deletedbiblio).
99    --deleted-patrons DAYS  Purge patrons deleted more than DAYS days ago.
100    --old-issues DAYS       Purge checkouts (old_issues) returned more than DAYS days ago.
101    --old-reserves DAYS     Purge reserves (old_reserves) more than DAYS old.
102    --transfers DAYS        Purge transfers completed more than DAYS day ago.
103    --pseudo-transactions DAYS   Purge the pseudonymized transactions that have been originally created more than DAYS days ago
104                                 DAYS is optional and can be replaced by:
105                                     --pseudo-transactions-from YYYY-MM-DD and/or --pseudo-transactions-to YYYY-MM-DD
106    --labels DAYS           Purge item label batches last added to more than DAYS days ago.
107    --cards DAY             Purge card creator batches last added to more than DAYS days ago.
108    --return-claims         Purge all resolved return claims older than the number of days specified in
109                            the system preference CleanUpDatabaseReturnClaims.
110 USAGE
111     exit $_[0];
112 }
113
114 my $help;
115 my $confirm;
116 my $sessions;
117 my $sess_days;
118 my $verbose;
119 my $zebraqueue_days;
120 my $mail;
121 my $purge_merged;
122 my $pImport;
123 my $pLogs;
124 my $pSearchhistory;
125 my $pZ3950;
126 my $pListShareInvites;
127 my $pDebarments;
128 my $allDebarments;
129 my $return_claims;
130 my $pExpSelfReg;
131 my $pUnvSelfReg;
132 my $fees_days;
133 my $special_holidays_days;
134 my $temp_uploads;
135 my $temp_uploads_days;
136 my $uploads_missing;
137 my $oauth_tokens;
138 my $pStatistics;
139 my $pDeletedCatalog;
140 my $pDeletedPatrons;
141 my $pOldIssues;
142 my $pOldReserves;
143 my $pTransfers;
144 my ( $pPseudoTransactions, $pPseudoTransactionsFrom, $pPseudoTransactionsTo );
145 my $pMessages;
146 my $lock_days = C4::Context->preference('LockExpiredDelay');
147 my $labels;
148 my $cards;
149 my @log_modules;
150 my @preserve_logs;
151
152 GetOptions(
153     'h|help'            => \$help,
154     'confirm'           => \$confirm,
155     'sessions'          => \$sessions,
156     'sessdays:i'        => \$sess_days,
157     'v|verbose'         => \$verbose,
158     'm|mail:i'          => \$mail,
159     'zebraqueue:i'      => \$zebraqueue_days,
160     'merged'            => \$purge_merged,
161     'import:i'          => \$pImport,
162     'z3950'             => \$pZ3950,
163     'logs:i'            => \$pLogs,
164     'log-module:s'      => \@log_modules,
165     'preserve-log:s'    => \@preserve_logs,
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:i' => \$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     my $log_query = q{
347             DELETE FROM action_logs
348             WHERE timestamp < date_sub(curdate(), INTERVAL ? DAY)
349     };
350     my @query_params = ();
351     if( @preserve_logs ){
352         $log_query .= " AND module NOT IN (" . join(',',('?') x @preserve_logs ) . ")";
353         push @query_params, @preserve_logs;
354     }
355     if( @log_modules ){
356         $log_query .= " AND module IN (" . join(',',('?') x @log_modules ) . ")";
357         push @query_params, @log_modules;
358     }
359     $sth = $dbh->prepare( $log_query );
360     if ( $confirm ) {
361         $sth->execute($pLogs, @query_params) or die $dbh->errstr;
362     }
363     print "Done with purging action_logs.\n" if $verbose;
364 }
365
366 if ($pMessages) {
367     print "Purging messages older than $pMessages days.\n" if $verbose;
368     my $messages = Koha::Patron::Messages->filter_by_last_update(
369         { timestamp_column_name => 'message_date', days => $pMessages } );
370     my $count = $messages->count;
371     $messages->delete if $confirm;
372     if ( $verbose ) {
373         say $confirm
374           ? sprintf( "Done with purging %d messages", $count )
375           : sprintf( "%d messages would have been removed", $count );
376     }
377 }
378
379 if ($fees_days) {
380     print "Purging records from accountlines.\n" if $verbose;
381     purge_zero_balance_fees( $fees_days ) if $confirm;
382     print "Done purging records from accountlines.\n" if $verbose;
383 }
384
385 if ($pSearchhistory) {
386     print "Purging records older than $pSearchhistory from search_history.\n" if $verbose;
387     C4::Search::History::delete({ interval => $pSearchhistory }) if $confirm;
388     print "Done with purging search_history.\n" if $verbose;
389 }
390
391 if ($pListShareInvites) {
392     print "Purging unaccepted list share invites older than $pListShareInvites days.\n" if $verbose;
393     $sth = $dbh->prepare(
394         q{
395             DELETE FROM virtualshelfshares
396             WHERE invitekey IS NOT NULL
397             AND (sharedate + INTERVAL ? DAY) < NOW()
398         }
399     );
400     if ( $confirm ) {
401         $sth->execute($pListShareInvites);
402     }
403     print "Done with purging unaccepted list share invites.\n" if $verbose;
404 }
405
406 if ($pDebarments) {
407     print "Expired patrons restrictions purge triggered for $pDebarments days.\n" if $verbose;
408     my $count = PurgeDebarments($pDebarments, $confirm);
409     if ( $verbose ) {
410         say $confirm ? "$count restrictions were deleted." : "$count restrictions would have been deleted";
411         say "Done with restrictions purge.";
412     }
413 }
414
415 if($allDebarments) {
416     print "All expired patrons restrictions purge triggered.\n" if $verbose;
417     my $count = PurgeDebarments(0, $confirm);
418     if ( $verbose ) {
419         say $confirm ? "$count restrictions were deleted." : "$count restrictions would have been deleted";
420         say "Done with all restrictions purge.";
421     }
422 }
423
424 # Lock expired patrons?
425 if( defined $lock_days && $lock_days ne q{} ) {
426     say "Start locking expired patrons" if $verbose;
427     my $expired_patrons = Koha::Patrons->filter_by_expiration_date({ days => $lock_days })->search({ login_attempts => { '!=' => -1 } });
428     my $count = $expired_patrons->count;
429     $expired_patrons->lock({ remove => 1 }) if $confirm;
430     if( $verbose ) {
431         say $confirm ? sprintf("Locked %d patrons", $count) : sprintf("Found %d patrons", $count);
432     }
433 }
434
435 # Handle unsubscribe requests from GDPR consent form, depends on UnsubscribeReflectionDelay preference
436 say "Start lock unsubscribed, anonymize and delete" if $verbose;
437 my $unsubscribed_patrons = Koha::Patrons->search_unsubscribed;
438 my $count = $unsubscribed_patrons->count;
439 $unsubscribed_patrons->lock( { expire => 1, remove => 1 } ) if $confirm;
440 say $confirm ? sprintf("Locked %d patrons", $count) : sprintf("%d patrons would have been locked", $count) if $verbose;
441
442 # Anonymize patron data, depending on PatronAnonymizeDelay
443 my $anonymize_candidates = Koha::Patrons->search_anonymize_candidates( { locked => 1 } );
444 $count = $anonymize_candidates->count;
445 $anonymize_candidates->anonymize if $confirm;
446 say $confirm ? sprintf("Anonymized %d patrons", $count) : sprintf("%d patrons would have been anonymized", $count) if $verbose;
447
448 # Remove patron data, depending on PatronRemovalDelay (will raise an exception if problem encountered
449 my $anonymized_patrons = Koha::Patrons->search_anonymized;
450 $count = $anonymized_patrons->count;
451 if ( $confirm ) {
452     $anonymized_patrons->delete( { move => 1 } );
453     if ($@) {
454         warn $@;
455     }
456 }
457 if ($verbose) {
458     say $confirm ? sprintf("Deleted %d patrons", $count) : sprintf("%d patrons would have been deleted", $count);
459 }
460
461 # FIXME The output for dry-run mode needs to be improved
462 # But non trivial changes to C4::Members need to be done before.
463 if( $pExpSelfReg ) {
464     if ( $confirm ) {
465         DeleteExpiredSelfRegs();
466     } elsif ( $verbose ) {
467         say "self-registered borrowers may be deleted";
468     }
469 }
470 if( $pUnvSelfReg ) {
471     if ( $confirm ) {
472         DeleteUnverifiedSelfRegs( $pUnvSelfReg );
473     } elsif ( $verbose ) {
474         say "unverified self-registrations may be deleted";
475     }
476 }
477
478 if ($special_holidays_days) {
479     if ( $confirm ) {
480         DeleteSpecialHolidays( abs($special_holidays_days) );
481     } elsif ( $verbose ) {
482         say "self-registered borrowers may be deleted";
483     }
484 }
485
486 if( $temp_uploads ) {
487     # Delete temporary uploads, governed by a pref (unless you override)
488     print "Purging temporary uploads.\n" if $verbose;
489     if ( $confirm ) {
490         Koha::UploadedFiles->delete_temporary({
491             defined($temp_uploads_days)
492                 ? ( override_pref => $temp_uploads_days )
493                 : ()
494         });
495     }
496     print "Done purging temporary uploads.\n" if $verbose;
497 }
498
499 if( defined $uploads_missing ) {
500     print "Looking for missing uploads\n" if $verbose;
501     if ( $confirm ) {
502         my $keep = $uploads_missing == 1 ? 0 : 1;
503         my $count = Koha::UploadedFiles->delete_missing({ keep_record => $keep });
504         if( $keep ) {
505             print "Counted $count missing uploaded files\n";
506         } else {
507             print "Removed $count records for missing uploads\n";
508         }
509     } else {
510         # FIXME need to create a filter_by_missing method (then call ->delete) instead of delete_missing
511         say "Dry-run mode cannot guess how many uploads would have been deleted";
512     }
513 }
514
515 if ($oauth_tokens) {
516     require Koha::OAuthAccessTokens;
517
518     my $tokens = Koha::OAuthAccessTokens->search({ expires => { '<=', time } });
519     my $count = $tokens->count;
520     $tokens->delete if $confirm;
521     if ( $verbose ) {
522         say $confirm
523           ? sprintf( "Removed %d expired OAuth2 tokens", $count )
524           : sprintf( "%d expired OAuth tokens would have been removed", $count );
525     }
526 }
527
528 if ($pStatistics) {
529     print "Purging statistics older than $pStatistics days.\n" if $verbose;
530     my $statistics = Koha::Statistics->filter_by_last_update(
531         { timestamp_column_name => 'datetime', days => $pStatistics } );
532     my $count = $statistics->count;
533     $statistics->delete if $confirm;
534     if ( $verbose ) {
535         say $confirm
536           ? sprintf( "Done with purging %d statistics", $count )
537           : sprintf( "%d statistics would have been removed", $count );
538     }
539 }
540
541 if( $return_claims && ( my $days = C4::Context->preference('CleanUpDatabaseReturnClaims') )) {
542     print "Purging return claims older than $days days.\n" if $verbose;
543
544     $return_claims = Koha::Checkouts::ReturnClaims->filter_by_last_update(
545         {
546             timestamp_column_name => 'resolved_on',
547             days => $days,
548         }
549     );
550
551     my $count = $return_claims->count;
552     $return_claims->delete if $confirm;
553
554     if ($verbose) {
555         say $confirm
556             ? sprintf "Done with purging %d resolved return claims.", $count
557             : sprintf "%d resolved return claims would have been purged.", $count;
558     }
559 }
560
561 if ($pDeletedCatalog) {
562     print "Purging deleted catalog older than $pDeletedCatalog days.\n"
563       if $verbose;
564     my $old_items = Koha::Old::Items->filter_by_last_update( { days => $pDeletedCatalog } );
565     my $old_biblioitems = Koha::Old::Biblioitems->filter_by_last_update( { days => $pDeletedCatalog } );
566     my $old_biblios = Koha::Old::Biblios->filter_by_last_update( { days => $pDeletedCatalog } );
567     my ( $c_i, $c_bi, $c_b ) =
568       ( $old_items->count, $old_biblioitems->count, $old_biblios->count );
569     if ($confirm) {
570         $old_items->delete;
571         $old_biblioitems->delete;
572         $old_biblios->delete;
573     }
574     if ($verbose) {
575         say sprintf(
576             $confirm
577             ? "Done with purging deleted catalog (%d items, %d biblioitems, %d biblios)."
578             : "Deleted catalog would have been removed (%d items, %d biblioitems, %d biblios).",
579         $c_i, $c_bi, $c_b);
580     }
581 }
582
583 if ($pDeletedPatrons) {
584     print "Purging deleted patrons older than $pDeletedPatrons days.\n" if $verbose;
585     my $old_patrons = Koha::Old::Patrons->filter_by_last_update(
586         { timestamp_column_name => 'updated_on', days => $pDeletedPatrons } );
587     my $count = $old_patrons->count;
588     $old_patrons->delete if $confirm;
589     if ($verbose) {
590         say $confirm
591           ? sprintf "Done with purging %d deleted patrons.", $count
592           : sprintf "%d deleted patrons would have been purged.", $count;
593     }
594 }
595
596 if ($pOldIssues) {
597     print "Purging old checkouts older than $pOldIssues days.\n" if $verbose;
598     my $old_checkouts = Koha::Old::Checkouts->filter_by_last_update( { days => $pOldIssues } );
599     my $count = $old_checkouts->count;
600     $old_checkouts->delete if $confirm;
601     if ($verbose) {
602         say $confirm
603           ? sprintf "Done with purging %d old checkouts.", $count
604           : sprintf "%d old checkouts would have been purged.", $count;
605     }
606 }
607
608 if ($pOldReserves) {
609     print "Purging old reserves older than $pOldReserves days.\n" if $verbose;
610     my $old_reserves = Koha::Old::Holds->filter_by_last_update( { days => $pOldReserves } );
611     my $count = $old_reserves->count;
612     $old_reserves->delete if $verbose;
613     if ($verbose) {
614         say $confirm
615           ? sprintf "Done with purging %d old reserves.", $count
616           : sprintf "%d old reserves would have been purged.", $count;
617     }
618 }
619
620 if ($pTransfers) {
621     print "Purging arrived item transfers older than $pTransfers days.\n" if $verbose;
622     my $transfers = Koha::Item::Transfers->filter_by_last_update(
623         {
624             timestamp_column_name => 'datearrived',
625             days => $pTransfers,
626         }
627     );
628     my $count = $transfers->count;
629     $transfers->delete if $verbose;
630     if ($verbose) {
631         say $confirm
632           ? sprintf "Done with purging %d transfers.", $count
633           : sprintf "%d transfers would have been purged.", $count;
634     }
635 }
636
637 if (defined $pPseudoTransactions or $pPseudoTransactionsFrom or $pPseudoTransactionsTo ) {
638     print "Purging pseudonymized transactions\n" if $verbose;
639     my $anonymized_transactions = Koha::PseudonymizedTransactions->filter_by_last_update(
640         {
641             timestamp_column_name => 'datetime',
642             ( defined $pPseudoTransactions  ? ( days => $pPseudoTransactions     ) : () ),
643             ( $pPseudoTransactionsFrom      ? ( from => $pPseudoTransactionsFrom ) : () ),
644             ( $pPseudoTransactionsTo        ? ( to   => $pPseudoTransactionsTo   ) : () ),
645         }
646     );
647     my $count = $anonymized_transactions->count;
648     $anonymized_transactions->delete if $confirm;
649     if ($verbose) {
650         say $confirm
651           ? sprintf "Done with purging %d pseudonymized transactions.", $count
652           : sprintf "%d pseudonymized transactions would have been purged.", $count;
653     }
654 }
655
656 if ($labels) {
657     print "Purging item label batches last added to more than $labels days ago.\n" if $verbose;
658     my $count = PurgeCreatorBatches($labels, 'labels', $confirm);
659     if ($verbose) {
660         say $confirm
661           ? sprintf "Done with purging %d item label batches last added to more than %d days ago.\n", $count, $labels
662           : sprintf "%d item label batches would have been purged.", $count;
663     }
664 }
665
666 if ($cards) {
667     print "Purging card creator batches last added to more than $cards days ago.\n" if $verbose;
668     my $count = PurgeCreatorBatches($labels, 'patroncards', $confirm);
669     if ($verbose) {
670         say $confirm
671           ? sprintf "Done with purging %d card creator batches last added to more than %d days ago.\n", $count, $labels
672           : sprintf "%d card creator batches would have been purged.", $count;
673     }
674 }
675
676 exit(0);
677
678 sub RemoveOldSessions {
679     my ( $id, $a_session, $limit, $lasttime );
680     $limit = time() - 24 * 3600 * $sess_days;
681
682     $sth = $dbh->prepare(q{ SELECT id, a_session FROM sessions });
683     $sth->execute or die $dbh->errstr;
684     $sth->bind_columns( \$id, \$a_session );
685     $sth2  = $dbh->prepare(q{ DELETE FROM sessions WHERE id=? });
686     my $count = 0;
687
688     while ( $sth->fetch ) {
689         $lasttime = 0;
690         if ( $a_session =~ /lasttime:\s+'?(\d+)/ ) {
691             $lasttime = $1;
692         }
693         elsif ( $a_session =~ /(ATIME|CTIME):\s+'?(\d+)/ ) {
694             $lasttime = $2;
695         }
696         if ( $lasttime && $lasttime < $limit ) {
697             $sth2->execute($id) or die $dbh->errstr;
698             $count++;
699         }
700     }
701     if ($verbose) {
702         print "$count sessions were deleted.\n";
703     }
704 }
705
706 sub PurgeImportTables {
707
708     #First purge import_records
709     #Delete cascades to import_biblios, import_items and import_record_matches
710     $sth = $dbh->prepare(
711         q{
712             DELETE FROM import_records
713             WHERE upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
714         }
715     );
716     $sth->execute($pImport) or die $dbh->errstr;
717
718     # Now purge import_batches
719     # Timestamp cannot be used here without care, because records are added
720     # continuously to batches without updating timestamp (Z39.50 search).
721     # So we only delete older empty batches.
722     # This delete will therefore not have a cascading effect.
723     $sth = $dbh->prepare(
724         q{
725             DELETE ba
726             FROM import_batches ba
727             LEFT JOIN import_records re ON re.import_batch_id=ba.import_batch_id
728             WHERE re.import_record_id IS NULL AND
729             ba.upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
730         }
731     );
732     $sth->execute($pImport) or die $dbh->errstr;
733 }
734
735 sub PurgeZ3950 {
736     $sth = $dbh->prepare(
737         q{
738             DELETE FROM import_batches
739             WHERE batch_type = 'z3950'
740         }
741     );
742     $sth->execute() or die $dbh->errstr;
743 }
744
745 sub PurgeDebarments {
746     require Koha::Patron::Debarments;
747     my ( $days, $doit ) = @_;
748     my $count = 0;
749     $sth   = $dbh->prepare(
750         q{
751             SELECT borrower_debarment_id
752             FROM borrower_debarments
753             WHERE expiration < date_sub(curdate(), INTERVAL ? DAY)
754         }
755     );
756     $sth->execute($days) or die $dbh->errstr;
757     while ( my ($borrower_debarment_id) = $sth->fetchrow_array ) {
758         Koha::Patron::Debarments::DelDebarment($borrower_debarment_id) if $doit;
759         $count++;
760     }
761     return $count;
762 }
763
764 sub PurgeCreatorBatches {
765     require C4::Labels::Batch;
766     my ( $days, $creator, $doit ) = @_;
767     my $count = 0;
768     $sth = $dbh->prepare(
769         q{
770             SELECT batch_id, branch_code FROM creator_batches
771             WHERE batch_id in
772                 (SELECT batch_id
773                 FROM (SELECT batch_id
774                         FROM creator_batches
775                         WHERE creator=?
776                         GROUP BY batch_id
777                         HAVING max(timestamp) <= date_sub(curdate(),interval ? day)) a)
778         }
779     );
780     $sth->execute( $creator, $days ) or die $dbh->errstr;
781     while ( my ( $batch_id, $branch_code ) = $sth->fetchrow_array ) {
782         C4::Labels::Batch::delete(
783             batch_id    => $batch_id,
784             branch_code => $branch_code
785         ) if $doit;
786         $count++;
787     }
788     return $count;
789 }
790
791 sub DeleteExpiredSelfRegs {
792     my $cnt= C4::Members::DeleteExpiredOpacRegistrations();
793     print "Removed $cnt expired self-registered borrowers\n" if $verbose;
794 }
795
796 sub DeleteUnverifiedSelfRegs {
797     my $cnt= C4::Members::DeleteUnverifiedOpacRegistrations( $_[0] );
798     print "Removed $cnt unverified self-registrations\n" if $verbose;
799 }
800
801 sub DeleteSpecialHolidays {
802     my ( $days ) = @_;
803
804     my $sth = $dbh->prepare(q{
805         DELETE FROM special_holidays
806         WHERE DATE( CONCAT( year, '-', month, '-', day ) ) < DATE_SUB( CAST(NOW() AS DATE), INTERVAL ? DAY );
807     });
808     my $count = $sth->execute( $days ) + 0;
809     print "Removed $count unique holidays\n" if $verbose;
810 }