# 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;
#
# 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;
  }
}

sub getResellers {
  my @resellers;
  # empty
  return @resellers;
}

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

  my @clients;
  # empty
  return @clients;
}

sub getDomains {
  my $owner = shift; # 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.

  # The current implementation of cPanel migration deals with domains only.

  initialize();
  my @domains;

  unless ( defined $owner ) {
    foreach my $domain (sort keys %domain2client) {
      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/ ) {
    if ($cpAccountNode->getName() eq 'account' ) {

      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 $domain_name = $cpDomainNode->getAttribute( 'name' );
        $domainNode->setAttribute( 'name', $domain_name );

        # 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 ) {
          my $domainPreferencesNode = getDomainPreferences($cpDomainNode);
          $domainNode->addChild( $domainPreferencesNode );
        }

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

        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 );

          addDomainMailsystem( $domainNode, $cpAccountNode );

          addDomainDatabases( $domainNode, $cpDomainNode );

          addDomainMaillists( $domainNode, $cpDomainNode );

          addDomainuser( $domainNode, $cpAccountNode);
          
          $domainNode->addChild( $domainPhostingNode );
        }

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

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

        if ( $not_shallow_mode ) {
          # Array to store 'site' nodes. Then we'll create 'sites' node if any 'site' node is created
          my @sites = ();

          # 2) Transform cPanel account's subdomains into Plesk sites
          my @cpSubdomainNodes = $cpDomainNode->getChildren( 'subdomain' );
          if ( @cpSubdomainNodes ) {
            foreach my $cpSubdomainNode (sort @cpSubdomainNodes) {
              my $has_ftp_account = defined ( $cpSubdomainNode->getChild( 'ftpuser' ) );

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

              my $addon_domain_name ;
              $addon_domain_name = $cpAddonDomainNodes[0]->getAttribute('name') if ( @cpAddonDomainNodes );

              if ( $addon_domain_name ) {
                my $domainFromSubdomainNode = XmlNode->new ('site');
                $domainFromSubdomainNode->setAttribute( 'www', 'true' );
                $domainFromSubdomainNode->setAttribute( 'name', $addon_domain_name );
                # Plesk dump format requires guid, could be empty however, will be fixed with guid fixer before restore
                $domainFromSubdomainNode->setAttribute( 'guid', '' );

                # Subdomains which have addondomains and        have ftp account are mapped into Plesk sites with phosting
                # Subdomains which have addondomains but do not have ftp account are mapped into Plesk sites with shosting
                my $hosting_type = $has_ftp_account? 'phosting' : 'shosting' ;

                if ( $hosting_type eq 'phosting' ) {
                  # Transform subdomain to site with hosting
                  processSubdomain2Phosting($domainFromSubdomainNode, $cpSubdomainNode, $cpDomainNode, $cpAccountNode);
                }
                else {
                  # Transform subdomain to site with shosting
                  processSubdomain2Shosting($domainFromSubdomainNode, $cpSubdomainNode, $cpDomainNode, $cpAccountNode);
                }
                push @sites, $domainFromSubdomainNode;
              }
            }
          }
          if ( @sites ) {
            my $sitesNode = XmlNode->new( 'sites' );
            foreach my $siteNode ( @sites ) {
              $sitesNode->addChild( $siteNode );
            }
            $domainPhostingNode->addChild( $sitesNode );
          }
        }
      }

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

      return $clientNode;

    }
  }
  return;
}


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);
    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;
      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_shallow_mode');
        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 getDomainPreferences {
  my $cpDomainNode = shift;

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

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

  # Since PPP9.x cPanel account's addondomains are transformed into Plesk domain aliases instead of domain with shosting
  my @cpAddondomainNodes = $cpDomainNode->getChildren( 'addondomain' );
  if (@cpAddondomainNodes) {
    foreach my $cpAddondomainNode (sort @cpAddondomainNodes) {
      my $domainAliasNode = XmlNode->new( 'domain-alias' );
      $domainAliasNode->setAttribute( 'name', $cpAddondomainNode->getAttribute('name') );
      $domainAliasNode->setAttribute( 'mail', 'true' );
      $domainAliasNode->setAttribute( 'web', 'true' );
      $domainAliasNode->addChild( makeStatusEnabled() );
      $domainPreferencesNode->addChild( $domainAliasNode );
    }
  }
  return $domainPreferencesNode;
}

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) = @_;

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

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

  my $mboxQuota = 0;
  my $quotaSub = sub { if ($_[0] > $mboxQuota) { $mboxQuota = $_[0];} };

  XPath::Select $cpDomainNode, 'mail/mailname', sub {
    $quotaSub->( shift->getAttribute( 'quota' ) );
  };
  XPath::Select $cpDomainNode, 'addondomain/mail/mailname', sub {
    $quotaSub->( shift->getAttribute( 'quota' ) );
  };
  XPath::Select $cpDomainNode, 'subdomain/mail/mailname', sub {
    $quotaSub->( shift->getAttribute( 'quota' ) );
  };
  XPath::Select $cpDomainNode, 'subdomain/addondomain/mail/mailname', sub {
    $quotaSub->( shift->getAttribute( 'quota' ) );
  };

  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;
        }
        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( $mboxQuota );
  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' );

  if ( defined $cpMailNode ) {
    my $mailsystemNode = XmlNode->new( 'mailsystem' );

    $mailsystemNode->addChild( getMailsystemProperties() );

    addMailsystemMailusers( $mailsystemNode, $cpDomainNode, $cpDomainNode->getAttribute( 'name' ), $cpAccountNode, (my $preferAddonDomains = 0) );

    addMailsystemPreferences( $mailsystemNode, $cpMailNode );

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

sub addSubdomainMailsystem {
  my ($domainNode, $cpSubdomainNode, $cpAccountNode) = @_;
# cPanel subdomains are always mapped to Plesk sites since PPP10.0, so domain node here is 'site'
  unless ( testXmlNodeParam($domainNode,      'site'      ) ) {return;}
  unless ( testXmlNodeParam($cpSubdomainNode, 'subdomain' ) ) {return;}
  unless ( testXmlNodeParam($cpAccountNode,   'account'   ) ) {return;}

  my $cpMailNode = $cpSubdomainNode->getChild( 'mail' );

  if ( defined $cpMailNode ) {
    my $mailsystemNode = XmlNode->new( 'mailsystem' );

    $mailsystemNode->addChild( getMailsystemProperties() );

    my $mailDomainName = $cpSubdomainNode->getAttribute( 'name' ). "." . $cpAccountNode->getChildAttribute( 'domain', 'name' );
    addMailsystemMailusers( $mailsystemNode, $cpSubdomainNode, $mailDomainName, $cpAccountNode, (my $preferAddonDomains = 1) );

    addMailsystemPreferences( $mailsystemNode, $cpMailNode );

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

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

sub addMailsystemMailusers {
  my ($mailsystemNode, $cpDomainNode, $cpMailDomainName, $cpAccountNode, $preferAddonDomains) = @_;

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

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

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

  my %mailnames;
  my $cpDomainMailNode = $cpDomainNode->getChild( 'mail' );
  if ( ref($cpDomainMailNode) =~ /XmlNode/ ) {
    my @cpMailnameNodes = $cpDomainMailNode->getChildren( 'mailname' );
    if ( @cpMailnameNodes ) {
      foreach my $cpMailnameNode ( @cpMailnameNodes ) {
        $mailnames{ $cpMailnameNode->getAttribute('name') } = [ $cpMailDomainName, $cpMailnameNode ];
      }
    }
  }
  my @cpAddonDomainNodes = $cpDomainNode->getChildren( 'addondomain' );
  if ( @cpAddonDomainNodes ) {
    foreach my $cpAddonDomainNode ( @cpAddonDomainNodes ) {
      my $cpAddonDomainMailNode = $cpAddonDomainNode->getChild( 'mail' );
      if ( ref($cpAddonDomainMailNode) =~ /XmlNode/ ) {
        my @cpMailnameNodes = $cpAddonDomainMailNode->getChildren( 'mailname' );
        if ( @cpMailnameNodes ) {
          foreach my $cpMailnameNode ( @cpMailnameNodes ) {
            if ( exists ( $mailnames{$cpMailnameNode->getAttribute('name')} ) ){
              my $mailboxMainName = $cpMailnameNode->getAttribute('name')."@".$mailnames{ $cpMailnameNode->getAttribute('name') }->[0];
              my $mailboxAddonName = $cpMailnameNode->getAttribute('name')."@".$cpAddonDomainNode->getAttribute('name');
              if ( $preferAddonDomains ) {
                Logging::warning( "Couldn't migrate mailbox '$mailboxMainName'. Mailname is used in '$mailboxAddonName'.");
                $mailnames{ $cpMailnameNode->getAttribute('name') } = [ $cpAddonDomainNode->getAttribute( 'name' ), $cpMailnameNode ];
              }
              else {
                Logging::warning( "Couldn't migrate mailbox '$mailboxAddonName'. Mailname is used in '$mailboxMainName'.");
              }
            }
            else {
              $mailnames{ $cpMailnameNode->getAttribute('name') } = [ $cpAddonDomainNode->getAttribute( 'name' ), $cpMailnameNode ];
            }
          }
        }
      }
    }
  }
      
  while ( my ($mailname, $opts ) = each (%mailnames) ) {
      my $mailDomainName = $opts->[0];
      my $cpMailnameNode = $opts->[1];

      my $mailuserNode = XmlNode->new( 'mailuser' );

      my $cpMailnameName = $cpMailnameNode->getAttribute( 'name' );
      my %mailuserMetadata;
      $mailuserMetadata{'mailname'} = $cpMailnameName;
      $mailuserMetadata{'domain'} = $mailDomainName;
      $mailuserMetadata{'account'} = $cpAccountName;
      if ( ($cpMailnameName eq $cpAccountName) && ( $mailDomainName eq $cpDomainName) ) {
        #Default Email Account
        $mailuserMetadata{'exclude'} = _getDefaultMailboxExcludes($cpDomainNode);
      }
      $mailuserNode->setMetadata(\%mailuserMetadata);

      $mailuserNode->setAttribute( 'name', $cpMailnameName );
      # Plesk dump format requires guid, could be empty however, will be fixed with guid fixer before restore
      $mailuserNode->setAttribute( '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, $cpDomainMailNode, $cpAccountNode);
      $mailuserNode->addChild( $preferencesNode );

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

      $mailusersNode->addChild( $mailuserNode );
  }

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

  addMailusersFromAutoresponders($mailusersNode, $cpDomainMailNode, $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 ( !$mailuserNode->getMetadata() ) {
      my %mailuserMetadata;
      $mailuserMetadata{'mailname'} = $cpAutoresponderNode->getAttribute('mailname');
      $mailuserMetadata{'domain'} = $cpDomainName;
      $mailuserMetadata{'account'} = $cpAccountName;
      $mailuserNode->setMetadata(\%mailuserMetadata);
    }

    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 ( !$mailuserNode->getMetadata() ) {
      my %mailuserMetadata;
      $mailuserMetadata{'mailname'} = $mailname;
      $mailuserMetadata{'domain'} = $cpDomainName;
      $mailuserMetadata{'account'} = $cpAccountName;
      $mailuserNode->setMetadata(\%mailuserMetadata);
    }

    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 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;

        my %maillistMetadata;
        $maillistMetadata{'domain'} = $domainName;
        $maillistMetadata{'listname'} = $cpMaillistNode->getAttribute( 'name' );
        $maillistNode->setMetadata(\%maillistMetadata);

        $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');
      $dbServerNode->setAttribute( 'type',    'mysql' );
      $dbServerNode->addChild(XmlNode->new('host', 'content' => ''));
      $dbServerNode->addChild(XmlNode->new('port', 'content' => ''));
      $databaseNode->addChild($dbServerNode);

      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' );

    my %databasesMetadata;
    $databasesMetadata{'domain'} = $cpDomainNode->getAttribute( 'name' );

    $databasesNode->setMetadata(\%databasesMetadata);

    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 %phostingMetadata;
  $phostingMetadata{'domain'} = $cpDomainNode->getAttribute( 'name' );
  $phostingMetadata{'account'} = $cpAccountNode->getAttribute( 'name' );
  $phostingMetadata{'www_root'} = $cpDomainNode->getAttribute( 'www_root' );
  $phostingMetadata{'cgi_root'} = $cpDomainNode->getAttribute( 'cgi_root' );
  $phostingMetadata{'cgi_bin_mode'} = $cgi_bin_mode;

  $phostingNode->setMetadata(\%phostingMetadata);

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

  addPhostingLimitsAndPermissions( $phostingNode);

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

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

  return $phostingNode;
}

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

  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 );
        }
        # sysuser password isn't migrated because of unsupported encryption
        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 );
  }

  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) {
            if ( $cpLimitNode->getAttribute( 'name' ) eq 'max_ftpusers' ) {
              $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 addPhostingSubdomains {
  my ($phostingNode, $cpDomainNode, $cpAccountNode) = @_;

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

  my @cpSubdomainNodes = $cpDomainNode->getChildren( 'subdomain' );
  if (@cpSubdomainNodes) {
    my @subdomainNodes;

    foreach my $cpSubdomainNode ( @cpSubdomainNodes ) {
      my @cpAddonDomainNodes = $cpSubdomainNode->getChildren( 'addondomain' );
      my $cpFtpuserNode = $cpSubdomainNode->getChild( 'ftpuser' );
      if ( ( !defined $cpFtpuserNode ) || ( !@cpAddonDomainNodes ) ) {
        # Convert cPanel subdomains without addon domains and ftpusers to Plesk subdomains
        my $subdomainNode = XmlNode->new( 'subdomain' );
        $subdomainNode->setAttribute( 'name', $cpSubdomainNode->getAttribute( 'name' ) );
        $subdomainNode->setAttribute( 'guid', '' );
        $subdomainNode->setAttribute( 'shared-content', 'true' );
        $subdomainNode->setAttribute( 'https', 'true');
        $subdomainNode->setAttribute( 'www-root', getWwwRootAttribute( $cpSubdomainNode->getAttribute( 'www_root' ), $cpAccountNode->getAttribute( 'home' )) );

        my %subdomainMetadata;
        $subdomainMetadata{'www_root'} = $cpSubdomainNode->getAttribute( 'www_root' );
        $subdomainMetadata{'cgi_root'} = $cpSubdomainNode->getAttribute( 'cgi_root' );
        $subdomainMetadata{'cgi_bin_mode'} = getCgiBinModeAttribute( $cpSubdomainNode->getAttribute( 'cgi_root' ), $cpSubdomainNode->getAttribute( 'www_root' ), $cpAccountNode->getAttribute( 'home' ));
        $subdomainNode->setMetadata(\%subdomainMetadata);

        $subdomainNode->addChild( makeScriptingAll() );
        push @subdomainNodes, $subdomainNode;
      }
    }

    if ( @subdomainNodes ) {
      my $subdomainsNode = XmlNode->new( 'subdomains' );
      foreach my $subdomainNode ( @subdomainNodes ) {
        $subdomainsNode->addChild( $subdomainNode );
      }
      $phostingNode->addChild( $subdomainsNode );
      return $subdomainsNode;
    }
  }
  return;
}

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';
  }
}

sub processSubdomain2Phosting {
  my ($domainFromSubdomainNode, $cpSubdomainNode, $cpDomainNode, $cpAccountNode) = @_;

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

  my $domainName = $domainFromSubdomainNode->getAttribute( 'name' );

  # Get domain preferences (aliases only)
  my $domainPreferencesNode = XmlNode->new( 'preferences' );
  my @aliases;
  my @cpSubdomainAddondomainNodes = $cpSubdomainNode->getChildren( 'addondomain' );
  # At least one addondomain should always exist, because addondomain is required for subdomain-to-domain conversion
  foreach my $cpSubdomainAddondomainNode ( @cpSubdomainAddondomainNodes ) {
    next if ( $cpSubdomainAddondomainNode->getAttribute('name') eq $domainName );
    push @aliases, $cpSubdomainAddondomainNode->getAttribute('name');
  }

  my $subdomainName = ($cpSubdomainNode->getAttribute( 'name' )).'.'.($cpDomainNode->getAttribute( 'name' ));
  push @aliases, $subdomainName;

  for my $alias ( @aliases ) {
    my $domainAliasNode = XmlNode->new( 'domain-alias' );
    $domainAliasNode->setAttribute( 'name', $alias );
    $domainAliasNode->setAttribute( 'mail', 'true');
    $domainAliasNode->setAttribute( 'web',  'true');
    $domainAliasNode->addChild( makeStatusEnabled() );
    $domainPreferencesNode->addChild( $domainAliasNode );
  }

  $domainFromSubdomainNode->addChild( $domainPreferencesNode );

  # Get domain properties (ip and status only)
  my $domainPropertiesNode = getDomainProperties($cpAccountNode, 'skipIpNode' );
  $domainFromSubdomainNode->addChild( $domainPropertiesNode );

  addSubdomainMailsystem( $domainFromSubdomainNode, $cpSubdomainNode, $cpAccountNode );

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

  my %phostingMetadata;
  $phostingMetadata{'domain'} = $cpDomainNode->getAttribute( 'name' );
  $phostingMetadata{'account'} = $cpAccountNode->getAttribute( 'name' );
  $phostingMetadata{'from_subdomain'} = $cpSubdomainNode->getAttribute( 'name' );
  $phostingMetadata{'to_domain'} = $domainName;
  $phostingMetadata{'www_root'} = $cpSubdomainNode->getAttribute( 'www_root' );
  $phostingMetadata{'cgi_root'} = $cpSubdomainNode->getAttribute( 'cgi_root' );
  $phostingMetadata{'cgi_bin_mode'} = $cgi_bin_mode;
  $phostingNode->setMetadata(\%phostingMetadata);

  my $preferencesNode = getPhostingPreferences( $cpSubdomainNode, $cpAccountNode, (my $allowSysUser = 0) );
  $phostingNode->addChild( $preferencesNode );

  addPhostingLimitsAndPermissions($phostingNode);

  $domainFromSubdomainNode->addChild( $phostingNode );
  return $phostingNode;
}

sub processSubdomain2Shosting {
  my ($domainFromSubdomainNode, $cpSubdomainNode, $cpDomainNode, $cpAccountNode) = @_;

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

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

  my $domainPreferencesNode = XmlNode->new( 'preferences' );
  $domainFromSubdomainNode->addChild( $domainPreferencesNode );

  # Get domain properties (ip and status only)
  my $domainPropertiesNode = getDomainProperties($cpAccountNode, 'skipIpNode');
  $domainFromSubdomainNode->addChild( $domainPropertiesNode );

  $domainFromSubdomainNode->addChild( makeZeroDomainLimitsAndPermissions() );

  my $shostingNode = XmlNode->new( 'shosting' );
  my $urlNode = XmlNode->new( 'url' );

  my $subdomainName = $cpSubdomainNode->getAttribute( 'name' );
  $urlNode->setText( $subdomainName.".".$domainName);

  $shostingNode->addChild( $urlNode );
  $domainFromSubdomainNode->addChild( $shostingNode );

  return $shostingNode;
}
#
# 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', 'true' );
  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;
}
#
# End of miscellanneous subs
#


1;
