Bug 19008: More database cleanups - deleted catalog
[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_SEARCHHISTORY_PURGEDAYS      => 30;
27 use constant DEFAULT_SHARE_INVITATION_EXPIRY_DAYS => 14;
28 use constant DEFAULT_DEBARMENTS_PURGEDAYS         => 30;
29
30 BEGIN {
31     # find Koha's Perl modules
32     # test carefully before changing this
33     use FindBin;
34     eval { require "$FindBin::Bin/../kohalib.pl" };
35 }
36
37 use Koha::Script -cron;
38 use C4::Context;
39 use C4::Search;
40 use C4::Search::History;
41 use Getopt::Long;
42 use C4::Log;
43 use C4::Accounts;
44 use Koha::UploadedFiles;
45
46 sub usage {
47     print STDERR <<USAGE;
48 Usage: $0 [-h|--help] [--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]
49
50    -h --help          prints this help message, and exits, ignoring all
51                       other options
52    --sessions         purge the sessions table.  If you use this while users 
53                       are logged into Koha, they will have to reconnect.
54    --sessdays DAYS    purge only sessions older than DAYS days.
55    -v --verbose       will cause the script to give you a bit more information
56                       about the run.
57    --zebraqueue DAYS  purge completed zebraqueue entries older than DAYS days.
58                       Defaults to 30 days if no days specified.
59    -m --mail DAYS     purge items from the mail queue that are older than DAYS days.
60                       Defaults to 30 days if no days specified.
61    --merged           purged completed entries from need_merge_authorities.
62    --import DAYS      purge records from import tables older than DAYS days.
63                       Defaults to 60 days if no days specified.
64    --z3950            purge records from import tables that are the result
65                       of Z39.50 searches
66    --fees DAYS        purge entries accountlines older than DAYS days, where
67                       amountoutstanding is 0 or NULL.
68                       In the case of --fees, DAYS must be greater than
69                       or equal to 1.
70    --logs DAYS        purge entries from action_logs older than DAYS days.
71                       Defaults to 180 days if no days specified.
72    --searchhistory DAYS  purge entries from search_history older than DAYS days.
73                          Defaults to 30 days if no days specified
74    --list-invites  DAYS  purge (unaccepted) list share invites older than DAYS
75                          days.  Defaults to 14 days if no days specified.
76    --restrictions DAYS   purge patrons restrictions expired since more than DAYS days.
77                          Defaults to 30 days if no days specified.
78     --all-restrictions   purge all expired patrons restrictions.
79    --del-exp-selfreg  Delete expired self registration accounts
80    --del-unv-selfreg  DAYS  Delete unverified self registrations older than DAYS
81    --unique-holidays DAYS  Delete all unique holidays older than DAYS
82    --temp-uploads     Delete temporary uploads.
83    --temp-uploads-days DAYS Override the corresponding preference value.
84    --uploads-missing FLAG Delete upload records for missing files when FLAG is true, count them otherwise
85    --oauth-tokens     Delete expired OAuth2 tokens
86    --statistics DAYS       Purge entries from statistics older than DAYS days.
87    --deleted-catalog  DAYS Purge deleted catalog older than DAYS
88                            in tables deleteditems, deletedbiblioitems, deletedbiblio_metadata and deletedbiblio
89 USAGE
90     exit $_[0];
91 }
92
93 my $help;
94 my $sessions;
95 my $sess_days;
96 my $verbose;
97 my $zebraqueue_days;
98 my $mail;
99 my $purge_merged;
100 my $pImport;
101 my $pLogs;
102 my $pSearchhistory;
103 my $pZ3950;
104 my $pListShareInvites;
105 my $pDebarments;
106 my $allDebarments;
107 my $pExpSelfReg;
108 my $pUnvSelfReg;
109 my $fees_days;
110 my $special_holidays_days;
111 my $temp_uploads;
112 my $temp_uploads_days;
113 my $uploads_missing;
114 my $oauth_tokens;
115 my $pStatistics;
116 my $pDeletedCatalog;
117
118 GetOptions(
119     'h|help'            => \$help,
120     'sessions'          => \$sessions,
121     'sessdays:i'        => \$sess_days,
122     'v|verbose'         => \$verbose,
123     'm|mail:i'          => \$mail,
124     'zebraqueue:i'      => \$zebraqueue_days,
125     'merged'            => \$purge_merged,
126     'import:i'          => \$pImport,
127     'z3950'             => \$pZ3950,
128     'logs:i'            => \$pLogs,
129     'fees:i'            => \$fees_days,
130     'searchhistory:i'   => \$pSearchhistory,
131     'list-invites:i'    => \$pListShareInvites,
132     'restrictions:i'    => \$pDebarments,
133     'all-restrictions'  => \$allDebarments,
134     'del-exp-selfreg'   => \$pExpSelfReg,
135     'del-unv-selfreg'   => \$pUnvSelfReg,
136     'unique-holidays:i' => \$special_holidays_days,
137     'temp-uploads'      => \$temp_uploads,
138     'temp-uploads-days:i' => \$temp_uploads_days,
139     'uploads-missing:i' => \$uploads_missing,
140     'oauth-tokens'      => \$oauth_tokens,
141     'statistics:i'      => \$pStatistics,
142     'deleted-catalog:i' => \$pDeletedCatalog,
143 ) || usage(1);
144
145 # Use default values
146 $sessions          = 1                                    if $sess_days                  && $sess_days > 0;
147 $pImport           = DEFAULT_IMPORT_PURGEDAYS             if defined($pImport)           && $pImport == 0;
148 $pLogs             = DEFAULT_LOGS_PURGEDAYS               if defined($pLogs)             && $pLogs == 0;
149 $zebraqueue_days   = DEFAULT_ZEBRAQ_PURGEDAYS             if defined($zebraqueue_days)   && $zebraqueue_days == 0;
150 $mail              = DEFAULT_MAIL_PURGEDAYS               if defined($mail)              && $mail == 0;
151 $pSearchhistory    = DEFAULT_SEARCHHISTORY_PURGEDAYS      if defined($pSearchhistory)    && $pSearchhistory == 0;
152 $pListShareInvites = DEFAULT_SHARE_INVITATION_EXPIRY_DAYS if defined($pListShareInvites) && $pListShareInvites == 0;
153 $pDebarments       = DEFAULT_DEBARMENTS_PURGEDAYS         if defined($pDebarments)       && $pDebarments == 0;
154
155 if ($help) {
156     usage(0);
157 }
158
159 unless ( $sessions
160     || $zebraqueue_days
161     || $mail
162     || $purge_merged
163     || $pImport
164     || $pLogs
165     || $fees_days
166     || $pSearchhistory
167     || $pZ3950
168     || $pListShareInvites
169     || $pDebarments
170     || $allDebarments
171     || $pExpSelfReg
172     || $pUnvSelfReg
173     || $special_holidays_days
174     || $temp_uploads
175     || defined $uploads_missing
176     || $oauth_tokens
177     || $pStatistics
178     || $pDeletedCatalog
179 ) {
180     print "You did not specify any cleanup work for the script to do.\n\n";
181     usage(1);
182 }
183
184 if ($pDebarments && $allDebarments) {
185     print "You can not specify both --restrictions and --all-restrictions.\n\n";
186     usage(1);
187 }
188
189 cronlogaction();
190
191 my $dbh = C4::Context->dbh();
192 my $sth;
193 my $sth2;
194 my $count;
195
196 if ( $sessions && !$sess_days ) {
197     if ($verbose) {
198         print "Session purge triggered.\n";
199         $sth = $dbh->prepare(q{ SELECT COUNT(*) FROM sessions });
200         $sth->execute() or die $dbh->errstr;
201         my @count_arr = $sth->fetchrow_array;
202         print "$count_arr[0] entries will be deleted.\n";
203     }
204     $sth = $dbh->prepare(q{ TRUNCATE sessions });
205     $sth->execute() or die $dbh->errstr;
206     if ($verbose) {
207         print "Done with session purge.\n";
208     }
209 }
210 elsif ( $sessions && $sess_days > 0 ) {
211     print "Session purge triggered with days>$sess_days.\n" if $verbose;
212     RemoveOldSessions();
213     print "Done with session purge with days>$sess_days.\n" if $verbose;
214 }
215
216 if ($zebraqueue_days) {
217     $count = 0;
218     print "Zebraqueue purge triggered for $zebraqueue_days days.\n" if $verbose;
219     $sth = $dbh->prepare(
220         q{
221             SELECT id,biblio_auth_number,server,time
222             FROM zebraqueue
223             WHERE done=1 AND time < date_sub(curdate(), INTERVAL ? DAY)
224         }
225     );
226     $sth->execute($zebraqueue_days) or die $dbh->errstr;
227     $sth2 = $dbh->prepare(q{ DELETE FROM zebraqueue WHERE id=? });
228     while ( my $record = $sth->fetchrow_hashref ) {
229         $sth2->execute( $record->{id} ) or die $dbh->errstr;
230         $count++;
231     }
232     print "$count records were deleted.\nDone with zebraqueue purge.\n" if $verbose;
233 }
234
235 if ($mail) {
236     print "Mail queue purge triggered for $mail days.\n" if $verbose;
237     $sth = $dbh->prepare(
238         q{
239             DELETE FROM message_queue
240             WHERE time_queued < date_sub(curdate(), INTERVAL ? DAY)
241         }
242     );
243     $sth->execute($mail) or die $dbh->errstr;
244     $count = $sth->rows;
245     $sth->finish;
246     print "$count messages were deleted from the mail queue.\nDone with message_queue purge.\n" if $verbose;
247 }
248
249 if ($purge_merged) {
250     print "Purging completed entries from need_merge_authorities.\n" if $verbose;
251     $sth = $dbh->prepare(q{ DELETE FROM need_merge_authorities WHERE done=1 });
252     $sth->execute() or die $dbh->errstr;
253     print "Done with purging need_merge_authorities.\n" if $verbose;
254 }
255
256 if ($pImport) {
257     print "Purging records from import tables.\n" if $verbose;
258     PurgeImportTables();
259     print "Done with purging import tables.\n" if $verbose;
260 }
261
262 if ($pZ3950) {
263     print "Purging Z39.50 records from import tables.\n" if $verbose;
264     PurgeZ3950();
265     print "Done with purging Z39.50 records from import tables.\n" if $verbose;
266 }
267
268 if ($pLogs) {
269     print "Purging records from action_logs.\n" if $verbose;
270     $sth = $dbh->prepare(
271         q{
272             DELETE FROM action_logs
273             WHERE timestamp < date_sub(curdate(), INTERVAL ? DAY)
274         }
275     );
276     $sth->execute($pLogs) or die $dbh->errstr;
277     print "Done with purging action_logs.\n" if $verbose;
278 }
279
280 if ($fees_days) {
281     print "Purging records from accountlines.\n" if $verbose;
282     purge_zero_balance_fees( $fees_days );
283     print "Done purging records from accountlines.\n" if $verbose;
284 }
285
286 if ($pSearchhistory) {
287     print "Purging records older than $pSearchhistory from search_history.\n" if $verbose;
288     C4::Search::History::delete({ interval => $pSearchhistory });
289     print "Done with purging search_history.\n" if $verbose;
290 }
291
292 if ($pListShareInvites) {
293     print "Purging unaccepted list share invites older than $pListShareInvites days.\n" if $verbose;
294     $sth = $dbh->prepare(
295         q{
296             DELETE FROM virtualshelfshares
297             WHERE invitekey IS NOT NULL
298             AND (sharedate + INTERVAL ? DAY) < NOW()
299         }
300     );
301     $sth->execute($pListShareInvites);
302     print "Done with purging unaccepted list share invites.\n" if $verbose;
303 }
304
305 if ($pDebarments) {
306     print "Expired patrons restrictions purge triggered for $pDebarments days.\n" if $verbose;
307     $count = PurgeDebarments($pDebarments);
308     print "$count restrictions were deleted.\nDone with restrictions purge.\n" if $verbose;
309 }
310
311 if($allDebarments) {
312     print "All expired patrons restrictions purge triggered.\n" if $verbose;
313     $count = PurgeDebarments(0);
314     print "$count restrictions were deleted.\nDone with all restrictions purge.\n" if $verbose;
315 }
316
317 # Handle unsubscribe requests from GDPR consent form, depends on UnsubscribeReflectionDelay preference
318 Koha::Patrons->search_unsubscribed->lock({ expire => 1, remove => 1, verbose => $verbose });
319 # Anonymize patron data, depending on PatronAnonymizeDelay
320 Koha::Patrons->search_anonymize_candidates({ locked => 1 })->anonymize({ verbose => $verbose });
321 # Remove patron data, depending on PatronRemovalDelay (will raise an exception if problem encountered
322 eval { Koha::Patrons->search_anonymized->delete({ move => 1, verbose => $verbose }) };
323 warn $@ if $@;
324
325 if( $pExpSelfReg ) {
326     DeleteExpiredSelfRegs();
327 }
328 if( $pUnvSelfReg ) {
329     DeleteUnverifiedSelfRegs( $pUnvSelfReg );
330 }
331
332 if ($special_holidays_days) {
333     DeleteSpecialHolidays( abs($special_holidays_days) );
334 }
335
336 if( $temp_uploads ) {
337     # Delete temporary uploads, governed by a pref (unless you override)
338     print "Purging temporary uploads.\n" if $verbose;
339     Koha::UploadedFiles->delete_temporary({
340         defined($temp_uploads_days)
341             ? ( override_pref => $temp_uploads_days )
342             : ()
343     });
344     print "Done purging temporary uploads.\n" if $verbose;
345 }
346
347 if( defined $uploads_missing ) {
348     print "Looking for missing uploads\n" if $verbose;
349     my $keep = $uploads_missing == 1 ? 0 : 1;
350     my $count = Koha::UploadedFiles->delete_missing({ keep_record => $keep });
351     if( $keep ) {
352         print "Counted $count missing uploaded files\n";
353     } else {
354         print "Removed $count records for missing uploads\n";
355     }
356 }
357
358 if ($oauth_tokens) {
359     require Koha::OAuthAccessTokens;
360
361     my $count = int Koha::OAuthAccessTokens->search({ expires => { '<=', time } })->delete;
362     say "Removed $count expired OAuth2 tokens" if $verbose;
363 }
364
365 if ($pStatistics) {
366     print "Purging statistics older than $pStatistics days.\n" if $verbose;
367     $sth = $dbh->prepare(
368         q{
369             DELETE FROM statistics
370             WHERE datetime < DATE_SUB(CURDATE(), INTERVAL ? DAY)
371         }
372     );
373     $sth->execute($pStatistics);
374     print "Done with purging statistics.\n" if $verbose;
375 }
376
377 if ($pDeletedCatalog) {
378     print "Purging deleted catalog older than $pDeletedCatalog days.\n" if $verbose;
379     my $sth1 = $dbh->prepare(
380         q{
381             DELETE FROM deleteditems
382             WHERE timestamp < DATE_SUB(CURDATE(), INTERVAL ? DAY)
383         }
384     );
385     my $sth2 = $dbh->prepare(
386         q{
387             DELETE FROM deletedbiblioitems
388             WHERE timestamp < DATE_SUB(CURDATE(), INTERVAL ? DAY)
389         }
390     );
391     my $sth3 = $dbh->prepare(
392         q{
393             DELETE FROM deletedbiblio
394             WHERE timestamp < DATE_SUB(CURDATE(), INTERVAL ? DAY)
395         }
396     );
397     # deletedbiblio_metadata is managed by FK with deletedbiblio
398     $sth1->execute($pDeletedCatalog);
399     $sth2->execute($pDeletedCatalog);
400     $sth3->execute($pDeletedCatalog);
401     print "Done with purging deleted catalog.\n" if $verbose;
402 }
403
404 exit(0);
405
406 sub RemoveOldSessions {
407     my ( $id, $a_session, $limit, $lasttime );
408     $limit = time() - 24 * 3600 * $sess_days;
409
410     $sth = $dbh->prepare(q{ SELECT id, a_session FROM sessions });
411     $sth->execute or die $dbh->errstr;
412     $sth->bind_columns( \$id, \$a_session );
413     $sth2  = $dbh->prepare(q{ DELETE FROM sessions WHERE id=? });
414     $count = 0;
415
416     while ( $sth->fetch ) {
417         $lasttime = 0;
418         if ( $a_session =~ /lasttime:\s+'?(\d+)/ ) {
419             $lasttime = $1;
420         }
421         elsif ( $a_session =~ /(ATIME|CTIME):\s+'?(\d+)/ ) {
422             $lasttime = $2;
423         }
424         if ( $lasttime && $lasttime < $limit ) {
425             $sth2->execute($id) or die $dbh->errstr;
426             $count++;
427         }
428     }
429     if ($verbose) {
430         print "$count sessions were deleted.\n";
431     }
432 }
433
434 sub PurgeImportTables {
435
436     #First purge import_records
437     #Delete cascades to import_biblios, import_items and import_record_matches
438     $sth = $dbh->prepare(
439         q{
440             DELETE FROM import_records
441             WHERE upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
442         }
443     );
444     $sth->execute($pImport) or die $dbh->errstr;
445
446     # Now purge import_batches
447     # Timestamp cannot be used here without care, because records are added
448     # continuously to batches without updating timestamp (Z39.50 search).
449     # So we only delete older empty batches.
450     # This delete will therefore not have a cascading effect.
451     $sth = $dbh->prepare(
452         q{
453             DELETE ba
454             FROM import_batches ba
455             LEFT JOIN import_records re ON re.import_batch_id=ba.import_batch_id
456             WHERE re.import_record_id IS NULL AND
457             ba.upload_timestamp < date_sub(curdate(), INTERVAL ? DAY)
458         }
459     );
460     $sth->execute($pImport) or die $dbh->errstr;
461 }
462
463 sub PurgeZ3950 {
464     $sth = $dbh->prepare(
465         q{
466             DELETE FROM import_batches
467             WHERE batch_type = 'z3950'
468         }
469     );
470     $sth->execute() or die $dbh->errstr;
471 }
472
473 sub PurgeDebarments {
474     require Koha::Patron::Debarments;
475     my $days = shift;
476     $count = 0;
477     $sth   = $dbh->prepare(
478         q{
479             SELECT borrower_debarment_id
480             FROM borrower_debarments
481             WHERE expiration < date_sub(curdate(), INTERVAL ? DAY)
482         }
483     );
484     $sth->execute($days) or die $dbh->errstr;
485     while ( my ($borrower_debarment_id) = $sth->fetchrow_array ) {
486         Koha::Patron::Debarments::DelDebarment($borrower_debarment_id);
487         $count++;
488     }
489     return $count;
490 }
491
492 sub DeleteExpiredSelfRegs {
493     my $cnt= C4::Members::DeleteExpiredOpacRegistrations();
494     print "Removed $cnt expired self-registered borrowers\n" if $verbose;
495 }
496
497 sub DeleteUnverifiedSelfRegs {
498     my $cnt= C4::Members::DeleteUnverifiedOpacRegistrations( $_[0] );
499     print "Removed $cnt unverified self-registrations\n" if $verbose;
500 }
501
502 sub DeleteSpecialHolidays {
503     my ( $days ) = @_;
504
505     my $sth = $dbh->prepare(q{
506         DELETE FROM special_holidays
507         WHERE DATE( CONCAT( year, '-', month, '-', day ) ) < DATE_SUB( CAST(NOW() AS DATE), INTERVAL ? DAY );
508     });
509     my $count = $sth->execute( $days ) + 0;
510     print "Removed $count unique holidays\n" if $verbose;
511 }