Datenbankupgradescript für Mandanten
[kivitendo-erp.git] / sql / Pg-upgrade2-auth / clients.pl
diff --git a/sql/Pg-upgrade2-auth/clients.pl b/sql/Pg-upgrade2-auth/clients.pl
new file mode 100644 (file)
index 0000000..074a15b
--- /dev/null
@@ -0,0 +1,340 @@
+# @tag: clients
+# @description: Einführung von Mandaten
+# @depends: release_3_0_0
+# @ignore: 0
+package SL::DBUpgrade2::clients;
+
+use strict;
+use utf8;
+
+use parent qw(SL::DBUpgrade2::Base);
+
+use List::MoreUtils qw(any all);
+use List::Util qw(first);
+
+use SL::DBConnect;
+use SL::DBUtils;
+use SL::Template;
+use SL::Helper::Flash;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar                  => [ qw(clients) ],
+  'scalar --get_set_init' => [ qw(users groups templates auth_db_settings data_dbhs) ],
+);
+
+sub init_users {
+  my ($self) = @_;
+  my @users  = selectall_hashref_query($::form, $self->dbh, qq|SELECT * FROM auth."user" ORDER BY lower(login)|);
+
+  foreach my $user (@users) {
+    my @attributes = selectall_hashref_query($::form, $self->dbh, <<SQL, $user->{id});
+     SELECT cfg_key, cfg_value
+     FROM auth.user_config
+     WHERE user_id = ?
+SQL
+
+    $user->{ $_->{cfg_key} } = $_->{cfg_value} for @attributes;
+  }
+
+  return \@users;
+}
+
+sub init_groups {
+  my ($self) = @_;
+  return [ selectall_hashref_query($::form, $self->dbh, qq|SELECT * FROM auth."group" ORDER BY lower(name)|) ];
+}
+
+sub init_templates {
+  my %templates = SL::Template->available_templates;
+  return $templates{print_templates};
+}
+
+sub init_auth_db_settings {
+  my $cfg = $::lx_office_conf{'authentication/database'};
+  return {
+    dbhost => $cfg->{host} || 'localhost',
+    dbport => $cfg->{port} || 5432,
+    dbname => $cfg->{name},
+  };
+}
+
+sub init_data_dbhs {
+  return [];
+}
+
+sub _clear_field {
+  my ($text) = @_;
+
+  $text ||= '';
+  $text   =~ s/^\s+|\s+$//g;
+
+  return $text;
+}
+
+sub _group_into_clients {
+  my ($self) = @_;
+
+  my @match_fields = qw(dbhost dbport dbname);
+  my @copy_fields  = (@match_fields, qw(address company co_ustid dbuser dbpasswd duns sepa_creditor_id taxnumber templates));
+  my @clients;
+
+  # Group users into clients. Users which have identical database
+  # settings (host, port and name) will be grouped. The other fields
+  # like tax number etc. are taken from the first user and only filled
+  # from user users if they're still unset.
+  foreach my $user (@{ $self->users }) {
+    $user->{$_} = _clear_field($user->{$_}) for @copy_fields;
+
+    my $existing_client = first { my $client = $_; all { ($user->{$_} || '') eq ($client->{$_} || '') } @match_fields } @clients;
+
+    if ($existing_client) {
+      push @{ $existing_client->{users} }, $user->{id};
+      $existing_client->{$_} ||= $user->{$_} for @copy_fields;
+      next;
+    }
+
+    push @clients, {
+      map({ $_ => $user->{$_} } @copy_fields),
+      name    => $::locale->text('Client #1', scalar(@clients) + 1),
+      users   => [ $user->{id} ],
+      groups  => [ map { $_->{id} } @{ $self->groups } ],
+      enabled => 1,
+    };
+  }
+
+  # Ignore users (and therefore clients) for which no database
+  # configuration has been given.
+  @clients = grep { my $client = $_; any { $client->{$_} } @match_fields } @clients;
+
+  # If there's only one client set that one as default.
+  $clients[0]->{is_default} = 1 if scalar(@clients) == 1;
+
+  # Set a couple of defaults for database fields.
+  my $num = 0;
+  foreach my $client (@clients) {
+    $num                += 1;
+    $client->{dbhost}  ||= 'localhost';
+    $client->{dbport}  ||= 5432;
+    $client->{templates} =~ s:templates/::;
+  }
+
+  $self->clients(\@clients);
+}
+
+sub _analyze {
+  my ($self, %params) = @_;
+
+  $self->_group_into_clients;
+
+  return $self->_do_convert if !@{ $self->clients };
+
+  print $::form->parse_html_template('dbupgrade/auth/clients', { SELF => $self });
+
+  return 2;
+}
+
+sub _verify_clients {
+  my ($self) = @_;
+
+  my (%names, @errors);
+
+  my $num = 0;
+  foreach my $client (@{ $self->clients }) {
+    $num += 1;
+
+    next if !$client->{enabled};
+
+    $client->{$_} = _clear_field($client->{$_}) for qw(address co_ustid company dbhost dbname dbpasswd dbport dbuser duns sepa_creditor_id taxnumber templates);
+
+    if (!$client->{name} || $names{ $client->{name} }) {
+      push @errors, $::locale->text('New client #1: The name must be unique and not empty.', $num);
+    }
+
+    $names{ $client->{name} } = 1;
+
+    if (any { !$client->{$_} } qw(dbhost dbport dbname dbuser)) {
+      push @errors, $::locale->text('New client #1: The database configuration fields "host", "port", "name" and "user" must not be empty.', $num);
+    }
+  }
+
+  return @errors;
+}
+
+sub _alter_auth_database_structure {
+  my ($self) = @_;
+
+  my @queries = (
+    qq|CREATE TABLE auth.clients (
+         id         SERIAL  PRIMARY KEY,
+         name       TEXT    NOT NULL UNIQUE,
+         dbhost     TEXT    NOT NULL,
+         dbport     INTEGER NOT NULL DEFAULT 5432,
+         dbname     TEXT    NOT NULL,
+         dbuser     TEXT    NOT NULL,
+         dbpasswd   TEXT    NOT NULL,
+         is_default BOOLEAN NOT NULL DEFAULT FALSE,
+
+         UNIQUE (dbhost, dbport, dbname)
+       )|,
+    qq|CREATE TABLE auth.clients_users (
+         client_id INTEGER NOT NULL REFERENCES auth.clients (id),
+         user_id   INTEGER NOT NULL REFERENCES auth."user"  (id),
+
+         PRIMARY KEY (client_id, user_id)
+       )|,
+    qq|CREATE TABLE auth.clients_groups (
+         client_id INTEGER NOT NULL REFERENCES auth.clients (id),
+         group_id  INTEGER NOT NULL REFERENCES auth."group" (id),
+
+         PRIMARY KEY (client_id, group_id)
+       )|,
+  );
+
+  $self->db_query($_, may_fail => 0) for @queries;
+}
+
+sub _alter_data_database_structure {
+  my ($self, $dbh) = @_;
+
+  my @queries = (
+    qq|ALTER TABLE defaults ADD COLUMN company          TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN address          TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN taxnumber        TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN co_ustid         TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN duns             TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN sepa_creditor_id TEXT|,
+    qq|ALTER TABLE defaults ADD COLUMN templates        TEXT|,
+    qq|INSERT INTO schema_info (tag, login) VALUES ('clients', 'admin')|,
+  );
+
+  foreach my $query (@queries) {
+    $dbh->do($query) || die $self->db_errstr($dbh);
+  }
+}
+
+sub _create_clients_in_auth_database {
+  my ($self)  = @_;
+
+  my @client_columns   = qw(name dbhost dbport dbname dbuser dbpasswd is_default);
+  my $q_client         = qq|INSERT INTO auth.clients (| . join(', ', @client_columns) . qq|) VALUES (| . join(', ', ('?') x @client_columns) . qq|) RETURNING id|;
+  my $sth_client       = $self->dbh->prepare($q_client) || die $self->db_errstr;
+
+  my $q_client_user    = qq|INSERT INTO auth.clients_users (client_id, user_id) VALUES (?, ?)|;
+  my $sth_client_user  = $self->dbh->prepare($q_client_user) || die $self->db_errstr;
+
+  my $q_client_group   = qq|INSERT INTO auth.clients_groups (client_id, group_id) VALUES (?, ?)|;
+  my $sth_client_group = $self->dbh->prepare($q_client_group) || die $self->db_errstr;
+
+  foreach my $client (@{ $self->clients }) {
+    next unless $client->{enabled};
+
+    $client->{is_default} = $client->{is_default} ? 1 : 0;
+
+    $sth_client->execute(@{ $client }{ @client_columns }) || die;
+    my $client_id = $sth_client->fetch->[0];
+
+    $sth_client_user ->execute($client_id, $_) || die for @{ $client->{users}  || [] };
+    $sth_client_group->execute($client_id, $_) || die for @{ $client->{groups} || [] };
+  }
+
+  $sth_client      ->finish;
+  $sth_client_user ->finish;
+  $sth_client_group->finish;
+}
+
+sub _clean_auth_database {
+  my ($self) = @_;
+
+  my @keys_to_delete = qw(acs address admin anfragen angebote bestellungen businessnumber charset companies company co_ustid currency dbconnect dbdriver dbhost dbname dboptions dbpasswd dbport dbuser duns
+                          einkaufsrechnungen in_numberformat lieferantenbestellungen login pdonumber printer rechnungen role sdonumber sepa_creditor_id sid steuernummer taxnumber templates);
+
+  $self->dbh->do(qq|DELETE FROM auth.user_config WHERE cfg_key IN (| . join(', ', ('?') x @keys_to_delete) . qq|)|, undef, @keys_to_delete)
+    || die $self->db_errstr;
+}
+
+sub _copy_fields_to_data_database {
+  my ($self, $client) = @_;
+
+  my $dbh = SL::DBConnect->connect('dbi:Pg:dbname=' . $client->{dbname} . ';host=' . $client->{dbhost} . ';port=' . $client->{dbport},
+                                   $client->{dbuser}, $client->{dbpasswd},
+                                   SL::DBConnect->get_options(AutoCommit => 0));
+  if (!$dbh) {
+    die join("\n",
+             $::locale->text('The connection to the configured client database "#1" on host "#2:#3" failed.', $client->{dbname}, $client->{dbhost}, $client->{dbport}),
+             $::locale->text('Please correct the settings and try again or deactivate that client.'),
+             $::locale->text('Error message from the database: #1', $self->db_errstr('DBI')));
+  }
+
+  my ($has_been_applied) = $dbh->selectrow_array(qq|SELECT tag FROM schema_info WHERE tag = 'clients'|);
+
+  if (!$has_been_applied) {
+    $self->_alter_data_database_structure($dbh);
+  }
+
+  my @columns = qw(company address taxnumber co_ustid duns sepa_creditor_id templates);
+  my $query   = join ', ', map { "$_ = ?" } @columns;
+  my @values  = @{ $client }{ @columns };
+
+  if (!$dbh->do(qq|UPDATE defaults SET $query|, undef, @values)) {
+    die join("\n",
+             $::locale->text('Updating the client fields in the database "#1" on host "#2:#3" failed.', $client->{dbname}, $client->{dbhost}, $client->{dbport}),
+             $::locale->text('Please correct the settings and try again or deactivate that client.'),
+             $::locale->text('Error message from the database: #1', $self->db_errstr('DBI')));
+  }
+
+  $self->data_dbhs([ @{ $self->data_dbhs }, $dbh ]);
+}
+
+sub _commit_data_database_changes {
+  my ($self) = @_;
+
+  foreach my $dbh (@{ $self->data_dbhs }) {
+    $dbh->commit;
+    $dbh->disconnect;
+  }
+}
+
+sub _do_convert {
+  my ($self) = @_;
+
+  # Skip clients that are not enabled. Clean fields.
+  my $num = 0;
+  foreach my $client (@{ $self->clients }) {
+    $num += 1;
+
+    next if !$client->{enabled};
+
+    $client->{$_}        = _clear_field($client->{$_}) for qw(dbhost dbport dbname dbuser dbpasswd address company co_ustid dbuser dbpasswd duns sepa_creditor_id taxnumber templates);
+    $client->{templates} = 'templates/' . $client->{templates};
+  }
+
+  $self->_copy_fields_to_data_database($_) for grep { $_->{enabled} } @{ $self->clients };
+
+  $self->_alter_auth_database_structure;
+  $self->_create_clients_in_auth_database;
+  $self->_clean_auth_database;
+
+  $self->_commit_data_database_changes;
+
+  return 1;
+}
+
+sub run {
+  my ($self) = @_;
+
+  return $self->_analyze if !$::form->{clients} || !@{ $::form->{clients} };
+
+  $self->clients($::form->{clients});
+
+  my @errors = $self->_verify_clients;
+
+  return $self->_do_convert if !@errors;
+
+  flash('error', @errors);
+
+  print $::form->parse_html_template('dbupgrade/auth/clients', { SELF => $self });
+
+  return 1;
+}
+
+1;