Increment version for 23.11.05 release
[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 use constant DEFAULT_JOBS_PURGEDAYS               => 1;
31 use constant DEFAULT_JOBS_PURGETYPES              => qw{ update_elastic_index };
32 use constant DEFAULT_EDIFACT_MSG_PURGEDAYS        => 365;
33
34 use Getopt::Long qw( GetOptions );
35
36 use Koha::Script -cron;
37
38 use C4::Accounts qw( purge_zero_balance_fees );
39 use C4::Context;
40 use C4::Log qw( cronlogaction );
41 use C4::Search::History;
42 use C4::Search;
43 use Koha::BackgroundJobs;
44 use Koha::Database;
45 use Koha::DateUtils qw( dt_from_string );
46 use Koha::Item::Transfers;
47 use Koha::Old::Biblioitems;
48 use Koha::Old::Biblios;
49 use Koha::Old::Checkouts;
50 use Koha::Old::Holds;
51 use Koha::Old::Items;
52 use Koha::Old::Patrons;
53 use Koha::Patron::Debarments qw( DelDebarment );
54 use Koha::Patron::Messages;
55 use Koha::PseudonymizedTransactions;
56 use Koha::UploadedFiles;
57
58 sub usage {
59     print STDERR <<USAGE;
60 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] [--bg-days DAYS [--bg-type TYPE] ] [--edifact-messages DAYS]
61
62    -h --help          prints this help message, and exits, ignoring all
63                       other options
64    --confirm          Confirmation flag, the script will be running in dry-run mode is not set.
65    --sessions         purge the sessions table.  If you use this while users 
66                       are logged into Koha, they will have to reconnect.
67    --sessdays DAYS    purge only sessions older than DAYS days.
68    -v --verbose       will cause the script to give you a bit more information
69                       about the run.
70    --zebraqueue DAYS  purge completed zebraqueue entries older than DAYS days.
71                       Defaults to 30 days if no days specified.
72    -m --mail DAYS     purge items from the mail queue that are older than DAYS days.
73                       Defaults to 30 days if no days specified.
74    --merged           purged completed entries from need_merge_authorities.
75    --messages DAYS    purge entries from messages table older than DAYS days.
76                       Defaults to 365 days if no days specified.
77    --import DAYS      purge records from import tables older than DAYS days.
78                       Defaults to 60 days if no days specified.
79    --z3950            purge records from import tables that are the result
80                       of Z39.50 searches
81    --fees DAYS        purge entries accountlines older than DAYS days, where
82                       amountoutstanding is 0 or NULL.
83                       In the case of --fees, DAYS must be greater than
84                       or equal to 1.
85    --log-module       Specify which action log modules to trim. Repeatable.
86    --preserve-log     Specify which action logs to exclude. Repeatable.
87    --log-action       Specify which action log action entries to trim. Repeatable.
88    --logs DAYS        purge entries from action_logs older than DAYS days.
89                       Defaults to 180 days if no days specified.
90    --searchhistory DAYS  purge entries from search_history older than DAYS days.
91                          Defaults to 30 days if no days specified
92    --list-invites  DAYS  purge (unaccepted) list share invites older than DAYS days.
93                          This parameter is prioritised over the
94                          PurgeListShareInvitesOlderThan system preference.
95                          Defaults to 14 days if no days specified for this parameter and
96                          the PurgeListShareInvitesOlderThan system preference is empty.
97    --restrictions DAYS   purge patrons restrictions expired since more than DAYS days.
98                          Defaults to 30 days if no days specified.
99    --all-restrictions   purge all expired patrons restrictions.
100    --del-exp-selfreg  Delete expired self registration accounts
101    --del-unv-selfreg  DAYS  Delete unverified self registrations older than DAYS
102    --unique-holidays DAYS  Delete all unique holidays older than DAYS
103    --temp-uploads     Delete temporary uploads.
104    --temp-uploads-days DAYS Override the corresponding preference value.
105    --uploads-missing FLAG Delete upload records for missing files when FLAG is true, count them otherwise
106    --oauth-tokens     Delete expired OAuth2 tokens
107    --statistics DAYS       Purge statistics entries more than DAYS days old.
108                            This table is used to build reports, make sure you are aware of the consequences of this before using it!
109    --deleted-catalog  DAYS Purge catalog records deleted more then DAYS days ago
110                            (from tables deleteditems, deletedbiblioitems, deletedbiblio_metadata and deletedbiblio).
111    --deleted-patrons DAYS  Purge patrons deleted more than DAYS days ago.
112    --old-issues DAYS       Purge checkouts (old_issues) returned more than DAYS days ago.
113    --old-reserves DAYS     Purge reserves (old_reserves) more than DAYS old.
114    --transfers DAYS        Purge transfers completed more than DAYS day ago.
115    --pseudo-transactions DAYS   Purge the pseudonymized transactions that have been originally created more than DAYS days ago
116                                 DAYS is optional and can be replaced by:
117                                     --pseudo-transactions-from YYYY-MM-DD and/or --pseudo-transactions-to YYYY-MM-DD
118    --labels DAYS           Purge item label batches last added to more than DAYS days ago.
119    --cards DAY             Purge card creator batches last added to more than DAYS days ago.
120    --return-claims         Purge all resolved return claims older than the number of days specified in
121                            the system preference CleanUpDatabaseReturnClaims.
122    --jobs-days DAYS        Purge all finished background jobs this many days old. Defaults to 1 if no DAYS provided.
123    --jobs-type TYPES       What type of background job to purge. Defaults to "update_elastic_index" if omitted
124                            Specifying "all" will purge all types. Repeatable.
125    --reports DAYS          Purge reports data saved more than DAYS days ago. The data is created by running runreport.pl with the --store-results option.
126    --edifact-messages DAYS   Purge process and failed EDIFACT messages handled more than DAYS days.
127                              Defaults to 365 days if no days specified.
128 USAGE
129     exit $_[0];
130 }
131
132 my $help;
133 my $confirm;
134 my $sessions;
135 my $sess_days;
136 my $verbose;
137 my $zebraqueue_days;
138 my $mail;
139 my $purge_merged;
140 my $pImport;
141 my $pLogs;
142 my $pSearchhistory;
143 my $pZ3950;
144 my $pListShareInvites;
145 my $pDebarments;
146 my $allDebarments;
147 my $return_claims;
148 my $pExpSelfReg;
149 my $pUnvSelfReg;
150 my $fees_days;
151 my $special_holidays_days;
152 my $temp_uploads;
153 my $temp_uploads_days;
154 my $uploads_missing;
155 my $oauth_tokens;
156 my $pStatistics;
157 my $pDeletedCatalog;
158 my $pDeletedPatrons;
159 my $pOldIssues;
160 my $pOldReserves;
161 my $pTransfers;
162 my ( $pPseudoTransactions, $pPseudoTransactionsFrom, $pPseudoTransactionsTo );
163 my $pMessages;
164 my $lock_days = C4::Context->preference('LockExpiredDelay');
165 my $labels;
166 my $cards;
167 my @log_modules;
168 my @preserve_logs;
169 my @log_actions;
170 my $jobs_days;
171 my @jobs_types;
172 my $reports;
173 my $edifact_msg_days;
174
175 my $command_line_options = join( " ", @ARGV );
176
177 GetOptions(
178     'h|help'                     => \$help,
179     'confirm'                    => \$confirm,
180     'sessions'                   => \$sessions,
181     'sessdays:i'                 => \$sess_days,
182     'v|verbose'                  => \$verbose,
183     'm|mail:i'                   => \$mail,
184     'zebraqueue:i'               => \$zebraqueue_days,
185     'merged'                     => \$purge_merged,
186     'import:i'                   => \$pImport,
187     'z3950'                      => \$pZ3950,
188     'logs:i'                     => \$pLogs,
189     'log-module:s'               => \@log_modules,
190     'preserve-log:s'             => \@preserve_logs,
191     'log-action:s'               => \@log_actions,
192     'messages:i'                 => \$pMessages,
193     'fees:i'                     => \$fees_days,
194     'searchhistory:i'            => \$pSearchhistory,
195     'list-invites:i'             => \$pListShareInvites,
196     'restrictions:i'             => \$pDebarments,
197     'all-restrictions'           => \$allDebarments,
198     'del-exp-selfreg'            => \$pExpSelfReg,
199     'del-unv-selfreg:i'          => \$pUnvSelfReg,
200     'unique-holidays:i'          => \$special_holidays_days,
201     'temp-uploads'               => \$temp_uploads,
202     'temp-uploads-days:i'        => \$temp_uploads_days,
203     'uploads-missing:i'          => \$uploads_missing,
204     'oauth-tokens'               => \$oauth_tokens,
205     'statistics:i'               => \$pStatistics,
206     'deleted-catalog:i'          => \$pDeletedCatalog,
207     'deleted-patrons:i'          => \$pDeletedPatrons,
208     'old-issues:i'               => \$pOldIssues,
209     'old-reserves:i'             => \$pOldReserves,
210     'transfers:i'                => \$pTransfers,
211     'pseudo-transactions:i'      => \$pPseudoTransactions,
212     'pseudo-transactions-from:s' => \$pPseudoTransactionsFrom,
213     'pseudo-transactions-to:s'   => \$pPseudoTransactionsTo,
214     'labels'                     => \$labels,
215     'cards'                      => \$cards,
216     'return-claims'              => \$return_claims,
217     'jobs-type:s'                => \@jobs_types,
218     'jobs-days:i'                => \$jobs_days,
219     'reports:i'                  => \$reports,
220     'edifact-messages:i'         => \$edifact_msg_days,
221 ) || usage(1);
222
223 # Use default values
224 $sessions         = 1                               if $sess_days                 && $sess_days > 0;
225 $pImport          = DEFAULT_IMPORT_PURGEDAYS        if defined($pImport)          && $pImport == 0;
226 $pLogs            = DEFAULT_LOGS_PURGEDAYS          if defined($pLogs)            && $pLogs == 0;
227 $zebraqueue_days  = DEFAULT_ZEBRAQ_PURGEDAYS        if defined($zebraqueue_days)  && $zebraqueue_days == 0;
228 $mail             = DEFAULT_MAIL_PURGEDAYS          if defined($mail)             && $mail == 0;
229 $pSearchhistory   = DEFAULT_SEARCHHISTORY_PURGEDAYS if defined($pSearchhistory)   && $pSearchhistory == 0;
230 $pDebarments      = DEFAULT_DEBARMENTS_PURGEDAYS    if defined($pDebarments)      && $pDebarments == 0;
231 $pMessages        = DEFAULT_MESSAGES_PURGEDAYS      if defined($pMessages)        && $pMessages == 0;
232 $jobs_days        = DEFAULT_JOBS_PURGEDAYS          if defined($jobs_days)        && $jobs_days == 0;
233 @jobs_types       = (DEFAULT_JOBS_PURGETYPES)       if $jobs_days                 && @jobs_types == 0;
234 $edifact_msg_days = DEFAULT_EDIFACT_MSG_PURGEDAYS   if defined($edifact_msg_days) && $edifact_msg_days == 0;
235
236 # Choose the number of days at which to purge unaccepted list invites:
237 # - DAYS defined in the list-invites parameter is prioritised first
238 # - PurgeListShareInvitesOlderThan system preference is prioritised second
239 # - Default value of 14 days is prioritised last - if the list-invites parameter is passed without a DAYS value and the PurgeListShareInvitesOlderThan syspref is empty.
240 if ( !defined($pListShareInvites) ) {
241     if ( C4::Context->preference('PurgeListShareInvitesOlderThan') ) {
242         $pListShareInvites = C4::Context->preference('PurgeListShareInvitesOlderThan');
243     }
244 } elsif ( defined($pListShareInvites) && $pListShareInvites == 0 ) {
245     $pListShareInvites = DEFAULT_SHARE_INVITATION_EXPIRY_DAYS;
246 }
247
248 if ($help) {
249     usage(0);
250 }
251
252 unless ( $sessions
253     || $zebraqueue_days
254     || $mail
255     || $purge_merged
256     || $pImport
257     || $pLogs
258     || $fees_days
259     || $pSearchhistory
260     || $pZ3950
261     || $pListShareInvites
262     || $pDebarments
263     || $allDebarments
264     || $pExpSelfReg
265     || $pUnvSelfReg
266     || $special_holidays_days
267     || $temp_uploads
268     || defined $uploads_missing
269     || $oauth_tokens
270     || $pStatistics
271     || $pDeletedCatalog
272     || $pDeletedPatrons
273     || $pOldIssues
274     || $pOldReserves
275     || $pTransfers
276     || defined $pPseudoTransactions
277     || $pPseudoTransactionsFrom
278     || $pPseudoTransactionsTo
279     || $pMessages
280     || defined $lock_days && $lock_days ne q{}
281     || $labels
282     || $cards
283     || $return_claims
284     || $jobs_days
285     || $reports
286     || $edifact_msg_days )
287 {
288     print "You did not specify any cleanup work for the script to do.\n\n";
289     usage(1);
290 }
291
292 if ( $pDebarments && $allDebarments ) {
293     print "You can not specify both --restrictions and --all-restrictions.\n\n";
294     usage(1);
295 }
296
297 say "Confirm flag not passed, running in dry-run mode..." unless $confirm;
298
299 cronlogaction( { info => $command_line_options } );
300
301 my $dbh = C4::Context->dbh();
302 my $sth;
303 my $sth2;
304
305 if ( $sessions && !$sess_days ) {
306     if ($verbose) {
307         say "Session purge triggered.";
308         $sth = $dbh->prepare(q{ SELECT COUNT(*) FROM sessions });
309         $sth->execute() or die $dbh->errstr;
310         my @count_arr = $sth->fetchrow_array;
311         say $confirm ? "$count_arr[0] entries will be deleted." : "$count_arr[0] entries would be deleted.";
312     }
313     if ($confirm) {
314         $sth = $dbh->prepare(q{ TRUNCATE sessions });
315         $sth->execute() or die $dbh->errstr;
316     }
317     if ($verbose) {
318         print "Done with session purge.\n";
319     }
320 } elsif ( $sessions && $sess_days > 0 ) {
321     print "Session purge triggered with days>$sess_days.\n" if $verbose;
322     RemoveOldSessions()                                     if $confirm;
323     print "Done with session purge with days>$sess_days.\n" if $verbose;
324 }
325
326 if ($zebraqueue_days) {
327     my $count = 0;
328     print "Zebraqueue purge triggered for $zebraqueue_days days.\n" if $verbose;
329     $sth = $dbh->prepare(
330         q{
331             SELECT id,biblio_auth_number,server,time
332             FROM zebraqueue
333             WHERE done=1 AND time < date_sub(curdate(), INTERVAL ? DAY)
334         }
335     );
336     $sth->execute($zebraqueue_days) or die $dbh->errstr;
337     $sth2 = $dbh->prepare(q{ DELETE FROM zebraqueue WHERE id=? });
338     while ( my $record = $sth->fetchrow_hashref ) {
339         if ($confirm) {
340             $sth2->execute( $record->{id} ) or die $dbh->errstr;
341         }
342         $count++;
343     }
344     if ($verbose) {
345         say $confirm ? "$count records were deleted." : "$count records would have been deleted.";
346         say "Done with zebraqueue purge.";
347     }
348 }
349
350 if ($mail) {
351     my $count = 0;
352     print "Mail queue purge triggered for $mail days.\n" if $verbose;
353     $sth = $dbh->prepare(
354         q{
355             DELETE FROM message_queue
356             WHERE time_queued < date_sub(curdate(), INTERVAL ? DAY)
357         }
358     );
359     if ($confirm) {
360         $sth->execute($mail) or die $dbh->errstr;
361         $count = $sth->rows;
362     }
363     if ($verbose) {
364         say $confirm
365             ? "$count messages were deleted from the mail queue."
366             : "Message from message_queue would have been deleted";
367         say "Done with message_queue purge.";
368     }
369 }
370
371 if ($purge_merged) {
372     print "Purging completed entries from need_merge_authorities.\n" if $verbose;
373     if ($confirm) {
374         $sth = $dbh->prepare(q{ DELETE FROM need_merge_authorities WHERE done=1 });
375         $sth->execute() or die $dbh->errstr;
376     }
377     print "Done with purging need_merge_authorities.\n" if $verbose;
378 }
379
380 if ($pImport) {
381     print "Purging records from import tables.\n" if $verbose;
382     PurgeImportTables()                           if $confirm;
383     print "Done with purging import tables.\n"    if $verbose;
384 }
385
386 if ($pZ3950) {
387     print "Purging Z39.50 records from import tables.\n"           if $verbose;
388     PurgeZ3950()                                                   if $confirm;
389     print "Done with purging Z39.50 records from import tables.\n" if $verbose;
390 }
391
392 if ($pLogs) {
393     print "Purging records from action_logs.\n" if $verbose;
394     my $log_query = q{
395             DELETE FROM action_logs
396             WHERE timestamp < date_sub(curdate(), INTERVAL ? DAY)
397     };
398     my @query_params = ();
399     if (@preserve_logs) {
400         $log_query .= " AND module NOT IN (" . join( ',', ('?') x @preserve_logs ) . ")";
401         push @query_params, @preserve_logs;
402     }
403     if (@log_modules) {
404         $log_query .= " AND module IN (" . join( ',', ('?') x @log_modules ) . ")";
405         push @query_params, @log_modules;
406     }
407     if (@log_actions) {
408         $log_query .= " AND action IN (" . join( ',', ('?') x @log_actions ) . ")";
409         push @query_params, @log_actions;
410     }
411     $sth = $dbh->prepare($log_query);
412     if ($confirm) {
413         $sth->execute( $pLogs, @query_params ) or die $dbh->errstr;
414     }
415     print "Done with purging action_logs.\n" if $verbose;
416 }
417
418 if ($pMessages) {
419     print "Purging messages older than $pMessages days.\n" if $verbose;
420     my $messages = Koha::Patron::Messages->filter_by_last_update(
421         { timestamp_column_name => 'message_date', days => $pMessages } );
422     my $count = $messages->count;
423     $messages->delete if $confirm;
424     if ($verbose) {
425         say $confirm
426             ? sprintf( "Done with purging %d messages",       $count )
427             : sprintf( "%d messages would have been removed", $count );
428     }
429 }
430
431 if ($fees_days) {
432     print "Purging records from accountlines.\n"      if $verbose;
433     purge_zero_balance_fees($fees_days)               if $confirm;
434     print "Done purging records from accountlines.\n" if $verbose;
435 }
436
437 if ($pSearchhistory) {
438     print "Purging records older than $pSearchhistory from search_history.\n" if $verbose;
439     C4::Search::History::delete( { interval => $pSearchhistory } )            if $confirm;
440     print "Done with purging search_history.\n"                               if $verbose;
441 }
442
443 if ($pListShareInvites) {
444     print "Purging unaccepted list share invites older than $pListShareInvites days.\n" if $verbose;
445     $sth = $dbh->prepare(
446         q{
447             DELETE FROM virtualshelfshares
448             WHERE invitekey IS NOT NULL
449             AND (sharedate + INTERVAL ? DAY) < NOW()
450         }
451     );
452     if ($confirm) {
453         $sth->execute($pListShareInvites);
454     }
455     print "Done with purging unaccepted list share invites.\n" if $verbose;
456 }
457
458 if ($pDebarments) {
459     print "Expired patrons restrictions purge triggered for $pDebarments days.\n" if $verbose;
460     my $count = PurgeDebarments( $pDebarments, $confirm );
461     if ($verbose) {
462         say $confirm ? "$count restrictions were deleted." : "$count restrictions would have been deleted";
463         say "Done with restrictions purge.";
464     }
465 }
466
467 if ($allDebarments) {
468     print "All expired patrons restrictions purge triggered.\n" if $verbose;
469     my $count = PurgeDebarments( 0, $confirm );
470     if ($verbose) {
471         say $confirm ? "$count restrictions were deleted." : "$count restrictions would have been deleted";
472         say "Done with all restrictions purge.";
473     }
474 }
475
476 # Lock expired patrons?
477 if ( defined $lock_days && $lock_days ne q{} ) {
478     say "Start locking expired patrons" if $verbose;
479     my $expired_patrons = Koha::Patrons->filter_by_expiration_date( { days => $lock_days } )
480         ->search( { login_attempts => { '!=' => -1 } } );
481     my $count = $expired_patrons->count;
482     $expired_patrons->lock( { remove => 1 } ) if $confirm;
483     if ($verbose) {
484         say $confirm ? sprintf( "Locked %d patrons", $count ) : sprintf( "Found %d patrons", $count );
485     }
486 }
487
488 # Handle unsubscribe requests from GDPR consent form, depends on UnsubscribeReflectionDelay preference
489 say "Start lock unsubscribed, anonymize and delete" if $verbose;
490 my $unsubscribed_patrons = Koha::Patrons->search_unsubscribed;
491 my $count                = $unsubscribed_patrons->count;
492 $unsubscribed_patrons->lock( { expire => 1, remove => 1 } ) if $confirm;
493 say $confirm ? sprintf( "Locked %d patrons", $count ) : sprintf( "%d patrons would have been locked", $count )
494     if $verbose;
495
496 # Anonymize patron data, depending on PatronAnonymizeDelay
497 my $anonymize_candidates = Koha::Patrons->search_anonymize_candidates( { locked => 1 } );
498 $count = $anonymize_candidates->count;
499 $anonymize_candidates->anonymize if $confirm;
500 say $confirm ? sprintf( "Anonymized %d patrons", $count ) : sprintf( "%d patrons would have been anonymized", $count )
501     if $verbose;
502
503 # Remove patron data, depending on PatronRemovalDelay (will raise an exception if problem encountered
504 my $anonymized_patrons = Koha::Patrons->search_anonymized;
505 $count = $anonymized_patrons->count;
506 if ($confirm) {
507     $anonymized_patrons->delete( { move => 1 } );
508     if ($@) {
509         warn $@;
510     }
511 }
512 if ($verbose) {
513     say $confirm ? sprintf( "Deleted %d patrons", $count ) : sprintf( "%d patrons would have been deleted", $count );
514 }
515
516 # FIXME The output for dry-run mode needs to be improved
517 # But non trivial changes to C4::Members need to be done before.
518 if ($pExpSelfReg) {
519     if ($confirm) {
520         DeleteExpiredSelfRegs();
521     } elsif ($verbose) {
522         say "self-registered borrowers may be deleted";
523     }
524 }
525 if ($pUnvSelfReg) {
526     if ($confirm) {
527         DeleteUnverifiedSelfRegs($pUnvSelfReg);
528     } elsif ($verbose) {
529         say "unverified self-registrations may be deleted";
530     }
531 }
532
533 if ($special_holidays_days) {
534     if ($confirm) {
535         DeleteSpecialHolidays( abs($special_holidays_days) );
536     } elsif ($verbose) {
537         say "self-registered borrowers may be deleted";
538     }
539 }
540
541 if ($temp_uploads) {
542
543     # Delete temporary uploads, governed by a pref (unless you override)
544     print "Purging temporary uploads.\n" if $verbose;
545     if ($confirm) {
546         Koha::UploadedFiles->delete_temporary(
547             {
548                 defined($temp_uploads_days)
549                 ? ( override_pref => $temp_uploads_days )
550                 : ()
551             }
552         );
553     }
554     print "Done purging temporary uploads.\n" if $verbose;
555 }
556
557 if ( defined $uploads_missing ) {
558     print "Looking for missing uploads\n" if $verbose;
559     if ($confirm) {
560         my $keep  = $uploads_missing == 1 ? 0 : 1;
561         my $count = Koha::UploadedFiles->delete_missing( { keep_record => $keep } );
562         if ($keep) {
563             print "Counted $count missing uploaded files\n";
564         } else {
565             print "Removed $count records for missing uploads\n";
566         }
567     } else {
568
569         # FIXME need to create a filter_by_missing method (then call ->delete) instead of delete_missing
570         say "Dry-run mode cannot guess how many uploads would have been deleted";
571     }
572 }
573
574 if ($oauth_tokens) {
575     require Koha::OAuthAccessTokens;
576
577     my $tokens = Koha::OAuthAccessTokens->search( { expires => { '<=', time } } );
578     my $count  = $tokens->count;
579     $tokens->delete if $confirm;
580     if ($verbose) {
581         say $confirm
582             ? sprintf( "Removed %d expired OAuth2 tokens",                $count )
583             : sprintf( "%d expired OAuth tokens would have been removed", $count );
584     }
585 }
586
587 if ($pStatistics) {
588     print "Purging statistics older than $pStatistics days.\n" if $verbose;
589     my $statistics =
590         Koha::Statistics->filter_by_last_update( { timestamp_column_name => 'datetime', days => $pStatistics } );
591     my $count = $statistics->count;
592     $statistics->delete if $confirm;
593     if ($verbose) {
594         say $confirm
595             ? sprintf( "Done with purging %d statistics",       $count )
596             : sprintf( "%d statistics would have been removed", $count );
597     }
598 }
599
600 if ( $return_claims && ( my $days = C4::Context->preference('CleanUpDatabaseReturnClaims') ) ) {
601     print "Purging return claims older than $days days.\n" if $verbose;
602
603     $return_claims = Koha::Checkouts::ReturnClaims->filter_by_last_update(
604         {
605             timestamp_column_name => 'resolved_on',
606             days                  => $days,
607         }
608     );
609
610     my $count = $return_claims->count;
611     $return_claims->delete if $confirm;
612
613     if ($verbose) {
614         say $confirm
615             ? sprintf "Done with purging %d resolved return claims.", $count
616             : sprintf "%d resolved return claims would have been purged.", $count;
617     }
618 }
619
620 if ($pDeletedCatalog) {
621     print "Purging deleted catalog older than $pDeletedCatalog days.\n"
622         if $verbose;
623     my $old_items       = Koha::Old::Items->filter_by_last_update( { days => $pDeletedCatalog } );
624     my $old_biblioitems = Koha::Old::Biblioitems->filter_by_last_update( { days => $pDeletedCatalog } );
625     my $old_biblios     = Koha::Old::Biblios->filter_by_last_update( { days => $pDeletedCatalog } );
626     my ( $c_i, $c_bi, $c_b ) = ( $old_items->count, $old_biblioitems->count, $old_biblios->count );
627     if ($confirm) {
628         $old_items->delete;
629         $old_biblioitems->delete;
630         $old_biblios->delete;
631     }
632     if ($verbose) {
633         say sprintf(
634             $confirm
635             ? "Done with purging deleted catalog (%d items, %d biblioitems, %d biblios)."
636             : "Deleted catalog would have been removed (%d items, %d biblioitems, %d biblios).",
637             $c_i, $c_bi, $c_b
638         );
639     }
640 }
641
642 if ($pDeletedPatrons) {
643     print "Purging deleted patrons older than $pDeletedPatrons days.\n" if $verbose;
644     my $old_patrons = Koha::Old::Patrons->filter_by_last_update(
645         { timestamp_column_name => 'updated_on', days => $pDeletedPatrons } );
646     my $count = $old_patrons->count;
647     $old_patrons->delete if $confirm;
648     if ($verbose) {
649         say $confirm
650             ? sprintf "Done with purging %d deleted patrons.", $count
651             : sprintf "%d deleted patrons would have been purged.", $count;
652     }
653 }
654
655 if ($pOldIssues) {
656     print "Purging old checkouts older than $pOldIssues days.\n" if $verbose;
657     my $old_checkouts = Koha::Old::Checkouts->filter_by_last_update( { days => $pOldIssues } );
658     my $count         = $old_checkouts->count;
659     $old_checkouts->delete if $confirm;
660     if ($verbose) {
661         say $confirm
662             ? sprintf "Done with purging %d old checkouts.", $count
663             : sprintf "%d old checkouts would have been purged.", $count;
664     }
665 }
666
667 if ($pOldReserves) {
668     print "Purging old reserves older than $pOldReserves days.\n" if $verbose;
669     my $old_reserves = Koha::Old::Holds->filter_by_last_update( { days => $pOldReserves } );
670     my $count        = $old_reserves->count;
671     $old_reserves->delete if $confirm;
672     if ($verbose) {
673         say $confirm
674             ? sprintf "Done with purging %d old reserves.", $count
675             : sprintf "%d old reserves would have been purged.", $count;
676     }
677 }
678
679 if ($pTransfers) {
680     print "Purging arrived item transfers older than $pTransfers days.\n" if $verbose;
681     my $transfers = Koha::Item::Transfers->filter_by_last_update(
682         {
683             timestamp_column_name => 'datearrived',
684             days                  => $pTransfers,
685         }
686     );
687     my $count = $transfers->count;
688     $transfers->delete if $confirm;
689     if ($verbose) {
690         say $confirm
691             ? sprintf "Done with purging %d transfers.", $count
692             : sprintf "%d transfers would have been purged.", $count;
693     }
694 }
695
696 if ( defined $pPseudoTransactions or $pPseudoTransactionsFrom or $pPseudoTransactionsTo ) {
697     print "Purging pseudonymized transactions\n" if $verbose;
698     if ($pPseudoTransactionsTo) {
699         $pPseudoTransactionsTo = dt_from_string($pPseudoTransactionsTo);
700         if ( $pPseudoTransactionsTo->hour == 0 && $pPseudoTransactionsTo->minute == 0 ) {
701             $pPseudoTransactionsTo->set( hour => 23, minute => 59, second => 59 );
702         }
703     }
704     my $anonymized_transactions = Koha::PseudonymizedTransactions->filter_by_last_update(
705         {
706             timestamp_column_name => 'datetime',
707             ( defined $pPseudoTransactions ? ( days => $pPseudoTransactions )     : () ),
708             ( $pPseudoTransactionsFrom     ? ( from => $pPseudoTransactionsFrom ) : () ),
709             ( $pPseudoTransactionsTo       ? ( to   => $pPseudoTransactionsTo )   : () ),
710         }
711     );
712     my $count = $anonymized_transactions->count;
713     $anonymized_transactions->delete if $confirm;
714     if ($verbose) {
715         say $confirm
716             ? sprintf "Done with purging %d pseudonymized transactions.", $count
717             : sprintf "%d pseudonymized transactions would have been purged.", $count;
718     }
719 }
720
721 if ($labels) {
722     print "Purging item label batches last added to more than $labels days ago.\n" if $verbose;
723     my $count = PurgeCreatorBatches( $labels, 'labels', $confirm );
724     if ($verbose) {
725         say $confirm
726             ? sprintf "Done with purging %d item label batches last added to more than %d days ago.\n", $count, $labels
727             : sprintf "%d item label batches would have been purged.", $count;
728     }
729 }
730
731 if ($cards) {
732     print "Purging card creator batches last added to more than $cards days ago.\n" if $verbose;
733     my $count = PurgeCreatorBatches( $labels, 'patroncards', $confirm );
734     if ($verbose) {
735         say $confirm
736             ? sprintf "Done with purging %d card creator batches last added to more than %d days ago.\n", $count,
737             $labels
738             : sprintf "%d card creator batches would have been purged.", $count;
739     }
740 }
741
742 if ($jobs_days) {
743     print "Purging background jobs more than $jobs_days days ago.\n"
744         if $verbose;
745     my $jobs = Koha::BackgroundJobs->search(
746         {
747             status => 'finished',
748             ( $jobs_types[0] eq 'all' ? () : ( type => \@jobs_types ) )
749         }
750     )->filter_by_last_update(
751         {
752             timestamp_column_name => 'ended_on',
753             days                  => $jobs_days,
754         }
755     );
756     my $count = $jobs->count;
757     $jobs->delete if $confirm;
758     if ($verbose) {
759         say $confirm
760             ? sprintf "Done with purging %d background jobs of type(s): %s added more than %d days ago.\n",
761             $count, join( ',', @jobs_types ), $jobs_days
762             : sprintf "%d background jobs of type(s): %s added more than %d days ago would have been purged.",
763             $count, join( ',', @jobs_types ), $jobs_days;
764     }
765 }
766
767 if ($reports) {
768     if ($confirm) {
769         PurgeSavedReports($reports);
770     }
771     if ($verbose) {
772         say "Purging reports data saved more than $reports days ago.\n";
773     }
774 }
775
776 if ($edifact_msg_days) {
777     print "Purging EDIFACT messages older than $edifact_msg_days days.\n" if $verbose;
778     my $count = PurgeEdifactMessages( $edifact_msg_days, $confirm );
779     if ($verbose) {
780         say $confirm
781             ? sprintf( "Done with purging %d EDIFACT messages",       $count )
782             : sprintf( "%d EDIFACT messages would have been removed", $count );
783     }
784 }
785
786 cronlogaction( { action => 'End', info => "COMPLETED" } );
787
788 exit(0);
789
790 sub RemoveOldSessions {
791     my ( $id, $a_session, $limit, $lasttime );
792     $limit = time() - 24 * 3600 * $sess_days;
793
794     $sth = $dbh->prepare(q{ SELECT id, a_session FROM sessions });
795     $sth->execute or die $dbh->errstr;
796     $sth->bind_columns( \$id, \$a_session );
797     $sth2 = $dbh->prepare(q{ DELETE FROM sessions WHERE id=? });
798     my $count = 0;
799
800     while ( $sth->fetch ) {
801         $lasttime = 0;
802         if ( $a_session =~ /lasttime:\s+'?(\d+)/ ) {
803             $lasttime = $1;
804         } elsif ( $a_session =~ /(ATIME|CTIME):\s+'?(\d+)/ ) {
805             $lasttime = $2;
806         }
807         if ( $lasttime && $lasttime < $limit ) {
808             $sth2->execute($id) or die $dbh->errstr;
809             $count++;
810         }
811     }
812     if ($verbose) {
813         print "$count sessions were deleted.\n";
814     }
815 }
816
817 sub PurgeImportTables {
818
819     #First purge import_records
820     #Delete cascades to import_biblios, import_items and import_record_matches
821     $sth = $dbh->prepare(
822         q{
823             DELETE FROM import_records
824             WHERE upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
825         }
826     );
827     $sth->execute($pImport) or die $dbh->errstr;
828
829     # Now purge import_batches
830     # Timestamp cannot be used here without care, because records are added
831     # continuously to batches without updating timestamp (Z39.50 search).
832     # So we only delete older empty batches.
833     # This delete will therefore not have a cascading effect.
834     $sth = $dbh->prepare(
835         q{
836             DELETE ba
837             FROM import_batches ba
838             LEFT JOIN import_records re ON re.import_batch_id=ba.import_batch_id
839             WHERE re.import_record_id IS NULL AND
840             ba.upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
841         }
842     );
843     $sth->execute($pImport) or die $dbh->errstr;
844 }
845
846 sub PurgeZ3950 {
847     $sth = $dbh->prepare(
848         q{
849             DELETE FROM import_batches
850             WHERE batch_type = 'z3950'
851         }
852     );
853     $sth->execute() or die $dbh->errstr;
854 }
855
856 sub PurgeDebarments {
857     require Koha::Patron::Debarments;
858     my ( $days, $doit ) = @_;
859     my $count = 0;
860     $sth = $dbh->prepare(
861         q{
862             SELECT borrower_debarment_id
863             FROM borrower_debarments
864             WHERE expiration < date_sub(curdate(), INTERVAL ? DAY)
865         }
866     );
867     $sth->execute($days) or die $dbh->errstr;
868     while ( my ($borrower_debarment_id) = $sth->fetchrow_array ) {
869         Koha::Patron::Debarments::DelDebarment($borrower_debarment_id) if $doit;
870         $count++;
871     }
872     return $count;
873 }
874
875 sub PurgeCreatorBatches {
876     require C4::Labels::Batch;
877     my ( $days, $creator, $doit ) = @_;
878     my $count = 0;
879     $sth = $dbh->prepare(
880         q{
881             SELECT batch_id, branch_code FROM creator_batches
882             WHERE batch_id in
883                 (SELECT batch_id
884                 FROM (SELECT batch_id
885                         FROM creator_batches
886                         WHERE creator=?
887                         GROUP BY batch_id
888                         HAVING max(timestamp) <= date_sub(curdate(),interval ? day)) a)
889         }
890     );
891     $sth->execute( $creator, $days ) or die $dbh->errstr;
892     while ( my ( $batch_id, $branch_code ) = $sth->fetchrow_array ) {
893         C4::Labels::Batch::delete(
894             batch_id    => $batch_id,
895             branch_code => $branch_code
896         ) if $doit;
897         $count++;
898     }
899     return $count;
900 }
901
902 sub DeleteExpiredSelfRegs {
903     my $cnt = C4::Members::DeleteExpiredOpacRegistrations();
904     print "Removed $cnt expired self-registered borrowers\n" if $verbose;
905 }
906
907 sub DeleteUnverifiedSelfRegs {
908     my $cnt = C4::Members::DeleteUnverifiedOpacRegistrations( $_[0] );
909     print "Removed $cnt unverified self-registrations\n" if $verbose;
910 }
911
912 sub DeleteSpecialHolidays {
913     my ($days) = @_;
914
915     my $sth = $dbh->prepare(
916         q{
917         DELETE FROM special_holidays
918         WHERE DATE( CONCAT( year, '-', month, '-', day ) ) < DATE_SUB( CAST(NOW() AS DATE), INTERVAL ? DAY );
919     }
920     );
921     my $count = $sth->execute($days) + 0;
922     print "Removed $count unique holidays\n" if $verbose;
923 }
924
925 sub PurgeSavedReports {
926     my ($reports) = @_;
927
928     my $sth = $dbh->prepare(
929         q{
930             DELETE FROM saved_reports
931             WHERE date(date_run) < DATE_SUB(CURDATE(),INTERVAL ? DAY );
932         }
933     );
934     $sth->execute($reports);
935 }
936
937 sub PurgeEdifactMessages {
938     my ( $days, $doit ) = @_;
939
940     my $schema    = Koha::Database->new()->schema();
941     my $dtf       = $schema->storage->datetime_parser;
942     my $resultset = $schema->resultset('EdifactMessage')->search(
943         {
944             transfer_date => { '<'  => $dtf->format_datetime( dt_from_string->subtract( days => $days ) ) },
945             status        => { '!=' => 'new' },
946         }
947     );
948     my $count = $resultset->count;
949
950     $resultset->delete if $doit;
951
952     return $count;
953 }