| \n"; # Header local @tds = $_[1] ? ( "width=5" ) : ( ); if ($s->[0] <= $s->[1]) { local @hcols; foreach $t (@{$_[0]}) { push(@hcols, $t); } print &ui_columns_start(\@hcols, 100, 0, \@tds); } for($i=$s->[0]; $i<=$s->[1]; $i++) { local @cols; push(@cols, "$_[3]->[$i]"); for($j=4; $j<@_; $j++) { push(@cols, $_[$j]->[$i]); } if ($_[1]) { print &ui_checked_columns_row(\@cols, \@tds, "d", $_[1]->[$i]); } else { print &ui_columns_row(\@cols, \@tds); } } if ($s->[0] <= $s->[1]) { print &ui_columns_end(); } print " | \n"; } print "
".&html_escape($out)."" : $out; } return !$rv; } return 1; } # execute_after(db, handle, escape, path, db-for-config) sub execute_after { local $cmd = $config{'backup_after_'.$_[4]}; if ($cmd) { $ENV{'BACKUP_FILE'} = $_[3]; local $h = $_[1]; local $out; local $rv = &execute_command($cmd, undef, \$out, \$out); if ($h && $out) { print $h $_[2] ? "
".&html_escape($out)."" : $out; } return !$rv; } return 1; } # show_table_form(count) sub show_table_form { my $rv; $rv = &ui_columns_start([ $text{'field_name'}, $text{'field_type'}, $text{'field_size'}, $text{'table_nkey'}, $text{'field_auto'}, $text{'field_null'}, $text{'field_unsigned'}, $text{'field_default'} ]); for(my $i=0; $i<$_[0]; $i++) { my @cols; push(@cols, &ui_textbox("field_$i", undef, 20)); push(@cols, &ui_select("type_$i", "", [ "", @type_list ])); push(@cols, &ui_textbox("size_$i", undef, 10)); push(@cols, &ui_checkbox("key_$i", 1, $text{'yes'}, 0)); push(@cols, &ui_checkbox("auto_$i", 1, $text{'yes'}, 0)); push(@cols, &ui_checkbox("null_$i", 1, $text{'yes'}, 1)); push(@cols, &ui_checkbox("unsigned_$i", 1, $text{'yes'}, 0)); push(@cols, &ui_textbox("default_$i", undef, 20)); $rv .= &ui_columns_row(\@cols); } $rv .= &ui_columns_end(); return $rv; } # parse_table_form(&extrafields, tablename) sub parse_table_form { local @fields = @{$_[0]}; local $i; local (@auto, @pri); for($i=0; defined($in{"field_$i"}); $i++) { next if (!$in{"field_$i"}); $in{"field_$i"} =~ /^\S+$/ || &error(&text('table_efield', $in{"field_$i"})); $in{"type_$i"} || &error(&text('table_etype', $in{"field_$i"})); if ($in{"type_$i"} eq 'enum' || $in{"type_$i"} eq 'set') { local @ev = split(/\s+/, $in{"size_$i"}); @ev || &error(&text('table_eenum', $in{"type_$i"}, $in{"field_$i"})); $in{"size_$i"} = join(",", map { "'$_'" } @ev); } if ($in{"size_$i"}) { push(@fields, sprintf "%s %s(%s)", "estr($in{"field_$i"}), $in{"type_$i"},$in{"size_$i"}); } else { push(@fields, sprintf "%s %s", "estr($in{"field_$i"}), $in{"type_$i"}); } if ($in{"unsigned_$i"}) { $fields[@fields-1] .= " unsigned"; } if (!$in{"null_$i"}) { $fields[@fields-1] .= " not null"; } if ($in{"key_$i"}) { $in{"null_$i"} && &error(&text('table_epnull',$in{"field_$i"})); push(@pri, $in{"field_$i"}); } if ($in{"auto_$i"}) { push(@auto, $fields[@fields-1]); push(@autokey, $in{"key_$i"}); } if ($in{"default_$i"}) { $fields[@fields-1] .= " default '".$in{"default_$i"}."'"; } } @auto < 2 || &error($text{'table_eauto'}); @fields || &error($text{'table_enone'}); local @sql; local $sql = "create table "."estr($_[1])." (".join(",", @fields).")"; $sql .= " engine $in{'type'}" if ($in{'type'}); push(@sql, $sql); if (@pri) { # Setup primary fields too push(@sql, "alter table "."estr($_[1])." add primary key (". join(",", map { "estr($_) } @pri).")"); } if (@auto) { # Make field auto-increment push(@sql, "alter table "."estr($_[1]). " modify $auto[0] auto_increment ". ($autokey[0] ? "" : "unique")); } return @sql; } # execute_sql_file(database, file, [user, pass], [unix-user]) # Executes some file of SQL commands, and returns the exit status and output sub execute_sql_file { if (&is_readonly_mode()) { return (0, undef); } my ($db, $file, $user, $pass) = @_; -r $file || return (1, "$file does not exist"); my $authstr = &make_authstr($user, $pass); my $cs = $sql_charset ? "--default-character-set=".quotemeta($sql_charset) : ""; my $temp = &transname(); $file = &fix_collation($file); &open_tempfile(TEMP, ">$temp"); &print_tempfile(TEMP, "source ".$file.";\n"); &close_tempfile(TEMP); &set_ownership_permissions(undef, undef, 0644, $temp); &set_authstr_env(); my $cmd = "$config{'mysql'} $authstr -t ".quotemeta($db)." ".$cs. " <".quotemeta($temp); if ($_[4] && $_[4] ne 'root' && $< == 0) { # Restoring as a Unix user $cmd = &command_as_user($_[4], 0, $cmd); } my $out = &backquote_logged("$cmd 2>&1"); my @rv; if ($?) { # Total failure @rv = ($?, $out || "$cmd failed"); } elsif ($out =~ /(^|\n)(ERROR\s+\d+.*)/) { # Some command in the file failed @rv = (1, $2); } else { # All OK @rv = (0, $out); } &make_authstr(); # Put back old password environment variable return @rv; } # start_mysql() # Starts the MySQL database server, and returns undef on success or an # error message on failure. sub start_mysql { local $temp = &transname(); local $rv = &system_logged("($config{'start_cmd'}) >$temp 2>&1"); local $out = `cat $temp`; unlink($temp); if ($rv || $out =~ /failed/i) { return "
".&html_escape($out).""; } return undef; } # stop_mysql() # Halts the MySQL database server, and returns undef on success or an # error message on failure. sub stop_mysql { local $out; if ($config{'stop_cmd'}) { $out = &backquote_logged("$config{'stop_cmd'} 2>&1"); } else { $out = &backquote_logged("$config{'mysqladmin'} $authstr shutdown 2>&1"); } if ($? || $out =~ /failed/i) { return "
".&html_escape($out).""; } return undef; } # split_enum(type) # Returns a list of allowed values for an enum sub split_enum { local ($type) = @_; if ($type =~ /^(enum|set)\((.*)\)$/) { $type = $2; } local $esize = $type; local @ev; while($esize =~ /^'([^']*)'(,?)(.*)$/) { push(@ev, $1); $esize = $3; } return @ev; } # Returns 1 if the mysql server being managed is on this system sub is_mysql_local { return $config{'host'} eq '' || $config{'host'} eq 'localhost' || $config{'host'} eq &get_system_hostname() || &to_ipaddress($config{'host'}) eq &to_ipaddress(&get_system_hostname()); } # get_mysql_config() # Returns the parsed my.cnf file sub get_mysql_config { if (!scalar(@mysql_config_cache)) { if (!-r $config{'my_cnf'}) { return []; } @mysql_config_cache = &parse_mysql_config($config{'my_cnf'}); } return \@mysql_config_cache; } # parse_mysql_config(file) # Reads one MySQL config file sub parse_mysql_config { local ($file) = @_; local @rv; local $sect; local $lnum = 0; local $lref = &read_file_lines($file, 1); local $_; foreach (@$lref) { s/\r|\n//g; s/\s+$//; if (/^\s*(#|;)/) { $lnum++; next; } elsif (/^\s*\[(\S+)\]$/) { # Start of a section $sect = { 'name' => $1, 'members' => [ ], 'file' => $file, 'line' => $lnum, 'eline' => $lnum }; push(@rv, $sect); } elsif (/^\s*(\S+)\s*=\s*(.*)$/ && $sect) { # Variable in a section push(@{$sect->{'members'}}, { 'name' => $1, 'value' => $2, 'file' => $file, 'line' => $lnum }); $sect->{'eline'} = $lnum; } elsif (/^\s*(\S+)$/ && $sect) { # Single directive in a section push(@{$sect->{'members'}}, { 'name' => $1, 'file' => $file, 'line' => $lnum }); $sect->{'eline'} = $lnum; } elsif (/^\s*\!include\s+(\S+)/) { # Including sections from a file foreach my $file (glob($1)) { push(@rv, &parse_mysql_config($file)); } } elsif (/^\s*\!includedir\s+(\S+)/) { # Including sections from files in a directory my $dir = $1; $dir =~ s/\/$//; opendir(DIR, $dir); my @files = map { $dir."/".$_ } readdir(DIR); closedir(DIR); foreach my $file (@files) { push(@rv, &parse_mysql_config($file)); } } $lnum++; } return @rv; } # find(name, &conf) sub find { local ($name, $conf) = @_; local @rv = grep { lc($_->{'name'}) eq lc($name) } @$conf; return wantarray ? @rv : $rv[0]; } # find_value(name, &conf) sub find_value { local ($name, $conf) = @_; local @rv = map { $_->{'value'} } &find($name, $conf); return wantarray ? @rv : $rv[0]; } # save_directive(&conf, §ion, name, &values) # Updates one or multiple lines in a my.cnf section sub save_directive { local ($conf, $sect, $name, $values) = @_; local @old = &find($name, $sect->{'members'}); local $file = @old ? $old[0]->{'file'} : $sect ? $sect->{'file'} : $config{'my_cnf'}; local $lref = &read_file_lines($file); for(my $i=0; $i<@old || $i<@$values; $i++) { local $old = $i < @old ? $old[$i] : undef; local $line = $i >= @$values || $values->[$i] eq "" ? $name : "$name = $values->[$i]"; if ($old && defined($values->[$i])) { # Updating $lref->[$old->{'line'}] = $line; $old->{'value'} = $values->[$i]; } elsif (!$old && defined($values->[$i])) { # Adding splice(@$lref, $sect->{'eline'}+1, 0, $line); &renumber($conf, $sect->{'eline'}+1, 1, $file); push(@{$sect->{'members'}}, { 'name' => $name, 'value' => $values->[$i], 'line' => $sect->{'eline'}+1 }); } elsif ($old && !defined($values->[$i])) { # Deleting splice(@$lref, $old->{'line'}, 1); &renumber($conf, $old->{'line'}, -1, $file); @{$sect->{'members'}} = grep { $_ ne $old } @{$sect->{'members'}}; } } } sub renumber { local ($conf, $line, $offset, $file) = @_; foreach my $sect (@$conf) { next if ($sect->{'file'} ne $file); $sect->{'line'} += $offset if ($sect->{'line'} >= $line); $sect->{'eline'} += $offset if ($sect->{'eline'} >= $line); foreach my $m (@{$sect->{'members'}}) { $m->{'line'} += $offset if ($m->{'line'} >= $line); } } } # parse_set_variables(value, ...) # Returns a hash of variable mappings sub parse_set_variables { local %vars; foreach my $v (@_) { if ($v =~ /^(\S+)=(\S+)$/) { $vars{$1} = $2; } } return %vars; } sub mysql_size_input { local ($name, $value) = @_; local $units; if ($value =~ /^(\d+)([a-z])$/i) { $value = $1; $units = $2; } $units = "" if ($units eq "b"); return &ui_textbox($name, $value, 8)."\n". &ui_select($name."_units", $units, [ [ "", "bytes" ], [ "K", "kB" ], [ "M", "MB" ], [ "G", "GB" ] ]); } # get_table_index_stats(db) # Retrieves index stats for all tables in the given database sub get_table_index_stats { my ($db) = @_; my @tables = &list_tables($db); my $sql_query = " SELECT TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME, COLLATION, CARDINALITY, SUB_PART, PACKED, NULLABLE, INDEX_TYPE, COMMENT, INDEX_COMMENT FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (" . join(", ", ("?") x @tables) . ") "; my $rs = &execute_sql_safe($db, $sql_query, $db, @tables); return $rs; } # get_all_tables_size(db) # Retrieves the size of all tables in the given database sub get_all_tables_size { my ($db) = @_; my @tables = list_tables($db); my $sql_query = " SELECT TABLE_SCHEMA, TABLE_NAME, ENGINE, DATA_LENGTH + INDEX_LENGTH AS total_size_bytes FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (" . join(", ", ("?") x @tables) . ") "; my $rs = &execute_sql_safe($db, $sql_query, $db, @tables); return $rs; } # list_indexes(db) # Returns the names of all indexes in some database sub list_indexes { local ($db) = @_; local (@rv, $r); foreach my $table (&list_tables($db)) { local $s = &execute_sql_safe($db, "show index from "."estr($table)); local (%tp, $i); for($i=0; $i<@{$s->{'titles'}}; $i++) { $tp{lc($s->{'titles'}->[$i])} = $i; } foreach $r (@{$s->{'data'}}) { if ($r->[$tp{'key_name'}] ne 'PRIMARY') { push(@rv, $r->[$tp{'key_name'}]); } } } return &unique(@rv); } # index_structure(db, indexname) # Returns information on an index sub index_structure { local ($db, $index) = @_; local (@rv, $r); local $info; foreach my $table (&list_tables($db)) { local $s = &execute_sql_safe($db, "show index from "."estr($table)); local (%tp, $i); for($i=0; $i<@{$s->{'titles'}}; $i++) { $tp{lc($s->{'titles'}->[$i])} = $i; } foreach $r (@{$s->{'data'}}) { if ($r->[$tp{'key_name'}] eq $index) { # Found some info $info->{'table'} = $r->[$tp{'table'}]; $info->{'name'} = $index; $info->{'type'} = lc($r->[$tp{'index_type'}]) || lc($r->[$tp{'comment'}]); push(@{$info->{'cols'}}, $r->[$tp{'column_name'}]); } } } return $info; } # parse_index_structure(&db_stats, db, indexname) # Returns information on an index based on the database stats hash sub parse_index_structure { my ($db_stats, $db, $index) = @_; my ($r, $info); foreach my $table (&list_tables($db)) { my $s = { %$db_stats }; $s->{'data'} = [grep { $_->[1] eq $table } @{$s->{'data'}}]; my (%tp, $i); for($i=0; $i<@{$s->{'titles'}}; $i++) { $tp{lc($s->{'titles'}->[$i])} = $i; } foreach $r (@{$s->{'data'}}) { if ($r->[$tp{'index_name'}] eq $index) { # Found some info $info->{'table'} = $r->[$tp{'table_name'}]; $info->{'name'} = $index; $info->{'type'} = lc($r->[$tp{'index_type'}]) || lc($r->[$tp{'comment'}]); push(@{$info->{'cols'}}, $r->[$tp{'column_name'}]); } } } return $info; } # list_views(db) # Returns the names of all views in some database sub list_views { local ($db) = @_; local @rv; local $d = &execute_sql($db, "select table_schema,table_name from information_schema.views where table_schema = ?", $db); foreach $r (@{$d->{'data'}}) { push(@rv, $r->[1]); } return @rv; } # view_structure(db, viewname) # Returns information about a view sub view_structure { local ($db, $view) = @_; local $info = { 'name' => $view }; local $d = &execute_sql($db, "show create view $view"); local $c = $d->{'data'}->[0]->[1]; if ($c =~ /algorithm\s*=\s*(\S+)/i) { $info->{'algorithm'} = lc($1); } if ($c =~ /definer\s*=\s*`(\S+)`\@`(\S+)`/i) { $info->{'definer'} = "$1\@$2"; } elsif ($c =~ /definer\s*=\s*(\S+)/i) { $info->{'definer'} = $1; } if ($c =~ /sql\s+security\s+(\S+)/i) { $info->{'security'} = lc($1); } if ($c =~ s/\s+with\s+(cascaded|local)\s+check\s+option//i) { $info->{'check'} = lc($1); } if ($c =~ /view\s+(`\S+`|\S+)\s+as\s+(.*)/i) { $info->{'query'} = $2; } return $info; } # list_character_sets([db]) # Returns a list of supported character sets. Each row is an array ref of # a code and name sub list_character_sets { local @rv; local $db = $_[0] || $master_db; if (&compare_version_numbers(&get_remote_mysql_version(), "4.1") < 0) { local $d = &execute_sql($db, "show variables like 'character_sets'"); @rv = map { [ $_, $_ ] } split(/\s+/, $d->{'data'}->[0]->[1]); } else { local $d = &execute_sql($db, "show character set"); @rv = map { [ $_->[0], "$_->[1] ($_->[0])" ] } @{$d->{'data'}}; } return sort { lc($a->[1]) cmp lc($b->[1]) } @rv; } # get_character_set(db) # Returns the character set for a database sub get_character_set { my ($db) = @_; my $d; eval { local $main::error_must_die = 1; $d = &execute_sql($db, 'select @@character_set_database'); }; return undef if ($@); return undef if (!@{$d->{'data'}}); return $d->{'data'}->[0]->[0]; } # list_collation_orders([db]) # Returns a list of supported collation orders. Each row is an array ref of # a code and character set it can work with. sub list_collation_orders { local @rv; local $db = $_[0] || $master_db; if (&compare_version_numbers(&get_remote_mysql_version(), "5") >= 0) { local $d = &execute_sql($db, "show collation"); @rv = map { [ $_->[0], $_->[1] ] } @{$d->{'data'}}; } return sort { lc($a->[0]) cmp lc($b->[0]) } @rv; } # get_collation_order(db) # Returns the collation order for a database sub get_collation_order { my ($db) = @_; my $d; eval { local $main::error_must_die = 1; $d = &execute_sql($db, 'select @@collation_database'); }; return undef if ($@); return undef if (!@{$d->{'data'}}); return $d->{'data'}->[0]->[0]; } # fix_collation(file) # Fixes unsupported collations on restore, by replacing # unsuported with the closest supported variant sub fix_collation { my ($file) = @_; my ($version, $variant) = &get_remote_mysql_variant(); if ($variant eq 'mariadb') { my $tfile = &transname(); open(IN, '<' . $file) or die $!; open(OUT, '>' . $tfile) or die $!; while(