Bug 26133: Remove GetMarcHosts
[koha.git] / C4 / Installer.pm
1 package C4::Installer;
2
3 # Copyright (C) 2008 LibLime
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use Encode qw( encode is_utf8 );
23 use DBIx::RunSQL;
24 use YAML::Syck qw( LoadFile );
25 use C4::Context;
26 use DBI;
27 use Koha;
28
29 use vars qw(@ISA @EXPORT);
30 BEGIN {
31     require Exporter;
32     @ISA = qw( Exporter );
33     push @EXPORT, qw( foreign_key_exists index_exists column_exists TableExists);
34 };
35
36 =head1 NAME
37
38 C4::Installer
39
40 =head1 SYNOPSIS
41
42  use C4::Installer;
43  my $installer = C4::Installer->new();
44  my $all_languages = getAllLanguages();
45  my $error = $installer->load_db_schema();
46  my $list;
47  #fill $list with list of sql files
48  my ($fwk_language, $error_list) = $installer->load_sql_in_order($all_languages, @$list);
49  $installer->set_version_syspref();
50  $installer->set_marcflavour_syspref('MARC21');
51
52 =head1 DESCRIPTION
53
54 =cut
55
56 =head1 METHODS
57
58 =head2 new
59
60   my $installer = C4::Installer->new();
61
62 Creates a new installer.
63
64 =cut
65
66 sub new {
67     my $class = shift;
68
69     my $self = {};
70
71     # get basic information from context
72     $self->{'dbname'}   = C4::Context->config("database");
73     $self->{'dbms'}     = C4::Context->config("db_scheme") ? C4::Context->config("db_scheme") : "mysql";
74     $self->{'hostname'} = C4::Context->config("hostname");
75     $self->{'port'}     = C4::Context->config("port");
76     $self->{'user'}     = C4::Context->config("user");
77     $self->{'password'} = C4::Context->config("pass");
78     $self->{'tls'} = C4::Context->config("tls");
79     if( $self->{'tls'} && $self->{'tls'} eq 'yes' ) {
80         $self->{'ca'} = C4::Context->config('ca');
81         $self->{'cert'} = C4::Context->config('cert');
82         $self->{'key'} = C4::Context->config('key');
83         $self->{'tlsoptions'} = ";mysql_ssl=1;mysql_ssl_client_key=".$self->{key}.";mysql_ssl_client_cert=".$self->{cert}.";mysql_ssl_ca_file=".$self->{ca};
84         $self->{'tlscmdline'} =  " --ssl-cert ". $self->{cert} . " --ssl-key " . $self->{key} . " --ssl-ca ".$self->{ca}." "
85     }
86     $self->{'dbh'} = DBI->connect("DBI:$self->{dbms}:dbname=$self->{dbname};host=$self->{hostname}" .
87                                   ( $self->{port} ? ";port=$self->{port}" : "" ).
88                                   ( $self->{tlsoptions} ? $self->{tlsoptions} : ""),
89                                   $self->{'user'}, $self->{'password'});
90     $self->{'language'} = undef;
91     $self->{'marcflavour'} = undef;
92         $self->{'dbh'}->do('set NAMES "utf8"');
93     $self->{'dbh'}->{'mysql_enable_utf8'}=1;
94
95     bless $self, $class;
96     return $self;
97 }
98
99 =head2 marc_framework_sql_list
100
101   my ($defaulted_to_en, $list) = 
102      $installer->marc_framework_sql_list($lang, $marcflavour);
103
104 Returns in C<$list> a structure listing the filename, description, section,
105 and mandatory/optional status of MARC framework scripts available for C<$lang>
106 and C<$marcflavour>.
107
108 If the C<$defaulted_to_en> return value is true, no scripts are available
109 for language C<$lang> and the 'en' ones are returned.
110
111 =cut
112
113 sub marc_framework_sql_list {
114     my $self = shift;
115     my $lang = shift;
116     my $marcflavour = shift;
117
118     my $defaulted_to_en = 0;
119
120     undef $/;
121     my $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/$lang/marcflavour/".lc($marcflavour);
122     unless (opendir( MYDIR, $dir )) {
123         if ($lang eq 'en') {
124             warn "cannot open MARC frameworks directory $dir";
125         } else {
126             # if no translated MARC framework is available,
127             # default to English
128             $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/en/marcflavour/".lc($marcflavour);
129             opendir(MYDIR, $dir) or warn "cannot open English MARC frameworks directory $dir";
130             $defaulted_to_en = 1;
131         }
132     }
133     my @listdir = sort grep { !/^\.|marcflavour/ && -d "$dir/$_" } readdir(MYDIR);
134     closedir MYDIR;
135
136     my @fwklist;
137     my $request = $self->{'dbh'}->prepare("SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'");
138     $request->execute;
139     my ($frameworksloaded) = $request->fetchrow;
140     $frameworksloaded = '' unless defined $frameworksloaded; # avoid warning
141     my %frameworksloaded;
142     foreach ( split( /\|/, $frameworksloaded ) ) {
143         $frameworksloaded{$_} = 1;
144     }
145
146     foreach my $requirelevel (@listdir) {
147         opendir( MYDIR, "$dir/$requirelevel" );
148         my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.(sql|yml)$/ } readdir(MYDIR);
149         closedir MYDIR;
150         my %cell;
151         my @frameworklist;
152         map {
153             my ( $name, $ext ) = split /\./, $_;
154             my @lines;
155             if ( $ext =~ /yml/ ) {
156                 my $yaml = LoadFile("$dir/$requirelevel/$name\.$ext");
157                 @lines = map { Encode::decode('UTF-8', $_) } @{ $yaml->{'description'} };
158             } else {
159                 open my $fh, "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
160                 my $line = <$fh>;
161                 $line = Encode::encode('UTF-8', $line) unless ( Encode::is_utf8($line) );
162                 @lines = split /\n/, $line;
163             }
164             my $mandatory = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i);
165             push @frameworklist,
166               {
167                 'fwkname'        => $name,
168                 'fwkfile'        => "$dir/$requirelevel/$_",
169                 'fwkdescription' => \@lines,
170                 'checked'        => ( ( $frameworksloaded{$_} || $mandatory ) ? 1 : 0 ),
171                 'mandatory'      => $mandatory,
172               };
173         } @listname;
174         my @fwks =
175           sort { $a->{'fwkname'} cmp $b->{'fwkname'} } @frameworklist;
176
177         $cell{"frameworks"} = \@fwks;
178         $cell{"label"}      = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i)?'mandatory':'optional';
179         $cell{"code"}       = lc($requirelevel);
180         push @fwklist, \%cell;
181     }
182
183     return ($defaulted_to_en, \@fwklist);
184 }
185
186 =head2 sample_data_sql_list
187
188   my ($defaulted_to_en, $list) = $installer->sample_data_sql_list($lang);
189
190 Returns in C<$list> a structure listing the filename, description, section,
191 and mandatory/optional status of sample data scripts available for C<$lang>.
192 If the C<$defaulted_to_en> return value is true, no scripts are available
193 for language C<$lang> and the 'en' ones are returned.
194
195 =cut
196
197 sub sample_data_sql_list {
198     my $self = shift;
199     my $lang = shift;
200
201     my $defaulted_to_en = 0;
202
203     undef $/;
204     my $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/$lang";
205     unless (opendir( MYDIR, $dir )) {
206         if ($lang eq 'en') {
207             warn "cannot open sample data directory $dir";
208         } else {
209             # if no sample data is available,
210             # default to English
211             $dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/en";
212             opendir(MYDIR, $dir) or warn "cannot open English sample data directory $dir";
213             $defaulted_to_en = 1;
214         }
215     }
216     my @listdir = sort grep { !/^\.|marcflavour/ && -d "$dir/$_" } readdir(MYDIR);
217     closedir MYDIR;
218
219     my @levellist;
220     my $request = $self->{'dbh'}->prepare("SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'");
221     $request->execute;
222     my ($frameworksloaded) = $request->fetchrow;
223     $frameworksloaded = '' unless defined $frameworksloaded; # avoid warning
224     my %frameworksloaded;
225     foreach ( split( /\|/, $frameworksloaded ) ) {
226         $frameworksloaded{$_} = 1;
227     }
228
229     foreach my $requirelevel (@listdir) {
230         opendir( MYDIR, "$dir/$requirelevel" );
231         my @listname = grep { !/^\./ && -f "$dir/$requirelevel/$_" && $_ =~ m/\.(sql|yml)$/ } readdir(MYDIR);
232         closedir MYDIR;
233         my %cell;
234         my @frameworklist;
235         map {
236             my ( $name, $ext ) = split /\./, $_;
237             my @lines;
238             if ( $ext =~ /yml/ ) {
239                 my $yaml = LoadFile("$dir/$requirelevel/$name\.$ext");
240                 @lines = map { Encode::decode('UTF-8', $_) } @{ $yaml->{'description'} };
241             } else {
242                 open my $fh, "<:encoding(UTF-8)", "$dir/$requirelevel/$name.txt";
243                 my $line = <$fh>;
244                 $line = Encode::encode('UTF-8', $line) unless ( Encode::is_utf8($line) );
245                 @lines = split /\n/, $line;
246             }
247             my $mandatory = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i);
248             push @frameworklist,
249               {
250                 'fwkname'        => $name,
251                 'fwkfile'        => "$dir/$requirelevel/$_",
252                 'fwkdescription' => \@lines,
253                 'checked'        => ( ( $frameworksloaded{$_} || $mandatory ) ? 1 : 0 ),
254                 'mandatory'      => $mandatory,
255               };
256         } @listname;
257         my @fwks = sort { $a->{'fwkname'} cmp $b->{'fwkname'} } @frameworklist;
258
259         $cell{"frameworks"} = \@fwks;
260         $cell{"label"}      = ($requirelevel =~ /(mandatory|requi|oblig|necess)/i)?'mandatory':'optional';
261         $cell{"code"}       = lc($requirelevel);
262         push @levellist, \%cell;
263     }
264
265     return ($defaulted_to_en, \@levellist);
266 }
267
268 =head2 load_db_schema
269
270   my $error = $installer->load_db_schema();
271
272 Loads the SQL script that creates Koha's tables and indexes.  The
273 return value is a string containing error messages reported by the
274 load.
275
276 =cut
277
278 sub load_db_schema {
279     my $self = shift;
280
281     my $datadir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}";
282     my $error = $self->load_sql("$datadir/kohastructure.sql");
283     return $error;
284
285 }
286
287 =head2 load_sql_in_order
288
289   my ($fwk_language, $list) = $installer->load_sql_in_order($all_languages, @sql_list);
290
291 Given a list of SQL scripts supplied in C<@sql_list>, loads each of them
292 into the database and sets the FrameworksLoaded system preference to names
293 of the scripts that were loaded.
294
295 The SQL files are loaded in alphabetical order by filename (not including
296 directory path).  This means that dependencies among the scripts are to
297 be resolved by carefully naming them, keeping in mind that the directory name
298 does *not* currently count.
299
300 B<FIXME:> this is a rather delicate way of dealing with dependencies between
301 the install scripts.
302
303 The return value C<$list> is an arrayref containing a hashref for each
304 "level" or directory containing SQL scripts; the hashref in turns contains
305 a list of hashrefs containing a list of each script load and any error
306 messages associated with the loading of each script.
307
308 B<FIXME:> The C<$fwk_language> code probably doesn't belong and needs to be
309 moved to a different method.
310
311 =cut
312
313 sub load_sql_in_order {
314     my $self = shift;
315     my $all_languages = shift;
316     my @sql_list = @_;
317
318     my $lang;
319     my %hashlevel;
320     my @fnames = sort {
321         my @aa = split /\/|\\/, ($a);
322         my @bb = split /\/|\\/, ($b);
323         $aa[-1] cmp $bb[-1]
324     } @sql_list;
325     my $request = $self->{'dbh'}->prepare( "SELECT value FROM systempreferences WHERE variable='FrameworksLoaded'" );
326     $request->execute;
327     my ($systempreference) = $request->fetchrow;
328     $systempreference = '' unless defined $systempreference; # avoid warning
329
330     my $global_mandatory_dir = C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/mandatory";
331
332     # Make sure some stuffs are loaded first
333     unshift(@fnames, C4::Context->config('intranetdir') . "/installer/data/$self->{dbms}/sysprefs.sql");
334     unshift(@fnames,
335         "$global_mandatory_dir/subtag_registry.sql",
336         "$global_mandatory_dir/auth_val_cat.sql",
337         "$global_mandatory_dir/message_transport_types.sql",
338         "$global_mandatory_dir/sample_notices_message_attributes.sql",
339         "$global_mandatory_dir/sample_notices_message_transports.sql",
340         "$global_mandatory_dir/keyboard_shortcuts.sql",
341     );
342
343     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/userflags.sql";
344     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/userpermissions.sql";
345     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/audio_alerts.sql";
346     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_offset_types.sql";
347     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_credit_types.sql";
348     push @fnames, C4::Context->config('intranetdir') . "/installer/data/mysql/account_debit_types.sql";
349     foreach my $file (@fnames) {
350         #      warn $file;
351         undef $/;
352         my $error = $self->load_sql($file);
353         my @file = split qr(\/|\\), $file;
354         $lang = $file[ scalar(@file) - 3 ] unless ($lang);
355         my $level = $file[ scalar(@file) - 2 ];
356         unless ($error) {
357             $systempreference .= "$file[scalar(@file)-1]|"
358               unless ( index( $systempreference, $file[ scalar(@file) - 1 ] ) >= 0 );
359         }
360
361         #Bulding here a hierarchy to display files by level.
362         push @{ $hashlevel{$level} },
363           { "fwkname" => $file[ scalar(@file) - 1 ], "error" => $error };
364     }
365
366     #systempreference contains an ending |
367     chop $systempreference;
368     my @list;
369     map { push @list, { "level" => $_, "fwklist" => $hashlevel{$_} } } keys %hashlevel;
370     my $fwk_language;
371     for my $each_language (@$all_languages) {
372
373         #       warn "CODE".$each_language->{'language_code'};
374         #       warn "LANG:".$lang;
375         if ( $lang eq $each_language->{'language_code'} ) {
376             $fwk_language = $each_language->{language_locale_name};
377         }
378     }
379     my $updateflag =
380       $self->{'dbh'}->do(
381         "UPDATE systempreferences set value=\"$systempreference\" where variable='FrameworksLoaded'"
382       );
383
384     unless ( $updateflag == 1 ) {
385         my $string =
386             "INSERT INTO systempreferences (value, variable, explanation, type) VALUES (\"$systempreference\",'FrameworksLoaded','Frameworks loaded through webinstaller','choice')";
387         my $rq = $self->{'dbh'}->prepare($string);
388         $rq->execute;
389     }
390     return ($fwk_language, \@list);
391 }
392
393 =head2 set_marcflavour_syspref
394
395   $installer->set_marcflavour_syspref($marcflavour);
396
397 Set the 'marcflavour' system preference.  The incoming
398 C<$marcflavour> references to a subdirectory of
399 installer/data/$dbms/$lang/marcflavour, and is
400 normalized to MARC21, UNIMARC or NORMARC.
401
402 FIXME: this method assumes that the MARC flavour will be either
403 MARC21, UNIMARC or NORMARC.
404
405 =cut
406
407 sub set_marcflavour_syspref {
408     my $self = shift;
409     my $marcflavour = shift;
410
411     # we can have some variants of marc flavour, by having different directories, like : unimarc_small and unimarc_full, for small and complete unimarc frameworks.
412     # marc_cleaned finds the marcflavour, without the variant.
413     my $marc_cleaned = 'MARC21';
414     $marc_cleaned = 'UNIMARC' if $marcflavour =~ /unimarc/i;
415     $marc_cleaned = 'NORMARC' if $marcflavour =~ /normarc/i;
416     my $request =
417         $self->{'dbh'}->prepare(
418           "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');"
419         );
420     $request->execute;
421 }
422
423 =head2 set_version_syspref
424
425   $installer->set_version_syspref();
426
427 Set or update the 'Version' system preference to the current
428 Koha software version.
429
430 =cut
431
432 sub set_version_syspref {
433     my $self = shift;
434
435     my $kohaversion = Koha::version();
436     # remove the 3 last . to have a Perl number
437     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
438     if (C4::Context->preference('Version')) {
439         warn "UPDATE Version";
440         my $finish=$self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='Version'");
441         $finish->execute($kohaversion);
442     } else {
443         warn "INSERT Version";
444         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')");
445         $finish->execute($kohaversion);
446     }
447     C4::Context->clear_syspref_cache();
448 }
449
450 =head2 set_languages_syspref
451
452   $installer->set_languages_syspref();
453
454 Add the installation language to 'language' and 'opaclanguages' system preferences
455 if different from 'en'
456
457 =cut
458
459 sub set_languages_syspref {
460     my $self     = shift;
461     my $language = shift;
462
463     return if ( not $language or $language eq 'en' );
464
465     warn "UPDATE Languages";
466     # intranet
467     my $pref = $self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='language'");
468     $pref->execute("en,$language");
469     # opac
470     $pref = $self->{'dbh'}->prepare("UPDATE systempreferences SET value=? WHERE variable='opaclanguages'");
471     $pref->execute("en,$language");
472
473     C4::Context->clear_syspref_cache();
474 }
475
476 =head2 process_yml_table
477
478   my $query_info   = $installer->process_yml_table($table);
479
480 Analyzes a table loaded in YAML format.
481 Returns the values required to build an insert statement.
482
483 =cut
484
485 sub process_yml_table {
486     my ($table) = @_;
487     my $table_name   = ( keys %$table )[0];                          # table name
488     my @rows         = @{ $table->{$table_name}->{rows} };           #
489     my @columns      = ( sort keys %{$rows[0]} );                    # column names
490     my $fields       = join ",", map{sprintf("`%s`", $_)} @columns;  # idem, joined
491     my $query        = "INSERT INTO $table_name ( $fields ) VALUES ";
492     my @multiline    = @{ $table->{$table_name}->{'multiline'} };    # to check multiline values;
493     my $placeholders = '(' . join ( ",", map { "?" } @columns ) . ')'; # '(?,..,?)' string
494     my @values;
495     foreach my $row ( @rows ) {
496         push @values, [ map {
497                         my $col = $_;
498                         ( @multiline and grep { $_ eq $col } @multiline )
499                         ? join "\r\n", @{$row->{$col}}                # join multiline values
500                         : $row->{$col};
501                      } @columns ];
502     }
503     return { query => $query, placeholders => $placeholders, values => \@values };
504 }
505
506 =head2 load_sql
507
508   my $error = $installer->load_sql($filename);
509
510 Runs the specified input file using a sql loader DBIx::RunSQL, or a yaml loader
511 Returns any strings sent to STDERR
512
513 # FIXME This should be improved: sometimes the caller and load_sql warn the same
514 error.
515
516 =cut
517
518 sub load_sql {
519     my $self = shift;
520     my $filename = shift;
521     my $error;
522
523     my $dbh = $self->{ dbh };
524
525     my $dup_stderr;
526     do {
527         local *STDERR;
528         open STDERR, ">>", \$dup_stderr;
529
530         if ( $filename =~ /sql$/ ) {                                                        # SQL files
531             eval {
532                 DBIx::RunSQL->run_sql_file(
533                     dbh     => $dbh,
534                     sql     => $filename,
535                 );
536             };
537         }
538         else {                                                                       # YAML files
539             eval {
540                 my $yaml         = LoadFile( $filename );                            # Load YAML
541                 for my $table ( @{ $yaml->{'tables'} } ) {
542                     my $query_info   = process_yml_table($table);
543                     my $query        = $query_info->{query};
544                     my $placeholders = $query_info->{placeholders};
545                     my $values       = $query_info->{values};
546                     # Doing only 1 INSERT query for the whole table
547                     my @all_rows_values = map { @$_ } @$values;
548                     $query .= join ', ', ( $placeholders ) x scalar @$values;
549                     $dbh->do( $query, undef, @all_rows_values );
550                 }
551                 for my $statement ( @{ $yaml->{'sql_statements'} } ) {               # extra SQL statements
552                     $dbh->do($statement);
553                 }
554             };
555         }
556         if ($@){
557             warn "Something went wrong loading file $filename ($@)";
558         }
559     };
560     #   errors thrown while loading installer data should be logged
561     if( $dup_stderr ) {
562         warn "C4::Installer::load_sql returned the following errors while attempting to load $filename:\n";
563         $error = $dup_stderr;
564     }
565
566     return $error;
567 }
568
569 =head2 get_file_path_from_name
570
571   my $filename = $installer->get_file_path_from_name('script_name');
572
573 searches through the set of known SQL scripts and finds the fully
574 qualified path name for the script that mathches the input.
575
576 returns undef if no match was found.
577
578
579 =cut
580
581 sub get_file_path_from_name {
582     my $self = shift;
583     my $partialname = shift;
584
585     my $lang = 'en'; # FIXME: how do I know what language I want?
586
587     my ($defaulted_to_en, $list) = $self->sample_data_sql_list($lang);
588     # warn( Data::Dumper->Dump( [ $list ], [ 'list' ] ) );
589
590     my @found;
591     foreach my $frameworklist ( @$list ) {
592         push @found, grep { $_->{'fwkfile'} =~ /$partialname$/ } @{$frameworklist->{'frameworks'}};
593     }
594
595     # warn( Data::Dumper->Dump( [ \@found ], [ 'found' ] ) );
596     if ( 0 == scalar @found ) {
597         return;
598     } elsif ( 1 < scalar @found ) {
599         warn "multiple results found for $partialname";
600         return;
601     } else {
602         return $found[0]->{'fwkfile'};
603     }
604
605 }
606
607 sub foreign_key_exists {
608     my ( $table_name, $constraint_name ) = @_;
609     my $dbh = C4::Context->dbh;
610     my (undef, $infos) = $dbh->selectrow_array(qq|SHOW CREATE TABLE $table_name|);
611     return $infos =~ m|CONSTRAINT `$constraint_name` FOREIGN KEY|;
612 }
613
614 sub index_exists {
615     my ( $table_name, $key_name ) = @_;
616     my $dbh = C4::Context->dbh;
617     my ($exists) = $dbh->selectrow_array(
618         qq|
619         SHOW INDEX FROM $table_name
620         WHERE key_name = ?
621         |, undef, $key_name
622     );
623     return $exists;
624 }
625
626 sub column_exists {
627     my ( $table_name, $column_name ) = @_;
628     return unless TableExists($table_name);
629     my $dbh = C4::Context->dbh;
630     my ($exists) = $dbh->selectrow_array(
631         qq|
632         SHOW COLUMNS FROM $table_name
633         WHERE Field = ?
634         |, undef, $column_name
635     );
636     return $exists;
637 }
638
639 sub TableExists { # Could be renamed table_exists for consistency
640     my $table = shift;
641     eval {
642                 my $dbh = C4::Context->dbh;
643                 local $dbh->{PrintError} = 0;
644                 local $dbh->{RaiseError} = 1;
645                 $dbh->do(qq{SELECT * FROM $table WHERE 1 = 0 });
646             };
647     return 1 unless $@;
648     return 0;
649 }
650
651
652 =head1 AUTHOR
653
654 C4::Installer is a refactoring of logic originally from installer/installer.pl, which was
655 originally written by Henri-Damien Laurant.
656
657 Koha Development Team <http://koha-community.org/>
658
659 Galen Charlton <galen.charlton@liblime.com>
660
661 =cut
662
663 1;