package CCSHooks;

# cpanel - /var/cpanel/perl5/lib/CCSHooks.pm       Copyright 2019 cPanel, L.L.C.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

use strict;
use warnings;

use lib '/var/cpanel/perl';

# CCS specific modules, thus the 'use lib' line above.
# These *MUST* come in here, due to most cP bins doing pretty cool manouevers
# where the code engages in insane forgetting of @INC entries you added here.
# This causes requiring these later to fail.
use Cpanel::CCS::DBUtils   ();
use Cpanel::CCS::Delegates ();
use Cpanel::CCS::DomainTLS ();
use Cpanel::CCS::Constants ();
use Cpanel::CCS::Userdata  ();

our $ccs_dir     = Cpanel::CCS::Constants::CCS_CPDATA_DIR();
our $install_dir = Cpanel::CCS::Constants::CCS_INSTALL_DIR();

=head1 NAME

CCSHooks

=head1 SYNOPSIS

See cPanel standardized hooks documentation on documentation.cpanel.net
then read the describe subroutine, as this is a script hook module.
Most hook actions are basically just wrappers around _doit subroutine, as
most actions just require rebuilding the CCS user store.

=head1 DESCRIPTION

See the 'describe' subroutine ;)

=head1 SUBROUTINES

=head2 describe

Returns a gigantic ARRAYREF of hook action descriptions for consumption
by manage_hooks.

=cut

sub describe {
    my @normal_actions = map {
        {
            'category' => 'Whostmgr',
            'event'    => $_,
            'stage'    => 'post',
            'hook'     => 'CCSHooks::_doit',
            'exectype' => 'module',
        }
    } (
        'Accounts::Create',
        'Accounts::Remove',
        'Accounts::suspendacct',
        'Accounts::unsuspendacct',
        'Hostname::sethostname',
        'Domain::park',
        'Domain::addsubdomain',
    );
    my @admin_actions = map {
        {
            'category' => 'Cpanel',
            'event'    => $_,
            'stage'    => 'post',
            'hook'     => 'CCSHooks::_do_adminbin',
            'exectype' => 'module',
        }
    } (
        'UAPI::Email::add_pop',
        'UAPI::Email::delete_pop',
        'UAPI::Email::suspend_login',
        'UAPI::Email::unsuspend_login',
        'Api2::Email::addpop',
        'Api2::Email::delpop',
        'Api1::Email::addpop',
        'Api1::Email::delpop',
        'Api2::SubDomain::addsubdomain',
        'Api2::AddonDomain::addaddondomain',
        'Api2::Park::park',
        'Api1::Park::park',
    );
    my @domain_del_actions = map {
        {
            'category' => 'Cpanel',
            'event'    => $_,
            'stage'    => 'post',
            'hook'     => 'CCSHooks::admin_domainremove',
            'exectype' => 'module',
        }
    } (
        'Api2::SubDomain::delsubdomain',
        'Api2::AddonDomain::deladdondomain',
        'Api2::Park::unpark',
        'Api1::Park::unpark',
    );
    my $hook_ar = [
        @normal_actions,
        @admin_actions,
        @domain_del_actions,
        {
            'category' => 'Whostmgr',
            'event'    => 'Accounts::Modify',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::modify',
            'exectype' => 'module',
        },
        {
            'category' => 'scripts',
            'event'    => 'addpop',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::_do_note',
            'exectype' => 'module',
        },
        {
            'category' => 'scripts',
            'event'    => 'delpop',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::_do_note',
            'exectype' => 'module',
        },
        {
            'category' => 'Whostmgr',
            'event'    => 'Domain::unpark',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::whostmgr_deldomain',
            'exectype' => 'module',
        },
        {
            'category' => 'Whostmgr',
            'event'    => 'Domain::delsubdomain',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::whostmgr_deldomain',
            'exectype' => 'module',
        },
        {
            'category' => 'PkgAcct',
            'event'    => 'Restore',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::pkgacct_restore',
            'exectype' => 'module',
        },
        {
            'category' => 'Whostmgr',
            'event'    => 'SSL::install_service_ssl_certificate',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::APNS_post_install_hook',
            'exectype' => 'module',
        },
        {
            'category' => 'Whostmgr',
            'event'    => 'SSL::installssl',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::copy_certs_for_domains',
            'exectype' => 'module',
        },
        {
            'category' => 'Whostmgr',
            'event'    => 'AutoSSL::installssl',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::copy_certs_for_domains',
            'exectype' => 'module',
        },
        {
            'category' => 'Whostmgr',
            'event'    => 'SSL::delssl',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::remove_certs_for_domains',
            'exectype' => 'module',
        },
        {
            'category' => 'Cpanel',
            'event'    => 'UAPI::SSL::install_ssl',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::admin_certcopy',
            'exectype' => 'module',
        },
        {
            'category' => 'Cpanel',
            'event'    => 'UAPI::SSL::delete_ssl',
            'stage'    => 'post',
            'hook'     => 'CCSHooks::admin_certremove',
            'exectype' => 'module',
        },
    ];

    # NOTE: We do not need to hook to UAPI::UserManager::create_user or
    # delete_user, because those functions call Email::add_pop and delete_pop

    return $hook_ar;
}

=head2 modify

Hook that runs when modifyacct runs

=cut

sub modify {
    my ( $context, $data ) = @_;    # Get the data that the system passes to the hook.

    my $user      = $data->{'user'};
    my $newuser   = $data->{'newuser'};
    my $domain    = $data->{'domain'};
    my $olddomain = $domain;
    $newuser = $user if !defined $newuser;

    # In the case of a modify, the email accounts json cache that we depend on
    # is not updated, we will force that update

    my $hr           = Cpanel::CCS::Userdata::load_persistence_file();
    my $user_lookup  = $hr->{'users'};
    my $group_lookup = $hr->{'groups'};

    # Note when the domain changes, there is no indication of what the
    # previous domain is from the hook, we will have to guess the domain

    # determine if the domain has changed

    foreach my $email ( keys %{$user_lookup} ) {
        if ( $email =~ m/^$user@(.*)$/ ) {
            $olddomain = $1;
            last;
        }
    }

    # Just set newuser to user if not set, same with domain
    $newuser ||= $user;
    $domain  ||= $olddomain;
    if ( $domain && $user && ( $newuser ne $user ) )    # username changed
    {
        my $group_lookup = Cpanel::CCS::Userdata::get_group_uuid_map();
        $group_lookup->{$newuser} = $group_lookup->{$user};
        delete $group_lookup->{$user};

        my $oldemail = $user . '@' . $olddomain;
        my $newemail = $newuser . '@' . $domain;

        $user_lookup->{$newemail} = $user_lookup->{$oldemail};
        delete $user_lookup->{$oldemail};
    }

    if ( $domain && ( $olddomain ne $domain ) )    # domain changed
    {
        # the domain name changed, so
        my @keys = keys %{$user_lookup};
        foreach my $email (@keys) {

            # deal with the emails of the primary domain changing
            if ( $email =~ m/^(.*)\@$olddomain$/ ) {
                my $user     = $1;
                my $newemail = $user . '@' . $domain;

                $user_lookup->{$newemail} = $user_lookup->{$email};
                delete $user_lookup->{$email};
            }

            # deal with the emails of a subdomain changing
            if ( $email =~ m/^(.*)\@(.*)\.$olddomain$/ ) {
                my $user      = $1;
                my $subdomain = $2;
                my $newemail  = $user . '@' . $subdomain . '.' . $domain;

                $user_lookup->{$newemail} = $user_lookup->{$email};
                delete $user_lookup->{$email};
            }
        }
    }

    goto &_doit;
}

=head2 whostmgr_deldomain

rebuilds CCS users and removes any certs on CCS for deleted domain from WHM.

=cut

sub whostmgr_deldomain {
    my ( $context, $data ) = @_;    # Get the data that the system passes to the hook.
    _doit( $context, $data );
    eval { remove_certs_for_domains( $context, $data ); };
    return $@ ? ( 0, $@ ) : ( 1, "Success" );
}

=head2 pkgacct_restore

Hook that runs during scripts/restorepkg

=cut

sub pkgacct_restore {
    my ( $context, $data ) = @_;    # Get the data that the system passes to the hook.

    my $ccs_dir = $data->{'extract_dir'} . '/calendar_and_contacts';
    return if ( !-d $ccs_dir );

    my $backup_persist_fname = "$ccs_dir/persistence.json";
    return if ( !-e $backup_persist_fname );

    print "Updating Email/UUID database\n";
    my $backup_persist_hr = Cpanel::JSON::SafeLoadFile($backup_persist_fname);
    foreach my $email ( keys %{ $backup_persist_hr->{'users'} } ) {
        print "Updating $email with " . $backup_persist_hr->{'users'}->{$email} . "\n";
        Cpanel::CCS::Userdata::update_user_uuid( $email, $backup_persist_hr->{'users'}->{$email} );
    }

    # commit the new uuids to the CCS database
    Cpanel::CCS::Userdata::commit_updated_uuids();

    print "Populating mailuser directory entries for CCS user $data->{'user'}...";
    Cpanel::CCS::Userdata::output_xml( Cpanel::CCS::Userdata::create_directory( $data->{'user'} ) );
    print "Done!\n";

    # now import the ics files
    # the calendarserver_export script MUST be run from /opt/cpanel-ccs or it
    # fails.
    my %type_map = (
        'calendars' => 'ics',
        'contacts'  => 'vcard',
    );

    # Open a database handle
    # Connect to the DB but disable autocommit, as I'll be managing that
    print "[DEBUG] CCSHooks::pkgacct_restore: Get DBH\n" if $ENV{'CPANEL_DEBUG_LEVEL'};
    my $dbh = Cpanel::CCS::DBUtils::get_dbh();

    print "[DEBUG] CCSHooks::pkgacct_restore: Require modules\n" if $ENV{'CPANEL_DEBUG_LEVEL'};

    # We'll need these later.
    require Digest::MD5;
    require File::Basename;

    print "Restoring event data state from backup...\n";
    foreach my $type ( keys(%type_map) ) {

        # Each file represents a collection for a user/UUID.
        my $dir   = "$ccs_dir/$type";
        my @files = glob("$dir/*.$type_map{$type}");
        foreach my $file (@files) {
            my $uuid = File::Basename::basename( $file, qw{.ics .vcard} );
            $uuid = substr( $uuid, 0, index( $uuid, "_$type" ) );
            print "Restoring $type for UUID $uuid...\n";

            # Try to create calendar/contacts collection if it does not exist.
            # Returns the calendar/contact collection's "resource_id"
            # so that we can do INSERTs on the DB properly.
            my $resource_id = Cpanel::CCS::DBUtils::insert_collection_if_needed_for_type( $type, $dbh, $uuid );

            # So, we used to not really clear out old data for the UUID. Do this
            # up front so that any "bad" entries don't cause trouble (or result
            # in odd/irrelevant/duped data from showing for users).
            Cpanel::CCS::DBUtils::scrub_data_for_uuid(
                'type'        => $type,
                'dbh'         => $dbh,
                'resource_id' => $resource_id,
                'uuid'        => $uuid,
            );

            # Now insert the event/vcard data.
            Cpanel::CCS::DBUtils::do_restorepkg_inserts_for_type( $type, $dbh, $file, $resource_id, $uuid );
        }
    }

    # Restore delegates if they exist.
    # Should probably make a bulk adder at some point?
    # Need to performance test this.
    print "Event data should now be restored.\n";
    my $delegates_fname = "$ccs_dir/delegates.json";
    return if ( !-e $delegates_fname );
    my $decoded = Cpanel::JSON::SafeLoadFile($delegates_fname);
    foreach my $delegate_hr (@$decoded) {
        print "Restoring delegate relation: Delegator '$delegate_hr->{'delegator'}' -> '$delegate_hr->{'delegatee'}'\n";
        local $@;
        eval {
            Cpanel::CCS::Delegates::manage(
                {
                    delegator => $delegate_hr->{'delegator'},
                    delegatee => $delegate_hr->{'delegatee'},
                    readonly  => $delegate_hr->{'readonly'},
                    action    => 'INSERT',
                    users_hr  => Cpanel::CCS::Userdata::get_users( $data->{'user'} ),
                    dbh       => $dbh,
                    cpuser    => $data->{'user'},
                }
            );
        };
        print "[ERROR]: $@" if $@;
    }

    return;
}

=head2 copy_certs_for_domains

Hook to be ran when autossl is ran so that it installs to CCS as well.
Also hook for installssl

=cut

sub copy_certs_for_domains {
    my ( $context, $data ) = @_;

    my @domains = map { $data->{$_} } grep { exists $data->{$_} && $data->{$_} } qw{web_vhost_name domain domain_set_name};
    die "No domains passed in to CCSHooks::copy_certs_for_domains! Context: $context" if !@domains;

    # TODO grab all domains on the web_vhost_name
    return Cpanel::CCS::DomainTLS::real_copy_certs_for_domains(@domains);
}

=head2 remove_certs_for_domains

Triggered on cert removal, removes cert from CCS' datastore

=cut

sub remove_certs_for_domains {
    my ( $context, $data ) = @_;

    # The payload from Domain:delsubdomain will look like
    # $data->{subdomain}  = 'sub'
    # $data->{rootdomain} = 'domain.tld'
    # Since we are looking for domain in the map below, just concat them with the domain key
    if ( exists $data->{subdomain} && exists $data->{rootdomain} ) {
        $data->{domain} = $data->{subdomain} . '.' . $data->{rootdomain};
    }

    my @domains = map { $data->{$_} } grep { exists $data->{$_} && $data->{$_} } qw{domain parent_domain host};
    die "No domains passed in to CCSHooks::remove_certs_for_domains! Context: $context" if !@domains;

    return Cpanel::CCS::DomainTLS::real_remove_certs_for_domains(@domains);
}

=head2 admin_certcopy

adminbin version of add_certs_for_domains

=cut

sub admin_certcopy {
    my ( $context, $data ) = @_;

    # Handle the API1 stuff, otherwise assume hash
    my @domains;
    if ( ref $data eq 'ARRAY' ) {
        @domains = ( $data->[0] );
    }
    else {
        $data = $data->{'result'} if ref $data->{'result'} eq 'HASH';
        @domains = map { $data->{$_} } grep { exists $data->{$_} && $data->{$_} } qw{domain parent_domain};
    }
    die "No domains passed in to CCSHooks::admin_certcopy! Context: $context" if !@domains;

    require Cpanel::AdminBin::Call;
    Cpanel::AdminBin::Call::call( 'Cpanel', 'ccs', 'INSTALLSSL', @domains );
    return;
}

=head2 admin_certremove

adminbin version of remove_certs_for_domains

=cut

sub admin_certremove {
    my ( $context, $data ) = @_;

    my @domains;
    if ( ref $data eq 'ARRAY' ) {
        @domains = ( $data->[0] );
    }
    else {
        @domains = map { $data->{$_} } grep { exists $data->{$_} && $data->{$_} } qw{domain parent_domain host};
    }
    die "No domains passed in to CCSHooks::admin_certremove! Context: $context" if !@domains;

    require Cpanel::AdminBin::Call;
    Cpanel::AdminBin::Call::call( 'Cpanel', 'ccs', 'DELSSL', @domains );
    return;
}

=head2 admin_domainremove

adminbin version of unpark/domain removal routines.

=cut

sub admin_domainremove {
    require Cpanel::AdminBin::Call;
    Cpanel::AdminBin::Call::call( 'Cpanel', 'ccs', 'OUTPUT_ACCOUNTS' );
    goto &admin_certremove;
}

=head2 APNS_post_install_hook

Queues config rebuild for CCS after the APNS certs are installed for CCS.

=cut

sub APNS_post_install_hook {
    my ( $context, $data ) = @_;
    if ( grep { $data->{'service'} eq $_ } qw{caldav_apns carddav_apns} ) {
        return _queue_config_rebuild();
    }
    return ( 1, "Success" );
}

sub _queue_config_rebuild {
    local $@;
    eval {
        require Cpanel::ServerTasks;
        Cpanel::ServerTasks::queue_task( ['CCSTasks'], 'rebuild_ccs_config' );
    };
    return $@ ? ( 0, $@ ) : ( 1, "Success" );
}

sub _doit {
    local $@;
    eval {
        require Cpanel::ServerTasks;

        # Schedule it for the same time 'rebuild_email_accounts_cache' runs
        # as then the deferral tags should prevent it from running before it
        Cpanel::ServerTasks::schedule_task( ['CCSTasks'], 60, 'rebuild_ccs_users' );
    };
    return $@ ? ( 0, $@ ) : ( 1, "Success" );
}

sub _do_adminbin {
    require Cpanel::AdminBin::Call;
    Cpanel::AdminBin::Call::call( 'Cpanel', 'ccs', 'OUTPUT_ACCOUNTS' );
    return;
}

# Note, this script already emits HTML tags, so presumably it is used in some
# interface somewhere? (scripts/addpop & delpop).
sub _do_note {
    my $msg = <<'EOM';
<br />
<pre>
********
* NOTE *
********

This script does not run in right user context to modify calendar users for the
email account you have just added or deleted.

If you wish for calendar users to be synchronized with the current state of
users on the system, please run:
/opt/cpanel-ccs/bin/populate_mailuser_directory_for_ccs

As this will create or remove any calendar users based on the status of
currently existing cPanel and WebMail accounts.

If you wish for this to be done automatically for you during email account
creation or deletion, please consider using
/usr/local/cpanel/bin/uapi --user=$USER Email add_pop
or
/usr/local/cpanel/bin/uapi --user=?$USER Email delete_pop
instead.
</pre>
EOM
    print $msg;
}

1;

__END__
