epic-s6ts
[kivitendo-erp.git] / SL / DBUpgrade2.pm
1 package SL::DBUpgrade2;
2
3 use English qw(-no_match_vars);
4 use IO::File;
5 use List::MoreUtils qw(any);
6
7 use SL::Common;
8 use SL::DBUpgrade2::Base;
9 use SL::DBUtils;
10 use SL::System::Process;
11
12 use strict;
13
14 sub new {
15   my $package = shift;
16
17   return bless({}, $package)->init(@_);
18 }
19
20 sub init {
21   my ($self, %params) = @_;
22
23   if ($params{auth}) {
24     $params{path_suffix} = "-auth";
25     $params{schema}      = "auth.";
26   }
27
28   $params{path_suffix} ||= '';
29   $params{schema}      ||= '';
30   $params{path}        ||= SL::System::Process->exe_dir . "/sql/Pg-upgrade2" . $params{path_suffix};
31
32   map { $self->{$_} = $params{$_} } keys %params;
33
34   return $self;
35 }
36
37 sub path {
38   $_[0]{path};
39 }
40
41 sub parse_dbupdate_controls {
42   my ($self) = @_;
43
44   my $form   = $self->{form};
45   my $locale = $::locale;
46
47   local *IN;
48   my %all_controls;
49
50   my $path = $self->path;
51
52   foreach my $file_name (<$path/*.sql>, <$path/*.pl>) {
53     next unless (open(IN, "<:encoding(UTF-8)", $file_name));
54
55     my $file = $file_name;
56     $file =~ s|.*/||;
57
58     my $control = {
59       priority    => 1000,
60       depends     => [],
61       required_by => [],
62       locales     => [],
63     };
64
65     while (<IN>) {
66       chomp();
67       next unless (/^(--|\#)\s*\@/);
68       s/^(--|\#)\s*\@//;
69       s/\s*$//;
70       next if ($_ eq "");
71
72       my @fields = split(/\s*:\s*/, $_, 2);
73       next unless (scalar(@fields) == 2);
74
75       if ($fields[0] =~ m{^(?:depends|required_by)$}) {
76         push(@{$control->{$fields[0]}}, split(/\s+/, $fields[1]));
77       } elsif ($fields[0] eq "locales") {
78         push @{$control->{locales}}, $fields[1];
79       } else {
80         $control->{$fields[0]} = $fields[1];
81       }
82     }
83
84     next if ($control->{ignore});
85
86     if (!$control->{"tag"}) {
87       _control_error($form, $file_name, $locale->text("Missing 'tag' field.")) ;
88     }
89
90     if ($control->{"tag"} =~ /[^a-zA-Z0-9_\(\)\-]/) {
91       _control_error($form, $file_name, $locale->text("The 'tag' field must only consist of alphanumeric characters or the carachters - _ ( )"))
92     }
93
94     if (defined($all_controls{$control->{"tag"}})) {
95       _control_error($form, $file_name, sprintf($locale->text("More than one control file with the tag '%s' exist."), $control->{"tag"}))
96     }
97
98     if (!$control->{"description"}) {
99       _control_error($form, $file_name, sprintf($locale->text("Missing 'description' field."))) ;
100     }
101
102     delete @{$control}{qw(depth applied)};
103
104     my @unknown_keys = grep { !m{^ (?: depends | required_by | description | file | ignore | locales | may_fail | priority | superuser_privileges | tag ) $}x } keys %{ $control };
105     if (@unknown_keys) {
106       _control_error($form, $file_name, sprintf($locale->text("Unknown control fields: #1", join(' ', sort({ lc $a cmp lc $b } @unknown_keys)))));
107     }
108
109     $control->{"priority"}  *= 1;
110     $control->{"priority"} ||= 1000;
111     $control->{"file"}       = $file;
112
113     $all_controls{$control->{"tag"}} = $control;
114
115     close(IN);
116   }
117
118   foreach my $name (keys %all_controls) {
119     my $control = $all_controls{$name};
120
121     foreach my $dependency (@{ delete $control->{required_by} }) {
122       _control_error($form, $control->{"file"}, sprintf($locale->text("Unknown dependency '%s'."), $dependency)) if (!defined($all_controls{$dependency}));
123       push @{ $all_controls{$dependency}->{depends} }, $name;
124     }
125   }
126
127   foreach my $control (values(%all_controls)) {
128     foreach my $dependency (@{$control->{"depends"}}) {
129       _control_error($form, $control->{"file"}, sprintf($locale->text("Unknown dependency '%s'."), $dependency)) if (!defined($all_controls{$dependency}));
130     }
131
132     map({ $_->{"loop"} = 0; } values(%all_controls));
133     _check_for_loops($form, $control->{"file"}, \%all_controls, $control->{"tag"});
134   }
135
136   map({ _dbupdate2_calculate_depth(\%all_controls, $_->{"tag"}) }
137       values(%all_controls));
138
139   $self->{all_controls} = \%all_controls;
140
141   return $self;
142 }
143
144 sub process_query {
145   $::lxdebug->enter_sub();
146
147   my ($self, $dbh, $filename, $version_or_control) = @_;
148
149   my $form  = $self->{form};
150   my $fh    = IO::File->new($filename, "<:encoding(UTF-8)");
151   my $query = "";
152   my $sth;
153   my @quote_chars;
154
155   if (!$fh) {
156     return "No such file: $filename" if $self->{return_on_error};
157     $form->error("$filename : $!\n");
158   }
159
160   $dbh->begin_work();
161
162   while (<$fh>) {
163     # Remove DOS and Unix style line endings.
164     chomp;
165
166     for (my $i = 0; $i < length($_); $i++) {
167       my $char = substr($_, $i, 1);
168
169       # Are we inside a string?
170       if (@quote_chars) {
171         if ($char eq $quote_chars[-1]) {
172           pop(@quote_chars);
173         } elsif (length $quote_chars[-1] > 1
174              &&  substr($_, $i, length $quote_chars[-1]) eq $quote_chars[-1]) {
175           $i   += length($quote_chars[-1]) - 1;
176           $char = $quote_chars[-1];
177           pop(@quote_chars);
178         }
179         $query .= $char;
180
181       } else {
182         my ($tag, $tag_end);
183         if (($char eq "'") || ($char eq "\"")) {
184           push(@quote_chars, $char);
185
186         } elsif ($char eq '$'                                            # start of dollar quoting
187              && ($tag_end  = index($_, '$', $i + 1)) > -1                # ends on same line
188              && (do { $tag = substr($_, $i + 1, $tag_end - $i - 1); 1 }) # extract tag
189              &&  $tag      =~ /^ (?= [A-Za-z_] [A-Za-z0-9_]* | ) $/x) {  # tag is identifier
190           push @quote_chars, $char = '$' . $tag . '$';
191           $i = $tag_end;
192         } elsif ($char eq "-") {
193           if ( substr($_, $i+1, 1) eq "-") {
194             # found a comment outside quote
195             last;
196           }
197         } elsif ($char eq ";") {
198
199           # Query is complete. Send it.
200
201           $sth = $dbh->prepare($query);
202           if (!$sth->execute()) {
203             my $errstr = $dbh->errstr;
204             return $errstr // '<unknown database error>' if $self->{return_on_error};
205             $sth->finish();
206             $dbh->rollback();
207             if (!ref $version_or_control || ref $version_or_control ne 'HASH' || !$version_or_control->{may_fail})  {
208               $form->dberror("The database update/creation did not succeed. " .
209                              "The file ${filename} containing the following " .
210                              "query failed:<br>${query}<br>" .
211                              "The error message was: ${errstr}<br>" .
212                              "All changes in that file have been reverted.")
213             }
214           }
215           $sth->finish();
216
217           $char  = "";
218           $query = "";
219         }
220
221         $query .= $char;
222       }
223     }
224
225     # Insert a space at the end of each line so that queries split
226     # over multiple lines work properly.
227     if ($query ne '') {
228       $query .= @quote_chars ? "\n" : ' ';
229     }
230   }
231
232   if (ref($version_or_control) eq "HASH") {
233     $dbh->do("INSERT INTO " . $self->{schema} . "schema_info (tag, login) VALUES (" . $dbh->quote($version_or_control->{"tag"}) . ", " . $dbh->quote($form->{"login"}) . ")");
234   } elsif ($version_or_control) {
235     $dbh->do("UPDATE defaults SET version = " . $dbh->quote($version_or_control));
236   }
237   $dbh->commit();
238
239   $fh->close();
240
241   $::lxdebug->leave_sub();
242
243   # Signal "no error"
244   return undef;
245 }
246
247 # Process a Perl script which updates the database.
248 # If the script returns 1 then the update was successful.
249 # Return code "2" means "needs more interaction; unlock
250 # the system and end current request".
251 # All other return codes are fatal errors.
252 sub process_perl_script {
253   $::lxdebug->enter_sub();
254
255   my ($self, $dbh, $filename, $version_or_control) = @_;
256
257   my %form_values = %$::form;
258
259   $dbh->begin_work;
260
261   # setup dbup_ export vars & run script
262   my %dbup_myconfig = map { ($_ => $::form->{$_}) } qw(dbname dbuser dbpasswd dbhost dbport dbconnect);
263   my $result        = eval {
264     SL::DBUpgrade2::Base::execute_script(
265       file_name => $filename,
266       tag       => $version_or_control->{tag},
267       dbh       => $dbh,
268       myconfig  => \%dbup_myconfig,
269     );
270   };
271
272   my $error = $EVAL_ERROR;
273
274   $dbh->rollback if 1 != ($result // -1);
275
276   return $error if $self->{return_on_error} && (1 != ($result // -1));
277
278   if (!defined($result)) {
279     print $::form->parse_html_template("dbupgrade/error", { file  => $filename, error => $error });
280     $::dispatcher->end_request;
281   } elsif (1 != $result) {
282     SL::System::InstallationLock->unlock if 2 == $result;
283     $::dispatcher->end_request;
284   }
285
286   if (ref($version_or_control) eq "HASH") {
287     $dbh->do("INSERT INTO " . $self->{schema} . "schema_info (tag, login) VALUES (" . $dbh->quote($version_or_control->{tag}) . ", " . $dbh->quote($::form->{login}) . ")");
288   } elsif ($version_or_control) {
289     $dbh->do("UPDATE defaults SET version = " . $dbh->quote($version_or_control));
290   }
291
292   $dbh->commit if !$dbh->{AutoCommit} || $dbh->{BegunWork};
293
294   # Clear $::form of values that may have been set so that following
295   # Perl upgrade scripts won't have to work with old data (think of
296   # the usual 'continued' mechanism that's used for determining
297   # whether or not the upgrade form must be displayed).
298   delete @{ $::form }{ keys %{ $::form } };
299   $::form->{$_} = $form_values{$_} for keys %form_values;
300
301   $::lxdebug->leave_sub();
302
303   return undef;
304 }
305
306 sub process_file {
307   my ($self, $dbh, $filename, $version_or_control) = @_;
308
309   my $result = $filename =~ m/sql$/ ? $self->process_query(      $dbh, $filename, $version_or_control)
310                                     : $self->process_perl_script($dbh, $filename, $version_or_control);
311
312   $::lxdebug->log_time("DB upgrade script '${filename}' finished");
313
314   return $result;
315 }
316
317 sub unapplied_upgrade_scripts {
318   my ($self, $dbh) = @_;
319
320   my @all_scripts = map { $_->{applied} = 0; $_ } $self->sort_dbupdate_controls;
321
322   my $query = qq|SELECT tag FROM | . $self->{schema} . qq|schema_info|;
323   my $sth   = $dbh->prepare($query);
324   $sth->execute || $self->{form}->dberror($query);
325   while (my ($tag) = $sth->fetchrow_array()) {
326     $self->{all_controls}->{$tag}->{applied} = 1 if defined $self->{all_controls}->{$tag};
327   }
328   $sth->finish;
329
330   return grep { !$_->{applied} } @all_scripts;
331 }
332
333 sub apply_admin_dbupgrade_scripts {
334   my ($self, $called_from_admin) = @_;
335
336   return 0 if !$self->{auth};
337
338   my $dbh               = $::auth->dbconnect;
339   my @unapplied_scripts = $self->unapplied_upgrade_scripts($dbh);
340
341   return 0 if !@unapplied_scripts;
342
343   $self->{form}->{login} ||= 'admin';
344
345   if ($called_from_admin) {
346     $self->{form}->{title} = $::locale->text('Dataset upgrade');
347     $self->{form}->header;
348   }
349
350   print $self->{form}->parse_html_template("dbupgrade/header", { dbname => $::auth->{DB_config}->{db} });
351
352   $::lxdebug->log_time("DB upgrades commencing");
353
354   foreach my $control (@unapplied_scripts) {
355     $::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}");
356     print $self->{form}->parse_html_template("dbupgrade/upgrade_message2", $control);
357
358     $self->process_file($dbh, "sql/Pg-upgrade2-auth/$control->{file}", $control);
359   }
360
361   $::lxdebug->log_time("DB upgrades finished");
362
363   print $self->{form}->parse_html_template("dbupgrade/footer", { is_admin => 1 }) if $called_from_admin;
364
365   return 1;
366 }
367
368 sub _check_for_loops {
369   my ($form, $file_name, $controls, $tag, @path) = @_;
370
371   push(@path, $tag);
372
373   my $ctrl = $controls->{$tag};
374
375   if ($ctrl->{"loop"} == 1) {
376     # Not done yet.
377     _control_error($form, $file_name, $::locale->text("Dependency loop detected:") . " " . join(" -> ", @path))
378
379   } elsif ($ctrl->{"loop"} == 0) {
380     # Not checked yet.
381     $ctrl->{"loop"} = 1;
382     map({ _check_for_loops($form, $file_name, $controls, $_, @path); } @{ $ctrl->{"depends"} });
383     $ctrl->{"loop"} = 2;
384   }
385 }
386
387 sub _control_error {
388   my ($form, $file_name, $message) = @_;
389
390   $form = $::form;
391   my $locale = $::locale;
392
393   $form->error(sprintf($locale->text("Error in database control file '%s': %s"), $file_name, $message));
394 }
395
396 sub _dbupdate2_calculate_depth {
397   my ($tree, $tag) = @_;
398
399   my $node = $tree->{$tag};
400
401   return if (defined($node->{"depth"}));
402
403   my $max_depth = 0;
404
405   foreach $tag (@{$node->{"depends"}}) {
406     _dbupdate2_calculate_depth($tree, $tag);
407     my $value = $tree->{$tag}->{"depth"};
408     $max_depth = $value if ($value > $max_depth);
409   }
410
411   $node->{"depth"} = $max_depth + 1;
412 }
413
414 sub sort_dbupdate_controls {
415   my $self = shift;
416
417   $self->parse_dbupdate_controls unless $self->{all_controls};
418
419   return sort { ($a->{depth} <=> $b->{depth}) || ($a->{priority} <=> $b->{priority}) || ($a->{tag} cmp $b->{tag}) } values %{ $self->{all_controls} };
420 }
421
422 1;
423 __END__
424
425 =pod
426
427 =encoding utf8
428
429 =head1 NAME
430
431 SL::DBUpgrade2 - Parse database upgrade files stored in
432 C<sql/Pg-upgrade2> and C<sql/Pg-upgrade2-auth>
433
434 =head1 SYNOPSIS
435
436   use SL::User;
437   use SL::DBUpgrade2;
438
439   # Apply outstanding updates to the authentication database
440   my $scripts = SL::DBUpgrade2->new(
441     form     => $::form,
442     auth     => 1
443   );
444   $scripts->apply_admin_dbupgrade_scripts(1);
445
446   # Apply updates to a user database
447   my $scripts = SL::DBUpgrade2->new(
448     form     => $::form,
449     auth     => 1
450   );
451   User->dbupdate2(form     => $form,
452                   updater  => $scripts->parse_dbupdate_controls,
453                   database => $dbname);
454
455 =head1 OVERVIEW
456
457 Database upgrade files are used to upgrade the database structure and
458 content of both the authentication database and the user
459 databases. They're applied when a user logs in. As long as the
460 authentication database is not up to date users cannot log in in
461 general, and the admin has to log in first in order to get his
462 database updated.
463
464 Database scripts form a tree by specifying which upgrade file depends
465 on which other upgrade file. This means that such files are always
466 applied in a well-defined order.
467
468 Each script is run in a separate transaction. If a script fails the
469 current transaction is rolled back and the whole upgrade process is
470 stopped. The user/admin is required to fix the issue manually.
471
472 A list of applied upgrade scripts is maintained in a table called
473 C<schema_info> for the user database and C<auth.schema_info>) for the
474 authentication database. They contain the tags, the login name of the
475 user having applied the script and the timestamp when the script was
476 applied.
477
478 Database upgrade files come in two flavours: SQL files and Perl
479 files. For both there are control fields that determine the order in
480 which they're executed etc. The control fields are tag/value pairs
481 contained in comments.
482
483 =head1 CONTROL FIELDS
484
485 =head2 SYNTAX
486
487 Control fields for Perl files:
488
489   # @tag1: value1
490   # @tag2: some more values
491   sub do_stuff {
492   }
493   1;
494
495 Control fields for SQL files:
496
497   -- @tag1: value1
498   -- @tag2: some more values
499   ALTER TABLE ...;
500
501 =head2 TAGS AND THEIR MEANING
502
503 The following tags are recognized:
504
505 =over 4
506
507 =item tag
508
509 The name for this file. The C<tag> is also used for dependency
510 resolution (see C<depends>).
511
512 This is mandatory.
513
514 =item description
515
516 A description presented to the user when the update is applied.
517
518 This is mandatory.
519
520 =item depends
521
522 A space-separated list of tags of scripts this particular script
523 depends on. All other upgrades listed in C<depends> will be applied
524 before the current one is applied.
525
526 =item priority
527
528 Ordering the scripts by their dependencies alone produces a lot of
529 groups of scripts that could be applied at the same time (e.g. if both
530 B and C depend only on A then B could be applied before C or the other
531 way around). This field determines the order inside such a
532 group. Scripts with lower priority fields are executed before scripts
533 with higher priority fields.
534
535 If two scripts have equal priorities then their tag name decides.
536
537 The priority defaults to 1000.
538
539 =back
540
541 =head1 FUNCTIONS
542
543 =over 4
544
545 =item C<apply_admin_dbupgrade_scripts $called_from_admin>
546
547 Applies all unapplied upgrade files to the authentication/admin
548 database. The parameter C<$called_from_admin> should be truish if the
549 function is called from the web interface and falsish if it's called
550 from e.g. a command line script like C<scripts/dbupgrade2_tool.pl>.
551
552 =item C<init %params>
553
554 Initializes the object. Is called directly from L<new> and should not
555 be called again.
556
557 =item C<new %params>
558
559 Creates a new object. Possible parameters are:
560
561 =over 4
562
563 =item path
564
565 Path to the upgrade files to parse. Required.
566
567 =item form
568
569 C<SL::Form> object to use. Required.
570
571 =item auth
572
573 Optional parameter defaulting to 0. If trueish then the scripts read
574 are the ones applying to the authentication database.
575
576 =back
577
578 =item C<parse_dbupdate_controls>
579
580 Parses all files located in C<path> (see L<new>), ananlyzes their
581 control fields, builds the tree, and signals errors if control fields
582 are missing/wrong (e.g. a tag name listed in C<depends> is not
583 found). Sets C<$Self-&gt;{all_controls}> to the list of database
584 scripts.
585
586 =item C<process_file $dbh, $filename, $version_or_control>
587
588 Applies a single database upgrade file. Calls L<process_perl_script>
589 for Perl update files and C<process_query> for SQL update
590 files. Requires an open database handle(C<$dbh>), the file name
591 (C<$filename>) and a hash structure of the file's control fields as
592 produced by L<parse_dbupdate_controls> (C<$version_or_control>).
593
594 Returns the result of the actual function called.
595
596 =item C<process_perl_script $dbh, $filename, $version_or_control>
597
598 Applies a single Perl database upgrade file. Requires an open database
599 handle(C<$dbh>), the file name (C<$filename>) and a hash structure of
600 the file's control fields as produced by L<parse_dbupdate_controls>
601 (C<$version_or_control>).
602
603 Perl scripts are executed via L<eval>. If L<eval> returns falsish then
604 an error is expected. There are two special return values: If the
605 script returns C<1> then the update was successful. Return code C<2>
606 means "needs more interaction from the user; unlock the system and
607 end current upgrade process". All other return codes are fatal errors.
608
609 Inside the Perl script several local variables exist that can be used:
610
611 =over 4
612
613 =item $dbup_locale
614
615 A locale object for translating messages
616
617 =item $dbh
618
619 The database handle (inside a transaction).
620
621 =item $::form
622
623 The global C<SL::Form> object.
624
625 =back
626
627 A Perl script can actually implement queries that fail while
628 continuing the process by handling the transaction itself, e.g. with
629 the following function:
630
631   sub do_query {
632     my ($query, $may_fail) = @_;
633
634     if (!$dbh->do($query)) {
635       die($dbup_locale->text("Database update error:") . "<br>$msg<br>" . $DBI::errstr) unless $may_fail;
636       $dbh->rollback();
637       $dbh->begin_work();
638     }
639   }
640
641 =item C<process_query $dbh, $filename, $version_or_control>
642
643 Applies a single SQL database upgrade file. Requires an open database
644 handle(C<$dbh>), the file name (C<$filename>), and a hash structure of
645 the file's control fields as produced by L<parse_dbupdate_controls>
646 (C<$version_or_control>).
647
648 =item C<sort_dbupdate_controls>
649
650 Sorts the database upgrade scripts according to their C<tag> and
651 C<priority> control fields. Returns a list of their hash
652 representations that can be applied in order.
653
654 =item C<unapplied_upgrade_scripts $dbh>
655
656 Returns a list if upgrade scripts (their internal hash representation)
657 that haven't been applied to a database yet. C<$dbh> is an open handle
658 to the database that is checked.
659
660 Requires that the scripts have been parsed.
661
662 =back
663
664 =head1 BUGS
665
666 Nothing here yet.
667
668 =head1 AUTHOR
669
670 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
671
672 =cut