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