#!/usr/local/cpanel/3rdparty/bin/perl

# cpanel - SOURCES/whmcs_post_install              Copyright 2022 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

package scripts::whmcs_post_install;

use strict;
use warnings;

use lib "/usr/local/cpanel/cpaddons/";

use Cpanel::MysqlUtils::Connect        ();
use Cpanel::Encoder::Tiny              ();
use cPanel::Automation::WHMCS::BestPHP ();    # provided by this cpaddon
use Cpanel::SafeRun::Object            ();
use File::Path                         ();
use Getopt::Long                       ();
use Pod::Usage                         ();

my $NOT_IN_CONFIG = 0;
my $NOT_IN_STDIN  = 1;
my $OK            = 2;

# Required fields.
my %required = (
    db_host       => $NOT_IN_CONFIG,
    db_port       => $NOT_IN_CONFIG,
    db_username   => $NOT_IN_CONFIG,
    db_password   => $NOT_IN_CONFIG,
    db_name       => $NOT_IN_CONFIG,
    adminuser     => $NOT_IN_CONFIG,
    adminpassword => $NOT_IN_CONFIG,
    systemurl     => $NOT_IN_CONFIG,
    admindir      => $NOT_IN_CONFIG,
);

=head1 DESCRIPTION

This script completes the automated setup steps for whmcs.

=head1 USAGE

whmcs_post_install --dir={string} --order={csv string}

Where:

=over

=item * --dir= - the install directory for whmcs on the file system.

=item * --order= - the order parameters are sent via STDIN. Use a comma delimited string.

Example:

  db_host,db_port,db_username,db_password,db_name,adminuser,adminpassword,systemurl

All of the following must be provided, but any order is fine. Make sure to send the parameters to STDIN in the order you select.

=over

=item * db_host - the database hostname. If on the same server use 'localhost', otherwise it must be a resolvable hostname.

=item * db_port - the port the database listens on. May be blank for the default.

=item * db_username - the username with permission to access the database.

=item * db_password - the password for the above database user.

=item * db_name - the name of the database where whmcs data is stored.

=item * adminuser - the desired whmcs administrator user name.

=item * adminpassword - the desired whmcs administrator password.

=item * systemurl - the url to the site where users will access whmcs.

=back

=back

=cut

exit( __PACKAGE__->run(@ARGV) ) if !caller;

=head2 run(@ARGS)

Modulino for the whmcs_post_install program.

=head3 ARGUMENTS

HASH with the following structure:

=over

=item installdir

String - The location where whmcs php files are located for the specific install. Required.

=back

=head3 RETURNS

1 if successful

=head3 THROWS

=over

=item * When you do not pass in the required arguments.

=item * When the installer.php script is missing or unreadable.

=item * When the php runtime can not be run the user.

=item * When the installer.php script fails in certain ways.

=back

=cut

sub run {
    my ( $self, @args ) = @_;
    my ( $help, $man, $dir, $order, $verbose, $debug ) = ( 0, 0, '', '', 0, 0 );

    Getopt::Long::GetOptionsFromArray(
        \@args,
        'help|?'    => \$help,
        'man'       => \$man,
        'dir=s'     => \$dir,
        'order=s'   => \$order,
        'verbose|v' => \$verbose,
        'debug'     => \$debug,

    ) or Pod::Usage::pod2usage(2);
    Pod::Usage::pod2usage(1)                              if $help;
    Pod::Usage::pod2usage( -exitval => 0, -verbose => 2 ) if $man;

    if ( !$dir ) {
        die 'You must pass the install directory parameter to this script: --dir';
    }

    if ( !$order ) {
        die 'You must pass the configuration order parameter to this script: --order';
    }

    my @props = split( m{,}, $order );
    my $prop;
    foreach $prop (@props) {
        $required{$prop} = $NOT_IN_STDIN;
    }

    if ( my @missing = grep { $required{$_} == $NOT_IN_CONFIG } keys %required ) {
        die "You did not provide some required keys in the --order argument: " . join ', ', @missing;
    }

    my @stdin;
    {
        local $/;
        @stdin = split( m{\n}, readline( \*STDIN ) );
    }

    my %config_opts;
    foreach $prop (@props) {
        if ( !@stdin ) {
            my @not_provided = grep { $required{$_} == $NOT_IN_STDIN } keys %required;
            die "You did not send the values for the following options via STDIN: " . join( ', ', @not_provided );
        }

        my $val = shift @stdin;
        $config_opts{$prop} = $val;

        $required{$prop} = $OK;
    }

    my $dbh = Cpanel::MysqlUtils::Connect::get_dbi_handle(
        database => $config_opts{db_name},
        dbserver => $config_opts{db_server},
        dbuser   => $config_opts{db_username},
        dbpass   => $config_opts{db_password},
    );

    # HTML encode the password before hashing it. This has nothing to do with preparing
    # it for display on a page but is still required for it to work because that's the
    # way WHMCS does it.
    my $html_encoded_password = Cpanel::Encoder::Tiny::safe_html_encode_str( $config_opts{adminpassword} );
    my $password_hash         = _generate_php_password_hash($html_encoded_password);

    $dbh->do(
        "UPDATE tbladmins set username=?,password=?,passwordhash=? where username='Admin';", {},
        Cpanel::Encoder::Tiny::safe_html_encode_str( $config_opts{adminuser} ),
        $password_hash,
        $password_hash,
    );
    $dbh->do(
        "UPDATE tblconfiguration set value=? where setting='SystemURL';", {},
        $config_opts{systemurl},
    );

    eval { _rename_admin_directory( $dir, \%config_opts ); };
    if ( my $exception = $@ ) {
        warn "Failed to rename admin directory: $exception";
    }

    File::Path::rmtree("$dir/install");

    return 0;

}

sub _generate_php_password_hash {
    my ($password) = @_;

    # Use PHP to generate the password hash so we have the best chance of
    # our generation remaining compatible with WHMCS as PHP evolves.
    # Read the password from stdin so we don't disclose it in the process
    # table or the environment.

    my $php_bin_path = cPanel::Automation::WHMCS::BestPHP::get_php_cli();
    my $run          = Cpanel::SafeRun::Object->new(
        program => $php_bin_path,
        args    => [
            '-r',
            '$password = rtrim(fgets(STDIN)); echo password_hash($password, PASSWORD_DEFAULT);',
        ],
        stdin   => $password,
        timeout => 30,
    );
    if ( $run->CHILD_ERROR() ) {
        die 'Could not generate the password hash: ' . $run->stderr();
    }

    return $run->stdout;
}

sub _rename_admin_directory {
    my ( $dir, $config_opts ) = @_;

    my ($admindir_without_slash) = $config_opts->{admindir} =~ m{^(admin[a-z0-9]{4})$}
      or die "Could not find a valid admin directory name in “$config_opts->{admindir}”.";

    rename $dir . '/admin', $dir . '/' . $admindir_without_slash
      or die "rename: $!";

    my $conf = $dir . '/configuration.php';
    open my $conf_fh, '>>', $conf or die "$conf: $!";
    print {$conf_fh} "\n\$customadminpath = '$admindir_without_slash';\n";
    close $conf_fh or die "$conf: $!";

    return;
}

1;
