#!/usr/local/bin/perl # exec.cgi # Execute some SQL command and display output require './mysql-lib.pl'; &ReadParseMime(); &can_edit_db($in{'db'}) || &error($text{'dbase_ecannot'}); $access{'edonly'} && &error($text{'dbase_ecannot'}); &error_setup($text{'exec_err'}); $sql_charset = $in{'charset'}; # normalize_sql_for_history(command) # Collapse textarea SQL into a single history line. sub normalize_sql_for_history { my ($cmd) = @_; $cmd =~ s/\r//g; return join(" ", split(/\n+/, $cmd)); } # count_sql_terminators(sql) # Count semicolons in SQL text, skipping those inside single/double/backtick # quotes, line comments (# and --), and block comments (/* ... */). Used to # detect multi-statement input. Miscounts should not happen in practice but are # safe either way: on undercount, multi-statement SQL goes through the # single-statement path and fails visibly; on overcount, a single statement goes # through the file executor which handles it fine. sub count_sql_terminators { my ($sql) = @_; my $count = 0; my ($sq, $dq, $bq, $line_comment, $block_comment, $escaped); for (my $i = 0; $i < length($sql); $i++) { my $c = substr($sql, $i, 1); my $n = $i+1 < length($sql) ? substr($sql, $i+1, 1) : ''; if ($line_comment) { $line_comment = 0 if ($c eq "\n"); next; } if ($block_comment) { if ($c eq '*' && $n eq '/') { $block_comment = 0; $i++; } next; } if ($sq) { if ($c eq "\\" && !$escaped) { $escaped = 1; next; } $sq = 0 if ($c eq "'" && !$escaped); $escaped = 0; next; } if ($dq) { if ($c eq "\\" && !$escaped) { $escaped = 1; next; } $dq = 0 if ($c eq '"' && !$escaped); $escaped = 0; next; } if ($bq) { $bq = 0 if ($c eq '`'); next; } if ($c eq '#' ) { $line_comment = 1; next; } if ($c eq '-' && $n eq '-') { my $p = $i ? substr($sql, $i-1, 1) : ''; my $a = $i+2 < length($sql) ? substr($sql, $i+2, 1) : ''; if (($i == 0 || $p =~ /\s/) && ($a eq '' || $a =~ /\s/)) { $line_comment = 1; $i++; next; } } if ($c eq '/' && $n eq '*') { $block_comment = 1; $i++; next; } if ($c eq "'") { $sq = 1; $escaped = 0; next; } if ($c eq '"') { $dq = 1; $escaped = 0; next; } if ($c eq '`') { $bq = 1; next; } if ($c eq ';') { $count++; } } return $count; } # looks_like_sql_script(command) # Detect pasted SQL that should use the file-style script executor. sub looks_like_sql_script { my ($cmd) = @_; return 1 if ($cmd =~ /^\s*(?:delimiter|source|\\\.)\b/im); return 1 if ($cmd =~ /^\s*(?:--(?:\s|$)|#)/m); return &count_sql_terminators($cmd) > 1; } # execute_textarea_script(database, command) # Run pasted SQL through the same path used for uploaded script files. sub execute_textarea_script { my ($db, $cmd) = @_; my $file = &transname(); &open_tempfile(TEMP, ">$file"); &print_tempfile(TEMP, $cmd); &print_tempfile(TEMP, "\n") if ($cmd !~ /\n\z/); &close_tempfile(TEMP); my @rv = &execute_sql_file($db, $file, undef, undef, $access{'buser'}); &unlink_file($file); return @rv; } # summarize_sql_script(sql) # Count common DDL and DML actions for friendly success messages. sub summarize_sql_script { my ($sql) = @_; my %rv = ( 'create_count' => 0, 'insert_count' => 0 ); foreach my $line (split(/\n/, $sql)) { if ($line =~ /^\s*insert\s+into\s+`(\S+)`/i || $line =~ /^\s*insert\s+into\s+(\S+)/i) { $rv{'insert_count'}++; } if ($line =~ /^\s*create\s+table\s+`(\S+)`/i || $line =~ /^\s*create\s+table\s+(\S+)/i) { $rv{'create_count'}++; } } return \%rv; } # extract_execute_error_text(error) # Strip Webmin's wrapper text down to the database error itself. sub extract_execute_error_text { my ($err) = @_; $err =~ s/\s+at\s+\S+\s+line\s+\d+.*$//s; if ($err =~ /failed\s*:\s*(.*?)<\/tt>\s*$/s) { $err = $1; } elsif ($err =~ /failed\s*:\s*"([^"]+)"\s*$/s) { $err = $1; } elsif ($err =~ /failed\s*:\s*(.*?)\s*$/s) { $err = $1; } $err =~ s/<[^>]+>//g; # Normalize to plain text: undo any HTML escaping from shared helpers # so the caller can re-escape once at final display time. $err = &html_unescape($err); return $err; } # print_exec_status_lines(first, rest...) # Print one or more status lines with predictable wrapper spans. sub print_exec_status_lines { my ($first, @rest) = @_; print &ui_tag('span', $first, { 'data-first-print' => undef }); if (@rest) { print &ui_tag('br'); for (my $i = 0; $i < @rest; $i++) { print &ui_tag('span', $rest[$i], { 'data-second-print' => undef }); print &ui_tag('br') if ($i+1 < @rest); } } } # print_exec_error_block(first, second, detail) # Print a wrapped error block with a preformatted message body. sub print_exec_error_block { my ($first, $second, $detail) = @_; $detail = defined($detail) && $detail =~ /\S/ ? $detail : $text{'exec_noout'}; &print_exec_status_lines($first, $second); print &ui_tag('br'); print &ui_tag('pre', &html_escape($detail), { 'style' => 'white-space: pre-wrap; margin-left: 10px;' }); } if ($in{'clear'}) { # Delete the history file &unlink_file($commands_file.".".$in{'db'}); &redirect("exec_form.cgi?db=$in{'db'}"); } else { # Run some SQL $rawcmd = defined($in{'cmd'}) ? $in{'cmd'} : undef; defined($rawcmd) && $rawcmd =~ /\S/ || &error($text{'exec_ecmd'}); $rawcmd =~ s/\r//g; $cmd = &normalize_sql_for_history($rawcmd); # Multi-statement input uses the same executor as uploaded SQL files. $script = &looks_like_sql_script($rawcmd); if ($script) { ($ex, $out) = &execute_textarea_script($in{'db'}, $rawcmd); $summary = &summarize_sql_script($rawcmd); &ui_print_header(undef, $text{'exec_title'}, ""); if ($ex) { &print_exec_error_block($text{'exec_scriptout'}, $text{'exec_scriptfailed'}, $out); } else { @lines = ( ); if ($summary->{'create_count'}) { push(@lines, &text('exec_scriptcreated', $summary->{'create_count'})); } if ($summary->{'insert_count'}) { push(@lines, &text('exec_scriptinserted', $summary->{'insert_count'})); } if (!@lines) { push(@lines, $text{'exec_scriptok'}); } &print_exec_status_lines($text{'exec_scriptout'}, @lines); print "
\n"; } } else { eval { local $main::error_must_die = 1; # Capture DBI/mysql errors so we can render them consistently. $d = &execute_sql_logged($in{'db'}, $cmd); }; if ($@) { $failed = 1; $err = &extract_execute_error_text($@); &ui_print_header(undef, $text{'exec_title'}, ""); &print_exec_error_block($text{'exec_cmdout'}, $text{'exec_cmdfailed'}, $err); } else { @data = @{$d->{'data'}}; &ui_print_header(undef, $text{'exec_title'}, ""); if (@data) { print &text('exec_out', "".&html_escape($cmd).""),"
\n"; print &ui_columns_start($d->{'titles'}); foreach $r (@data) { print &ui_columns_row([ map { &html_escape($_) } @$r ]); } print &ui_columns_end(); } else { &print_exec_status_lines($text{'exec_cmdout'}, $text{'exec_cmdok'}); print "
\n";
}
}
}
&open_readfile(OLD, "$commands_file.$in{'db'}");
while(