# Copyright 1999-2022. Plesk International GmbH. All rights reserved.
package Db::MysqlShellBackend;

use strict;
use Db::ShellBackend;
use AgentConfig;
use Db::MysqlUtils;
use File::Temp qw/tempdir tmpnam/;

use vars qw|@ISA|;

@ISA = qw|Db::ShellBackend|;

#
# 'name', 'user', 'password'[, 'host'][, 'socket'][, 'port'][, 'preload_dirs'][, 'utf8names']
#

sub _init {
  my ($self, %params) = @_;

  $self->SUPER::_init(%params, 'type'=>'mysql');

  if (defined $params{host} and defined $params{socket}) {
    return "socket & host are mutually exclusive";
  }

  $self->{socket} = $params{socket} if defined $params{socket};
  $self->{preload_dirs} = $params{preload_dirs} if defined $params{preload_dirs};
  $self->{utf8names} = $params{utf8names} if defined $params{utf8names};
  $self->{not_compatible_mysql_323} = $params{not_compatible_mysql_323} if defined $params{not_compatible_mysql_323};
  $self->{tables} = join(' ', @{$params{tables}}) if defined $params{tables};
  $self->{replace_and_no_create_info} = 1 if defined $params{replace_and_no_create_info};
  $self->{dump_charset} = $params{dump_charset} if defined $params{dump_charset};
  $self->{extra_dump_options} = '--no-data' if defined $params{dummyContent};

  my @version = Db::MysqlUtils::getVersionArray();
  $self->{mysql_major_version} = @version[0];
  $self->{mysql_minor_version} = @version[1];
  $self->{mysql_build_version} = @version[2];
}

sub description {
  my ($self) = @_;
  return "mysql shell connection. " . $self->SUPER::description();
}

sub _getCmd {
  my ($self, $cmd, $additionalOptions) = @_;

  my $addTables = $cmd eq AgentConfig::mysqldumpBin();

  if ($self->{password}) {
    my $fileName = tmpnam();
    if (open OPTSFILE, ">$fileName") {
      close OPTSFILE;

      chmod 0600, $fileName;

      open OPTSFILE, ">$fileName";

      if ($cmd eq AgentConfig::mysqldumpBin()) {
        print OPTSFILE "[mysqldump]\npassword=";
      } else {
        print OPTSFILE "[mysql]\npassword=";
      }
      if (!$self->_mysql41OrNewer()) {
        print OPTSFILE $self->{password};
      } else {
        print OPTSFILE '"' . $self->{password} . '"';
      }

      close OPTSFILE;
      $cmd .= " --defaults-file=" . $fileName;
      $self->_setFileToRemove($fileName);
    } else {
      $self->_set_errstr("Can't create temporary file");
    }
  }

  $cmd .= " --socket='$self->{socket}'" if $self->{socket};
  $cmd .= " -h '$self->{host}'" if $self->{host};
  $cmd .= " -u '$self->{user}'";
  $cmd .= " -P '$self->{port}'" if $self->{port};
  $cmd .= " $additionalOptions" if $additionalOptions;
  if ($self->{name} =~ /^-.*/) {
    $cmd .= " -- ";
  }
  $cmd .= " '$self->{name}'";
  $cmd .= " --tables $self->{tables} " if $self->{tables} and $addTables;
  $cmd .= " --no-create-info --insert-ignore" if  $self->{replace_and_no_create_info} and $addTables;

  $cmd = "LD_PRELOAD=$self->{preload_dirs} $cmd" if $self->{preload_dirs};

  # ~/.my.cnf is the top priority file for Linux and can interfere with
  # DB dumps, so we cheat here pretending HOME directory is empty
  # and there is no ~/.my.cnf
  my $fakeHomeDir = tempdir();
  $self->_setFileToRemove($fakeHomeDir);
  $cmd = "HOME=\"$fakeHomeDir\" $cmd";

  return $cmd;
}

sub _mysql41OrNewer {
  my ($self) = @_;

  if (defined $self->{mysql_major_version} and defined $self->{mysql_minor_version}) {
    return $self->{mysql_major_version} > 4 || ($self->{mysql_major_version} == 4 and $self->{mysql_minor_version} >= 1);
  }
}

sub _mysql512OrNewer {
  my ($self) = @_;

  return if (!$self->{mysql_major_version} || !$self->{mysql_minor_version});
  return 1 if ($self->{mysql_major_version} > 5);
  if ($self->{mysql_major_version} == 5) {
    return 1 if ($self->{mysql_minor_version} > 1 || ($self->{mysql_minor_version} == 1 and $self->{mysql_build_version} >= 2));
  }
}

sub connect {
  my ($self) = @_;

  if (!AgentConfig::mysqlBin()) {
    $self->_set_errstr("Unable to find 'mysql'");
    return;
  }

  my $version = Db::MysqlUtils::getVersion();

  $self->{cmdline} = $self->_getCmd(AgentConfig::mysqlBin(), "-B" . ($version && $version eq "3.23" ? "" : " --no-beep"));

  if ($self->{utf8names} && Db::MysqlUtils::doesSupportCharacterSets($self)) {
    $self->{sql_prefix} = Db::MysqlUtils::getCharacterSetsSupportSql() . ";";
    $self->{sql_prefix} .= Db::MysqlUtils::getNamesCharacterSetsSupportSql() . ";";
  }

  if (!AgentConfig::mysqldumpBin()) {
    $self->_set_errstr("Unable to find 'mysqldump'");
    return;
  }

  my $dumpoptions = "--quick --quote-names --add-drop-table";
  if( $self->_mysql41OrNewer() ){
#  $dumpoptions .= " --default-character-set=utf8 --set-charset" if $self->_mysql41OrNewer();
#  $dumpoptions .= " --compatible=mysql323 " if !$self->{not_compatible_mysql_323} && $self->_mysql41OrNewer();
    if ($self->{utf8names}) {
      $dumpoptions .= " --default-character-set=utf8 --set-charset";
    }else{
      my $dumpCharset = $self->{dump_charset} || 'utf8';
      $dumpoptions .= " --default-character-set=$dumpCharset --set-charset" if $dumpCharset;
    }

    if ($self->_mysql512OrNewer()) {
      $dumpoptions .= " --routines";
    }
  }

  if ($self->{extra_dump_options}) {
    $dumpoptions .= ' '.$self->{extra_dump_options}; 
  }
  my $dumpcmd = $self->_getCmd(AgentConfig::mysqldumpBin(), $dumpoptions);

  if (!$self->_mysql41OrNewer()) {
    $dumpcmd .= " | grep -v '^---'"; # http://bugs.mysql.com/bug.php?id=1056
  }

  $self->_setDumpCmdline($dumpcmd);

  my $res = $self->execute("SELECT 1");
  if (!$self->rownum()) {
    $res = 0;
  }
  $self->finish();

  return $res;
}

#
# unquotes \n \t \\ in the string
# used in parsing mysql batch output
#

sub _unquote {
  my ($str) = @_;

  my $len = length($str);
  my $res = "";
  for (my $i = 0; $i <= $len; ++$i) {
    my $s = substr($str, $i, 1);

    if ($i != $len and $s eq "\\") {
      my $s = substr($str, ++$i, 1);
      $res .= "\n" if $s eq "n";
      $res .= "\t" if $s eq "t";
      $res .= "\\" if $s eq "\\";
    } else {
      $res .= $s;
    }
  }
  return $res;
}

sub execute {
  my ($self, $sql, $quiet, @params) = @_;

  my $resultSql = $self->_getSql($sql, @params);
  if (!defined($resultSql)) {
    $self->_set_errstr(sprintf("Unable to replace placeholders in SQL query: invalid parameters number (%d)\n%s", scalar(@params), $sql));
    return;
  }
  $sql = $resultSql;

  if (defined $self->{sql_prefix}) {
    $sql = $self->{sql_prefix} . $sql;
  }

  # quote ` from the shell and ' from the string
  $sql =~ s/'/'\\''/g;

  my @out = `echo '$sql' | $self->{cmdline}`;
  chomp @out;

  if ($#out > 0 and $out[0] =~ /^error/i) {
    $self->_set_errstr("Error: unable to execute $sql:\n" . (join "\n", @out));
    return;
  }

  my @res = map {[map {$_ eq 'NULL' ? undef : _unquote($_)} split (/\t/, $_, -1)]} @out;

  $self->_set_execute_result(\@res);
  return 1;
}

1;
