Bug 9998: new tool to import/export and compare system preferences
[koha_fer] / misc / maintenance / cmp_sysprefs.pl
1 #!/usr/bin/perl
2
3 # Copyright 2013 Rijksmuseum
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 # This script imports/exports systempreferences to file.
21 # Two interesting features are:
22 # 1) It may help you to compare systempreferences between Koha instances.
23 # 2) You can also quickly restore subsets of preferences while testing.
24 #    Just leave only e.g. some circulations prefs in a file and compare with
25 #    the update flag.
26
27 use strict;
28 use warnings;
29 use open OUT => ':encoding(UTF-8)', ':std';
30
31 use Getopt::Long;
32 use Pod::Usage;
33
34 use C4::Context;
35 my $dbh = C4::Context->dbh;
36
37 my ( $help, $cmd, $filename, $override, $compare_add, $compare_del, $compare_upd, $ignore_opt );
38 GetOptions(
39     'help'    => \$help,
40     'cmd:s'   => \$cmd,
41     'file:s'  => \$filename,
42     'add'     => \$compare_add,
43     'del'     => \$compare_del,
44     'upd'     => \$compare_upd,
45     'ign-opt' => \$ignore_opt,
46 );
47
48 if ( $filename && !-e $filename && $cmd !~ /^b/ ) {
49     die "File $filename not found";
50 }
51 if ( !$cmd || !$filename || $help ) {
52     pod2usage( -verbose => 2 );
53     exit;
54 }
55
56 #------------------------------------------------------------------------------
57
58 #backup prefs
59 if ( $cmd =~ /^b/i && $filename ) {
60     my $dbprefs = ReadPrefsFromDb();
61     open my $fh, '>:encoding(UTF-8)', $filename;
62     SavePrefsToFile( $dbprefs, $fh );
63     close $fh;
64 }
65
66 #test pref file: read and save for gaining confidence :) run a diff
67 if ( $cmd =~ /^t/i && $filename ) {
68     my $fileprefs = ReadPrefsFromFile($filename);
69     open my $fh, '>:encoding(UTF-8)', $filename . ".sav";
70     SavePrefsToFile( $fileprefs, $fh );
71     close $fh;
72 }
73
74 #compare prefs (with db)
75 if ( $cmd =~ /^c/i && $filename ) {
76     my $dbprefs   = ReadPrefsFromDb();
77     my $fileprefs = ReadPrefsFromFile($filename);
78
79     #compare now
80     my $cmp = ComparePrefs( $dbprefs, $fileprefs );
81     PrintCompare( $cmp, "database", "file $filename" );
82     HandleCompareChanges( $cmp, $dbprefs, $fileprefs )
83       if $compare_add || $compare_del || $compare_upd;
84 }
85
86 #restore prefs
87 if ( $cmd =~ /^r/i && $filename ) {
88     my $fileprefs = ReadPrefsFromFile($filename);
89     CheckVersionPref($fileprefs);
90
91     #override this check by removing Version from your file
92     #if you know what you are doing of course
93     SavePrefsToDb($fileprefs);
94 }
95
96 #------------------------------------------------------------------------------
97
98 sub PrintCompare {
99     my ( $ch, $s1, $s2 ) = @_;
100     foreach ( sort keys %$ch ) {
101         my $v = $ch->{$_};
102         print "$_: ";
103         if    ( $v eq '1' ) { print "Not in $s2"; }
104         elsif ( $v eq '2' ) { print "Not in $s1"; }
105         else                { print "Different values: $v"; }
106         print "\n";
107     }
108 }
109
110 sub HandleCompareChanges {
111     my ( $cmp_pref, $dbpref, $filepref ) = @_;
112     my $t = 0;
113     foreach my $k ( sort keys %$cmp_pref ) {
114         my $cmp = $cmp_pref->{$k};
115         if ( $cmp eq '1' ) {
116             $t += DeleteOnePref($k) if $compare_del;
117         } elsif ( $cmp eq '2' ) {
118             my $kwc  = $filepref->{$k}->{orgkey};
119             my $val  = $filepref->{$k}->{value};
120             my $type = $filepref->{$k}->{type};
121             $t += InsertIgnoreOnePref( $kwc, $val, $type ) if $compare_add;
122         } elsif ($cmp) {    #should contain something..
123             my $val = $filepref->{$k}->{value};
124             $t += UpdateOnePref( $k, $val ) if $compare_upd;
125         }
126     }
127     print "Adjusted $t prefs from this compare.\n";
128 }
129
130 sub ComparePrefs {
131     my ( $ph1, $ph2 ) = @_;
132     my $res = {};
133     foreach my $k ( keys %$ph1 ) {
134         if ( !exists $ph2->{$k} ) {
135             $res->{$k} = 1;
136         } else {
137             my $v1 = $ph1->{$k}->{value} // 'NULL';
138             my $v2 = $ph2->{$k}->{value} // 'NULL';
139             if ( $v1 ne $v2 ) {
140                 $res->{$k} = "$v1 / $v2";
141             }
142         }
143     }
144     foreach my $k ( keys %$ph2 ) {
145         if ( !exists $ph1->{$k} ) {
146             $res->{$k} = 2;
147         }
148     }
149     return $res;
150 }
151
152 sub ReadPrefsFromDb {
153     my $sql = 'SELECT variable AS orgkey, LOWER(variable) AS variable, value, type FROM systempreferences ORDER BY variable';
154     my $hash = $dbh->selectall_hashref( $sql, 'variable' );
155     return $hash;
156 }
157
158 sub ReadPrefsFromFile {
159     my ($file) = @_;
160     open my $fh, '<:encoding(UTF-8)', $filename;
161     my @lines = <$fh>;
162     close $fh;
163     my $hash;
164     for ( my $t = 0 ; $t < @lines ; $t++ ) {
165         next if $lines[$t] =~ /^\s*#|^\s*$/;    # comment line or empty line
166         my @l = split ",", $lines[$t], 4;
167         die "Invalid pref file; check line " . ++$t if @l < 4 || $l[0] !~ /^\d+$/ || $t + $l[0] >= @lines;
168         my $key = lc $l[1];
169         $hash->{$key} = { orgkey => $l[1], value => $l[3], type => $l[2] };
170         for ( my $j = 0 ; $j < $l[0] ; $j++ ) { $hash->{$key}->{value} .= $lines[ $t + $j + 1 ]; }
171         $t = $t + $l[0];
172         $hash->{$key}->{value} =~ s/\n$//;      #only 'last' line
173     }
174     return $hash;
175 }
176
177 sub SavePrefsToFile {
178     my ( $hash, $fh ) = @_;
179     print $fh '#cmp_sysprefs.pl: ' . C4::Context->config('database') . ', ' . localtime . "\n";
180     foreach my $k ( sort keys %$hash ) {
181
182         #sort handles underscore differently than mysql?
183         my $c   = CountLines( $hash->{$k}->{value} );
184         my $kwc = $hash->{$k}->{orgkey};                # key-with-case
185         print $fh "$c,$kwc," . ( $hash->{$k}->{type} // 'Free' ) . ',' . ( $hash->{$k}->{value} // 'NULL' ) . "\n";
186     }
187 }
188
189 sub SavePrefsToDb {
190     my ($hash) = @_;
191     my $t = 0;
192
193     #will not erase everything! you can do that in mysql :)
194     foreach my $k ( keys %$hash ) {
195         my $v = $hash->{$k}->{value} eq 'NULL' ? undef : $hash->{$k}->{value};
196         my $kwc  = $hash->{$k}->{orgkey} // $k;
197         my $type = $hash->{$k}->{type}   // 'Free';
198
199         #insert and update seem overkill, but better than delete and insert
200         #you cannot assume that the pref IS or IS NOT there
201         InsertIgnoreOnePref( $kwc, $v, $type );
202         UpdateOnePref( $k, $v );
203         $t++;
204     }
205     print "Updated $t prefs\n";
206 }
207
208 sub InsertIgnoreOnePref {
209     my ( $kwc, $v, $t ) = @_;
210     my $i = $dbh->do(
211         'INSERT IGNORE INTO systempreferences (variable, value, type)
212         VALUES (?,?,?)', undef, ( $kwc, $v, $t )
213     );
214     return !defined($i) || $i eq '0E0'? 0: $i;
215 }
216
217 sub UpdateOnePref {
218     my ( $k, $v ) = @_;
219     return if lc $k eq 'version';
220     my $i = $dbh->do( 'UPDATE systempreferences SET value=? WHERE variable=?', undef, ( $v, $k ) );
221     return !defined($i) || $i eq '0E0'? 0: $i;
222 }
223
224 sub DeleteOnePref {
225     my ($k) = @_;
226     return if lc $k eq 'version';
227     my $sql = 'DELETE FROM systempreferences WHERE variable=?';
228     unless ($ignore_opt) {
229         $sql .= " AND COALESCE(explanation,'')='' AND COALESCE(options,'')=''";
230     }
231     my $i = $dbh->do( $sql, undef, ($k) );
232     return !defined($i) || $i eq '0E0'? 0: $i;
233 }
234
235 sub CheckVersionPref {    #additional precaution
236                           #if there are versions, compare them
237     my ($hash) = @_;
238     my $hv = $hash->{version}->{value};
239     return if !defined $hv;
240     my ($dv) = $dbh->selectrow_array(
241         'SELECT value FROM systempreferences
242         WHERE variable LIKE ?', undef, ('version')
243     );
244     return if !defined $dv;
245     die "Versions do not match ($dv, $hv)" if $dv ne $hv;
246 }
247
248 sub CountLines {
249     my @ma;
250     return ( $_[0] && ( @ma = $_[0] =~ /\r?\n|\r\n?/g ) ) ? scalar @ma : 0;
251 }
252
253 =head1 NAME
254
255 cmp_sysprefs.pl
256
257 =head1 SYNOPSIS
258
259 cmp_sysprefs.pl -help
260
261 cmp_sysprefs.pl -cmd backup -file prefbackup
262
263 cmp_sysprefs.pl -cmd compare -file prefbackup -upd
264
265 cmp_sysprefs.pl -cmd compare -file prefbackup -del -ign-opt
266
267 cmp_sysprefs.pl -cmd restore -file prefbackup
268
269 =head1 DESCRIPTION
270
271 This script may backup, compare and restore system preferences from file.
272
273 Precaution: only the last command or file name will be used. The add, del and
274 upd parameters are extensions for the compare command. They allow you to act
275 immediately on the compare results.
276
277 When restoring a preferences file containing a version pref to a database having
278 another version, the restore will not be made. Similarly, a version pref will
279 never be overwritten. A restore will overwrite prefs but not delete them.
280
281 It is possible to edit the preference backup files. But be careful. The first
282 parameter for each preference is a line count. Some preference values use more
283 than one line. If you edit a file, make sure that the line counts are still
284 valid.
285
286 You can compare/restore using edited/partial preference files. Take special
287 care when using the del parameter in comparing such a partial file. It will
288 delete all prefs in the database not found in your partial file. Partial pref
289 files can however be very useful when testing or monitoring a limited set of
290 prefs.
291
292 The ign-opt flag allows you to delete preferences that have explanation or
293 options in the database. If you do not set this flag, a compare with delete
294 will by default only delete preferences without explanation/options. Use this
295 option only if you understand the risk. Note that a restore will recover value,
296 not explanation or options. (See also BZ 10199.)
297
298 =over 8
299
300 =item B<-help>
301
302 Print this usage statement.
303
304 =item B<-cmd>
305
306 Command: backup, compare, restore or test.
307
308 =item B<-file>
309
310 Name of the file used in command.
311
312 =item B<-add>
313
314 Only for compares: restore preferences not present in database.
315
316 =item B<-del>
317
318 Only for compares: delete preferences not present in file.
319
320 =item B<-upd>
321
322 Only for compares: update preferences when values differ.
323
324 =item B<-ign-opt>
325
326 Ignore options/explanation when comparing with delete flag. Use this flag with care.
327
328 =back
329
330 =cut