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