#
# This file is part of Koha.
#
-# 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 2 of the License, or (at your option) any later
-# version.
+# 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.
+# 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, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
-use strict;
-#use warnings; FIXME - Bug 2505
+use Modern::Perl;
-our $VERSION = 3.07.00.049;
+use Encode qw( encode is_utf8 );
+use DBIx::RunSQL;
+use YAML::XS;
use C4::Context;
-use C4::Installer::PerlModules;
+use DBI;
+use Koha;
+
+use vars qw(@ISA @EXPORT);
+BEGIN {
+ require Exporter;
+ @ISA = qw( Exporter );
+ push @EXPORT, qw( primary_key_exists foreign_key_exists index_exists column_exists TableExists);
+};
=head1 NAME
my $installer = C4::Installer->new();
my $all_languages = getAllLanguages();
my $error = $installer->load_db_schema();
- my $list = $installer->sql_file_list('en', 'marc21', { optional => 1, mandatory => 1 });
+ my $list;
+ #fill $list with list of sql files
my ($fwk_language, $error_list) = $installer->load_sql_in_order($all_languages, @$list);
$installer->set_version_syspref();
$installer->set_marcflavour_syspref('MARC21');
$self->{'port'} = C4::Context->config("port");
$self->{'user'} = C4::Context->config("user");
$self->{'password'} = C4::Context->config("pass");
+ $self->{'tls'} = C4::Context->config("tls");
+ if( $self->{'tls'} && $self->{'tls'} eq 'yes' ) {
+ $self->{'ca'} = C4::Context->config('ca');
+ $self->{'cert'} = C4::Context->config('cert');
+ $self->{'key'} = C4::Context->config('key');
+ $self->{'tlsoptions'} = ";mysql_ssl=1;mysql_ssl_client_key=".$self->{key}.";mysql_ssl_client_cert=".$self->{cert}.";mysql_ssl_ca_file=".$self->{ca};
+ $self->{'tlscmdline'} = " --ssl-cert ". $self->{cert} . " --ssl-key " . $self->{key} . " --ssl-ca ".$self->{ca}." "
+ }
$self->{'dbh'} = DBI->connect("DBI:$self->{dbms}:dbname=$self->{dbname};host=$self->{hostname}" .
- ( $self->{port} ? ";port=$self->{port}" : "" ),
+ ( $self->{port} ? ";port=$self->{port}" : "" ).
+ ( $self->{tlsoptions} ? $self->{tlsoptions} : ""),
$self->{'user'}, $self->{'password'});
$self->{'language'} = undef;
$self->{'marcflavour'} = undef;
return $self;
}
-=head2 marcflavour_list
-
- my ($marcflavours) = $installer->marcflavour_list($lang);
-
-Return a arrayref of the MARC flavour sets available for the
-specified language C<$lang>. Returns 'undef' if a directory
-for the language does not exist.
-
-=cut
-
-sub marcflavour_list {
- my $self = shift;
- my $lang = shift;
-
- my $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/$lang/marcflavour";
- opendir(MYDIR, $dir) or return;
- my @list = grep { !/^\.|CVS/ && -d "$dir/$_" } readdir(MYDIR);
- closedir MYDIR;
- return \@list;
-}
-
=head2 marc_framework_sql_list
my ($defaulted_to_en, $list) =
foreach my $requirelevel (@listdir) {
opendir( MYDIR, "$dir/$requirelevel" );
- my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.sql$/ } readdir(MYDIR);
+ my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.(sql|yml)$/ } readdir(MYDIR);
closedir MYDIR;
my %cell;
my @frameworklist;
map {
- my $name = substr( $_, 0, -4 );
- open my $fh, "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
- my $lines = <$fh>;
- $lines =~ s/\n|\r/<br \/>/g;
- use utf8;
- utf8::encode($lines) unless ( utf8::is_utf8($lines) );
+ my ( $name, $ext ) = split /\./, $_;
+ my @lines;
+ if ( $ext =~ /yml/ ) {
+ my $yaml = YAML::XS::LoadFile("$dir/$requirelevel/$name\.$ext");
+ @lines = @{ $yaml->{'description'} };
+ } else {
+ open my $fh, "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
+ my $line = <$fh>;
+ $line = Encode::encode('UTF-8', $line) unless ( Encode::is_utf8($line) );
+ @lines = split /\n/, $line;
+ }
my $mandatory = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i);
push @frameworklist,
{
'fwkname' => $name,
'fwkfile' => "$dir/$requirelevel/$_",
- 'fwkdescription' => $lines,
+ 'fwkdescription' => \@lines,
'checked' => ( ( $frameworksloaded{$_} || $mandatory ) ? 1 : 0 ),
'mandatory' => $mandatory,
};
sort { $a->{'fwkname'} cmp $b->{'fwkname'} } @frameworklist;
$cell{"frameworks"} = \@fwks;
- $cell{"label"} = ucfirst($requirelevel);
+ $cell{"label"} = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i)?'mandatory':'optional';
$cell{"code"} = lc($requirelevel);
push @fwklist, \%cell;
}
foreach my $requirelevel (@listdir) {
opendir( MYDIR, "$dir/$requirelevel" );
- my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.sql$/ } readdir(MYDIR);
+ my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.(sql|yml)$/ } readdir(MYDIR);
closedir MYDIR;
my %cell;
my @frameworklist;
map {
- my $name = substr( $_, 0, -4 );
- open my $fh , "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
- my $lines = <$fh>;
- $lines =~ s/\n|\r/<br \/>/g;
- use utf8;
- utf8::encode($lines) unless ( utf8::is_utf8($lines) );
+ my ( $name, $ext ) = split /\./, $_;
+ my @lines;
+ if ( $ext =~ /yml/ ) {
+ my $yaml = YAML::XS::LoadFile("$dir/$requirelevel/$name\.$ext");
+ @lines = @{ $yaml->{'description'} };
+ } else {
+ open my $fh, "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
+ my $line = <$fh>;
+ $line = Encode::encode('UTF-8', $line) unless ( Encode::is_utf8($line) );
+ @lines = split /\n/, $line;
+ }
my $mandatory = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i);
push @frameworklist,
{
'fwkname' => $name,
'fwkfile' => "$dir/$requirelevel/$_",
- 'fwkdescription' => $lines,
+ 'fwkdescription' => \@lines,
'checked' => ( ( $frameworksloaded{$_} || $mandatory ) ? 1 : 0 ),
'mandatory' => $mandatory,
};
my @fwks = sort { $a->{'fwkname'} cmp $b->{'fwkname'} } @frameworklist;
$cell{"frameworks"} = \@fwks;
- $cell{"label"} = ucfirst($requirelevel);
+ $cell{"label"} = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i)?'mandatory':'optional';
$cell{"code"} = lc($requirelevel);
push @levellist, \%cell;
}
return ($defaulted_to_en, \@levellist);
}
-=head2 sql_file_list
-
- my $list = $installer->sql_file_list($lang, $marcflavour, $subset_wanted);
-
-Returns an arrayref containing the filepaths of installer SQL scripts
-available for laod. The C<$lang> and C<$marcflavour> arguments
-specify the desired language and MARC flavour. while C<$subset_wanted>
-is a hashref containing possible named parameters 'mandatory' and 'optional'.
-
-=cut
-
-sub sql_file_list {
- my $self = shift;
- my $lang = shift;
- my $marcflavour = shift;
- my $subset_wanted = shift;
-
- my ($marc_defaulted_to_en, $marc_sql) = $self->marc_framework_sql_list($lang, $marcflavour);
- my ($sample_defaulted_to_en, $sample_sql) = $self->sample_data_sql_list($lang);
-
- my @sql_list = ();
- map {
- map {
- if ($subset_wanted->{'mandatory'}) {
- push @sql_list, $_->{'fwkfile'} if $_->{'mandatory'};
- }
- if ($subset_wanted->{'optional'}) {
- push @sql_list, $_->{'fwkfile'} unless $_->{'mandatory'};
- }
- } @{ $_->{'frameworks'} }
- } (@$marc_sql, @$sample_sql);
-
- return \@sql_list
-}
-
=head2 load_db_schema
my $error = $installer->load_db_schema();
my $self = shift;
my $datadir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}";
+
+ # Disable checks before load
+ $self->{'dbh'}->do(q{SET NAMES utf8mb4});
+ $self->{'dbh'}->do(q{SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0});
+ $self->{'dbh'}->do(q{SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0});
+ $self->{'dbh'}->do(q{SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO'});
+ $self->{'dbh'}->do(q{SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0});
+
+ # Load kohastructure
my $error = $self->load_sql("$datadir/kohastructure.sql");
+
+ # Re-enable checks after load
+ $self->{'dbh'}->do(q{SET SQL_MODE=@OLD_SQL_MODE});
+ $self->{'dbh'}->do(q{SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS});
+ $self->{'dbh'}->do(q{SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS});
+ $self->{'dbh'}->do(q{SET SQL_NOTES=@OLD_SQL_NOTES});
+
return $error;
}
sub load_sql_in_order {
my $self = shift;
+ my $langchoice = shift;
my $all_languages = shift;
my @sql_list = @_;
$request->execute;
my ($systempreference) = $request->fetchrow;
$systempreference = '' unless defined $systempreference; # avoid warning
- # Make sure the global sysprefs.sql file is loaded first
- my $globalsysprefs = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/sysprefs.sql";
- unshift(@fnames, $globalsysprefs);
+
+ my $global_mandatory_dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/mandatory";
+
+ # Make sure some stuffs are loaded first
+ unshift(@fnames,
+ "$global_mandatory_dir/sysprefs.sql",
+ "$global_mandatory_dir/subtag_registry.sql",
+ "$global_mandatory_dir/auth_val_cat.sql",
+ "$global_mandatory_dir/message_transport_types.sql",
+ "$global_mandatory_dir/sample_notices_message_attributes.sql",
+ "$global_mandatory_dir/sample_notices_message_transports.sql",
+ "$global_mandatory_dir/keyboard_shortcuts.sql",
+ );
+
+ push @fnames, "$global_mandatory_dir/userflags.sql",
+ "$global_mandatory_dir/userpermissions.sql",
+ "$global_mandatory_dir/audio_alerts.sql",
+ "$global_mandatory_dir/account_offset_types.sql",
+ "$global_mandatory_dir/account_credit_types.sql",
+ "$global_mandatory_dir/account_debit_types.sql",
+ ;
+ my $localization_file = C4::Context->config('intranetdir') .
+ "/installer/data/$self->{dbms}/localization/$langchoice/custom.sql";
+ if ( $langchoice ne 'en' and -f $localization_file ) {
+ push @fnames, $localization_file;
+ }
foreach my $file (@fnames) {
# warn $file;
undef $/;
my $error = $self->load_sql($file);
my @file = split qr(\/|\\), $file;
$lang = $file[ scalar(@file) - 3 ] unless ($lang);
- my $level = $file[ scalar(@file) - 2 ];
+ my $level = ( $file =~ /(localization)/ ) ? $1 : $file[ scalar(@file) - 2 ];
unless ($error) {
$systempreference .= "$file[scalar(@file)-1]|"
unless ( index( $systempreference, $file[ scalar(@file) - 1 ] ) >= 0 );
sub set_version_syspref {
my $self = shift;
- my $kohaversion=C4::Context::KOHAVERSION;
+ my $kohaversion = Koha::version();
# remove the 3 last . to have a Perl number
$kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
if (C4::Context->preference('Version')) {
C4::Context->clear_syspref_cache();
}
+=head2 set_languages_syspref
+
+ $installer->set_languages_syspref();
+
+Add the installation language to 'language' and 'OPACLanguages' system preferences
+if different from 'en'
+
+=cut
+
+sub set_languages_syspref {
+ my $self = shift;
+ my $language = shift;
+
+ return if ( not $language or $language eq 'en' );
+
+ warn "UPDATE Languages";
+ # intranet
+ my $pref = $self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='language'");
+ $pref->execute("en,$language");
+ # opac
+ $pref = $self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='OPACLanguages'");
+ $pref->execute("en,$language");
+
+ C4::Context->clear_syspref_cache();
+}
+
+=head2 process_yml_table
+
+ my $query_info = $installer->process_yml_table($table);
+
+Analyzes a table loaded in YAML format.
+Returns the values required to build an insert statement.
+
+=cut
+
+sub process_yml_table {
+ my ($table) = @_;
+ my $table_name = ( keys %$table )[0]; # table name
+ my @rows = @{ $table->{$table_name}->{rows} }; #
+ my @columns = ( sort keys %{$rows[0]} ); # column names
+ my $fields = join ",", map{sprintf("`%s`", $_)} @columns; # idem, joined
+ my $query = "INSERT INTO $table_name ( $fields ) VALUES ";
+ my @multiline = @{ $table->{$table_name}->{'multiline'} }; # to check multiline values;
+ my $placeholders = '(' . join ( ",", map { "?" } @columns ) . ')'; # '(?,..,?)' string
+ my @values;
+ foreach my $row ( @rows ) {
+ push @values, [ map {
+ my $col = $_;
+ ( @multiline and grep { $_ eq $col } @multiline )
+ ? join "\r\n", @{$row->{$col}} # join multiline values
+ : $row->{$col};
+ } @columns ];
+ }
+ return { query => $query, placeholders => $placeholders, values => \@values };
+}
+
=head2 load_sql
my $error = $installer->load_sql($filename);
-Runs a the specified SQL using the DB's command-line
-SQL tool, and returns any strings sent to STDERR
-by the command-line tool.
+Runs the specified input file using a sql loader DBIx::RunSQL, or a yaml loader
+Returns any strings sent to STDERR
-B<FIXME:> there has been a long-standing desire to
-replace this with an SQL loader that goes
-through DBI; partly for portability issues
-and partly to improve error handling.
-
-B<FIXME:> even using the command-line loader, some more
-basic error handling should be added - deal
-with missing files, e.g.
+# FIXME This should be improved: sometimes the caller and load_sql warn the same
+error.
=cut
sub load_sql {
my $self = shift;
my $filename = shift;
-
- my $datadir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}";
my $error;
- my $strcmd;
- my $cmd;
- if ( $self->{dbms} eq 'mysql' ) {
- $cmd = qx(which mysql 2>/dev/null || whereis mysql 2>/dev/null);
- chomp $cmd;
- $cmd = $1 if ($cmd && $cmd =~ /^(.+?)[\r\n]+$/);
- $cmd = 'mysql' if (!$cmd || !-x $cmd);
- $strcmd = "$cmd "
- . ( $self->{hostname} ? " -h $self->{hostname} " : "" )
- . ( $self->{port} ? " -P $self->{port} " : "" )
- . ( $self->{user} ? " -u $self->{user} " : "" )
- . ( $self->{password} ? " -p'$self->{password}'" : "" )
- . " $self->{dbname} ";
- $error = qx($strcmd --default-character-set=utf8 <$filename 2>&1 1>/dev/null);
- } elsif ( $self->{dbms} eq 'Pg' ) {
- $cmd = qx(which psql 2>/dev/null || whereis psql 2>/dev/null);
- chomp $cmd;
- $cmd = $1 if ($cmd && $cmd =~ /^(.+?)[\r\n]+$/);
- $cmd = 'psql' if (!$cmd || !-x $cmd);
- $strcmd = "$cmd "
- . ( $self->{hostname} ? " -h $self->{hostname} " : "" )
- . ( $self->{port} ? " -p $self->{port} " : "" )
- . ( $self->{user} ? " -U $self->{user} " : "" )
-# . ( $self->{password} ? " -W $self->{password}" : "" ) # psql will NOT accept a password, but prompts...
- . " $self->{dbname} "; # Therefore, be sure to run 'trust' on localhost in pg_hba.conf -fbcit
- $error = qx($strcmd -f $filename 2>&1 1>/dev/null);
- # Be sure to set 'client_min_messages = error' in postgresql.conf
- # so that only true errors are returned to stderr or else the installer will
- # report the import a failure although it really succeded -fbcit
- }
-# errors thrown while loading installer data should be logged
- if($error) {
- warn "C4::Installer::load_sql returned the following errors while attempting to load $filename:\n";
- warn "$error";
+
+ my $dbh = $self->{ dbh };
+
+ my $dup_stderr;
+ do {
+ local *STDERR;
+ open STDERR, ">>", \$dup_stderr;
+
+ if ( $filename =~ /sql$/ ) { # SQL files
+ eval {
+ DBIx::RunSQL->run_sql_file(
+ dbh => $dbh,
+ sql => $filename,
+ );
+ };
+ }
+ else { # YAML files
+ eval {
+ my $yaml = YAML::XS::LoadFile( $filename ); # Load YAML
+ for my $table ( @{ $yaml->{'tables'} } ) {
+ my $query_info = process_yml_table($table);
+ my $query = $query_info->{query};
+ my $placeholders = $query_info->{placeholders};
+ my $values = $query_info->{values};
+ # Doing only 1 INSERT query for the whole table
+ my @all_rows_values = map { @$_ } @$values;
+ $query .= join ', ', ( $placeholders ) x scalar @$values;
+ $dbh->do( $query, undef, @all_rows_values );
+ }
+ for my $statement ( @{ $yaml->{'sql_statements'} } ) { # extra SQL statements
+ $dbh->do($statement);
+ }
+ };
+ }
+ if ($@){
+ warn "Something went wrong loading file $filename ($@)";
+ }
+ };
+ # errors thrown while loading installer data should be logged
+ if( $dup_stderr ) {
+ warn "C4::Installer::load_sql returned the following errors while attempting to load $filename:\n";
+ $error = $dup_stderr;
}
+
return $error;
}
}
+sub primary_key_exists {
+ my ( $table_name, $key_name ) = @_;
+ my $dbh = C4::Context->dbh;
+ my ($exists) = $dbh->selectrow_array(
+ qq|
+ SHOW INDEX FROM $table_name
+ WHERE key_name = 'PRIMARY' AND column_name = ?
+ |, undef, $key_name
+ );
+ return $exists;
+}
+
+sub foreign_key_exists {
+ my ( $table_name, $constraint_name ) = @_;
+ my $dbh = C4::Context->dbh;
+ my (undef, $infos) = $dbh->selectrow_array(qq|SHOW CREATE TABLE $table_name|);
+ return $infos =~ m|CONSTRAINT `$constraint_name` FOREIGN KEY|;
+}
+
+sub index_exists {
+ my ( $table_name, $key_name ) = @_;
+ my $dbh = C4::Context->dbh;
+ my ($exists) = $dbh->selectrow_array(
+ qq|
+ SHOW INDEX FROM $table_name
+ WHERE key_name = ?
+ |, undef, $key_name
+ );
+ return $exists;
+}
+
+sub column_exists {
+ my ( $table_name, $column_name ) = @_;
+ return unless TableExists($table_name);
+ my $dbh = C4::Context->dbh;
+ my ($exists) = $dbh->selectrow_array(
+ qq|
+ SHOW COLUMNS FROM $table_name
+ WHERE Field = ?
+ |, undef, $column_name
+ );
+ return $exists;
+}
+
+sub TableExists { # Could be renamed table_exists for consistency
+ my $table = shift;
+ eval {
+ my $dbh = C4::Context->dbh;
+ local $dbh->{PrintError} = 0;
+ local $dbh->{RaiseError} = 1;
+ $dbh->do(qq{SELECT * FROM $table WHERE 1 = 0 });
+ };
+ return 1 unless $@;
+ return 0;
+}
+
=head1 AUTHOR