Refactoring
[kivitendo-erp.git] / sql / Pg-upgrade2-auth / clients.pl
1 # @tag: clients
2 # @description: Einführung von Mandanten
3 # @depends: release_3_0_0
4 # @ignore: 0
5 package SL::DBUpgrade2::clients;
6
7 use strict;
8 use utf8;
9
10 use parent qw(SL::DBUpgrade2::Base);
11
12 use List::MoreUtils qw(any all);
13 use List::Util qw(first);
14
15 use SL::DBConnect;
16 use SL::DBUtils;
17 use SL::Template;
18 use SL::Helper::Flash;
19
20 use Rose::Object::MakeMethods::Generic (
21   scalar                  => [ qw(clients) ],
22   'scalar --get_set_init' => [ qw(users groups templates auth_db_settings data_dbhs) ],
23 );
24
25 sub init_users {
26   my ($self) = @_;
27   my @users  = selectall_hashref_query($::form, $self->dbh, qq|SELECT * FROM auth."user" ORDER BY lower(login)|);
28
29   foreach my $user (@users) {
30     my @attributes = selectall_hashref_query($::form, $self->dbh, <<SQL, $user->{id});
31      SELECT cfg_key, cfg_value
32      FROM auth.user_config
33      WHERE user_id = ?
34 SQL
35
36     $user->{ $_->{cfg_key} } = $_->{cfg_value} for @attributes;
37   }
38
39   return \@users;
40 }
41
42 sub init_groups {
43   my ($self) = @_;
44   return [ selectall_hashref_query($::form, $self->dbh, qq|SELECT * FROM auth."group" ORDER BY lower(name)|) ];
45 }
46
47 sub init_templates {
48   my %templates = SL::Template->available_templates;
49   return $templates{print_templates};
50 }
51
52 sub init_auth_db_settings {
53   my $cfg = $::lx_office_conf{'authentication/database'};
54   return {
55     dbhost => $cfg->{host} || 'localhost',
56     dbport => $cfg->{port} || 5432,
57     dbname => $cfg->{name},
58   };
59 }
60
61 sub init_data_dbhs {
62   return [];
63 }
64
65 sub _clear_field {
66   my ($text) = @_;
67
68   $text ||= '';
69   $text   =~ s/^\s+|\s+$//g;
70
71   return $text;
72 }
73
74 sub _group_into_clients {
75   my ($self) = @_;
76
77   my @match_fields = qw(dbhost dbport dbname);
78   my @copy_fields  = (@match_fields, qw(address company co_ustid dbuser dbpasswd duns sepa_creditor_id taxnumber templates));
79   my @clients;
80
81   # Group users into clients. Users which have identical database
82   # settings (host, port and name) will be grouped. The other fields
83   # like tax number etc. are taken from the first user and only filled
84   # from user users if they're still unset.
85   foreach my $user (@{ $self->users }) {
86     $user->{$_} = _clear_field($user->{$_}) for @copy_fields;
87
88     my $existing_client = first { my $client = $_; all { ($user->{$_} || '') eq ($client->{$_} || '') } @match_fields } @clients;
89
90     if ($existing_client) {
91       push @{ $existing_client->{users} }, $user->{id};
92       $existing_client->{$_} ||= $user->{$_} for @copy_fields;
93       next;
94     }
95
96     push @clients, {
97       map({ $_ => $user->{$_} } @copy_fields),
98       users   => [ $user->{id} ],
99       groups  => [ map { $_->{id} } @{ $self->groups } ],
100       enabled => 1,
101     };
102   }
103
104   # Ignore users (and therefore clients) for which no database
105   # configuration has been given.
106   @clients = grep { my $client = $_; any { $client->{$_} } @match_fields } @clients;
107
108   # If there's only one client set that one as default.
109   $clients[0]->{is_default} = 1 if scalar(@clients) == 1;
110
111   # Set a couple of defaults for database fields.
112   my $num = 0;
113   foreach my $client (@clients) {
114     $num                += 1;
115     $client->{name}    ||= $::locale->text('Client #1', $num);
116     $client->{dbhost}  ||= 'localhost';
117     $client->{dbport}  ||= 5432;
118     $client->{templates} =~ s:templates/::;
119   }
120
121   $self->clients(\@clients);
122 }
123
124 sub _analyze {
125   my ($self, %params) = @_;
126
127   $self->_group_into_clients;
128
129   return $self->_do_convert if !@{ $self->clients };
130
131   print $::form->parse_html_template('dbupgrade/auth/clients', { SELF => $self });
132
133   return 2;
134 }
135
136 sub _verify_clients {
137   my ($self) = @_;
138
139   my (%names, @errors);
140
141   my $num = 0;
142   foreach my $client (@{ $self->clients }) {
143     $num += 1;
144
145     next if !$client->{enabled};
146
147     $client->{$_} = _clear_field($client->{$_}) for qw(address co_ustid company dbhost dbname dbpasswd dbport dbuser duns sepa_creditor_id taxnumber templates);
148
149     if (!$client->{name} || $names{ $client->{name} }) {
150       push @errors, $::locale->text('New client #1: The name must be unique and not empty.', $num);
151     }
152
153     $names{ $client->{name} } = 1;
154
155     if (any { !$client->{$_} } qw(dbhost dbport dbname dbuser)) {
156       push @errors, $::locale->text('New client #1: The database configuration fields "host", "port", "name" and "user" must not be empty.', $num);
157     }
158   }
159
160   return @errors;
161 }
162
163 sub _alter_auth_database_structure {
164   my ($self) = @_;
165
166   my @queries = (
167     qq|CREATE TABLE auth.clients (
168          id         SERIAL  PRIMARY KEY,
169          name       TEXT    NOT NULL UNIQUE,
170          dbhost     TEXT    NOT NULL,
171          dbport     INTEGER NOT NULL DEFAULT 5432,
172          dbname     TEXT    NOT NULL,
173          dbuser     TEXT    NOT NULL,
174          dbpasswd   TEXT    NOT NULL,
175          is_default BOOLEAN NOT NULL DEFAULT FALSE,
176
177          UNIQUE (dbhost, dbport, dbname)
178        )|,
179     qq|CREATE TABLE auth.clients_users (
180          client_id INTEGER NOT NULL REFERENCES auth.clients (id),
181          user_id   INTEGER NOT NULL REFERENCES auth."user"  (id),
182
183          PRIMARY KEY (client_id, user_id)
184        )|,
185     qq|CREATE TABLE auth.clients_groups (
186          client_id INTEGER NOT NULL REFERENCES auth.clients (id),
187          group_id  INTEGER NOT NULL REFERENCES auth."group" (id),
188
189          PRIMARY KEY (client_id, group_id)
190        )|,
191   );
192
193   $self->db_query($_, may_fail => 0) for @queries;
194 }
195
196 sub _alter_data_database_structure {
197   my ($self, $dbh) = @_;
198
199   my @queries = (
200     qq|ALTER TABLE defaults ADD COLUMN company          TEXT|,
201     qq|ALTER TABLE defaults ADD COLUMN address          TEXT|,
202     qq|ALTER TABLE defaults ADD COLUMN taxnumber        TEXT|,
203     qq|ALTER TABLE defaults ADD COLUMN co_ustid         TEXT|,
204     qq|ALTER TABLE defaults ADD COLUMN duns             TEXT|,
205     qq|ALTER TABLE defaults ADD COLUMN sepa_creditor_id TEXT|,
206     qq|ALTER TABLE defaults ADD COLUMN templates        TEXT|,
207     qq|INSERT INTO schema_info (tag, login) VALUES ('clients', 'admin')|,
208   );
209
210   foreach my $query (@queries) {
211     $dbh->do($query) || die $self->db_errstr($dbh);
212   }
213 }
214
215 sub _create_clients_in_auth_database {
216   my ($self)  = @_;
217
218   my @client_columns   = qw(name dbhost dbport dbname dbuser dbpasswd is_default);
219   my $q_client         = qq|INSERT INTO auth.clients (| . join(', ', @client_columns) . qq|) VALUES (| . join(', ', ('?') x @client_columns) . qq|) RETURNING id|;
220   my $sth_client       = $self->dbh->prepare($q_client) || die $self->db_errstr;
221
222   my $q_client_user    = qq|INSERT INTO auth.clients_users (client_id, user_id) VALUES (?, ?)|;
223   my $sth_client_user  = $self->dbh->prepare($q_client_user) || die $self->db_errstr;
224
225   my $q_client_group   = qq|INSERT INTO auth.clients_groups (client_id, group_id) VALUES (?, ?)|;
226   my $sth_client_group = $self->dbh->prepare($q_client_group) || die $self->db_errstr;
227
228   foreach my $client (@{ $self->clients }) {
229     next unless $client->{enabled};
230
231     $client->{is_default} = $client->{is_default} ? 1 : 0;
232
233     $sth_client->execute(@{ $client }{ @client_columns }) || die;
234     my $client_id = $sth_client->fetch->[0];
235
236     $sth_client_user ->execute($client_id, $_) || die for @{ $client->{users}  || [] };
237     $sth_client_group->execute($client_id, $_) || die for @{ $client->{groups} || [] };
238   }
239
240   $sth_client      ->finish;
241   $sth_client_user ->finish;
242   $sth_client_group->finish;
243 }
244
245 sub _clean_auth_database {
246   my ($self) = @_;
247
248   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
249                           einkaufsrechnungen in_numberformat lieferantenbestellungen login pdonumber printer rechnungen role sdonumber sepa_creditor_id sid steuernummer taxnumber templates);
250
251   $self->dbh->do(qq|DELETE FROM auth.user_config WHERE cfg_key IN (| . join(', ', ('?') x @keys_to_delete) . qq|)|, undef, @keys_to_delete)
252     || die $self->db_errstr;
253 }
254
255 sub _copy_fields_to_data_database {
256   my ($self, $client) = @_;
257
258   my $dbh = SL::DBConnect->connect('dbi:Pg:dbname=' . $client->{dbname} . ';host=' . $client->{dbhost} . ';port=' . $client->{dbport},
259                                    $client->{dbuser}, $client->{dbpasswd},
260                                    SL::DBConnect->get_options(AutoCommit => 0));
261   if (!$dbh) {
262     die join("\n",
263              $::locale->text('The connection to the configured client database "#1" on host "#2:#3" failed.', $client->{dbname}, $client->{dbhost}, $client->{dbport}),
264              $::locale->text('Please correct the settings and try again or deactivate that client.'),
265              $::locale->text('Error message from the database: #1', $self->db_errstr('DBI')));
266   }
267
268   my ($has_been_applied) = $dbh->selectrow_array(qq|SELECT tag FROM schema_info WHERE tag = 'clients'|);
269
270   if (!$has_been_applied) {
271     $self->_alter_data_database_structure($dbh);
272   }
273
274   my @columns = qw(company address taxnumber co_ustid duns sepa_creditor_id templates);
275   my $query   = join ', ', map { "$_ = ?" } @columns;
276   my @values  = @{ $client }{ @columns };
277
278   if (!$dbh->do(qq|UPDATE defaults SET $query|, undef, @values)) {
279     die join("\n",
280              $::locale->text('Updating the client fields in the database "#1" on host "#2:#3" failed.', $client->{dbname}, $client->{dbhost}, $client->{dbport}),
281              $::locale->text('Please correct the settings and try again or deactivate that client.'),
282              $::locale->text('Error message from the database: #1', $self->db_errstr('DBI')));
283   }
284
285   $self->data_dbhs([ @{ $self->data_dbhs }, $dbh ]);
286 }
287
288 sub _commit_data_database_changes {
289   my ($self) = @_;
290
291   foreach my $dbh (@{ $self->data_dbhs }) {
292     $dbh->commit;
293     $dbh->disconnect;
294   }
295 }
296
297 sub _do_convert {
298   my ($self) = @_;
299
300   # Skip clients that are not enabled. Clean fields.
301   my $num = 0;
302   foreach my $client (@{ $self->clients }) {
303     $num += 1;
304
305     next if !$client->{enabled};
306
307     $client->{$_}        = _clear_field($client->{$_}) for qw(dbhost dbport dbname dbuser dbpasswd address company co_ustid dbuser dbpasswd duns sepa_creditor_id taxnumber templates);
308     $client->{templates} = 'templates/' . $client->{templates};
309   }
310
311   $self->_copy_fields_to_data_database($_) for grep { $_->{enabled} } @{ $self->clients };
312
313   $self->_alter_auth_database_structure;
314   $self->_create_clients_in_auth_database;
315   $self->_clean_auth_database;
316
317   $self->_commit_data_database_changes;
318
319   return 1;
320 }
321
322 sub run {
323   my ($self) = @_;
324
325   return $self->_analyze if !$::form->{clients} || !@{ $::form->{clients} };
326
327   $self->clients($::form->{clients});
328
329   my @errors = $self->_verify_clients;
330
331   return $self->_do_convert if !@errors;
332
333   flash('error', @errors);
334
335   print $::form->parse_html_template('dbupgrade/auth/clients', { SELF => $self });
336
337   return 1;
338 }
339
340 1;