Bug 15427 : Enable TLS support for MySQL
[koha.git] / installer / install.pl
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use diagnostics;
6
7 use C4::InstallAuth;
8 use CGI qw ( -utf8 );
9 use POSIX qw(strftime);
10
11 use C4::Context;
12 use C4::Output;
13 use C4::Templates;
14 use C4::Languages qw(getAllLanguages getTranslatedLanguages);
15 use C4::Installer;
16
17 use Koha;
18
19 my $query = new CGI;
20 my $step  = $query->param('step');
21
22 my $language = $query->param('language');
23 my ( $template, $loggedinuser, $cookie );
24
25 my $all_languages = getAllLanguages();
26
27 if ( defined($language) ) {
28     C4::Templates::setlanguagecookie( $query, $language, "install.pl?step=1" );
29 }
30 ( $template, $loggedinuser, $cookie ) = get_template_and_user(
31     {
32         template_name => "installer/step" . ( $step ? $step : 1 ) . ".tt",
33         query         => $query,
34         type          => "intranet",
35         authnotrequired => 0,
36         debug           => 1,
37     }
38 );
39
40 my $installer = C4::Installer->new();
41 my %info;
42 $info{'dbname'} = C4::Context->config("database");
43 $info{'dbms'} =
44   (   C4::Context->config("db_scheme")
45     ? C4::Context->config("db_scheme")
46     : "mysql" );
47 $info{'hostname'} = C4::Context->config("hostname");
48 $info{'port'}     = C4::Context->config("port");
49 $info{'user'}     = C4::Context->config("user");
50 $info{'password'} = C4::Context->config("pass");
51 $info{'tls'} = C4::Context->config("tls");
52     if ($info{'tls'} eq 'yes'){
53         $info{'ca'} = C4::Context->config('ca');
54         $info{'cert'} = C4::Context->config('cert');
55         $info{'key'} = C4::Context->config('key');
56         $info{'tlsoptions'} = ";mysql_ssl=1;mysql_ssl_client_key=".$info{key}.";mysql_ssl_client_cert=".$info{cert}.";mysql_ssl_ca_file=".$info{ca};
57         $info{'tlscmdline'} =  " --ssl-cert ". $info{cert} . " --ssl-key " . $info{key} . " --ssl-ca ".$info{ca}." "
58     }
59
60 my $dbh = DBI->connect(
61     "DBI:$info{dbms}:dbname=$info{dbname};host=$info{hostname}"
62       . ( $info{port} ? ";port=$info{port}" : "" )
63       . ( $info{tlsoptions} ? $info{tlsoptions} : "" ),
64     $info{'user'}, $info{'password'}
65 );
66
67 if ( $step && $step == 1 ) {
68     #First Step (for both fresh installations and upgrades)
69     #Checking ALL perl Modules and services needed are installed.
70     #Whenever there is an error, adding a report to the page
71     my $op = $query->param('op') || 'noop';
72     $template->param( language => 1 );
73     $template->param( 'checkmodule' => 1 ); # we start with the assumption that there are no problems and set this to 0 if there are
74
75     unless ( $] >= 5.010000 ) {    # Bug 7375
76         $template->param( problems => 1, perlversion => 1, checkmodule => 0 );
77     }
78
79     my $perl_modules = C4::Installer::PerlModules->new;
80     $perl_modules->versions_info;
81
82     my $modules = $perl_modules->get_attr('missing_pm');
83     if (scalar(@$modules)) {
84         my @components = ();
85         my $checkmodule = 1;
86         foreach (@$modules) {
87             my ($module, $stats) = each %$_;
88             $checkmodule = 0 if $stats->{'required'};
89             push(
90                 @components,
91                 {
92                     name    => $module,
93                     version => $stats->{'min_ver'},
94                     require => $stats->{'required'},
95                     usage   => $stats->{'usage'},
96                 }
97             );
98         }
99         @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
100         $template->param( missing_modules => \@components, checkmodule => $checkmodule, op => $op );
101     }
102 }
103 elsif ( $step && $step == 2 ) {
104 #
105 #STEP 2 Check Database connection and access
106 #
107     $template->param(%info);
108     my $checkdb = $query->param("checkdb");
109     $template->param( 'dbconnection' => $checkdb );
110     if ($checkdb) {
111         if ($dbh) {
112
113             # Can connect to the mysql
114             $template->param( "checkdatabaseaccess" => 1 );
115             if ( $info{dbms} eq "mysql" ) {
116
117                 #Check if database created
118                 my $rv = $dbh->do("SHOW DATABASES LIKE \'$info{dbname}\'");
119                 if ( $rv == 1 ) {
120                     $template->param( 'checkdatabasecreated' => 1 );
121                 }
122
123                 #Check if user have all necessary grants on this database.
124                 my $rq =
125                   $dbh->prepare(
126                     "SHOW GRANTS FOR \'$info{user}\'\@'$info{hostname}'");
127                 $rq->execute;
128                 my $grantaccess;
129                 while ( my ($line) = $rq->fetchrow ) {
130                     my $dbname = $info{dbname};
131                     if ( $line =~ m/^GRANT (.*?) ON `$dbname`\.\*/ || index( $line, '*.*' ) > 0 ) {
132                         $grantaccess = 1
133                           if (
134                             index( $line, 'ALL PRIVILEGES' ) > 0
135                             || (   ( index( $line, 'SELECT' ) > 0 )
136                                 && ( index( $line, 'INSERT' ) > 0 )
137                                 && ( index( $line, 'UPDATE' ) > 0 )
138                                 && ( index( $line, 'DELETE' ) > 0 )
139                                 && ( index( $line, 'CREATE' ) > 0 )
140                                 && ( index( $line, 'DROP' ) > 0 ) )
141                           );
142                     }
143                 }
144                 unless ($grantaccess) {
145                     $rq =
146                       $dbh->prepare("SHOW GRANTS FOR \'$info{user}\'\@'\%'");
147                     $rq->execute;
148                     while ( my ($line) = $rq->fetchrow ) {
149                         my $dbname = $info{dbname};
150                         if ( $line =~ m/$dbname/ || index( $line, '*.*' ) > 0 )
151                         {
152                             $grantaccess = 1
153                               if (
154                                 index( $line, 'ALL PRIVILEGES' ) > 0
155                                 || (   ( index( $line, 'SELECT' ) > 0 )
156                                     && ( index( $line, 'INSERT' ) > 0 )
157                                     && ( index( $line, 'UPDATE' ) > 0 )
158                                     && ( index( $line, 'DELETE' ) > 0 )
159                                     && ( index( $line, 'CREATE' ) > 0 )
160                                     && ( index( $line, 'DROP' ) > 0 ) )
161                               );
162                         }
163                     }
164                 }
165                 $template->param( "checkgrantaccess" => $grantaccess );
166             }   # End mysql connect check...
167
168             elsif ( $info{dbms} eq "Pg" ) {
169                 # Check if database has been created...
170                 my $rv = $dbh->do( "SELECT * FROM pg_catalog.pg_database WHERE datname = \'$info{dbname}\';" );
171                 if ( $rv == 1 ) {
172                         $template->param( 'checkdatabasecreated' => 1 );
173                 }
174
175                 # Check if user has all necessary grants on this database...
176                 my $rq = $dbh->do( "SELECT u.usesuper
177                                     FROM pg_catalog.pg_user as u
178                                     WHERE u.usename = \'$info{user}\';" );
179                 if ( $rq == 1 ) {
180                         $template->param( "checkgrantaccess" => 1 );
181                 }
182             }   # End Pg connect check...
183         }
184         else {
185             $template->param( "error" => DBI::err, "message" => DBI::errstr );
186         }
187     }
188 }
189 elsif ( $step && $step == 3 ) {
190 #
191 #
192 # STEP 3 : database setup
193 #
194 #
195     my $op = $query->param('op');
196     if ( $op && $op eq 'finished' ) {
197         #
198         # we have finished, just redirect to mainpage.
199         #
200         print $query->redirect("/cgi-bin/koha/mainpage.pl");
201         exit;
202     }
203     elsif ( $op && $op eq 'finish' ) {
204         $installer->set_version_syspref();
205
206         # Installation is finished.
207         # We just deny anybody access to install
208         # And we redirect people to mainpage.
209         # The installer will have to relogin since we do not pass cookie to redirection.
210         $template->param( "$op" => 1 );
211     }
212     elsif ( $op && $op eq 'addframeworks' ) {
213     #
214     # 1ST install, 3rd sub-step : insert the SQL files the user has selected
215     #
216
217         my ($fwk_language, $list) = $installer->load_sql_in_order($all_languages, $query->param('framework'));
218         $template->param(
219             "fwklanguage" => $fwk_language,
220             "list"        => $list
221         );
222         use Koha::SearchEngine::Elasticsearch;
223         Koha::SearchEngine::Elasticsearch->reset_elasticsearch_mappings;
224         $template->param( "$op" => 1 );
225     }
226     elsif ( $op && $op eq 'selectframeworks' ) {
227         #
228         #
229         # 1ST install, 2nd sub-step : show the user the sql datas he can insert in the database.
230         #
231         #
232         # (note that the term "selectframeworks is not correct. The user can select various files, not only frameworks)
233
234         #Framework Selection
235         #sql data for import are supposed to be located in installer/data/<language>/<level>
236         # Where <language> is en|fr or any international abbreviation (provided language hash is updated... This will be a problem with internationlisation.)
237         # Where <level> is a category of requirement : required, recommended optional
238         # level should contain :
239         #   SQL File for import With a readable name.
240         #   txt File that explains what this SQL File is meant for.
241         # Could be VERY useful to have A Big file for a kind of library.
242         # But could also be useful to have some Authorised values data set prepared here.
243         # Framework Selection is achieved through checking boxes.
244         my $langchoice = $query->param('fwklanguage');
245         $langchoice = $query->cookie('KohaOpacLanguage') unless ($langchoice);
246         $langchoice =~ s/[^a-zA-Z_-]*//g;
247         my $marcflavour = $query->param('marcflavour');
248         if ($marcflavour){
249             $installer->set_marcflavour_syspref($marcflavour);
250         };
251         $marcflavour = C4::Context->preference('marcflavour') unless ($marcflavour);
252         #Insert into database the selected marcflavour
253         undef $/;
254         my ($marc_defaulted_to_en, $fwklist) = $installer->marc_framework_sql_list($langchoice, $marcflavour);
255         $template->param('en_marc_frameworks' => $marc_defaulted_to_en);
256         $template->param( "frameworksloop" => $fwklist );
257         $template->param( "marcflavour" => ucfirst($marcflavour));
258
259         my ($sample_defaulted_to_en, $levellist) = $installer->sample_data_sql_list($langchoice);
260         $template->param( "en_sample_data" => $sample_defaulted_to_en);
261         $template->param( "levelloop" => $levellist );
262         $template->param( "$op"       => 1 );
263     }
264     elsif ( $op && $op eq 'choosemarc' ) {
265         #
266         #
267         # 1ST install, 2nd sub-step : show the user the marcflavour available.
268         #
269         #
270
271         #Choose Marc Flavour
272         #sql data are supposed to be located in installer/data/<dbms>/<language>/marcflavour/marcflavourname
273         # Where <dbms> is database type according to DBD syntax
274         # Where <language> is en|fr or any international abbreviation (provided language hash is updated... This will be a problem with internationlisation.)
275         # Where <level> is a category of requirement : required, recommended optional
276         # level should contain :
277         #   SQL File for import With a readable name.
278         #   txt File taht explains what this SQL File is meant for.
279         # Could be VERY useful to have A Big file for a kind of library.
280         # But could also be useful to have some Authorised values data set prepared here.
281         # Marcflavour Selection is achieved through radiobuttons.
282         my $langchoice = $query->param('fwklanguage');
283         $langchoice = $query->cookie('KohaOpacLanguage') unless ($langchoice);
284         $langchoice =~ s/[^a-zA-Z_-]*//g;
285         my $dir =
286           C4::Context->config('intranetdir') . "/installer/data/$info{dbms}/$langchoice/marcflavour";
287         unless (opendir( MYDIR, $dir )) {
288             if ($langchoice eq 'en') {
289                 warn "cannot open MARC frameworks directory $dir";
290             } else {
291                 # if no translated MARC framework is available,
292                 # default to English
293                 $dir = C4::Context->config('intranetdir') . "/installer/data/$info{dbms}/en/marcflavour";
294                 opendir(MYDIR, $dir) or warn "cannot open English MARC frameworks directory $dir";
295             }
296         }
297         my @listdir = grep { !/^\./ && -d "$dir/$_" } readdir(MYDIR);
298         closedir MYDIR;
299         my $marcflavour=C4::Context->preference("marcflavour");
300         my @flavourlist;
301         foreach my $marc (@listdir) {
302             my %cell=(
303             "label"=> ucfirst($marc),
304             "code"=>uc($marc),
305             "checked"=> defined($marcflavour) ? uc($marc) eq $marcflavour : 0);
306 #             $cell{"description"}= do { local $/ = undef; open INPUT "<$dir/$marc.txt"||"";<INPUT> };
307             push @flavourlist, \%cell;
308         }
309         $template->param( "flavourloop" => \@flavourlist );
310         $template->param( "$op"       => 1 );
311     }
312     elsif ( $op && $op eq 'importdatastructure' ) {
313         #
314         #
315         # 1st install, 1st "sub-step" : import kohastructure
316         #
317         #
318         my $error = $installer->load_db_schema();
319         $template->param(
320             "error" => $error,
321             "$op"   => 1,
322         );
323     }
324     elsif ( $op && $op eq 'updatestructure' ) {
325         #
326         # Not 1st install, the only sub-step : update database
327         #
328         #Do updatedatabase And report
329
330         if ( ! defined $ENV{PERL5LIB} ) {
331             my $find = "C4/Context.pm";
332             my $path = $INC{$find};
333             $path =~ s/\Q$find\E//;
334             $ENV{PERL5LIB} = "$path:$path/installer";
335             warn "# plack? inserted PERL5LIB $ENV{PERL5LIB}\n";
336         }
337
338         my $now = POSIX::strftime( "%Y-%m-%dT%H:%M:%S", localtime() );
339         my $logdir = C4::Context->config('logdir');
340         my $dbversion = C4::Context->preference('Version');
341         my $kohaversion = Koha::version;
342         $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
343
344         my $filename_suffix = join '_', $now, $dbversion, $kohaversion;
345         my ( $logfilepath, $logfilepath_errors ) = ( chk_log($logdir, "updatedatabase_$filename_suffix"), chk_log($logdir, "updatedatabase-error_$filename_suffix") );
346
347         my $cmd = C4::Context->config("intranetdir") . "/installer/data/$info{dbms}/updatedatabase.pl >> $logfilepath 2>> $logfilepath_errors";
348
349         system($cmd );
350
351         my $fh;
352         open( $fh, "<", $logfilepath ) or die "Cannot open log file $logfilepath: $!";
353         my @report = <$fh>;
354         close $fh;
355         if (@report) {
356             $template->param( update_report => [ map { { line => $_ } } split( /\n/, join( '', @report ) ) ] );
357             $template->param( has_update_succeeds => 1 );
358         } else {
359             eval{ `rm $logfilepath` };
360         }
361         open( $fh, "<", $logfilepath_errors ) or die "Cannot open log file $logfilepath_errors: $!";
362         @report = <$fh>;
363         close $fh;
364         if (@report) {
365             $template->param( update_errors => [ map { { line => $_ } } split( /\n/, join( '', @report ) ) ] );
366             $template->param( has_update_errors => 1 );
367             warn "The following errors were returned while attempting to run the updatedatabase.pl script:\n";
368             foreach my $line (@report) { warn "$line\n"; }
369         } else {
370             eval{ `rm $logfilepath_errors` };
371         }
372         $template->param( $op => 1 );
373     }
374     else {
375         #
376         # check whether it's a 1st install or an update
377         #
378         #Check if there are enough tables.
379         # Paul has cleaned up tables so reduced the count
380         #I put it there because it implied a data import if condition was not satisfied.
381         my $dbh = DBI->connect(
382                 "DBI:$info{dbms}:dbname=$info{dbname};host=$info{hostname}"
383                 . ( $info{port} ? ";port=$info{port}" : "" )
384                 . ( $info{tlsoptions} ? $info{tlsoptions} : "" ),
385                 $info{'user'}, $info{'password'}
386         );
387         my $rq;
388         if ( $info{dbms} eq 'mysql' ) { $rq = $dbh->prepare( "SHOW TABLES" ); }
389         elsif ( $info{dbms} eq 'Pg' ) { $rq = $dbh->prepare( "SELECT *
390                                                                 FROM information_schema.tables
391                                                                 WHERE table_schema='public' and table_type='BASE TABLE';" ); }
392         $rq->execute;
393         my $data = $rq->fetchall_arrayref( {} );
394         my $count = scalar(@$data);
395         #
396         # we don't have tables, propose DB import
397         #
398         if ( $count < 70 ) {
399             $template->param( "count" => $count, "proposeimport" => 1 );
400         }
401         else {
402             #
403             # we have tables, propose to select files to upload or updatedatabase
404             #
405             $template->param( "count" => $count, "default" => 1 );
406             #
407             # 1st part of step 3 : check if there is a databaseversion systempreference
408             # if there is, then we just need to upgrade
409             # if there is none, then we need to install the database
410             #
411             if (C4::Context->preference('Version')) {
412                 my $dbversion = C4::Context->preference('Version');
413                 $dbversion =~ /(.*)\.(..)(..)(...)/;
414                 $dbversion = "$1.$2.$3.$4";
415                 $template->param("upgrading" => 1,
416                                 "dbversion" => $dbversion,
417                                 "kohaversion" => Koha::version(),
418                                 );
419             }
420         }
421     }
422 }
423 else {
424
425     # LANGUAGE SELECTION page by default
426     # using opendir + language Hash
427     my $languages_loop = getTranslatedLanguages('intranet');
428     $template->param( installer_languages_loop => $languages_loop );
429     if ($dbh) {
430         my $rq =
431           $dbh->prepare(
432             "SELECT * from systempreferences WHERE variable='Version'");
433         if ( $rq->execute ) {
434             my ($version) = $rq->fetchrow;
435             if ($version) {
436                 print $query->redirect("/cgi-bin/koha/installer/install.pl?step=3");
437                 exit;
438             }
439         }
440     }
441 }
442 output_html_with_http_headers $query, $cookie, $template->output;
443
444 sub chk_log { #returns a logfile in $dir or - if that failed - in temp dir
445     my ($dir, $name) = @_;
446     my $fn=$dir.'/'.$name.'.log';
447     if( ! open my $fh, '>', $fn ) {
448         $name.= '_XXXX';
449         require File::Temp;
450         ($fh, $fn)= File::Temp::tempfile( $name, TMPDIR => 1, SUFFIX => '.log');
451         #if this should not work, let croak take over
452     }
453     return $fn;
454 }