Bug 34587: Move everything into a ERM/EUsage subfolder
[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     if ($pPseudoTransactionsTo) {
677         $pPseudoTransactionsTo = dt_from_string($pPseudoTransactionsTo);
678         if ( $pPseudoTransactionsTo->hour == 0 && $pPseudoTransactionsTo->minute == 0 ) {
679             $pPseudoTransactionsTo->set( hour => 23, minute => 59, second => 59 );
680         }
681     }
682     my $anonymized_transactions = Koha::PseudonymizedTransactions->filter_by_last_update(
683         {
684             timestamp_column_name => 'datetime',
685             ( defined $pPseudoTransactions  ? ( days => $pPseudoTransactions     ) : () ),
686             ( $pPseudoTransactionsFrom      ? ( from => $pPseudoTransactionsFrom ) : () ),
687             ( $pPseudoTransactionsTo        ? ( to   => $pPseudoTransactionsTo   ) : () ),
688         }
689     );
690     my $count = $anonymized_transactions->count;
691     $anonymized_transactions->delete if $confirm;
692     if ($verbose) {
693         say $confirm
694           ? sprintf "Done with purging %d pseudonymized transactions.", $count
695           : sprintf "%d pseudonymized transactions would have been purged.", $count;
696     }
697 }
698
699 if ($labels) {
700     print "Purging item label batches last added to more than $labels days ago.\n" if $verbose;
701     my $count = PurgeCreatorBatches($labels, 'labels', $confirm);
702     if ($verbose) {
703         say $confirm
704           ? sprintf "Done with purging %d item label batches last added to more than %d days ago.\n", $count, $labels
705           : sprintf "%d item label batches would have been purged.", $count;
706     }
707 }
708
709 if ($cards) {
710     print "Purging card creator batches last added to more than $cards days ago.\n" if $verbose;
711     my $count = PurgeCreatorBatches($labels, 'patroncards', $confirm);
712     if ($verbose) {
713         say $confirm
714           ? sprintf "Done with purging %d card creator batches last added to more than %d days ago.\n", $count, $labels
715           : sprintf "%d card creator batches would have been purged.", $count;
716     }
717 }
718
719 if ($jobs_days) {
720     print "Purging background jobs more than $jobs_days days ago.\n"
721       if $verbose;
722     my $jobs = Koha::BackgroundJobs->search(
723         {
724             status => 'finished',
725             ( $jobs_types[0] eq 'all' ? () : ( type => \@jobs_types ) )
726         }
727     )->filter_by_last_update(
728         {
729             timestamp_column_name => 'ended_on',
730             days => $jobs_days,
731         }
732     );
733     my $count = $jobs->count;
734     $jobs->delete if $confirm;
735     if ($verbose) {
736         say $confirm
737           ? sprintf "Done with purging %d background jobs of type(s): %s added more than %d days ago.\n",
738           $count, join( ',', @jobs_types ), $jobs_days
739           : sprintf "%d background jobs of type(s): %s added more than %d days ago would have been purged.",
740           $count, join( ',', @jobs_types ), $jobs_days;
741     }
742 }
743
744 if ($reports) {
745     if ( $confirm ) {
746         PurgeSavedReports($reports);
747     } if ( $verbose ) {
748         say "Purging reports data saved more than $reports days ago.\n";
749     }
750 }
751
752 if($edifact_msg_days) {
753     print "Purging EDIFACT messages older than $edifact_msg_days days.\n" if $verbose;
754     my $count = PurgeEdifactMessages($edifact_msg_days, $confirm);
755     if ( $verbose ) {
756         say $confirm
757           ? sprintf( "Done with purging %d EDIFACT messages", $count )
758           : sprintf( "%d EDIFACT messages would have been removed", $count );
759     }
760 }
761
762 cronlogaction({ action => 'End', info => "COMPLETED" });
763
764 exit(0);
765
766 sub RemoveOldSessions {
767     my ( $id, $a_session, $limit, $lasttime );
768     $limit = time() - 24 * 3600 * $sess_days;
769
770     $sth = $dbh->prepare(q{ SELECT id, a_session FROM sessions });
771     $sth->execute or die $dbh->errstr;
772     $sth->bind_columns( \$id, \$a_session );
773     $sth2  = $dbh->prepare(q{ DELETE FROM sessions WHERE id=? });
774     my $count = 0;
775
776     while ( $sth->fetch ) {
777         $lasttime = 0;
778         if ( $a_session =~ /lasttime:\s+'?(\d+)/ ) {
779             $lasttime = $1;
780         }
781         elsif ( $a_session =~ /(ATIME|CTIME):\s+'?(\d+)/ ) {
782             $lasttime = $2;
783         }
784         if ( $lasttime && $lasttime < $limit ) {
785             $sth2->execute($id) or die $dbh->errstr;
786             $count++;
787         }
788     }
789     if ($verbose) {
790         print "$count sessions were deleted.\n";
791     }
792 }
793
794 sub PurgeImportTables {
795
796     #First purge import_records
797     #Delete cascades to import_biblios, import_items and import_record_matches
798     $sth = $dbh->prepare(
799         q{
800             DELETE FROM import_records
801             WHERE upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
802         }
803     );
804     $sth->execute($pImport) or die $dbh->errstr;
805
806     # Now purge import_batches
807     # Timestamp cannot be used here without care, because records are added
808     # continuously to batches without updating timestamp (Z39.50 search).
809     # So we only delete older empty batches.
810     # This delete will therefore not have a cascading effect.
811     $sth = $dbh->prepare(
812         q{
813             DELETE ba
814             FROM import_batches ba
815             LEFT JOIN import_records re ON re.import_batch_id=ba.import_batch_id
816             WHERE re.import_record_id IS NULL AND
817             ba.upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
818         }
819     );
820     $sth->execute($pImport) or die $dbh->errstr;
821 }
822
823 sub PurgeZ3950 {
824     $sth = $dbh->prepare(
825         q{
826             DELETE FROM import_batches
827             WHERE batch_type = 'z3950'
828         }
829     );
830     $sth->execute() or die $dbh->errstr;
831 }
832
833 sub PurgeDebarments {
834     require Koha::Patron::Debarments;
835     my ( $days, $doit ) = @_;
836     my $count = 0;
837     $sth   = $dbh->prepare(
838         q{
839             SELECT borrower_debarment_id
840             FROM borrower_debarments
841             WHERE expiration < date_sub(curdate(), INTERVAL ? DAY)
842         }
843     );
844     $sth->execute($days) or die $dbh->errstr;
845     while ( my ($borrower_debarment_id) = $sth->fetchrow_array ) {
846         Koha::Patron::Debarments::DelDebarment($borrower_debarment_id) if $doit;
847         $count++;
848     }
849     return $count;
850 }
851
852 sub PurgeCreatorBatches {
853     require C4::Labels::Batch;
854     my ( $days, $creator, $doit ) = @_;
855     my $count = 0;
856     $sth = $dbh->prepare(
857         q{
858             SELECT batch_id, branch_code FROM creator_batches
859             WHERE batch_id in
860                 (SELECT batch_id
861                 FROM (SELECT batch_id
862                         FROM creator_batches
863                         WHERE creator=?
864                         GROUP BY batch_id
865                         HAVING max(timestamp) <= date_sub(curdate(),interval ? day)) a)
866         }
867     );
868     $sth->execute( $creator, $days ) or die $dbh->errstr;
869     while ( my ( $batch_id, $branch_code ) = $sth->fetchrow_array ) {
870         C4::Labels::Batch::delete(
871             batch_id    => $batch_id,
872             branch_code => $branch_code
873         ) if $doit;
874         $count++;
875     }
876     return $count;
877 }
878
879 sub DeleteExpiredSelfRegs {
880     my $cnt= C4::Members::DeleteExpiredOpacRegistrations();
881     print "Removed $cnt expired self-registered borrowers\n" if $verbose;
882 }
883
884 sub DeleteUnverifiedSelfRegs {
885     my $cnt= C4::Members::DeleteUnverifiedOpacRegistrations( $_[0] );
886     print "Removed $cnt unverified self-registrations\n" if $verbose;
887 }
888
889 sub DeleteSpecialHolidays {
890     my ( $days ) = @_;
891
892     my $sth = $dbh->prepare(q{
893         DELETE FROM special_holidays
894         WHERE DATE( CONCAT( year, '-', month, '-', day ) ) < DATE_SUB( CAST(NOW() AS DATE), INTERVAL ? DAY );
895     });
896     my $count = $sth->execute( $days ) + 0;
897     print "Removed $count unique holidays\n" if $verbose;
898 }
899
900 sub PurgeSavedReports {
901     my ( $reports ) = @_;
902
903     my $sth = $dbh->prepare(q{
904             DELETE FROM saved_reports
905             WHERE date(date_run) < DATE_SUB(CURDATE(),INTERVAL ? DAY );
906         });
907     $sth->execute( $reports );
908 }
909
910 sub PurgeEdifactMessages {
911     my ( $days, $doit ) = @_;
912
913     my $schema = Koha::Database->new()->schema();
914     my $dtf = $schema->storage->datetime_parser;
915     my $resultset = $schema->resultset('EdifactMessage')->search(
916         {
917             transfer_date => {
918                 '<' => $dtf->format_datetime(dt_from_string->subtract( days => $days ))
919             },
920             status => { '!=' => 'new' },
921         }
922     );
923     my $count = $resultset->count;
924
925     $resultset->delete if $doit;
926
927     return $count;
928 }