Koha/installer/install.pl
Julian Maurice a34d086a24
Bug 19735: Move Perl deps definitions into a cpanfile
cpanfile is a format for describing CPAN dependencies for Perl
applications.
It is more concise - thus easier to read and maintain - than
C4::Installer::PerlDependencies, and allows to describe requirements
more accurately (using version ranges or features for instance)
Additionally it can be read by tools such as cpanm or carton for an
easy way to install dependencies on non-Debian-based systems.

For more information on cpanfile, see
http://search.cpan.org/~miyagawa/Module-CPANfile-1.1002/lib/cpanfile.pod

This patch replace C4::Installer::PerlDependencies by an equivalent
cpanfile and update all scripts/modules that were using PerlDependencies
It also removes dead code from C4::Installer::PerlModules (some
subroutines were not used at all, except in unit tests)

Added dependencies:
 - Module::CPANfile
 - CPAN::Meta (dependency of Module::CPANfile, but we need a more recent
   version than the one Module::CPANfile requires)

Test plan:
  1. Go to About page, tab Perl modules and keep this browser tab open
  2. Apply patch
  3. Install Module::CPANfile and CPAN::Meta
    a. On Debian-based systems:
       # will install libcpan-meta-perl as a dependency
       sudo apt install libmodule-cpanfile-perl
    b. Others:
       # will install CPAN::Meta as a dependency
       sudo cpanm Module::CPANfile
  4. In a new browser tab, go to About page, tab Perl modules and compare
     the table with the one in the previous browser tab
     They should be identical, except for newly added dependencies
     (Module::CPANfile and CPAN::Meta)
  5. Do a 'standard' install
    a. perl Makefile.PL (select 'standard')
    b. make
    c. sudo make install
    d. Configure your database, web server, ... and go through the web
       install process
  6. Verify that the cpanfile got copied into PERL_MODULE_DIR (which
     should be /usr/share/koha/lib)
  7. Go to the about page of this fresh install and compare it with your
     dev install
  8. Verify that debian/list-deps still works
     This takes a lot of time and it may not be necessary to wait until
     the end. If you see some Debian package names that correspond to
     modules in cpanfile, it means it still works
     (you need apt-file for this script to work)
  9. Verify that koha_perl_deps.pl still works
 10. prove t/Installer_pm.t t/Installer_PerlModules.t

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
2020-02-12 16:33:02 +00:00

497 lines
18 KiB
Perl
Executable file

#!/usr/bin/perl
# This file is part of Koha.
#
# Copyright (C) YEAR YOURNAME-OR-YOUREMPLOYER
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use diagnostics;
use C4::InstallAuth;
use CGI qw ( -utf8 );
use POSIX qw(strftime);
use C4::Context;
use C4::Output;
use C4::Templates;
use C4::Languages qw(getAllLanguages getTranslatedLanguages);
use C4::Installer;
use C4::Installer::PerlModules;
use Koha;
my $query = new CGI;
my $step = $query->param('step');
my $language = $query->param('language');
my ( $template, $loggedinuser, $cookie );
my $all_languages = getAllLanguages();
if ( defined($language) ) {
C4::Templates::setlanguagecookie( $query, $language, "install.pl?step=1" );
}
( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => "installer/step" . ( $step ? $step : 1 ) . ".tt",
query => $query,
type => "intranet",
authnotrequired => 0,
debug => 1,
}
);
my $installer = C4::Installer->new();
my %info;
$info{'dbname'} = C4::Context->config("database");
$info{'dbms'} = (
C4::Context->config("db_scheme")
? C4::Context->config("db_scheme")
: "mysql"
);
$info{'hostname'} = C4::Context->config("hostname");
$info{'port'} = C4::Context->config("port");
$info{'user'} = C4::Context->config("user");
$info{'password'} = C4::Context->config("pass");
$info{'tls'} = C4::Context->config("tls");
if ($info{'tls'} && $info{'tls'} eq 'yes'){
$info{'ca'} = C4::Context->config('ca');
$info{'cert'} = C4::Context->config('cert');
$info{'key'} = C4::Context->config('key');
$info{'tlsoptions'} = ";mysql_ssl=1;mysql_ssl_client_key=".$info{key}.";mysql_ssl_client_cert=".$info{cert}.";mysql_ssl_ca_file=".$info{ca};
$info{'tlscmdline'} = " --ssl-cert ". $info{cert} . " --ssl-key " . $info{key} . " --ssl-ca ".$info{ca}." "
}
my $dbh = DBI->connect(
"DBI:$info{dbms}:dbname=$info{dbname};host=$info{hostname}"
. ( $info{port} ? ";port=$info{port}" : "" )
. ( $info{tlsoptions} ? $info{tlsoptions} : "" ),
$info{'user'}, $info{'password'}
);
if ( $step && $step == 1 ) {
#First Step (for both fresh installations and upgrades)
#Checking ALL perl Modules and services needed are installed.
#Whenever there is an error, adding a report to the page
my $op = $query->param('op') || 'noop';
$template->param( language => 1 );
my $checkmodule = 1;
$template->param( 'checkmodule' => 1 )
; # we start with the assumption that there are no problems and set this to 0 if there are
unless ( $] >= 5.010000 ) { # Bug 7375
$template->param( problems => 1, perlversion => 1, checkmodule => 0 );
$checkmodule = 0;
}
my $perl_modules = C4::Installer::PerlModules->new;
$perl_modules->versions_info;
my $modules = $perl_modules->get_attr('missing_pm');
if ( scalar(@$modules) ) {
my @components = ();
foreach (@$modules) {
my ( $module, $stats ) = each %$_;
$checkmodule = 0 if $stats->{'required'};
push(
@components,
{
name => $module,
version => $stats->{'min_ver'},
require => $stats->{'required'},
usage => $stats->{'usage'},
}
);
}
@components = sort { $a->{'name'} cmp $b->{'name'} } @components;
$template->param(
missing_modules => \@components,
checkmodule => $checkmodule,
op => $op
);
}
}
elsif ( $step && $step == 2 ) {
#STEP 2 Check Database connection and access
$template->param(%info);
my $checkdb = $query->param("checkdb");
$template->param( 'dbconnection' => $checkdb );
if ($checkdb) {
if ($dbh) {
# Can connect to the mysql
$template->param( "checkdatabaseaccess" => 1 );
if ( $info{dbms} eq "mysql" ) {
#Check if database created
my $rv = $dbh->do("SHOW DATABASES LIKE \'$info{dbname}\'");
if ( $rv == 1 ) {
$template->param( 'checkdatabasecreated' => 1 );
}
# Check if user have all necessary grants on this database.
# CURRENT_USER is ANSI SQL, and doesn't require mysql table
# privileges, making the % check pointless, since they
# couldn't even check GRANTS if they couldn't connect.
my $rq = $dbh->prepare('SHOW GRANTS FOR CURRENT_USER');
$rq->execute;
my $grantaccess;
while ( my ($line) = $rq->fetchrow ) {
my $dbname = $info{dbname};
if ( $line =~ m/^GRANT (.*?) ON `$dbname`\.\*/
|| index( $line, '*.*' ) > 0 )
{
$grantaccess = 1
if (
index( $line, 'ALL PRIVILEGES' ) > 0
|| ( ( index( $line, 'SELECT' ) > 0 )
&& ( index( $line, 'INSERT' ) > 0 )
&& ( index( $line, 'UPDATE' ) > 0 )
&& ( index( $line, 'DELETE' ) > 0 )
&& ( index( $line, 'CREATE' ) > 0 )
&& ( index( $line, 'DROP' ) > 0 ) )
);
}
}
$template->param( "checkgrantaccess" => $grantaccess );
} # End mysql connect check...
elsif ( $info{dbms} eq "Pg" ) {
# Check if database has been created...
my $rv = $dbh->do(
"SELECT * FROM pg_catalog.pg_database WHERE datname = \'$info{dbname}\';"
);
if ( $rv == 1 ) {
$template->param( 'checkdatabasecreated' => 1 );
}
# Check if user has all necessary grants on this database...
my $rq = $dbh->do(
"SELECT u.usesuper
FROM pg_catalog.pg_user as u
WHERE u.usename = \'$info{user}\';"
);
if ( $rq == 1 ) {
$template->param( "checkgrantaccess" => 1 );
}
} # End Pg connect check...
}
else {
$template->param( "error" => DBI::err, "message" => DBI::errstr );
}
}
}
elsif ( $step && $step == 3 ) {
# STEP 3 : database setup
my $op = $query->param('op');
if ( $op && $op eq 'finished' ) {
#
# we have finished, just redirect to mainpage.
#
print $query->redirect("/cgi-bin/koha/mainpage.pl");
exit;
}
elsif ( $op && $op eq 'finish' ) {
$installer->set_version_syspref();
# Installation is finished.
# We just deny anybody access to install
# And we redirect people to mainpage.
# The installer will have to relogin since we do not pass cookie to redirection.
$template->param( "$op" => 1 );
}
elsif ( $op && $op eq 'addframeworks' ) {
# 1ST install, 3rd sub-step : insert the SQL files the user has selected
my ( $fwk_language, $list ) =
$installer->load_sql_in_order( $all_languages,
$query->multi_param('framework') );
$template->param(
"fwklanguage" => $fwk_language,
"list" => $list
);
use Koha::SearchEngine::Elasticsearch;
Koha::SearchEngine::Elasticsearch->reset_elasticsearch_mappings;
$template->param( "$op" => 1 );
}
elsif ( $op && $op eq 'selectframeworks' ) {
#
#
# 1ST install, 2nd sub-step : show the user the sql datas they can insert in the database.
#
#
# (note that the term "selectframeworks is not correct. The user can select various files, not only frameworks)
#Framework Selection
#sql data for import are supposed to be located in installer/data/<language>/<level>
# Where <language> is en|fr or any international abbreviation (provided language hash is updated... This will be a problem with internationlisation.)
# Where <level> is a category of requirement : required, recommended optional
# level should contain :
# SQL File for import With a readable name.
# txt File that explains what this SQL File is meant for.
# Could be VERY useful to have A Big file for a kind of library.
# But could also be useful to have some Authorised values data set prepared here.
# Framework Selection is achieved through checking boxes.
my $langchoice = $query->param('fwklanguage');
$langchoice = $query->cookie('KohaOpacLanguage') unless ($langchoice);
$langchoice =~ s/[^a-zA-Z_-]*//g;
my $marcflavour = $query->param('marcflavour');
if ($marcflavour) {
$installer->set_marcflavour_syspref($marcflavour);
}
$marcflavour = C4::Context->preference('marcflavour')
unless ($marcflavour);
#Insert into database the selected marcflavour
undef $/;
my ( $marc_defaulted_to_en, $fwklist ) =
$installer->marc_framework_sql_list( $langchoice, $marcflavour );
$template->param( 'en_marc_frameworks' => $marc_defaulted_to_en );
$template->param( "frameworksloop" => $fwklist );
$template->param( "marcflavour" => ucfirst($marcflavour) );
my ( $sample_defaulted_to_en, $levellist ) =
$installer->sample_data_sql_list($langchoice);
$template->param( "en_sample_data" => $sample_defaulted_to_en );
$template->param( "levelloop" => $levellist );
$template->param( "$op" => 1 );
}
elsif ( $op && $op eq 'choosemarc' ) {
#
#
# 1ST install, 2nd sub-step : show the user the marcflavour available.
#
#
#Choose Marc Flavour
#sql data are supposed to be located in installer/data/<dbms>/<language>/marcflavour/marcflavourname
# Where <dbms> is database type according to DBD syntax
# Where <language> is en|fr or any international abbreviation (provided language hash is updated... This will be a problem with internationlisation.)
# Where <level> is a category of requirement : required, recommended optional
# level should contain :
# SQL File for import With a readable name.
# txt File that explains what this SQL File is meant for.
# Could be VERY useful to have A Big file for a kind of library.
# But could also be useful to have some Authorised values data set prepared here.
# Marcflavour Selection is achieved through radiobuttons.
my $langchoice = $query->param('fwklanguage');
$langchoice = $query->cookie('KohaOpacLanguage') unless ($langchoice);
$langchoice =~ s/[^a-zA-Z_-]*//g;
my $dir =
C4::Context->config('intranetdir')
. "/installer/data/$info{dbms}/$langchoice/marcflavour";
unless ( opendir( MYDIR, $dir ) ) {
if ( $langchoice eq 'en' ) {
warn "cannot open MARC frameworks directory $dir";
}
else {
# if no translated MARC framework is available,
# default to English
$dir = C4::Context->config('intranetdir')
. "/installer/data/$info{dbms}/en/marcflavour";
opendir( MYDIR, $dir )
or warn "cannot open English MARC frameworks directory $dir";
}
}
my @listdir = grep { !/^\./ && -d "$dir/$_" } readdir(MYDIR);
closedir MYDIR;
my $marcflavour = C4::Context->preference("marcflavour");
my @flavourlist;
foreach my $marc (@listdir) {
my %cell=(
"label"=> ucfirst($marc),
"code"=>uc($marc),
"checked"=> defined($marcflavour) ? uc($marc) eq $marcflavour : 0);
# $cell{"description"}= do { local $/ = undef; open INPUT "<$dir/$marc.txt"||"";<INPUT> };
push @flavourlist, \%cell;
}
$template->param( "flavourloop" => \@flavourlist );
$template->param( "$op" => 1 );
}
elsif ( $op && $op eq 'importdatastructure' ) {
#
#
# 1st install, 1st "sub-step" : import kohastructure
#
#
my $error = $installer->load_db_schema();
$template->param(
"error" => $error,
"$op" => 1,
);
}
elsif ( $op && $op eq 'updatestructure' ) {
#
# Not 1st install, the only sub-step : update database
#
#Do updatedatabase And report
if ( !defined $ENV{PERL5LIB} ) {
my $find = "C4/Context.pm";
my $path = $INC{$find};
$path =~ s/\Q$find\E//;
$ENV{PERL5LIB} = "$path:$path/installer";
warn "# plack? inserted PERL5LIB $ENV{PERL5LIB}\n";
}
my $now = POSIX::strftime( "%Y-%m-%dT%H:%M:%S", localtime() );
my $logdir = C4::Context->config('logdir');
my $dbversion = C4::Context->preference('Version');
my $kohaversion = Koha::version;
$kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
my $filename_suffix = join '_', $now, $dbversion, $kohaversion;
my ( $logfilepath, $logfilepath_errors ) = (
chk_log( $logdir, "updatedatabase_$filename_suffix" ),
chk_log( $logdir, "updatedatabase-error_$filename_suffix" )
);
my $cmd = C4::Context->config("intranetdir")
. "/installer/data/$info{dbms}/updatedatabase.pl >> $logfilepath 2>> $logfilepath_errors";
system($cmd );
my $fh;
open( $fh, "<:encoding(utf-8)", $logfilepath )
or die "Cannot open log file $logfilepath: $!";
my @report = <$fh>;
close $fh;
if (@report) {
$template->param( update_report =>
[ map { { line => $_ } } split( /\n/, join( '', @report ) ) ]
);
$template->param( has_update_succeeds => 1 );
}
else {
eval { `rm $logfilepath` };
}
open( $fh, "<:encoding(utf-8)", $logfilepath_errors )
or die "Cannot open log file $logfilepath_errors: $!";
@report = <$fh>;
close $fh;
if (@report) {
$template->param( update_errors =>
[ map { { line => $_ } } split( /\n/, join( '', @report ) ) ]
);
$template->param( has_update_errors => 1 );
warn
"The following errors were returned while attempting to run the updatedatabase.pl script:\n";
foreach my $line (@report) { warn "$line\n"; }
}
else {
eval { `rm $logfilepath_errors` };
}
$template->param( $op => 1 );
}
else {
#
# check whether it's a 1st install or an update
#
#Check if there are enough tables.
# Paul has cleaned up tables so reduced the count
#I put it there because it implied a data import if condition was not satisfied.
my $dbh = DBI->connect(
"DBI:$info{dbms}:dbname=$info{dbname};host=$info{hostname}"
. ( $info{port} ? ";port=$info{port}" : "" )
. ( $info{tlsoptions} ? $info{tlsoptions} : "" ),
$info{'user'}, $info{'password'}
);
my $rq;
if ( $info{dbms} eq 'mysql' ) { $rq = $dbh->prepare("SHOW TABLES"); }
elsif ( $info{dbms} eq 'Pg' ) {
$rq = $dbh->prepare(
"SELECT *
FROM information_schema.tables
WHERE table_schema='public' and table_type='BASE TABLE';"
);
}
$rq->execute;
my $data = $rq->fetchall_arrayref( {} );
my $count = scalar(@$data);
#
# we don't have tables, propose DB import
#
if ( $count < 70 ) {
$template->param( "count" => $count, "proposeimport" => 1 );
}
else {
#
# we have tables, propose to select files to upload or updatedatabase
#
$template->param( "count" => $count, "default" => 1 );
#
# 1st part of step 3 : check if there is a databaseversion systempreference
# if there is, then we just need to upgrade
# if there is none, then we need to install the database
#
if ( C4::Context->preference('Version') ) {
my $dbversion = C4::Context->preference('Version');
$dbversion =~ /(.*)\.(..)(..)(...)/;
$dbversion = "$1.$2.$3.$4";
$template->param(
"upgrading" => 1,
"dbversion" => $dbversion,
"kohaversion" => Koha::version(),
);
}
}
}
}
else {
# LANGUAGE SELECTION page by default
# using opendir + language Hash
my $languages_loop = getTranslatedLanguages('intranet');
$template->param( installer_languages_loop => $languages_loop );
if ($dbh) {
my $rq =
$dbh->prepare(
"SELECT * from systempreferences WHERE variable='Version'");
if ( $rq->execute ) {
my ($version) = $rq->fetchrow;
if ($version) {
print $query->redirect(
"/cgi-bin/koha/installer/install.pl?step=3");
exit;
}
}
}
}
output_html_with_http_headers $query, $cookie, $template->output;
sub chk_log { #returns a logfile in $dir or - if that failed - in temp dir
my ( $dir, $name ) = @_;
my $fn = $dir . '/' . $name . '.log';
if ( !open my $fh, '>', $fn ) {
$name .= '_XXXX';
require File::Temp;
( $fh, $fn ) =
File::Temp::tempfile( $name, TMPDIR => 1, SUFFIX => '.log' );
#if this should not work, let croak take over
}
return $fn;
}