# Copyright 1999-2014. Parallels IP Holdings GmbH. All Rights Reserved.
package Transformer;

use Logging;
use XPath;
use XmlNode;
use Dumper;
use Validator;
use HelpFuncs;
use Error qw(:try);
use Exception::XmlNode;

#
# Begin global variables
#

# Hash of client names and client XmlNode elements, Plesk format
my %clientNodes;

# Initialized in initQuickInfo:
# Hash of client names and shallow client XmlNode elements, Plesk format
my %shallowClientNodes;

# Hash of Plesk client and cPanel accounts
my %clients2cpAccount;

# Hash of domains and its owners (clients)
my %domain2client;

# Hash of domains and its hosting type
my %domain2hosting;

my $initialized;

# Shallow dump is a lightweight dump for building migration list. It contains resellers, customers and subscriptions.
my $shallowMode;

# Way to convert subdomains when dumping.
# Possible values: 'subdomain', 'addon-domain', 'addon-domain-if-mail'
# - 'subdomain' means that each cPanel subdomain will be converted to Plesk subdomain.
# Mailboxes on subdomain will be lost during migration, as Plesk does not allow to have mail on subdomains.
# - 'addon-domain' means that each cPanel subdomain will be converted to Plesk addon domain.
# - 'addon-domain-if-mail' means that each cPanel subdomain which has mailboxes will be converted to Plesk addon domain,
# while cPanel subdomain without mailboxes will be converted to Plesk subdomain.
# Default is 'addon-domain'.
my $convertSubdomainMode;

# Way to convert domain aliases when dumping.
# Possible values: 'alias', 'addon-domain', 'addon-domain-if-mail'
# - 'alias' means that each cPanel alias will be converted to Plesk alias.
# Mailboxes on alias will be lost during migration, as Plesk does not allow to have separate mail on aliases.
# - 'addon-domain' means that each cPanel alias will be converted to Plesk addon domain.
# - 'addon-domain-if-mail' means that each cPanel alias which has mailboxes will be converted to Plesk addon domain,
# while cPanel alias without mailboxes will be converted to Plesk alias.
# Default is 'alias'.
my $convertAliasMode;

# List of aliases to skip when dumping. These are the default aliases created by cPanel in Apache configs,
# but not visible to customer in list of aliases/domains in control panel. All of them are not necessary in Plesk:
# for example, webdisk feature is not migrated; for webmail Plesk creates its own DNS record, and so on.
my @ignoreAliases = (
  'cpanel.', 'mail.', 'autodiscover.', 'webdisk.', 'webmail.'
);

#
# End of global variables
#

#
# Begin constants:
#

# Maximum significant value for the disk space / traffic limit.
#	Values exceeding that would be counted as 'unlimited'
my $max_limit_value = 999999*1024*1024;

my %limit_map = ( 'max_traffic'  => 'max_traffic',
                  'disk_space'   => 'disk_space',
                  'max_subdom'   => 'max_subdom',
                  'max_addon'    => 'max_site',
                  'max_db'       => 'max_db',
                  'max_box'      => 'max_box',
                  'max_ftpusers' => 'max_subftp_users');
#
# End of constants
#


#
# Begin service subs
#
sub initialize {
  # Get all accounts from Dumper
  # Get account' basic information, only need for getting the domain list
  # Should initialize %resellerNodes, %clientNodes and %domainNodes with 'undef'
  #
  unless ( $initialized ) {
    Dumper::initialize();

    # Get accounts
    my @cp_accounts = Dumper::getAllAccounts();
    foreach my $account ( sort @cp_accounts ) {
      my $cpAccountNode = Dumper::makeAccountNode( $account, undef, undef, 'shallow_mode');
      if ( ref($cpAccountNode) =~ /XmlNode/ ) {
        # Transformation now is realized in 'one-to-one' mode. This does not preserve cPanel accounts hierarhy.
        my $accountNode = transformAccountNode($cpAccountNode);
        unless ( defined $accountNode ) {
          Logging::warning("Account '$accountNode' transformation failed");
        }
      }
    }
    $initialized = 1;
  }
}

# Way to dump. See comment for $shallowDump variable for more details.
sub setShallowDumpMode {
  $shallowMode = 1;
}

# Way to convert subdomains when dumping. See comment for $convertSubdomainMode variable for more details.
sub setConvertSubdomainMode {
  my $mode = shift;
  $convertSubdomainMode = $mode;
}

# Way to convert domain aliases when dumping. See comment for $convertAliasMode variable for more details.
sub setConvertAliasMode {
  my $mode = shift;
  $convertAliasMode = $mode;
}

sub getResellers {
  return Dumper::getResellerAccounts();
}

sub getClients {
  my $owner = shift; # reseller that owns the clients returned. Could be 'undef' for default reseller ('root' or 'admin')

  return Dumper::getClientAccounts($owner);
}

sub getDomains {
  my $owner = shift || "root"; # reseller or client that owns the domains returned. Could be 'undef' for default reseller or client ('root' or 'admin')

  # Should return an array of clients identifiers, that could be a number, name or guid. Should be unique through migration dump.

  initialize();
  my @domains;
  foreach my $domain (sort keys %domain2client) {
    if ($domain2client{$domain} eq $owner) {
      push @domains, $domain;
    }
  }
  return @domains;
}

#
# Basic transform routine
#
# Works in 'shallow mode' by default
# Important: dump in 'shallow_mode' is not required to be valid for plesk.xsd.
#
sub transformAccountNode {
  my ($cpAccountNode, $not_shallow_mode) = @_;

  if ( !(ref($cpAccountNode) =~ /XmlNode/) || $cpAccountNode->getName() ne 'account') {
    return;
  }

  my $clientNode = XmlNode->new ('client');
  my $cp_account_name = $cpAccountNode->getAttribute( 'name' );
  my $client_name = $cp_account_name;

  $clientNode->setAttribute( 'name', $client_name );
  $clientNode->setAttribute( 'guid', '' );

  if ( $not_shallow_mode ) {
    $clientNode->setAttribute( 'contact', 'client ' . $client_name );
    my $crDate = $cpAccountNode->getAttribute( 'date' );
    $clientNode->setAttribute( 'cr-date', $crDate ) if ( defined $crDate );
    $clientNode->addChild( XmlNode->new( 'preferences' ));
    my $clientPropertiesNode = XmlNode->new( 'properties' );
    my $clientPasswordNode = getClientPassword($cpAccountNode);
    $clientPropertiesNode->addChild( $clientPasswordNode ) if ( defined $clientPasswordNode );
    $clientPropertiesNode->addChild( getStatus($cpAccountNode) );
    $clientNode->addChild( $clientPropertiesNode );

    addClientLimitsAndPermissions( $clientNode, $cpAccountNode );
  }
  my $clientIpPoolNode = XmlNode->new( 'ip_pool' );
  $clientIpPoolNode->addChild( getIp($cpAccountNode) );
  $clientNode->addChild( $clientIpPoolNode );

  my $cpDomainNode = $cpAccountNode->getChild( 'domain' );
  if ( defined ( $cpDomainNode ) ) {

    # 1) Transform cPanel account's domain to Plesk domain
    my $domainNode = XmlNode->new ('domain');
    $domainNode->setAttribute( 'www', 'true' );

    my $domainName = $cpDomainNode->getAttribute( 'name' );
    $domainNode->setAttribute( 'name', $domainName );

    # Plesk dump format requires guid, could be empty however, will be fixed with guid fixer before restore
    $domainNode->setAttribute( 'guid', '' );

    my $crDate = $cpAccountNode->getAttribute( 'date' );
    $domainNode->setAttribute( 'cr-date', $crDate ) if ( defined $crDate );

    if ( $not_shallow_mode ) {
      $domainNode->addChild(XmlNode->new('preferences'));
    }

    my $domainPropertiesNode = getDomainProperties($cpAccountNode);
    $domainNode->addChild( $domainPropertiesNode );

    my @siteNodes = ();
    my @aliasNodes = ();
    my $extraAddonDomains = 0;

    if ( $not_shallow_mode ) {
      my @cpAddonDomainNodes = $cpDomainNode->getChildren('addondomain');
      foreach my $cpAddonDomainNode (@cpAddonDomainNodes) {
        my $aliasName = $cpAddonDomainNode->getAttribute('name');
        next if startsWithAny($aliasName, \@ignoreAliases);

        if ($convertAliasMode eq 'alias') {
          push @aliasNodes, convertAliasToAlias($cpAddonDomainNode);
        } elsif ($convertAliasMode eq 'addon-domain') {
          push @siteNodes, convertAliasToAddonDomain($cpAccountNode, $cpDomainNode, $cpAddonDomainNode);
          $extraAddonDomains++;
        } else {
          if (hasDomainMail($cpAddonDomainNode)) {
            push @siteNodes, convertAliasToAddonDomain($cpAccountNode, $cpDomainNode, $cpAddonDomainNode);
            $extraAddonDomains++;
          } else {
            push @aliasNodes, convertAliasToAlias($cpAddonDomainNode);
          }
        }
      }

      my @cpSubdomainNodes = $cpDomainNode->getChildren( 'subdomain' );
      if (@cpSubdomainNodes) {
        foreach my $cpSubdomainNode (sort @cpSubdomainNodes) {
          my ($subdomainSites, $subdomainExtraAddonDomainsCount) = convertSubdomainToSites(
              $cpAccountNode, $cpSubdomainNode, $domainName
          );
          push @siteNodes, @{$subdomainSites};
          $extraAddonDomains += $subdomainExtraAddonDomainsCount;
        }
      }
    }

    my $domainPhostingNode;
    if ( $not_shallow_mode ) {

      # Call of getPhosting should be before addDomainLimitsAndPermissions because some modifications with limits are inside
      $domainPhostingNode = getPhosting( $domainNode, $cpAccountNode);
      addDomainLimitsAndPermissions( $domainNode, $cpAccountNode, $extraAddonDomains );
      addDomainMailsystem( $domainNode, $cpAccountNode );
      addDomainDatabases( $domainNode, $cpDomainNode );
      addDomainMaillists( $domainNode, $cpDomainNode );
      addDomainDnssystem( $domainNode, $cpDomainNode );
      addDomainuser( $domainNode, $cpAccountNode);
      $domainNode->addChild( $domainPhostingNode );
    }

    unless( $not_shallow_mode ) {
      $domain2hosting{$domainName} = 'phosting';
    }

    my $domainsNode = XmlNode->new( 'domains' );
    $domainsNode->addChild( $domainNode );
    $clientNode->addChild( $domainsNode );
    $domain2client{$domainName} = $client_name;

    if ( $not_shallow_mode ) {
      if (@siteNodes) {
        $domainPhostingNode->addChild(
            XmlNode->new( 'sites', 'children' => \@siteNodes )
        );
      }

      if ( @aliasNodes ) {
        for my $aliasNode (@aliasNodes) {
          $domainNode->getChild('preferences')->addChild($aliasNode);
        }
      }
    }
  }

  if ( $not_shallow_mode ) {
    $clientNodes{$client_name} = $clientNode;
  }
  else {
    $clients2cpAccount{$client_name} = $cp_account_name;
    $shallowClientNodes{$client_name} = $clientNode;
  }

  return $clientNode;
}

sub convertSubdomainToSites {
  my ($cpAccountNode, $cpSubdomainNode, $domainName) = @_;

  my @cpAddonDomainNodes = $cpSubdomainNode->getChildren( 'addondomain' );
  my $extraAddonDomains = 0;

  my @siteNodes;
  my @aliasNodes;

  # dump cPanel's domain aliases
  foreach my $cpAddonDomainNode (@cpAddonDomainNodes) {
    next if $cpAddonDomainNode->getAttribute('type') eq 'addon';
    my $aliasName = $cpAddonDomainNode->getAttribute('name');
    next if startsWithAny($aliasName, \@ignoreAliases);

    if ($convertAliasMode eq 'alias') {
      push @aliasNodes, convertAliasToAlias($cpAddonDomainNode);
    } elsif ($convertAliasMode eq 'addon-domain') {
      push @siteNodes, convertAliasToAddonDomain($cpAccountNode, $cpSubdomainNode, $cpAddonDomainNode);
      $extraAddonDomains++;
    } else {
      if (hasDomainMail($cpAddonDomainNode)) {
        push @siteNodes, convertAliasToAddonDomain($cpAccountNode, $cpSubdomainNode, $cpAddonDomainNode);
        $extraAddonDomains++;
      } else {
        push @aliasNodes, convertAliasToAlias($cpAddonDomainNode);
      }
    }
  }

  # dump cPanel's addon domains
  foreach my $cpAddonDomainNode (@cpAddonDomainNodes) {
    next if $cpAddonDomainNode->getAttribute('type') ne 'addon';

    push @siteNodes, convertAddonDomain($cpAccountNode, $cpSubdomainNode, $cpAddonDomainNode);
  }

  # dump cPanel's subdomain
  if ($convertSubdomainMode eq 'subdomain') {
    push @siteNodes, convertSubdomainToSubdomain($cpAccountNode, $cpSubdomainNode, $domainName);
  } elsif ($convertSubdomainMode eq 'addon-domain') {
    push @siteNodes, convertSubdomainToAddon($cpAccountNode, $cpSubdomainNode, $domainName);
    $extraAddonDomains++;
  } else { # default - convert to addon domain only if it has mailboxes
    if (hasDomainMail($cpSubdomainNode)) {
      push @siteNodes, convertSubdomainToAddon($cpAccountNode, $cpSubdomainNode, $domainName);
      $extraAddonDomains++;
    } else {
      push @siteNodes, convertSubdomainToSubdomain($cpAccountNode, $cpSubdomainNode, $domainName);
    }
  }

  # attach domain aliases to the first site inside of cPanel's subdomain
  if (@siteNodes && @aliasNodes) {
    my $firstSiteNode = $siteNodes[0];
    for my $aliasNode (@aliasNodes) {
      $firstSiteNode->getChild('preferences')->addChild($aliasNode);
    }
  }

  return (\@siteNodes, $extraAddonDomains);
}

sub convertAliasToAlias {
  my ($cpAddonDomainNode) = @_;

  my $domainaliasNode = XmlNode->new(
    'domain-alias',
    'attributes' => {
      'name' => $cpAddonDomainNode->getAttribute('name'),
      'mail' => 'true',
      'web'  => 'true',
      'dns'  => 'false' # true - use DNS zone like in parent domain, false - own DNS zone
    },
    'children' => [ makeStatusEnabled() ]
  );

  addDomainaliasDnssystem( $domainaliasNode, $cpAddonDomainNode );

  return $domainaliasNode;
}

sub convertAliasToAddonDomain {
  my ($cpAccountNode, $cpDomainNode, $cpAddonDomainNode) = @_;

  my $cpAliasMailNode = $cpAddonDomainNode->getChild('mail');
  my $cpAliasDnsNode = $cpAddonDomainNode->getChild('dns-zone');
  my $aliasName = $cpAddonDomainNode->getAttribute('name');

  return getSiteNode($cpAccountNode, $cpDomainNode, $cpAliasMailNode, $cpAliasDnsNode, $aliasName);
}

sub convertAddonDomain {
  my ($cpAccountNode, $cpSubdomainNode, $cpAddonDomainNode) = @_;

  my $cpAddonDomainMailNode = $cpAddonDomainNode->getChild('mail');
  my $cpAddonDomainDnsNode = $cpAddonDomainNode->getChild('dns-zone');
  my $addonDomainName = $cpAddonDomainNode->getAttribute('name');

  return getSiteNode($cpAccountNode, $cpSubdomainNode, $cpAddonDomainMailNode, $cpAddonDomainDnsNode, $addonDomainName);
}

sub convertSubdomainToSubdomain {
  my ($cpAccountNode, $cpSubdomainNode, $parentDomainName) = @_;

  my $cpSubdomainMailNode = $cpSubdomainNode->getChild('mail');
  my $cpSubdomainDnsNode = $cpSubdomainNode->getChild('dns-zone');
  my $subdomainFullName = $cpSubdomainNode->getAttribute('name') . "." . $parentDomainName;

  return getSiteNode($cpAccountNode, $cpSubdomainNode, $cpSubdomainMailNode, $cpSubdomainDnsNode, $subdomainFullName, $parentDomainName);
}

sub convertSubdomainToAddon {
  my ($cpAccountNode, $cpSubdomainNode, $parentDomainName) = @_;

  my $cpSubdomainMailNode = $cpSubdomainNode->getChild('mail');
  my $cpSubdomainDnsNode = $cpSubdomainNode->getChild('dns-zone');
  my $subdomainFullName = $cpSubdomainNode->getAttribute('name') . "." . $parentDomainName;

  return getSiteNode($cpAccountNode, $cpSubdomainNode, $cpSubdomainMailNode, $cpSubdomainDnsNode, $subdomainFullName);
}

sub hasDomainMail {
  my ($cpDomainNode) = @_;

  my $cpMailNode = $cpDomainNode->getChild('mail');
  return $cpMailNode->getChildren('mailname') > 0;
}

sub getSiteNode {
  my ($cpAccountNode, $cpDomainNode, $cpMailNode, $cpDnsNode, $siteName, $parentDomainName) = @_;

  my $siteNode = XmlNode->new(
    'site',
    'attributes' => {
      'www' => 'true',
      'name' => $siteName,
      # Plesk dump format requires guid, could be empty however,
      # will be fixed with guid fixer before restore
      'guid' => ''
    },
    'children' => [
      XmlNode->new('preferences'),
      getDomainProperties($cpAccountNode, 'skipIpNode' )
    ]
  );

  if ($parentDomainName) {
    $siteNode->setAttribute('parent-domain-name', $parentDomainName);
  }

  unless ($parentDomainName) {
    addSiteMailsystem( $siteNode, $cpMailNode, $cpAccountNode );
    addSiteDnssystem( $siteNode, $cpDnsNode, $cpAccountNode );
  }

  my $phostingNode = makePhostingAttrs();
  my $wwwRoot = getWwwRootAttribute($cpDomainNode->getAttribute('www_root'), $cpAccountNode->getAttribute('home'));
  my $cgiBinMode = getCgiBinModeAttribute(
      $cpDomainNode->getAttribute('cgi_root'), $cpDomainNode->getAttribute('www_root'),
      $cpAccountNode->getAttribute('home')
  );
  $phostingNode->setAttribute( 'www-root' ,  $wwwRoot);
  $phostingNode->setAttribute( 'cgi_bin_mode', $cgiBinMode);
  my $preferencesNode = getPhostingPreferences(
      $cpDomainNode, $cpAccountNode, (my $allowSysUser = 0), (my $allowAnonFtp = 0)
  );
  $phostingNode->addChild($preferencesNode);
  addPhostingLimitsAndPermissions($phostingNode);
  $siteNode->addChild($phostingNode);

  return $siteNode;
}

sub getClientNode4Domain {
  my $id = shift; # domain identifier from 'getDomains' result

  initialize();

  if ( exists ( $domain2client{$id} ) ){
    my $client_name = $domain2client{$id};

    return $clientNodes{$client_name} if exists ( $clientNodes{$client_name} );

    my $cpAccountNode = Dumper::makeAccountNode( $client_name, undef, undef, $shallowMode);
    if ( Logging::getVerbosity() > 3 ) {
      my $cpAccountNodeDump = $cpAccountNode->serialize();
      Logging::debug("-" x 16 . "  begin cPanel account dump " . "-" x 16);
      Logging::debug($cpAccountNodeDump);
      Logging::debug("-" x 16 . " end of cPanel account dump " . "-" x 16);
    }

    if ( ref($cpAccountNode) =~ /XmlNode/ ) {
      my $error = undef;
      my $clientNode = undef;
      unless (defined $shallowMode) {
        try {
          Validator::validateCpAccountNode($cpAccountNode);
        }
        catch Exception::XmlNode with {
          my $ex = shift;
          Logging::warning($ex->print());
          Logging::warning("Domain '" . $id . "'will not be migrated due to errors above");
          $error = 1;
        };
      }

      unless ( $error ) {
        $clientNode = transformAccountNode($cpAccountNode, not defined $shallowMode);
        if ( Logging::getVerbosity() > 3 ) {
          my $clientNodeDump = $clientNode->serialize();
          Logging::debug("-" x 17 . " begin Plesk client dump  " . "-" x 17);
          Logging::debug($clientNodeDump);
          Logging::debug("-" x 17 . " end of Plesk client dump " . "-" x 17);
        }
      }

      if ( defined ( $clientNode ) ) {
        $clientNodes{$client_name} = $clientNode;
        return $clientNode;
      }
    }
    return;
  }
}
#
# End of service subs
#


#
# Begin node transformation subs
#
sub getClientPassword {
  my $cpAccountNode = shift;

  unless ( testXmlNodeParam($cpAccountNode, 'account') ) {return;}

  # do nothing
  return;
}

sub addClientLimitsAndPermissions {
  my ($clientNode, $cpAccountNode) = @_;

  unless ( testXmlNodeParam($clientNode,    'client') ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account') ) {return;}

  # do nothing
}

sub getStatus {
  my ($cpAccountNode) = @_;

  my $cpSuspendedNode = $cpAccountNode->getChild( 'suspended' );
  if ( defined $cpSuspendedNode) {
    return makeStatusDisabledBy('status', 'admin');
  }
  return makeStatusEnabled('status');
}

sub getIp {
  my $cpAccountNode = shift;

  unless ( testXmlNodeParam($cpAccountNode, 'account' ) ) {return;}

  my $cpIpNode = $cpAccountNode->getChild( 'ip' );
  my $ipNode;
  if ( defined $cpIpNode) {
    $ipNode = XmlNode->new('ip');

    my $cpDomainNode = $cpAccountNode->getChild( 'domain' );
    my $allow_anon_ftp = 'false';
    if (defined $cpDomainNode) {
      $allow_anon_ftp = $cpDomainNode->getChildAttribute( 'anonftp', 'pub' );
    }

    # Plesk requires exclusive IP on domain to allow anon tfp access
    my $ipTypeNode = XmlNode->new( 'ip-type' );
    $ipTypeNode->setText( $allow_anon_ftp eq 'true'? 'exclusive' : 'shared' );
    $ipNode->addChild( $ipTypeNode );

    my $ipAddressNode = XmlNode->new( 'ip-address' );
    $ipAddressNode->setText( $cpIpNode->getText() );
    $ipNode->addChild( $ipAddressNode );
  }
  return $ipNode;
}

sub getDomainProperties {
  my ($cpAccountNode, $skipIpNode) = @_;

  unless ( testXmlNodeParam($cpAccountNode, 'account' ) ) {return;}

  my $domainPropertiesNode = XmlNode->new( 'properties' );

  $domainPropertiesNode->addChild( getIp($cpAccountNode) ) unless $skipIpNode;
  if ( $skipIpNode ) {
    $domainPropertiesNode->addChild( getStatus($cpAccountNode) );
  } else {
    my $cpSuspendedNode = $cpAccountNode->getChild( 'suspended' );
    if ( defined $cpSuspendedNode) {
      $domainPropertiesNode->addChild( makeStatusDisabledBy('webspace-status', 'admin') );
      $domainPropertiesNode->addChild( makeStatusDisabledBy('status', 'parent') );
    } else {
      $domainPropertiesNode->addChild( makeStatusEnabled('webspace-status') );
	  $domainPropertiesNode->addChild( makeStatusEnabled('status') );
    }
  }

  return $domainPropertiesNode;
}

sub addDomainLimitsAndPermissions {
  my ($domainNode, $cpAccountNode, $extraAddonDomains) = @_;

  unless ( testXmlNodeParam($domainNode,    'domain'  ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account' ) ) {return;}

  my $cpDomainNode = $cpAccountNode->getChild('domain');

  my $domainMboxQuota = 0;
  Logging::debug("Calculating domain quota.");
  my $quotaSub = sub {
    my $node = shift;
	my $quotaAttribute = $node->getAttribute('quota');
	my $quotaValue = defined $quotaAttribute ? $quotaAttribute : -1;
    if ($quotaValue > 0) {
      $domainMboxQuota += $quotaValue;
    }
  };

  my @nodes = qw|mail/mailname addondomain/mail/mailname
    subdomain/mail/mailname subdomain/addondomain/mail/mailname|;
  for my $xpath (@nodes) {
    XPath::Select $cpDomainNode, $xpath, sub { $quotaSub->( shift ); };
  }

  my @limitNodes;
  my @cpLimitNodes = $cpAccountNode->getChildren( 'limit' );

  if ( @cpLimitNodes ) {
    foreach my $cpLimitNode ( @cpLimitNodes ) {
      my $cpLimitName = $cpLimitNode->getAttribute( 'name' );
      if ( exists $limit_map{$cpLimitName} ) {
        my $limitValue = int( $cpLimitNode->getText() || 0 );
        if ( int($limitValue) > $max_limit_value ) {
          $limitValue = -1;
        }
        my $limitName = $limit_map{$cpLimitName};
        if ( ( $limitName eq 'max_site' ) && ( $limitValue != -1 ) ){
          $limitValue += (1 + $extraAddonDomains);
        }
        my $limitNode = XmlNode->new( 'limit' );
        $limitNode->setAttribute( 'name', $limitName );
        $limitNode->setText( $limitValue );
        push @limitNodes, $limitNode;
      }
    }
  }

  my $mboxLimitNode = XmlNode->new( 'limit' );
  $mboxLimitNode->setAttribute( 'name', 'mbox_quota' );
  $mboxLimitNode->setText( $domainMboxQuota == 0 ? -1 : $domainMboxQuota );
  push @limitNodes, $mboxLimitNode;

  my $limitsAndPermissionsNode = XmlNode->new( 'limits-and-permissions' );
  foreach my $limitNode ( @limitNodes ) {
    $limitsAndPermissionsNode->addChild( $limitNode );
  }
  $domainNode->addChild( $limitsAndPermissionsNode );
  return $limitsAndPermissionsNode;
}

sub addDomainMailsystem {
  my ($domainNode, $cpAccountNode) = @_;

  unless ( testXmlNodeParam($domainNode,    'domain'  ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account' ) ) {return;}

  my $cpDomainNode = $cpAccountNode->getChild( 'domain' );
  my $cpMailNode = $cpDomainNode->getChild( 'mail' );

  unless ( defined $cpMailNode ) { return; }

  my $mailsystemNode = XmlNode->new( 'mailsystem', 'children' => [
    getMailsystemProperties()
  ]);

  addMailsystemMailusers( $mailsystemNode, $cpMailNode, $cpAccountNode);
  addMailsystemPreferences( $mailsystemNode, $cpMailNode );

  $domainNode->addChild( $mailsystemNode );
  return $mailsystemNode;
}

sub addSiteMailsystem {
  my ($domainNode, $cpMailNode, $cpAccountNode) = @_;

  unless ( testXmlNodeParam($domainNode,      'site'      ) ) {return;}
  unless ( testXmlNodeParam($cpMailNode,      'mail' ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode,   'account'   ) ) {return;}

  unless ( defined $cpMailNode ) { return; }

  my $mailsystemNode = XmlNode->new( 'mailsystem', 'children' => [
    getMailsystemProperties()
  ]);

  addMailsystemMailusers( $mailsystemNode, $cpMailNode, $cpAccountNode);
  addMailsystemPreferences( $mailsystemNode, $cpMailNode );

  $domainNode->addChild( $mailsystemNode );
}

sub getMailsystemProperties {
  my $propertiesNode = XmlNode->new( 'properties' );
  $propertiesNode->addChild( makeStatusEnabled() );
  return $propertiesNode;
}

sub addMailsystemMailusers {
  my ($mailsystemNode, $cpMailNode, $cpAccountNode) = @_;

  unless ( testXmlNodeParam($mailsystemNode, 'mailsystem'          ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode,  'account'             ) ) {return;}

  unless ( ref($cpMailNode) =~ /XmlNode/ ) {return;}

  my $mailusersNode = XmlNode->new( 'mailusers' );

  my @cpMailnameNodes = $cpMailNode->getChildren( 'mailname' );
  foreach my $cpMailnameNode ( @cpMailnameNodes ) {
    my $mailuserNode = XmlNode->new( 'mailuser', 'attributes' => {
      'name' => $cpMailnameNode->getAttribute( 'name' ),
      # Plesk dump format requires guid, could be empty however, will be fixed with guid fixer before restore
      'guid', ''
    });

    my $mailboxQuota = $cpMailnameNode->getAttribute( 'quota' );
    $mailuserNode->setAttribute( 'mailbox-quota', $mailboxQuota ) if ( defined ( $mailboxQuota ) );

    my $mailuserPropertiesNode = XmlNode->new( 'properties' );
    my $mailuserPasswordNode = XmlNode->new( 'password' );
    $mailuserPasswordNode->setAttribute( 'type', 'encrypted' );
    $mailuserPasswordNode->setText( $cpMailnameNode->getAttribute( 'password' ) );
    $mailuserPropertiesNode->addChild( $mailuserPasswordNode );
    $mailuserNode->addChild( $mailuserPropertiesNode );

    my $preferencesNode = getMailuserPreferences($cpMailnameNode, $cpMailNode, $cpAccountNode);
    $mailuserNode->addChild( $preferencesNode );

    if ( $preferencesNode->getChildren( 'forwarding' ) ) {
      $mailuserNode->setAttribute( 'forwarding-enabled', 'true' );
    }
    else {
      $mailuserNode->setAttribute( 'forwarding-enabled', 'false' );
    }

    $mailusersNode->addChild( $mailuserNode );
  }

  addMailusersFromForwards($mailusersNode, $cpMailNode, $cpAccountNode);

  addMailusersFromAutoresponders($mailusersNode, $cpMailNode, $cpAccountNode);

  if ( $mailusersNode->getChildren( 'mailuser' ) ) {
    $mailsystemNode->addChild( $mailusersNode );
    return $mailusersNode;
  }
  return;
}

sub getMailuserPreferences {
  my ($cpMailnameNode, $cpMailNode, $cpAccountNode) = @_;

  unless ( testXmlNodeParam($cpMailnameNode, 'mailname') ) {return;}
  unless ( testXmlNodeParam($cpMailNode,     'mail'    ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode,  'account' ) ) {return;}

  my $mailname = $cpMailnameNode->getAttribute( 'name' );

  my $mailuserPreferences = XmlNode->new( 'preferences' );

  my $mailboxNode = XmlNode->new( 'mailbox' );
  $mailboxNode->setAttribute( 'enabled' , 'true' );
  $mailboxNode->setAttribute( 'type' ,    'mdir' );

  $mailuserPreferences->addChild( $mailboxNode );

  my @cpForwardNodes = $cpMailNode->getChildren( 'forward' );
  foreach my $cpForwardNode ( @cpForwardNodes ) {
    next unless ( $cpForwardNode->getAttribute( 'mailname' ) eq $mailname );

    $mailuserPreferences->addChild( transformForwardNode($cpForwardNode) );
    last;
  }

  my @cpAutoresponderNodes = $cpMailNode->getChildren( 'autoresponder' );
  foreach my $cpAutoresponderNode ( @cpAutoresponderNodes ) {
    next unless ( $cpAutoresponderNode->getAttribute( 'mailname' ) eq $mailname );

    my $autoresponderNode = transformAutoresponderNode($cpAutoresponderNode);

    my $autorespondersNode = XmlNode->new( 'autoresponders' );
    $autorespondersNode->addChild( $autoresponderNode );

    $mailuserPreferences->addChild( $autorespondersNode );
    last;
  }

  my $cpSpamassassinNode = $cpMailnameNode->getChild( 'spamassassin' );
  $cpSpamassassinNode = $cpAccountNode->getChild( 'spamassassin' ) unless ( defined ( $cpSpamassassinNode ) );
  my $spamassassinNode = undef;

  if ( defined $cpSpamassassinNode ) {
    $mailuserPreferences->addChild( transformSpamassassinNode( $cpSpamassassinNode) );
  }

  return $mailuserPreferences;
}

sub addMailsystemPreferences {
  my ($mailsystemNode, $cpMailNode) = @_;

  unless ( testXmlNodeParam($mailsystemNode, 'mailsystem') ) {return;}
  unless ( testXmlNodeParam($cpMailNode,     'mail'      ) ) {return;}

  my $preferencesNode = XmlNode->new( 'preferences' );

  my $cpMailDefaultNode = $cpMailNode->getChild( 'default' );
  if ( defined ( $cpMailDefaultNode ) ) {

    my $cpTargetAttr = $cpMailDefaultNode->getAttribute( 'target' );
    my $target = '';
    if ( $cpTargetAttr eq 'fail' ) {
      $target .= 'bounce:';
    }
    if ($cpTargetAttr eq 'ignore') {
      $target .= 'reject';
    }
    else {
      $target .= $cpMailDefaultNode->getText();
    }

    my $catchAllNode = XmlNode->new( 'catch-all' );
    $catchAllNode->setText( $target );
    $preferencesNode->addChild( $catchAllNode );
  }

  my $webmailNode = XmlNode->new( 'web-mail' );
  $webmailNode->setText( 'horde' );
  $preferencesNode->addChild( $webmailNode );

  $mailsystemNode->addChild( $preferencesNode );
  return $preferencesNode;
}

sub transformSpamassassinNode {
  my $cpSpamassassinNode = shift;

  unless ( testXmlNodeParam($cpSpamassassinNode, 'spamassassin') ) {return;}

  # cPanel spamassassin element format is compatible with Plesk's one
  my $spamassassinNode = $cpSpamassassinNode->copy();

  return $spamassassinNode;
}

sub transformForwardNode {
  my $cpForwardNode = shift;

  unless ( testXmlNodeParam($cpForwardNode, 'forward') ) {return;}

  my $redirectNode = XmlNode->new( 'forwarding' );
  $redirectNode->setText( $cpForwardNode->getAttribute( 'redirect' ) );
  return $redirectNode;
}

sub transformAutoresponderNode {
  my $cpAutoresponderNode = shift;

  unless ( testXmlNodeParam($cpAutoresponderNode, 'autoresponder') ) {return;}

  my $autoresponderNode = XmlNode->new( 'autoresponder' );
  $autoresponderNode->setAttribute( 'status' , 'on' );
  my $from = $cpAutoresponderNode->getAttribute( 'from' );
  $autoresponderNode->setAttribute( 'replyto', $from ) if defined $from;

  my $subject = $cpAutoresponderNode->getAttribute( 'subject' );
  $autoresponderNode->setAttribute( 'subject', $subject ) if defined $subject;

  my $type = $cpAutoresponderNode->getAttribute( 'type' );
  if ( $type eq 'html' ) {
    $autoresponderNode->setAttribute( 'content-type', 'text/html' );
  }

  my $autoresponderTextNode = XmlNode->new( 'text' );
  my $charset = $cpAutoresponderNode->getAttribute( 'charset' );
  $autoresponderTextNode->setAttribute( 'charset', $charset ) if defined $charset;
  $autoresponderTextNode->setText( $cpAutoresponderNode->getText() );

  $autoresponderNode->addChild( $autoresponderTextNode );

  return $autoresponderNode;
}

sub addMailusersFromAutoresponders {
  my ($mailusersNode, $cpMailNode, $cpAccountNode) = @_;
  unless ( testXmlNodeParam($mailusersNode, 'mailusers') ) {return;}
  unless ( testXmlNodeParam($cpMailNode,    'mail'     ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account'  ) ) {return;}

  my $cpAccountName = $cpAccountNode->getAttribute( 'name' );
  my $cpDomainName = $cpAccountNode->getChildAttribute( 'domain', 'name' );

  my %mailuserNames = ();
  my @mailuserNodes = $mailusersNode->getChildren( 'mailuser' );
  foreach my $mailuserNode ( @mailuserNodes ) {
    $mailuserNames{$mailuserNode->getAttribute( 'name' )} = $mailuserNode;
  }

  my %mailnameNames = ();
  my @cpMailnameNodes = $cpMailNode->getChildren( 'mailname' );
  foreach my $cpMailnameNode ( @cpMailnameNodes ) {
    $mailnameNames{$cpMailnameNode->getAttribute( 'name' )} = 1;
  }

  my @cpAutoresponderNodes = $cpMailNode->getChildren( 'autoresponder' );
  foreach my $cpAutoresponderNode ( @cpAutoresponderNodes ) {
    my $mailname = $cpAutoresponderNode->getAttribute('mailname');
    # skip mailnames processed in 'getMailuserPreferences'
    next if ( exists ($mailnameNames{$mailname}) );
    my $mailuserNode;
    my $newnode;

    if ( exists ( $mailuserNames{$mailname} ) ) {
      $mailuserNode = $mailuserNames{$mailname};
    }
    else {
      $mailuserNode = XmlNode->new( 'mailuser' );
      $newnode = 1;
    }

    if ( $newnode ) {
      $mailuserNode->setAttribute( 'name', $mailname );
      $mailuserNode->setAttribute( 'forwarding-enabled', 'false' );
      # Plesk dump format requires guid, could be empty however, will be fixed with guid fixer before restore
      $mailuserNode->setAttribute( 'guid', '' );
      $mailuserNode->addChild( XmlNode->new( 'properties' ) );
    }

    my $preferencesNode = $mailuserNode->getChild( 'preferences', 1 );
    my $autorespondersNode = XmlNode->new( 'autoresponders' );
    $autorespondersNode->addChild( transformAutoresponderNode( $cpAutoresponderNode ) );
    $preferencesNode->addChild( $autorespondersNode );

    $mailusersNode->addChild( $mailuserNode ) if $newnode;
  }
  return;
}

sub addMailusersFromForwards {
  my ($mailusersNode, $cpMailNode, $cpAccountNode) = @_;
  unless ( testXmlNodeParam($mailusersNode, 'mailusers') ) {return;}
  unless ( testXmlNodeParam($cpMailNode,    'mail'     ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account'  ) ) {return;}

  my $cpAccountName = $cpAccountNode->getAttribute( 'name' );
  my $cpDomainName = $cpAccountNode->getChildAttribute( 'domain', 'name' );

  my %mailuserNames = ();
  my @mailuserNodes = $mailusersNode->getChildren( 'mailuser' );
  foreach my $mailuserNode ( @mailuserNodes ) {
    $mailuserNames{$mailuserNode->getAttribute( 'name' )} = $mailuserNode;
  }

  my %mailnameNames = ();
  my @cpMailnameNodes = $cpMailNode->getChildren( 'mailname' );
  foreach my $cpMailnameNode ( @cpMailnameNodes ) {
    $mailnameNames{$cpMailnameNode->getAttribute( 'name' )} = 1;
  }

  my @cpForwardNodes = $cpMailNode->getChildren( 'forward' );
  foreach my $cpForwardNode ( @cpForwardNodes ) {
    my $mailname = $cpForwardNode->getAttribute('mailname');
    # skip mailnames processed in 'getMailuserPreferences'
    next if ( exists ($mailnameNames{$mailname}) );

    my $mailuserNode;
    my $newnode;
    if ( exists ( $mailuserNames{$mailname} ) ) {
      $mailuserNode = $mailuserNames{$mailname};
    }
    else {
      $mailuserNode = XmlNode->new( 'mailuser' );
      $newnode = 1;
    }

    if ( $newnode ) {
      $mailuserNode->setAttribute( 'name', $mailname );
      # Plesk dump format requires guid, could be empty however, will be fixed with guid fixer before restore
      $mailuserNode->setAttribute( 'guid', '' );
      $mailuserNode->addChild( XmlNode->new( 'properties' ) );
    }

    $mailuserNode->setAttribute( 'forwarding-enabled', 'true' );
    my $preferencesNode = $mailuserNode->getChild( 'preferences', 1 );
    $preferencesNode->addChild( transformForwardNode( $cpForwardNode ) );

    if ( $newnode ) {
      $mailusersNode->addChild( $mailuserNode );
      $mailuserNames{$mailname} = $mailuserNode;
    }
  }
  return $mailusersNode;
}

sub addDomainDnssystem {
  my ($domainNode, $cpDomainNode) = @_;

  unless ( testXmlNodeParam($domainNode,   'domain') ) {return;}
  unless ( testXmlNodeParam($cpDomainNode, 'domain') ) {return;}

  my $cpDnsNode = $cpDomainNode->getChild( 'dns-zone' );
  unless ( defined $cpDnsNode ) { return; }

  return addDnssystem($domainNode, $cpDnsNode);
}

sub addSiteDnssystem {
  my ($domainNode, $cpDnsNode, $cpAccountNode) = @_;



  unless ( testXmlNodeParam($domainNode,      'site'      ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode,   'account'   ) ) {return;}

  unless ( defined $cpDnsNode ) { return; }
  unless ( testXmlNodeParam($cpDnsNode,      'dns-zone' ) ) {return;}

  return addDnssystem($domainNode, $cpDnsNode);
}

sub addDomainaliasDnssystem {
  my ($domainNode, $cpDomainNode) = @_;

  unless ( testXmlNodeParam($domainNode,   'domain-alias') ) {return;}
  unless ( testXmlNodeParam($cpDomainNode, 'addondomain') ) {return;}

  my $cpDnsNode = $cpDomainNode->getChild( 'dns-zone' );
  unless ( defined $cpDnsNode ) { return; }

  return addDnssystem($domainNode, $cpDnsNode, 1);
}

sub addDnssystem {
  my ($domainNode, $cpDnszoneNode, $skipPropertiesNode) = @_;

  my $domainName = $domainNode->getAttribute( 'name' );
  if ( defined ( $cpDnszoneNode ) ) {
    my @cpDnsRecNodes = $cpDnszoneNode->getChildren( 'dnsrec' );
    if ( @cpDnsRecNodes ) {
      my $dnsZoneNode = XmlNode->new( 'dns-zone' );
      $dnsZoneNode->setAttribute( 'type', 'master' );
      $dnsZoneNode->addChild( makeStatusEnabled() );
      foreach my $cpDnsRecNode ( @cpDnsRecNodes ) {
        my $src = $cpDnsRecNode->getAttribute( 'src' );
        my $type = $cpDnsRecNode->getAttribute( 'type' );

        my $dnsRecNode = XmlNode->new( 'dnsrec' );
        $dnsRecNode->setAttribute( 'type', $type );

        if ($type eq 'SRV') {
          # RFC and Plesk requires SRC to look like _service._proto.name
          # Cpanel doesn't validate this field
          if ($src !~ /^(xn--)?_[-\w]+\.(xn--)?_+[-\w]+/) {
            Logging::debug("Can't parse SRC attribute of SRV record: " . $src);
            next;
          }
          $dnsRecNode->setAttribute( 'src', $src . '.' . $domainName);
        } elsif ($src eq $domainName) {
          $dnsRecNode->setAttribute( 'src', $src . '.');
        } else {
          $dnsRecNode->setAttribute('src', $src . '.' . $domainName . '.');
        }

        if (( $type eq 'NS' ) || ( $type eq 'MX' ) || ( $type eq 'CNAME' ) || ( $type eq 'SRV' )) {
          $dnsRecNode->setAttribute('dst', $cpDnsRecNode->getAttribute('dst') . '.');
        } else {
          $dnsRecNode->setAttribute('dst', $cpDnsRecNode->getAttribute('dst'));
        }

        if ( $type eq 'MX' ) {
          my $opt = $cpDnsRecNode->getAttribute('opt');
          if (defined($opt)) {
            $dnsRecNode->setAttribute('opt', limit50($opt));
          }
        } elsif ( $type eq 'SRV' ) {
          my $opt = join( " ",
            limit50($cpDnsRecNode->getAttribute('priority')),
            limit50($cpDnsRecNode->getAttribute('weight')),
            $cpDnsRecNode->getAttribute('port')
          );
          $dnsRecNode->setAttribute('opt', $opt);
        } elsif ( $type eq 'CAA' ) {
          my $flag = $cpDnsRecNode->getAttribute('flags');
          unless ($flag == 0) {
            Logging::debug(
                "DNS record with type CAA '" . join(" ",
                    $cpDnsRecNode->getAttribute('src'),
                    $cpDnsRecNode->getAttribute('flags'),
                    $cpDnsRecNode->getAttribute('tag'),
                    $cpDnsRecNode->getAttribute('dst')
                ) . "' has component 'flag' with unsupported value '" . $flag ."'. It will be replaced to '0'."
            );
            $flag = 0;
          }
          my $opt = join( " ", $flag, $cpDnsRecNode->getAttribute('tag'));
          $dnsRecNode->setAttribute('opt', $opt);
        }

        $dnsZoneNode->addChild($dnsRecNode);
      }

      if (defined($skipPropertiesNode)) {
        $domainNode->addChild($dnsZoneNode);
      }  else {
        my $propertiesNode = $domainNode->getChild('properties');
        if (defined $propertiesNode) {
          $propertiesNode->addChild($dnsZoneNode);
        }
        else {
          $propertiesNode = XmlNode->new('properties');
          $propertiesNode->addChild($dnsZoneNode);
          $domainNode->addChild($propertiesNode);
        }
      }
    }
  }

  return $domainNode
}

sub limit50 {
  my ($value) = @_;
  if ($value > 50) {
      return 50;
  }

  return $value;
}

sub addDomainMaillists {
  my ($domainNode, $cpDomainNode) = @_;

  unless ( testXmlNodeParam($domainNode,   'domain') ) {return;}
  unless ( testXmlNodeParam($cpDomainNode, 'domain') ) {return;}

  my $cpMaillistsNode = $cpDomainNode->getChild( 'maillists' );
  if ( defined ( $cpMaillistsNode ) ) {
    my @cpMaillistNodes = $cpMaillistsNode->getChildren( 'maillist' );
    if ( @cpMaillistNodes ) {
      my $maillistsNode = XmlNode->new( 'maillists' );
      my $propertiesNode = XmlNode->new( 'properties' );
      $propertiesNode->addChild( makeStatusEnabled() );
      $maillistsNode->addChild( $propertiesNode );
      foreach my $cpMaillistNode ( @cpMaillistNodes ) {
        my $maillistNode = XmlNode->new( 'maillist' );
        my $domainName = $cpDomainNode->getAttribute( 'name' );
        my $listname = $cpMaillistNode->getAttribute( 'name' ) . "_" . $domainName;

        $maillistNode->setAttribute( 'name', $cpMaillistNode->getAttribute( 'name' ) );

        $maillistNode->addChild( makeStatusEnabled() );

        my @cpOwnerNodes = $cpMaillistNode->getChildren( 'owner' );
        foreach my $cpOwnerNode ( @cpOwnerNodes ) {
          # 'owner' format is the same, just copy element
          $maillistNode->addChild($cpOwnerNode->copy());
        }

        my $cpPasswordNode = $cpMaillistNode->getChild( 'password' );
        if ( defined ( $cpPasswordNode ) ) {
          my $passwordNode = XmlNode->new( 'password' );
          $passwordNode->setAttribute( 'type', $cpPasswordNode->getAttribute( 'type' ) );
          $passwordNode->setText( $cpPasswordNode->getText() );
          $maillistNode->addChild($passwordNode);
        }

        my @cpRecipientNodes = $cpMaillistNode->getChildren( 'recipient' );
        foreach my $cpRecipientNode ( @cpRecipientNodes ) {
          # 'recipient' format is the same, just copy element
          $maillistNode->addChild($cpRecipientNode->copy());
        }

        $maillistsNode->addChild( $maillistNode );
      }
      $domainNode->addChild( $maillistsNode );
    }
  }
  return $domainNode;
}

sub addDomainDatabases {
  my ($domainNode, $cpDomainNode) = @_;

  unless ( testXmlNodeParam($domainNode,   'domain') ) {return;}
  unless ( testXmlNodeParam($cpDomainNode, 'domain') ) {return;}

  my @databaseNodes = [];

  my @cpDatabaseNodes = $cpDomainNode->getChildren( 'database' );
  if (@cpDatabaseNodes) {
    foreach my $cpDatabaseNode (@cpDatabaseNodes) {
      my $databaseNode = XmlNode->new( 'database' );
      $databaseNode->setAttribute( 'name', $cpDatabaseNode->getAttribute( 'name' ) );
      $databaseNode->setAttribute( 'type', $cpDatabaseNode->getAttribute( 'type' ) );
      # Plesk dump format requires guid, could be empty however, will be fixed with guid fixer before restore
      $databaseNode->setAttribute( 'guid', '' );

      my $version = $cpDatabaseNode->getAttribute( 'version' );
      $databaseNode->setAttribute( 'version', $version ) if defined $version;

      # Plesk dump format requires db-server node, so append it with empty host and port (default one will be used instead)
      my $dbServerNode = XmlNode->new('db-server');
	  my $serverType = $databaseNode->getAttribute('type');
      $dbServerNode->setAttribute( 'type', $serverType);
      $dbServerNode->addChild(XmlNode->new('host', 'content' => ''));
      $dbServerNode->addChild(XmlNode->new('port', 'content' => ''));
      $databaseNode->addChild($dbServerNode);
      Logging::debug("Added a 'db-server' node to a '$serverType' database");

      my @cpDbuserNodes = $cpDatabaseNode->getChildren( 'dbuser' );
      if (@cpDbuserNodes) {
        my @dbuserNodes = [];

        foreach my $cpDbuserNode (@cpDbuserNodes) {
          my $dbuserNode = XmlNode->new( 'dbuser' );
          $dbuserNode->setAttribute( 'name', $cpDbuserNode->getAttribute( 'name' ) );
          my $cpDbuserPassword = $cpDbuserNode->getChild( 'password' );
          my $dbuserPassword = XmlNode->new( 'password' );
          # cPanel password type is 'encrypted' or 'empty'. Both are allowed to be Plesk' password type. No token conversion required.
          $dbuserPassword->setAttribute( 'type', $cpDbuserPassword->getAttribute( 'type') );
          $dbuserPassword->setText( $cpDbuserPassword->getText() );
          $dbuserNode->addChild( $dbuserPassword );

          my @cpAccesshostNodes = $cpDbuserNode->getChildren( 'accesshost' );
          if (@cpAccesshostNodes) {
            foreach my $cpAccesshostNode (@cpAccesshostNodes) {
              my $accesshostNode = XmlNode->new( 'accesshost' );
              $accesshostNode->setText( $cpAccesshostNode->getText() );
              $dbuserNode->addChild( $accesshostNode );
            }
          }

          $databaseNode->addChild( $dbuserNode );
        }
      }

      push @databaseNodes, $databaseNode;
    }
  }

  my $cpDbusersNode = $cpDomainNode->getChild('dbusers');
  if ($cpDbusersNode && $cpDbusersNode->getChildren('dbuser')) {
    my $dbusersNode = $cpDbusersNode->copy();
    push @databaseNodes, $dbusersNode;
  }

  if (@databaseNodes) {
    my $databasesNode = XmlNode->new( 'databases' );

    foreach my $databaseNode (@databaseNodes) {
      $databasesNode->addChild( $databaseNode );
    }
    $domainNode->addChild( $databasesNode );
    return $databasesNode;
  }
  return;
}

sub addDomainuser {
  my ($domainNode, $cpAccountNode) = @_;

  unless ( testXmlNodeParam($domainNode,    'domain' ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account') ) {return;}

  # cPanel account (not reseller) that belongs to reseller account should be mapped into Plesk domain user.
  # So far until only cPanel domains are migrated, no domain users mapping will be performed.
  return;
}

sub getPhosting {
  my ($domainNode, $cpAccountNode) = @_;

  unless ( testXmlNodeParam($domainNode,    'domain' ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account') ) {return;}

  my $quota;
  my @cpLimitNodes = $cpAccountNode->getChildren( 'limit' );
  if (@cpLimitNodes) {
    foreach my $cpLimitNode (@cpLimitNodes) {
      if ( $cpLimitNode->getAttribute( 'name' ) eq 'disk_space' ) {
        $quota = $cpLimitNode->getText();
        last;
      }
    }
  }

  my $cpDomainNode = $cpAccountNode->getChild( 'domain' );

  my $phostingNode = makePhostingAttrs();
  $phostingNode->setAttribute( 'www-root', getWwwRootAttribute( $cpDomainNode->getAttribute( 'www_root' ), $cpAccountNode->getAttribute( 'home' )) );
  my $cgi_bin_mode = getCgiBinModeAttribute( $cpDomainNode->getAttribute( 'cgi_root' ), $cpDomainNode->getAttribute( 'www_root' ), $cpAccountNode->getAttribute( 'home' ));
  $phostingNode->setAttribute( 'cgi_bin_mode', $cgi_bin_mode );

  my $phostingPreferencesNode = getPhostingPreferences(
      $cpDomainNode, $cpAccountNode, (my $allowSysUser = 1), (my $allowAnonFtp = 1)
  );
  $phostingNode->addChild( $phostingPreferencesNode );

  addPhostingLimitsAndPermissions( $phostingNode);

  addPhostingWebusersAndFtpusers( $phostingNode, $cpDomainNode, $cpAccountNode);

  return $phostingNode;
}

sub getPhostingPreferences {
  my ($cpDomainNode, $cpAccountNode, $allowSysUser, $allowAnonFtp) = @_;

  unless ( testXmlNodeParam($cpDomainNode,  'domain', 'subdomain' ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account'             ) ) {return;}

  my $account = $cpAccountNode->getAttribute( 'name' );
  my $preferencesNode = XmlNode->new( 'preferences' );

  my $sysuserNode;

  if ( $allowSysUser ) {
    my @cpFtpuserNodes = $cpDomainNode->getChildren( 'ftpuser' );
    if (@cpFtpuserNodes) {
      foreach my $cpFtpuserNode (@cpFtpuserNodes) {
        next if ( $cpFtpuserNode->getAttribute( 'name' ) ne $account );
        $sysuserNode = XmlNode->new( 'sysuser' );
        $sysuserNode->setAttribute( 'name', $account );
        $sysuserNode->setAttribute( 'quota', $cpFtpuserNode->getAttribute( 'quota' ) ) if ( defined ( $cpFtpuserNode->getAttribute( 'quota' ) ) );
        if ( defined ( my $shell = $cpFtpuserNode->getAttribute( 'shell' ) ) ) {
          if ( $shell eq '/usr/local/cpanel/bin/noshell' ) {
            $shell = '/bin/false';
          }
          $sysuserNode->setAttribute( 'shell', $shell );
        }
        my $passwordNode = XmlNode->new( 'password' );
        $passwordNode->setAttribute( 'type', 'encrypted' );
        $passwordNode->setText( $cpFtpuserNode->getAttribute( 'password' ) );
        $sysuserNode->addChild( $passwordNode );
        last;
      }
    }
    unless ( defined ( $sysuserNode ) ) {
      Logging::warning("Unable get ftp user account for domain '" . $cpDomainNode->getAttribute( 'name' ) . "'");
      $sysuserNode = XmlNode->new( 'sysuser' );
      $sysuserNode->setAttribute( 'name',  $account );
    }
    $preferencesNode->addChild( $sysuserNode );
  }

  if ( $allowSysUser ) {
    my $cpAnonftpNode = $cpDomainNode->getChild( 'anonftp' );
    if (defined ( $cpAnonftpNode )) {
      my $anonftpNode = XmlNode->new( 'anonftp' );
      if (defined ($cpAnonftpNode->getAttribute( 'pub' )) and $cpAnonftpNode->getAttribute( 'pub' ) eq 'true') {
        $anonftpNode->setAttribute( 'pub', 'true' );
        my $permissionNode = XmlNode->new( 'anonftp-permission' );
        $permissionNode->setAttribute( 'name', 'incoming-download' );
        $anonftpNode->addChild( $permissionNode );
      }
      if (defined ($cpAnonftpNode->getAttribute( 'incoming' )) and $cpAnonftpNode->getAttribute( 'incoming' ) eq 'true') {
        $anonftpNode->setAttribute( 'incoming', 'true' );
        my $permissionNode = XmlNode->new( 'anonftp-permission' );
        $permissionNode->setAttribute( 'name', 'incoming-mkdir' );
        $anonftpNode->addChild( $permissionNode );
      }
      $preferencesNode->addChild( $anonftpNode );
    }
  }

  addPhostingPreferencesPdirs( $preferencesNode, $cpDomainNode, $cpAccountNode );

  return $preferencesNode;
}

sub addPhostingLimitsAndPermissions {
  my ($phostingNode) = @_;

  unless ( testXmlNodeParam($phostingNode, 'phosting') ) {return;}

  my $limitsAndPermissionsNode = XmlNode->new( 'limits-and-permissions' );
  my $scriptingNode = makeScriptingAll();

  $limitsAndPermissionsNode->addChild( $scriptingNode );
  $phostingNode->addChild( $limitsAndPermissionsNode );
  return $limitsAndPermissionsNode;
}

sub addPhostingWebusersAndFtpusers {
  my ($phostingNode, $cpDomainNode, $cpAccountNode) = @_;

  unless ( testXmlNodeParam($phostingNode,  'phosting' ) ) {return;}
  unless ( testXmlNodeParam($cpDomainNode,  'domain'   ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode, 'account'  ) ) {return;}

  my $account = $cpAccountNode->getAttribute( 'name' );

  my @cpFtpuserNodes = $cpDomainNode->getChildren( 'ftpuser' );

  my @cpSubdomainNodes = $cpDomainNode->getChildren( 'subdomain' );
  if (@cpSubdomainNodes) {
    foreach my $cpSubdomainNode (@cpSubdomainNodes) {
      my @cpSubdomFtpuserNodes = $cpSubdomainNode->getChildren( 'ftpuser' );
      if ( @cpSubdomFtpuserNodes ) {
        my @cpLimitNodes = $cpAccountNode->getChildren( 'limit' );
        if (@cpLimitNodes) {
          foreach my $cpLimitNode (@cpLimitNodes) {
            next unless $cpLimitNode->getAttribute( 'name' ) eq 'max_ftpusers';
            my $isUnlimited = $cpLimitNode->getText() eq '-1';
            unless ($isUnlimited) {
              $cpLimitNode->setText($cpLimitNode->getText() + scalar(@cpSubdomFtpuserNodes));
            }
            last;
          }
        }
        push @cpFtpuserNodes, @cpSubdomFtpuserNodes;
      }
    }
  }
  if (@cpFtpuserNodes) {
    my @sysuserNodes;
    foreach my $cpFtpuserNode (@cpFtpuserNodes) {
      next if ( $cpFtpuserNode->getAttribute( 'name' ) eq $account );
      my $sysuserNode = XmlNode->new( 'sysuser' );
      $sysuserNode->setAttribute( 'name', $cpFtpuserNode->getAttribute( 'name' ) );
      $sysuserNode->setAttribute( 'home', $cpFtpuserNode->getAttribute( 'directory' ) ) if ( defined ( $cpFtpuserNode->getAttribute( 'directory' ) ) );
      $sysuserNode->setAttribute( 'quota', $cpFtpuserNode->getAttribute( 'quota' ) ) if ( defined ( $cpFtpuserNode->getAttribute( 'quota' ) ) );
      $sysuserNode->setAttribute( 'shell', $cpFtpuserNode->getAttribute( 'shell' ) ) if ( defined ( $cpFtpuserNode->getAttribute( 'shell' ) ) );
      # sysuser password isn't migrated because of unsupported encryption
      push @sysuserNodes, $sysuserNode;
    }
    if ( @sysuserNodes ) {
      my $ftpusersNode = XmlNode->new( 'ftpusers' );
      foreach my $sysuserNode ( @sysuserNodes ) {
        my $ftpuserNode = XmlNode->new( 'ftpuser' );
        $ftpuserNode->setAttribute( 'name', $sysuserNode->getAttribute( 'name' ) );
        $ftpuserNode->addChild( $sysuserNode );
        $ftpusersNode->addChild( $ftpuserNode );
      }
      $phostingNode->addChild( $ftpusersNode );
    }
  }
  return $phostingNode;
}

sub addPhostingPreferencesPdirs {
  my ($preferencesNode, $cpDomainNode, $cpAccountNode) = @_;
  unless ( testXmlNodeParam($preferencesNode, 'preferences'         ) ) {return;}
  unless ( testXmlNodeParam($cpDomainNode,    'domain', 'subdomain' ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode,   'account'             ) ) {return;}

  my $cpAccountDomainNode = $cpAccountNode->getChild( 'domain' );
  my $accountHome = $cpAccountNode->getAttribute( 'home' );

  my @cpPdirNodes = $cpAccountDomainNode->getChildren( 'pdir' );
  if (@cpPdirNodes) {
    foreach my $cpPdirNode (@cpPdirNodes) {
      my $path = $cpPdirNode->getAttribute( 'name' );

      my $name;
      my $type;
      my $www_relative = HelpFuncs::getRelativePath( $cpDomainNode->getAttribute( 'www_root' ), $accountHome );
      if ( defined $www_relative ) {
        if ( $path eq $www_relative ) {
          $name = '/';
        }
        else {
          $name = HelpFuncs::getRelativePath( $path, $www_relative );
        }
      }
      if ( defined $name ) {
        $type = 'nonssl';
      }
      else {
        my $cgi_relative = HelpFuncs::getRelativePath( $cpDomainNode->getAttribute( 'cgi_root' ), $accountHome );
        if ( defined $cgi_relative) {
          if ( $path eq $cgi_relative ) {
            $name = '/';
          }
          else {
            $name = HelpFuncs::getRelativePath( $path, $cgi_relative );
          }
          if ( defined $name ) {
            $type = 'cgi';
          }
        }
      }

      if ( defined $name ) {
        my $pdirNode = XmlNode->new( 'pdir' );
        $pdirNode->setAttribute( 'name', $name );
        my $title = $cpPdirNode->getAttribute( 'title' );
        $pdirNode->setAttribute( 'title', $title ) if ( defined $title );
        $pdirNode->setAttribute( $type, 'true' );

        my @cpPduserNodes = $cpPdirNode->getChildren( 'pduser' );
        if (@cpPduserNodes) {
          foreach my $cpPduserNode (@cpPduserNodes) {
            my $pduserNode = XmlNode->new( 'pduser' );
            $pduserNode->setAttribute( 'name', $cpPduserNode->getAttribute( 'name' ) );

            my $password = $cpPduserNode->getAttribute( 'password' );
            if ( defined ($password) ) {
              my $passwordNode = XmlNode->new( 'password' );
              $passwordNode->setAttribute( 'type', 'encrypted' );

              my $encoding = $cpPduserNode->getAttribute( 'encoding' );
              $passwordNode->setAttribute( 'encoding', $encoding ) if defined ( $encoding );
              $passwordNode->setText( $password );

              $pduserNode->addChild( $passwordNode );
            }

            $pdirNode->addChild( $pduserNode );
          }
        }
        $preferencesNode->addChild( $pdirNode );
      }
    }
  }
  return $preferencesNode;
}

sub getWwwRootAttribute {
  my ($wwwRootDir, $homeDir) = @_;

  my $wwwRootAttribute = HelpFuncs::getRelativePath( $wwwRootDir, $homeDir );
  if ( defined $wwwRootAttribute ) {
    return $wwwRootAttribute;
  }
  else {
    Logging::warning("Couldn't get 'www-root' attribute for domain where docroot dir '$wwwRootDir' is out of user webspace. ".
                     "Domain's docroot will be migrated into '$wwwRootDir' directory under webspace.");
    return $wwwRootDir;
  }
}

sub getCgiBinModeAttribute {
  my ($scriptAliasDir, $wwwRootDir, $homeDir) = @_;
  if ( $scriptAliasDir =~ /^($wwwRootDir)\/*cgi-bin$/ ) {
    return 'www-root';
  }
  if ( $scriptAliasDir =~ /^($homeDir)\/*cgi-bin$/ ) {
    return 'webspace';
  }
  else {
    Logging::warning("Couldn't get 'cgi_bin_mode' attribute for domain where cgi-bin dir is '$scriptAliasDir' and docroot dir is '$wwwRootDir'. ".
                     "Domain will be migrated in that way as if its cgi-bin directory was located under docroot.");
    return 'www-root';
  }
}

#
# End of node transformation subs
#

sub _convertMailnameToExcludeName {
  my ( $mailname, $domain ) = @_;

  $domain =~ tr/\./_/;
  return ".$mailname@".$domain;
}

sub _addDefaultMailboxExcludesFromMail {
  my ( $cpMailNode, $excludesPtr ) = @_;

  unless ( testXmlNodeParam($cpMailNode, 'mail' ) ) {return;}
  unless ( ref($excludesPtr) =~ /ARRAY/ ) {return;}

  my @cpMailnameNodes = $cpMailNode->getChildren( 'mailname' );
  if ( @cpMailnameNodes ) {
    foreach my $cpMailnameNode ( @cpMailnameNodes ) {
      push @{$excludesPtr}, _convertMailnameToExcludeName( $cpMailnameNode->getAttribute('name'), $cpMailnameNode->getAttribute('domainname'));
    }
  }
}

sub _addDefaultMailboxExcludesFromAddon {
  my ( $cpAddonDomainNode, $excludesPtr ) = @_;

  unless ( testXmlNodeParam($cpAddonDomainNode, 'addondomain' ) ) {return;}
  unless ( ref($excludesPtr) =~ /ARRAY/ ) {return;}

  my $addondomainName = $cpAddonDomainNode->getAttribute( 'name' );
  push @{$excludesPtr}, $addondomainName;

  my $cpMailNode = $cpAddonDomainNode->getChild( 'mail' );
  if ( $cpMailNode ) {
    _addDefaultMailboxExcludesFromMail( $cpMailNode, $excludesPtr);
  }
}

sub _addDefaultMailboxExcludesFromSubdom {
  my ( $cpDomainName, $cpSubdomainNode, $excludesPtr ) = @_;

  unless ( testXmlNodeParam($cpSubdomainNode, 'subdomain' ) ) {return;}
  unless ( ref($excludesPtr) =~ /ARRAY/ ) {return;}

  my $subdomainName = $cpSubdomainNode->getAttribute( 'name' );
  push @{$excludesPtr}, "$subdomainName.$cpDomainName";

  my @cpAddonDomainNodes = $cpSubdomainNode->getChildren( 'addondomain' );
  if( @cpAddonDomainNodes ) {
    foreach my $cpAddonDomainNode ( @cpAddonDomainNodes ) {
      _addDefaultMailboxExcludesFromAddon( $cpAddonDomainNode, $excludesPtr);
    }
  }

  my $cpMailNode = $cpSubdomainNode->getChild( 'mail' );
  if ( $cpMailNode ) {
    _addDefaultMailboxExcludesFromMail( $cpMailNode, $excludesPtr);
  }
}

sub _getDefaultMailboxExcludes {
  my ( $cpDomainNode ) = @_;

  unless ( testXmlNodeParam($cpDomainNode, 'domain' ) ) {return;}

  my $domainName = $cpDomainNode->getAttribute( 'name' );
  my @excludes;
  push @excludes, $domainName;

  my @cpSubdomainNodes = $cpDomainNode->getChildren( 'subdomain' );
  if( @cpSubdomainNodes ) {
    foreach my $cpSubdomainNode ( @cpSubdomainNodes ) {
      _addDefaultMailboxExcludesFromSubdom( $domainName, $cpSubdomainNode, \@excludes );
    }
  }

  my @cpAddonDomainNodes =  $cpDomainNode->getChildren( 'addondomain' );
  if( @cpAddonDomainNodes ) {
    foreach my $cpAddonDomainNode ( @cpAddonDomainNodes ) {
      _addDefaultMailboxExcludesFromAddon( $cpAddonDomainNode, \@excludes);
    }
  }

  my $cpMailNode = $cpDomainNode->getChild( 'mail' );
  if ( $cpMailNode ) {
    _addDefaultMailboxExcludesFromMail( $cpMailNode, \@excludes);
  }
  return \@excludes;
}

#
# Begin static content getters
#
sub makeStatusEnabled {
  my $nodeName = shift || 'status';
  my $statusEnabledNode = XmlNode->new( $nodeName );
  $statusEnabledNode->addChild( XmlNode->new( 'enabled' ) );
  return $statusEnabledNode;
}

sub makeStatusDisabledBy {
  my $nodeName = shift || 'status';
  my $name = shift || 'admin';
  my $statusNode = XmlNode->new( $nodeName );
  $statusNode->addChild( XmlNode->new( 'disabled-by', 'attributes' => { 'name' => $name } ) );
  return $statusNode;
}

sub makeScriptingAll {
  my $scriptingNode = XmlNode->new( 'scripting' );
  $scriptingNode->setAttribute( 'ssi',    'true' );
  $scriptingNode->setAttribute( 'php',    'true' );
  $scriptingNode->setAttribute( 'cgi',    'true' );
  $scriptingNode->setAttribute( 'perl',   'true' );
  $scriptingNode->setAttribute( 'python', 'false' );
  return $scriptingNode;
}

sub makePhostingAttrs {
  my $phostingNode = XmlNode->new( 'phosting' );
  $phostingNode->setAttribute( 'wu_script',      'true' );
  $phostingNode->setAttribute( 'guid',           '' );
  $phostingNode->setAttribute( 'https',          'true' );
  $phostingNode->setAttribute( 'webstat',        'none' );
  $phostingNode->setAttribute( 'errdocs',        'true' );
  $phostingNode->setAttribute( 'shared-content', 'true' );
  return $phostingNode;
}

sub makeZeroDomainLimitsAndPermissions {
  my $limitsAndPermissionsNode = XmlNode->new( 'limits-and-permissions' );
  $limitsAndPermissionsNode->addChild( 'limit', 'content' => '0',     'attributes' => { 'name' => 'max_traffic' } );
  $limitsAndPermissionsNode->addChild( 'limit', 'content' => '0',     'attributes' => { 'name' => 'max_maillists' } );
  $limitsAndPermissionsNode->addChild( 'limit', 'content' => '0',     'attributes' => { 'name' => 'max_box' } );
  $limitsAndPermissionsNode->addChild( 'limit', 'content' => '0',     'attributes' => { 'name' => 'max_db' } );
  $limitsAndPermissionsNode->addChild( 'limit', 'content' => '0',     'attributes' => { 'name' => 'max_subdom' } );
  $limitsAndPermissionsNode->addChild( 'limit', 'content' => '51200', 'attributes' => { 'name' => 'disk_space' } );
  $limitsAndPermissionsNode->addChild( 'limit', 'content' => '0',     'attributes' => { 'name' => 'max_wu' } );
  return $limitsAndPermissionsNode;
}
#
# End of static content getters
#



#
# Begin miscellanneous subs
#
sub testXmlNodeParam {
  my $param = shift;
  my @testarray = @_;

  my @caller = caller(1);

  unless ( ref($param) =~ /XmlNode/ ) {
    Logging::warning('XmlNode instance is required, got \''.
                   ( defined (ref($param))? ref($param) : 'undef' ). '\' in '.
                   $caller[3] .' called at '. $caller[1] .' ,line '. $caller[2] );
    return;
  }

  my $name = $param->getName();
  foreach my $testname ( @testarray ) {
    if( $name eq $testname ) {
      return 1;
    }
  }

  Logging::warning( 'One of \'' . "@testarray" . '\' XmlNode is required, got \'' . $name . '\' in '.
                  $caller[3] .' called at '. $caller[1] .' ,line '. $caller[2] );
  return;
}

sub startsWith {
    my ($haystack, $needle)= @_;
    return substr($haystack, 0, length($needle)) eq $needle;
}

sub startsWithAny {
  my ($haystack, $needles) = @_;
  for my $needle (@{$needles}) {
    if (startsWith($haystack, $needle)) {
        return 1;
    }
  }
  return 0;
}
#
# End of miscellanneous subs
#


1;
