#!/usr/bin/perl
# cpanel - autofixer/restore_account_plans        Copyright(c) 2009 cPanel, Inc.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

BEGIN { unshift @INC, '/usr/local/cpanel'; }

use strict;
use warnings;

# use Data::Dumper;
use Cpanel::PwCache             ();
use Cpanel::ContactInfo::Email  ();
use Cpanel::SafeRun::Simple     ();
use Cpanel::SafeRun::Errors     ();
use Cpanel::Config::LoadConfig  ();
use Cpanel::Config::FlushConfig ();
use HTTP::Date                  ();

$ENV{'LANG'} = 'C';

my $log_fh;
my $tool_version = '1.2';
my $changes_made = 0;
my $verbose      = ( @ARGV && grep { $_ eq '--quiet' } @ARGV ) ? 0 : 1;
my $warn_only    = ( @ARGV && grep { $_ eq '--dryrun' } @ARGV ) ? 1 : 0;
my $log_file     = $warn_only ? 'restore_account_plans_report' : 'restore_account_plans';
my %USERPLANS;
my %PLANS;
my %PLANKEYS;
my %ACCOUNT_CREATION_DATA;
my %RESTORED_DATA_LOG;

if ( open my $accounting_fh, '<', '/var/cpanel/accounting.log' ) {
    while ( my $line = readline $accounting_fh ) {
        chomp $line;
        my ( $cdate1, $cdate2, $cdate3, $action, $owner, $priv, $domain, $ip, $user ) = split( /:/, $line );
        next if ( !$action || $action ne 'CREATE' );
        my $ctime = HTTP::Date::str2time( join( ':', $cdate1, $cdate2, $cdate3 ) );
        $ACCOUNT_CREATION_DATA{$user} = { 'OWNER' => ( $owner || 'root' ), 'STARTDATE' => $ctime };
    }
    close $accounting_fh;
}
else {
    warn "Failed to read accounting.log. Account creation time and owner can not be restored.\n";
}

if ( open my $userplans_fh, '<', '/etc/userplans' ) {
    while ( my $line = readline $userplans_fh ) {
        next if $line =~ m/^\s*#/;
        chomp $line;
        my ( $user, $plan ) = split( /:\s*/, $line );
        next if ( !$plan || $plan eq 'undefined' || !-e '/var/cpanel/packages/' . $plan );
        if ( !exists $USERPLANS{$user} ) {
            print "Loading plan $plan for $user\n" if $verbose;

            #print "Found plan $plan for $user in current userplans\n";
            $USERPLANS{$user} = $plan;
            $PLANS{$plan}     = 1;
        }
    }
    close $userplans_fh;
}

foreach my $revision ( grep { $_ } map { ( $_ =~ /revision\s+([\.\d]+)/i )[0]; } split( /\n/, Cpanel::SafeRun::Simple::saferun( 'rlog', '/etc/userplans' ) ) ) {
    foreach my $line ( split( /\n/, Cpanel::SafeRun::Errors::saferunnoerror( 'co', '-p', $revision, '/etc/userplans' ) ) ) {
        next if $line =~ m/^#/;
        chomp $line;
        my ( $user, $plan ) = split( /:\s*/, $line );
        next if ( !$plan || $plan eq 'undefined' || !-e '/var/cpanel/packages/' . $plan );
        if ( !exists $USERPLANS{$user} ) {
            print "Found plan $plan for $user in revision $revision\n" if $verbose;
            $USERPLANS{$user} = $plan;
            $PLANS{$plan}     = 1;
        }
    }
}

foreach my $plan ( keys %PLANS ) {
    $PLANKEYS{$plan} = Cpanel::Config::LoadConfig::loadConfig( '/var/cpanel/packages/' . $plan );
    if ( !exists $PLANKEYS{$plan}->{'RS'} || $PLANKEYS{$plan}->{'RS'} eq '' ) {
        $PLANKEYS{$plan}->{'RS'} = $PLANKEYS{$plan}->{'CPMOD'} || 'x3';
    }
    if ( !exists $PLANKEYS{$plan}->{'HASCGI'} || $PLANKEYS{$plan}->{'HASCGI'} eq '' ) {
        $PLANKEYS{$plan}->{'HASCGI'} = ( $PLANKEYS{$plan}->{'CGI'} eq 'n' ? 0 : 1 );
    }
    delete $PLANKEYS{$plan}->{'CPMOD'};
    delete $PLANKEYS{$plan}->{'CGI'};
    delete $PLANKEYS{$plan}->{'FRONTPAGE'};
    delete $PLANKEYS{$plan}->{'QUOTA'};
    delete $PLANKEYS{$plan}->{'IP'};
}

#OWNER,CONTACTEMAIL,CONTACTEMAIL2
# print Data::Dumper::Dumper( \%PLANKEYS );
my $header = '#cPanel -- If you edit this file directly you must run /scripts/updateuserdomains after to rebuild the cache';
my $now    = time();

# Read cpuser directory contents
unless ( -d '/var/cpanel/users' ) {
    die "/var/cpanel/users directory is missing!";
}

opendir( my $users_dh, '/var/cpanel/users' ) || die "Could not open /var/cpanel/users for reading!";
my @users = readdir($users_dh);
closedir $users_dh;

my %cpusers = map { $_ => undef } @users;
my %HOMES;
my %UIDS;
my %GIDS;

Cpanel::PwCache::init_passwdless_pwcache();
my $pwcache_ref = Cpanel::PwCache::fetch_pwcache();
foreach my $pw (@$pwcache_ref) {
    next if ( !exists $cpusers{ $pw->[0] } );
    $HOMES{ $pw->[0] } = $pw->[7];
    $UIDS{ $pw->[0] }  = $pw->[2];
    $GIDS{ $pw->[0] }  = $pw->[3];
}

# Check each cpuser file
print "Checking all cpuser files for package data...\n\n" if $verbose;
my $modified_this_user = 0;
foreach my $user (@users) {
    next if $user =~ m/^\./;
    $modified_this_user = 0;
    my $cpuser_ref = Cpanel::Config::LoadConfig::loadConfig( '/var/cpanel/users/' . $user );
    if ( exists $USERPLANS{$user} ) {
        my $plan = $USERPLANS{$user};
        if ( !exists $cpuser_ref->{'PLAN'} || $cpuser_ref->{'PLAN'} eq 'undefined' ) {
            $cpuser_ref->{'PLAN'}               = $plan;
            $RESTORED_DATA_LOG{$user}->{'PLAN'} = $plan;
            $modified_this_user                 = 1;
        }

        foreach my $key ( keys %{ $PLANKEYS{$plan} } ) {
            if ( !exists $cpuser_ref->{$key} || $cpuser_ref->{$key} eq '' ) {
                $cpuser_ref->{$key} = $PLANKEYS{$plan}->{$key};
                $RESTORED_DATA_LOG{$user}->{$key} = $PLANKEYS{$plan}->{$key};

                # log_it("$user: set key $key to $PLANKEYS{$plan}->{$key}\n");
                $modified_this_user = 1;
            }
        }

    }
    elsif ( !exists $cpuser_ref->{'PLAN'} || $cpuser_ref->{'PLAN'} eq '' ) {    # 'undefined' is a valid plan
        $RESTORED_DATA_LOG{$user}->{'PLAN'} = '';
    }

    if ( exists $ACCOUNT_CREATION_DATA{$user} ) {
        if ( !exists $cpuser_ref->{'OWNER'} || !$cpuser_ref->{'OWNER'} ) {
            $cpuser_ref->{'OWNER'} = $ACCOUNT_CREATION_DATA{$user}->{'OWNER'};
            $RESTORED_DATA_LOG{$user}->{'OWNER'} = $ACCOUNT_CREATION_DATA{$user}->{'OWNER'};

            # log_it("$user should have an owner but it was missing (set to $ACCOUNT_CREATION_DATA{$user}->{'OWNER'})\n");
            $modified_this_user = 1;
        }
        if ( ( !exists $cpuser_ref->{'STARTDATE'} || !int $cpuser_ref->{'STARTDATE'} ) && int $ACCOUNT_CREATION_DATA{$user}->{'STARTDATE'} ) {
            $cpuser_ref->{'STARTDATE'} = $ACCOUNT_CREATION_DATA{$user}->{'STARTDATE'};
            $RESTORED_DATA_LOG{$user}->{'STARTDATE'} = $ACCOUNT_CREATION_DATA{$user}->{'STARTDATE'};

            # log_it("$user should have a start date but it was missing (set to $ACCOUNT_CREATION_DATA{$user}->{'STARTDATE'})\n");
            $modified_this_user = 1;
        }
    }
    else {
        if ( !exists $cpuser_ref->{'OWNER'} || !$cpuser_ref->{'OWNER'} ) {
            $RESTORED_DATA_LOG{$user}->{'OWNER'} = '';
        }

        # STARTDATE is really not necessary, but will affect the WHM List Accounts
        if ( !exists $cpuser_ref->{'STARTDATE'} || !int $cpuser_ref->{'STARTDATE'} ) {
            $RESTORED_DATA_LOG{$user}->{'STARTDATE'} = '';
        }
    }

    if ( !exists $cpuser_ref->{'CONTACTEMAIL'} || !$cpuser_ref->{'CONTACTEMAIL'} ) {
        my @user_contact_email = Cpanel::ContactInfo::Email::getcontactemail( $user, $HOMES{$user}, $UIDS{$user}, $GIDS{$user} );
        if (@user_contact_email) {

            # log_it( "$user: should have an contactemail but it was missing (set to " . join( ',', @user_contact_email ) . "\n" );
            if ( $user_contact_email[0] && ( !exists $cpuser_ref->{'CONTACTEMAIL'} || !$cpuser_ref->{'CONTACTEMAIL'} ) ) {
                $cpuser_ref->{'CONTACTEMAIL'}               = $user_contact_email[0];
                $RESTORED_DATA_LOG{$user}->{'CONTACTEMAIL'} = $user_contact_email[0];
                $modified_this_user                         = 1;
            }
            if ( $user_contact_email[1] && ( !exists $cpuser_ref->{'CONTACTEMAIL2'} || !$cpuser_ref->{'CONTACTEMAIL2'} ) ) {
                $cpuser_ref->{'CONTACTEMAIL2'}               = $user_contact_email[1];
                $RESTORED_DATA_LOG{$user}->{'CONTACTEMAIL2'} = $user_contact_email[1];
                $modified_this_user                          = 1;
            }
        }
        ## These settings are optional
        # else {
        #     $RESTORED_DATA_LOG{$user}->{'CONTACTEMAIL'} = '';
        #     if ( !exists $cpuser_ref->{'CONTACTEMAIL2'} || !$cpuser_ref->{'CONTACTEMAIL2'} ) {
        #         $RESTORED_DATA_LOG{$user}->{'CONTACTEMAIL2'} = '';
        #     }
        # }
    }
    if ( !exists $cpuser_ref->{'USER'} ) {

        # log_it("$user: should have a user field but it was missing (set to $user)\n");
        $cpuser_ref->{'USER'}               = $user;
        $RESTORED_DATA_LOG{$user}->{'USER'} = $user;
        $modified_this_user                 = 1;
    }
    $changes_made = 1 if $modified_this_user;
    if ($modified_this_user) {
        if ( !$warn_only ) {
            $cpuser_ref->{'REPAIRTOOL_VERSION'} = $tool_version;
            Cpanel::Config::FlushConfig::flushConfig( "/var/cpanel/users/$user", $cpuser_ref, '=', $header );    # TODO: Should we set defaults on any other cpuser items?
        }
    }
}

my $restoration_message = $warn_only ? 'calculated' : 'restored';
my $logged = 0;
foreach my $user ( sort keys %RESTORED_DATA_LOG ) {
    $logged = 1;
    log_it("$user: missing plan items detected\n");
    foreach my $item ( sort keys %{ $RESTORED_DATA_LOG{$user} } ) {
        if ( $RESTORED_DATA_LOG{$user}->{$item} ne '' ) {
            log_it("    - item $restoration_message: $item -> $RESTORED_DATA_LOG{$user}->{$item}\n");
        }
        else {
            log_it("    - item missing!: $item\n");
        }
    }
}

if ($changes_made) {
    print "\n" if $verbose;
    if ($warn_only) {
        print "A list of users that have inconsistent cpuser information was created at\n\t/var/cpanel/logs/${log_file}.${now}\n";
        print "It is highly recommended that you restore these cpuser files from backups.\n";
    }
    else {
        print "Some users were found in an inconsistent state.\n";
        print "A log of the updated users was created at\n\t/var/cpanel/logs/${log_file}.${now}\n\n";
        print "It is recommended that you review this log and restore these cpuser files from backup\n";
        print "if possible since it is impossible to determine how some settings and limits\n";
        print "were previously configured\n\n";
        print "Running /scripts/updateuserdomains to finish update...\n";

        run_command('/scripts/updateuserdomains');
    }
}
else {
    print "\n" if $verbose;
    if ($logged) {
        print "A list of users that have inconsistent cpuser\ninformation was created at\n\t/var/cpanel/logs/${log_file}.${now}\n\n";
        print "!! No missing data was restored. !!\n\n";
    }
    else {
        print "No errors encountered\n\n" if $verbose;
    }
}

sub log_it {
    unless ( defined $log_fh ) {
        mkdir( '/var/cpanel/logs', 0700 ) unless ( -d '/var/cpanel/logs' );
        my $old_umask = umask(0077);    # Case 92381: Logs should not be world-readable.
        open $log_fh, '>', "/var/cpanel/logs/${log_file}.${now}";
        umask($old_umask);
        if ( !$log_fh ) {
            die "Couldn't open /var/cpanel/logs/${log_file}.${now} for writing: $!";
        }
    }
    print $log_fh @_;
    print @_ if $verbose;
}

sub run_command {
    if ($verbose) {
        system(@_);
    }
    else {
        Cpanel::SafeRun::Errors::saferunallerrors(@_);
    }
}
