Bug 15004: Allow to change amounts of duplicated budgets
[koha.git] / C4 / Installer.pm
blob5fe2e16982fe3da1708dc6d33d48b0f18099c67f
1 package C4::Installer;
3 # Copyright (C) 2008 LibLime
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20 use strict;
21 #use warnings; FIXME - Bug 2505
23 use Encode qw( encode is_utf8 );
24 our $VERSION = 3.07.00.049;
25 use C4::Context;
26 use C4::Installer::PerlModules;
27 use DBI;
28 use Koha;
30 =head1 NAME
32 C4::Installer
34 =head1 SYNOPSIS
36 use C4::Installer;
37 my $installer = C4::Installer->new();
38 my $all_languages = getAllLanguages();
39 my $error = $installer->load_db_schema();
40 my $list;
41 #fill $list with list of sql files
42 my ($fwk_language, $error_list) = $installer->load_sql_in_order($all_languages, @$list);
43 $installer->set_version_syspref();
44 $installer->set_marcflavour_syspref('MARC21');
46 =head1 DESCRIPTION
48 =cut
50 =head1 METHODS
52 =head2 new
54 my $installer = C4::Installer->new();
56 Creates a new installer.
58 =cut
60 sub new {
61 my $class = shift;
63 my $self = {};
65 # get basic information from context
66 $self->{'dbname'} = C4::Context->config("database");
67 $self->{'dbms'} = C4::Context->config("db_scheme") ? C4::Context->config("db_scheme") : "mysql";
68 $self->{'hostname'} = C4::Context->config("hostname");
69 $self->{'port'} = C4::Context->config("port");
70 $self->{'user'} = C4::Context->config("user");
71 $self->{'password'} = C4::Context->config("pass");
72 $self->{'dbh'} = DBI->connect("DBI:$self->{dbms}:dbname=$self->{dbname};host=$self->{hostname}" .
73 ( $self->{port} ? ";port=$self->{port}" : "" ),
74 $self->{'user'}, $self->{'password'});
75 $self->{'language'} = undef;
76 $self->{'marcflavour'} = undef;
77 $self->{'dbh'}->do('set NAMES "utf8"');
78 $self->{'dbh'}->{'mysql_enable_utf8'}=1;
80 bless $self, $class;
81 return $self;
84 =head2 marc_framework_sql_list
86 my ($defaulted_to_en, $list) =
87 $installer->marc_framework_sql_list($lang, $marcflavour);
89 Returns in C<$list> a structure listing the filename, description, section,
90 and mandatory/optional status of MARC framework scripts available for C<$lang>
91 and C<$marcflavour>.
93 If the C<$defaulted_to_en> return value is true, no scripts are available
94 for language C<$lang> and the 'en' ones are returned.
96 =cut
98 sub marc_framework_sql_list {
99 my $self = shift;
100 my $lang = shift;
101 my $marcflavour = shift;
103 my $defaulted_to_en = 0;
105 undef $/;
106 my $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/$lang/marcflavour/".lc($marcflavour);
107 unless (opendir( MYDIR, $dir )) {
108 if ($lang eq 'en') {
109 warn "cannot open MARC frameworks directory $dir";
110 } else {
111 # if no translated MARC framework is available,
112 # default to English
113 $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/en/marcflavour/".lc($marcflavour);
114 opendir(MYDIR, $dir) or warn "cannot open English MARC frameworks directory $dir";
115 $defaulted_to_en = 1;
118 my @listdir = sort grep { !/^\.|marcflavour/ && -d "$dir/$_" } readdir(MYDIR);
119 closedir MYDIR;
121 my @fwklist;
122 my $request = $self->{'dbh'}->prepare("SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'");
123 $request->execute;
124 my ($frameworksloaded) = $request->fetchrow;
125 $frameworksloaded = '' unless defined $frameworksloaded; # avoid warning
126 my %frameworksloaded;
127 foreach ( split( /\|/, $frameworksloaded ) ) {
128 $frameworksloaded{$_} = 1;
131 foreach my $requirelevel (@listdir) {
132 opendir( MYDIR, "$dir/$requirelevel" );
133 my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.sql$/ } readdir(MYDIR);
134 closedir MYDIR;
135 my %cell;
136 my @frameworklist;
137 map {
138 my $name = substr( $_, 0, -4 );
139 open my $fh, "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
140 my $lines = <$fh>;
141 $lines =~ s/\n|\r/<br \/>/g;
142 $lines = Encode::encode('UTF-8', $lines) unless ( Encode::is_utf8($lines) );
143 my $mandatory = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i);
144 push @frameworklist,
146 'fwkname' => $name,
147 'fwkfile' => "$dir/$requirelevel/$_",
148 'fwkdescription' => $lines,
149 'checked' => ( ( $frameworksloaded{$_} || $mandatory ) ? 1 : 0 ),
150 'mandatory' => $mandatory,
152 } @listname;
153 my @fwks =
154 sort { $a->{'fwkname'} cmp $b->{'fwkname'} } @frameworklist;
156 $cell{"frameworks"} = \@fwks;
157 $cell{"label"} = ucfirst($requirelevel);
158 $cell{"code"} = lc($requirelevel);
159 push @fwklist, \%cell;
162 return ($defaulted_to_en, \@fwklist);
165 =head2 sample_data_sql_list
167 my ($defaulted_to_en, $list) = $installer->sample_data_sql_list($lang);
169 Returns in C<$list> a structure listing the filename, description, section,
170 and mandatory/optional status of sample data scripts available for C<$lang>.
171 If the C<$defaulted_to_en> return value is true, no scripts are available
172 for language C<$lang> and the 'en' ones are returned.
174 =cut
176 sub sample_data_sql_list {
177 my $self = shift;
178 my $lang = shift;
180 my $defaulted_to_en = 0;
182 undef $/;
183 my $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/$lang";
184 unless (opendir( MYDIR, $dir )) {
185 if ($lang eq 'en') {
186 warn "cannot open sample data directory $dir";
187 } else {
188 # if no sample data is available,
189 # default to English
190 $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/en";
191 opendir(MYDIR, $dir) or warn "cannot open English sample data directory $dir";
192 $defaulted_to_en = 1;
195 my @listdir = sort grep { !/^\.|marcflavour/ && -d "$dir/$_" } readdir(MYDIR);
196 closedir MYDIR;
198 my @levellist;
199 my $request = $self->{'dbh'}->prepare("SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'");
200 $request->execute;
201 my ($frameworksloaded) = $request->fetchrow;
202 $frameworksloaded = '' unless defined $frameworksloaded; # avoid warning
203 my %frameworksloaded;
204 foreach ( split( /\|/, $frameworksloaded ) ) {
205 $frameworksloaded{$_} = 1;
208 foreach my $requirelevel (@listdir) {
209 opendir( MYDIR, "$dir/$requirelevel" );
210 my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.sql$/ } readdir(MYDIR);
211 closedir MYDIR;
212 my %cell;
213 my @frameworklist;
214 map {
215 my $name = substr( $_, 0, -4 );
216 open my $fh , "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
217 my $lines = <$fh>;
218 $lines =~ s/\n|\r/<br \/>/g;
219 $lines = Encode::encode('UTF-8', $lines) unless ( Encode::is_utf8($lines) );
220 my $mandatory = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i);
221 push @frameworklist,
223 'fwkname' => $name,
224 'fwkfile' => "$dir/$requirelevel/$_",
225 'fwkdescription' => $lines,
226 'checked' => ( ( $frameworksloaded{$_} || $mandatory ) ? 1 : 0 ),
227 'mandatory' => $mandatory,
229 } @listname;
230 my @fwks = sort { $a->{'fwkname'} cmp $b->{'fwkname'} } @frameworklist;
232 $cell{"frameworks"} = \@fwks;
233 $cell{"label"} = ucfirst($requirelevel);
234 $cell{"code"} = lc($requirelevel);
235 push @levellist, \%cell;
238 return ($defaulted_to_en, \@levellist);
241 =head2 load_db_schema
243 my $error = $installer->load_db_schema();
245 Loads the SQL script that creates Koha's tables and indexes. The
246 return value is a string containing error messages reported by the
247 load.
249 =cut
251 sub load_db_schema {
252 my $self = shift;
254 my $datadir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}";
255 my $error = $self->load_sql("$datadir/kohastructure.sql");
256 return $error;
260 =head2 load_sql_in_order
262 my ($fwk_language, $list) = $installer->load_sql_in_order($all_languages, @sql_list);
264 Given a list of SQL scripts supplied in C<@sql_list>, loads each of them
265 into the database and sets the FrameworksLoaded system preference to names
266 of the scripts that were loaded.
268 The SQL files are loaded in alphabetical order by filename (not including
269 directory path). This means that dependencies among the scripts are to
270 be resolved by carefully naming them, keeping in mind that the directory name
271 does *not* currently count.
273 B<FIXME:> this is a rather delicate way of dealing with dependencies between
274 the install scripts.
276 The return value C<$list> is an arrayref containing a hashref for each
277 "level" or directory containing SQL scripts; the hashref in turns contains
278 a list of hashrefs containing a list of each script load and any error
279 messages associated with the loading of each script.
281 B<FIXME:> The C<$fwk_language> code probably doesn't belong and needs to be
282 moved to a different method.
284 =cut
286 sub load_sql_in_order {
287 my $self = shift;
288 my $all_languages = shift;
289 my @sql_list = @_;
291 my $lang;
292 my %hashlevel;
293 my @fnames = sort {
294 my @aa = split /\/|\\/, ($a);
295 my @bb = split /\/|\\/, ($b);
296 $aa[-1] cmp $bb[-1]
297 } @sql_list;
298 my $request = $self->{'dbh'}->prepare( "SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'" );
299 $request->execute;
300 my ($systempreference) = $request->fetchrow;
301 $systempreference = '' unless defined $systempreference; # avoid warning
302 # Make sure subtag_registry.sql is loaded second
303 my $subtag_registry = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/mandatory/subtag_registry.sql";
304 unshift(@fnames, $subtag_registry);
305 # Make sure the global sysprefs.sql file is loaded first
306 my $globalsysprefs = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/sysprefs.sql";
307 unshift(@fnames, $globalsysprefs);
308 push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/userflags.sql";
309 push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/userpermissions.sql";
310 push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/audio_alerts.sql";
311 foreach my $file (@fnames) {
312 # warn $file;
313 undef $/;
314 my $error = $self->load_sql($file);
315 my @file = split qr(\/|\\), $file;
316 $lang = $file[ scalar(@file) - 3 ] unless ($lang);
317 my $level = $file[ scalar(@file) - 2 ];
318 unless ($error) {
319 $systempreference .= "$file[scalar(@file)-1]|"
320 unless ( index( $systempreference, $file[ scalar(@file) - 1 ] ) >= 0 );
323 #Bulding here a hierarchy to display files by level.
324 push @{ $hashlevel{$level} },
325 { "fwkname" => $file[ scalar(@file) - 1 ], "error" => $error };
328 #systempreference contains an ending |
329 chop $systempreference;
330 my @list;
331 map { push @list, { "level" => $_, "fwklist" => $hashlevel{$_} } } keys %hashlevel;
332 my $fwk_language;
333 for my $each_language (@$all_languages) {
335 # warn "CODE".$each_language->{'language_code'};
336 # warn "LANG:".$lang;
337 if ( $lang eq $each_language->{'language_code'} ) {
338 $fwk_language = $each_language->{language_locale_name};
341 my $updateflag =
342 $self->{'dbh'}->do(
343 "UPDATE systempreferences set value=\"$systempreference\" where variable='FrameworksLoaded'"
346 unless ( $updateflag == 1 ) {
347 my $string =
348 "INSERT INTO systempreferences (value, variable, explanation, type) VALUES (\"$systempreference\",'FrameworksLoaded','Frameworks loaded through webinstaller','choice')";
349 my $rq = $self->{'dbh'}->prepare($string);
350 $rq->execute;
352 return ($fwk_language, \@list);
355 =head2 set_marcflavour_syspref
357 $installer->set_marcflavour_syspref($marcflavour);
359 Set the 'marcflavour' system preference. The incoming
360 C<$marcflavour> references to a subdirectory of
361 installer/data/$dbms/$lang/marcflavour, and is
362 normalized to MARC21, UNIMARC or NORMARC.
364 FIXME: this method assumes that the MARC flavour will be either
365 MARC21, UNIMARC or NORMARC.
367 =cut
369 sub set_marcflavour_syspref {
370 my $self = shift;
371 my $marcflavour = shift;
373 # we can have some variants of marc flavour, by having different directories, like : unimarc_small and unimarc_full, for small and complete unimarc frameworks.
374 # marc_cleaned finds the marcflavour, without the variant.
375 my $marc_cleaned = 'MARC21';
376 $marc_cleaned = 'UNIMARC' if $marcflavour =~ /unimarc/i;
377 $marc_cleaned = 'NORMARC' if $marcflavour =~ /normarc/i;
378 my $request =
379 $self->{'dbh'}->prepare(
380 "INSERT IGNORE INTO `systempreferences` (variable,value,explanation,options,type) VALUES('marcflavour','$marc_cleaned','Define global MARC flavor (MARC21, UNIMARC or NORMARC) used for character encoding','MARC21|UNIMARC|NORMARC','Choice');"
382 $request->execute;
385 =head2 set_version_syspref
387 $installer->set_version_syspref();
389 Set or update the 'Version' system preference to the current
390 Koha software version.
392 =cut
394 sub set_version_syspref {
395 my $self = shift;
397 my $kohaversion = Koha::version();
398 # remove the 3 last . to have a Perl number
399 $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
400 if (C4::Context->preference('Version')) {
401 warn "UPDATE Version";
402 my $finish=$self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='Version'");
403 $finish->execute($kohaversion);
404 } else {
405 warn "INSERT Version";
406 my $finish=$self->{'dbh'}->prepare("INSERT into systempreferences (variable,value,explanation) values ('Version',?,'The Koha database version. WARNING: Do not change this value manually, it is maintained by the webinstaller')");
407 $finish->execute($kohaversion);
409 C4::Context->clear_syspref_cache();
412 =head2 load_sql
414 my $error = $installer->load_sql($filename);
416 Runs a the specified SQL using the DB's command-line
417 SQL tool, and returns any strings sent to STDERR
418 by the command-line tool.
420 B<FIXME:> there has been a long-standing desire to
421 replace this with an SQL loader that goes
422 through DBI; partly for portability issues
423 and partly to improve error handling.
425 B<FIXME:> even using the command-line loader, some more
426 basic error handling should be added - deal
427 with missing files, e.g.
429 =cut
431 sub load_sql {
432 my $self = shift;
433 my $filename = shift;
435 my $datadir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}";
436 my $error;
437 my $strcmd;
438 my $cmd;
439 if ( $self->{dbms} eq 'mysql' ) {
440 $cmd = qx(which mysql 2>/dev/null || whereis mysql 2>/dev/null);
441 chomp $cmd;
442 $cmd = $1 if ($cmd && $cmd =~ /^(.+?)[\r\n]+$/);
443 $cmd = 'mysql' if (!$cmd || !-x $cmd);
444 $strcmd = "$cmd "
445 . ( $self->{hostname} ? " -h $self->{hostname} " : "" )
446 . ( $self->{port} ? " -P $self->{port} " : "" )
447 . ( $self->{user} ? " -u $self->{user} " : "" )
448 . ( $self->{password} ? " -p'$self->{password}'" : "" )
449 . " $self->{dbname} ";
450 $error = qx($strcmd --default-character-set=utf8 <$filename 2>&1 1>/dev/null);
451 } elsif ( $self->{dbms} eq 'Pg' ) {
452 $cmd = qx(which psql 2>/dev/null || whereis psql 2>/dev/null);
453 chomp $cmd;
454 $cmd = $1 if ($cmd && $cmd =~ /^(.+?)[\r\n]+$/);
455 $cmd = 'psql' if (!$cmd || !-x $cmd);
456 $strcmd = "$cmd "
457 . ( $self->{hostname} ? " -h $self->{hostname} " : "" )
458 . ( $self->{port} ? " -p $self->{port} " : "" )
459 . ( $self->{user} ? " -U $self->{user} " : "" )
460 # . ( $self->{password} ? " -W $self->{password}" : "" ) # psql will NOT accept a password, but prompts...
461 . " $self->{dbname} "; # Therefore, be sure to run 'trust' on localhost in pg_hba.conf -fbcit
462 $error = qx($strcmd -f $filename 2>&1 1>/dev/null);
463 # Be sure to set 'client_min_messages = error' in postgresql.conf
464 # so that only true errors are returned to stderr or else the installer will
465 # report the import as a failure although it really succeeded -fbcit
467 # errors thrown while loading installer data should be logged
468 if($error) {
469 warn "C4::Installer::load_sql returned the following errors while attempting to load $filename:\n";
470 warn "$error";
472 return $error;
475 =head2 get_file_path_from_name
477 my $filename = $installer->get_file_path_from_name('script_name');
479 searches through the set of known SQL scripts and finds the fully
480 qualified path name for the script that mathches the input.
482 returns undef if no match was found.
485 =cut
487 sub get_file_path_from_name {
488 my $self = shift;
489 my $partialname = shift;
491 my $lang = 'en'; # FIXME: how do I know what language I want?
493 my ($defaulted_to_en, $list) = $self->sample_data_sql_list($lang);
494 # warn( Data::Dumper->Dump( [ $list ], [ 'list' ] ) );
496 my @found;
497 foreach my $frameworklist ( @$list ) {
498 push @found, grep { $_->{'fwkfile'} =~ /$partialname$/ } @{$frameworklist->{'frameworks'}};
501 # warn( Data::Dumper->Dump( [ \@found ], [ 'found' ] ) );
502 if ( 0 == scalar @found ) {
503 return;
504 } elsif ( 1 < scalar @found ) {
505 warn "multiple results found for $partialname";
506 return;
507 } else {
508 return $found[0]->{'fwkfile'};
514 =head1 AUTHOR
516 C4::Installer is a refactoring of logic originally from installer/installer.pl, which was
517 originally written by Henri-Damien Laurant.
519 Koha Development Team <http://koha-community.org/>
521 Galen Charlton <galen.charlton@liblime.com>
523 =cut