package Cpanel::Easy::Utils::CloudLinux;

# cpanel - Cpanel/Easy/Utils/CloudLinux.pm        Copyright(c) 2014 cPanel, Inc.
#                                                           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 Cpanel::CloudLinux;
use Cpanel::FileUtils        ();
use Cpanel::Hostname         ();
use Cpanel::Version::Compare ();
use Cpanel::Version::Tiny    ();
use HTTP::Tiny               ();
use JSON::Syck               ();
no warnings qw(redefine);

=pod

=head1 Name

Cpanel::Easy::Utils::CloudLinux

=head1 Description

Various utility routines specific to the CloudLinux distribution

=head1 API

The following sections contain a description of each API in this module.

=cut

my $CLOUDLINUX_SUPPORT;

our $MANAGE2    = q{manage2.cpanel.net};
our $M2_TIMEOUT = 3600;                      # 1 hour
our $CAGEFSCTL  = '/usr/sbin/cagefsctl';
our $CAGEFSMP   = '/etc/cagefs/cagefs.mp';

# comment that denotes cagefs.mp modifications
our $KILROY = q{# ModSecurity has been enabled by EasyApache on};

my $HAS_CLOUDLINUX_SUPPORT;

# converts cpanel apache version number to cloudlinux version
sub cpanel_to_cloudlinux_version {
    my $self   = shift;
    my $cp_ver = shift;
    my $cl_ver;

    if ( $cp_ver eq 1 ) {
        $cl_ver = '1_3';
    }
    elsif ( $cp_ver eq 2 ) {
        $cl_ver = '2_0';
    }
    elsif ( $cp_ver eq '2_4' ) {
        $cl_ver = '2_2';    # Apache 2.4 uses same code as 2.2
    }
    else {
        $cl_ver = $cp_ver;
    }

    return $cl_ver;
}

# performs all the necessary work to ensure that mod_hostinglimits is properly
# compiled and installed
sub cloudlinux_compile_module {
    my $self    = shift;
    my $version = $self->cpanel_to_cloudlinux_version(shift);
    my $apdir   = $self->{'base_dir'};

    return $self->run_system_cmd_returnable( [ "$apdir/bin/apxs", '-i', '-c', "-DAPACHE$version", qw(-llve ../mod_hostinglimits.c) ] );
}

# Ensure there are no references to any hostinglimit configuration files (old or new)
sub cloudlinux_clean_modconf {
    my $self = shift;

    # remove references to these config files from httpd.conf
    for my $conf (qw( sumodcgid.conf modhostinglimits1_3.conf modhostinglimits.conf mod_hostinglimits.conf)) {
        my $path = sprintf( '%s/conf/%s', $self->{'base_dir'}, $conf );
        $self->strip_from_httpconf(qq{Include "$path"});
    }

    # remove old config files
    my $basedir = sprintf( '%s/conf/conf.d', $self->{'base_dir'} );
    if ( -d $basedir ) {
        foreach my $file (qw(modhostinglimits.conf sumodcgid.conf mod_fcgid.conf)) {
            unlink sprintf( '%s/%s', $basedir, $file );
        }
    }
}

# sets up httpd.conf and installs proper mod_hostinglimits configuration
sub cloudlinux_setup_configs {
    my $self = shift;
    my @result;

    @result = $self->ensure_loadmodule_in_httpdconf( 'hostinglimits', 'mod_hostinglimits.so' );
    return (@result) unless $result[0];

    # Clean up old configs
    $self->cloudlinux_clean_modconf();

    # Copy new config to proper location
    my $src = sprintf( '%s/modhostinglimits.conf',      $self->{'opt_mod_src_dir'} );
    my $dst = sprintf( '%s/conf/modhostinglimits.conf', $self->{'base_dir'} );
    @result = $self->copy_file( $src, $dst );
    return (@result) unless $result[0];
    chown 0, 0, $dst;
    chmod 0600, $dst;

    my $userfile = "$self->{'base_dir'}/conf/modhostinglimits.user.conf";
    if ( !-f $userfile ) {
        Cpanel::FileUtils::touchfile($userfile)
          or return ( 0, q{Could not touch '[_1]': [_2]}, $userfile, $! );
    }

    # Make sure module declared in http conf
    @result = $self->include_directive_handler(
        {
            'include_path' => $dst,
            'addmodule'    => qr{mod_hostinglimits},
            'loadmodule'   => qr{hostinglimits_module},
        }
    );

    return (@result);
}

# primary interface for installing CloudLinux module, mod_hostinglimits
sub cloudlinux_update {
    my $self = shift;
    my @result;

    my $mpm = $self->get_configured_mpm();

    # Ensure that we don't install mod_hostinglimits when MPM ITK is selected
    # NOTE: It's disabled with ITK because it would cause a process to
    #       "enter" into LVE twice.
    if ( $self->has_cloudlinux_support() && $mpm ne 'itk' ) {
        @result = $self->cloudlinux_compile_module( $self->{'working_profile'}{'Apache'}{'version'} );

        if ( $result[0] ) {
            @result = $self->cloudlinux_setup_configs();
        }
    }
    else {
        $self->cloudlinux_clean_modconf();
        @result = ( 1, 'mod_hostinglimits is not required' );
    }

    return @result;
}

# This checks to see if the current OS has cloudlinux installed.
sub has_cloudlinux_support {
    my $self = shift;
    return $CLOUDLINUX_SUPPORT if defined $CLOUDLINUX_SUPPORT;
    $CLOUDLINUX_SUPPORT = 0;
    $CLOUDLINUX_SUPPORT = Cpanel::CloudLinux::installed();
    return $CLOUDLINUX_SUPPORT;
}

# This checks to see if the current OS can install CloudLinux... not
# that it's currently installed.
sub is_cloudlinux_supported {
    return Cpanel::CloudLinux::supported_envtype();
}

# Add an directory to cagefs so that it's visible by cagefs users.
sub cloudlinux_add_cagefsmp {
    my $self      = shift;
    my $directory = shift;
    my @result;

    # ensure config file exists
    unless ( -e $CAGEFSMP ) {
        if ( open( my $fh, '>', $CAGEFSMP ) ) {
            print $fh "$directory\n";
            close $fh;
            chmod 0666, $CAGEFSMP;
            @result = ( 1, q{Ok} );
        }
        else {
            @result = ( 0, q{Failed to create config, '[_1]': '[_2]'}, $CAGEFSMP, $! );
        }

        return @result;
    }

    # try to add this directory to the cagefs.mp config file
    if ( $self->is_string_in_file( $CAGEFSMP, qr/$directory/ ) ) {
        @result = ( 1, 'Ok' );
    }

    # not in there, add it
    else {
        @result = $self->add_to_file_after_regex( $CAGEFSMP, $directory, "^\/" );
    }

    return @result;
}

# Ensures that the given mountpoint exists and is free of mp modifiers (#, !, and @)
# Adds an informative comment above the updated entry, will not accumulate old comments
sub cloudlinux_ensure_cagefsmp {
    my $self       = shift;
    my $mountpoint = shift;

    return ( 0, q{Mountpoint parameter must be provided.} ) if !$mountpoint;

    return ( 1, q{CageFS mount point file, '[_1]', not found for inspection.}, $CAGEFSMP ) if !-e $CAGEFSMP;

    my $fh = eval { open my $fh, '<', $CAGEFSMP; $fh } || return ( 0, q{Failed to open config file for checking, '[_1]': '[_2]'}, $CAGEFSMP, $! );

    my @ignore_entries = ();
    my @fix_entries    = ();

    # ensure record separator is newline for this scope
    local $/ = "\n";

    # filter $CAGEFSMP for lines to ignore and lines to fix
  FILTERIGNORED:
    while ( my $line = <$fh> ) {
        chomp $line;

        # don't save $KILROY since it's going to be added in an updated form later
        next FILTERIGNORED if $line =~ m/^$KILROY/;

        if ( $line =~ m/$mountpoint/ ) {
            push @fix_entries, $line;
        }
        else {
            push @ignore_entries, $line;
        }
    }

    close $fh;    # existing $CAGEFSMP

    # do nothing if there are no entries that need fixing
    if ( not @fix_entries ) {
        push @fix_entries, $mountpoint;
    }

    # eval encapsulates temp file writing and replacement of $CAGESFMP
    my $okay = eval {

        # fix mount point entries and ensure each mount point is unique
        my $uniq = {};
        foreach my $e (@fix_entries) {
            $e =~ s/^[#!@]+\s*\//\//g;    # completely remove offending symbols, including white space
            ++$uniq->{$e};
        }
        @fix_entries = keys %$uniq;

        # write new cagefs.mp ignored content to a temporary file
        my $tmpobj = Cpanel::TempFile->new();
        my ( $filename, $tmpfh ) = $tmpobj->file();
        foreach my $e (@ignore_entries) {
            print $tmpfh "$e\n";
        }

        # generate comment indicating that "EasyApache was here" in cagefs.mp
        my $time = localtime;
        print $tmpfh qq{$KILROY $time\n};

        # write "fixed" cagefs.mp entry(ies) to temp file
        foreach my $e (@fix_entries) {
            print $tmpfh "$e\n";
        }

        close $tmpfh;

        # atomically replace $CAGEFSMP with fixed temporary file - assuming not moving across an FS boundary
        File::Copy::move( $filename, qq{$CAGEFSMP} );

        1;
    };

    if ($@) {
        return ( 0, q{Failure when attempting to modify configuration file, '[_1]': '[_2]'}, $CAGEFSMP, $@ );
    }
    elsif ( not $okay ) {
        return ( 0, q{Unknown failure when attempting to modify configuration file, '[_1]'}, $CAGEFSMP );
    }

    return ( 1, q{Ok} );
}

# $CAGEFSMP is generated by the initialization procedure
sub cloudlinux_cagefs_initialized {
    my $self = shift;
    if ( -s $CAGEFSMP ) {

        # If we're properly initialized, there should be a nonzero
        # number of mounts already in the $CAGEFSMP file
        # Mounts are directories possibly preceded by #, !, or @
        my $slurp = $self->slurp_file_chomped($CAGEFSMP);
        my $count = grep { m~^\s*[!@#]?/\w+~ } split( /\n/, $slurp );
        return $count;
    }
    return 0;
}

# Determines if cagefs is enabled on a machine or not
# NOTE: cagefsctl doesn't emit correct UNIX exit codes, have to parse output
sub cloudlinux_cagefs_enabled {
    my $self = shift;
    my ( $rc, $out ) = $self->run_system_cmd_errors( $CAGEFSCTL, q{--cagefs-status} );
    return ( $out =~ /enabled/i ? 1 : 0 );    # cagefs not enabled
}

# Issue cagefs remount on a CloudLinux machine
sub cloudlinux_cagefs_remount {
    my $self = shift;

    my ( $rc, $out ) = $self->run_system_cmd_errors( $CAGEFSCTL, q{--remount-all} );
    my @result;

    # Unfortunately, cagefsctl doesn't return a proper UNIX exit code,
    # so we have to parse the output
    if ( $out =~ /error/im ) {
        @result = ( 0, qq{Failed to remount cagefs directories: $out} );
    }
    else {
        @result = ( 1, q{Ok} );
    }

    return @result;
}

##
# This method performs a web request to manage2 API
# to get purchase information for CloudLinux from the
# customer.
#
# Return: hash reference
# {
#   'is_url',
#   'server_timeout',
#   'url',
#   'email'
# }
##
sub get_purchase_cloudlinux_data {
    my $self = shift;
    my $key  = q{manage2_cloudlinux};
    my $data = $self->get_cache( 'name' => $key, 'timeout' => $M2_TIMEOUT );

    # Short-circuit any checking for cP versions older than 11.40, which are
    # completely unsupported, or if the disable preference is checked
    if ( Cpanel::Version::Compare::compare( $Cpanel::Version::Tiny::VERSION_BUILD, '<', '11.40' ) || ( defined $self->{'_'}{'prefs'}{'disable_cloudlinux_infrastructure'} && $self->{'_'}{'prefs'}{'disable_cloudlinux_infrastructure'} == 1 ) ) {
        $data = {
            'is_url'          => 0,
            'server_timeout'  => 0,
            'disable_upgrade' => 1,
        };
    }

    unless ($data) {
        my $hname = Cpanel::Hostname::gethostname();
        my $url = sprintf( 'https://%s/cloudlinux.cgi?hostname=%s', $MANAGE2, $hname );

        my $raw_resp = HTTP::Tiny->new( 'timeout' => 10 )->get($url);
        my $json_resp;

        if ( $raw_resp->{'success'} ) {
            eval { $json_resp = JSON::Syck::Load( $raw_resp->{'content'} ) };

            if ($@) {
                $json_resp = undef;
                $self->print_alert( q{Invalid server response from [_1]}, $MANAGE2 );
            }
        }

        $data = {
            'is_url'          => 0,
            'server_timeout'  => 0,
            'disable_upgrade' => 0
        };

        if ($json_resp) {
            if ( $json_resp->{'disabled'} ) {
                $data->{'disable_upgrade'} = 1;
            }
            else {
                if ( $json_resp->{'url'} ) {
                    $data->{'is_url'} = 1;
                    $data->{'url'}    = $json_resp->{'url'};
                }
                else {
                    $data->{'email'} = $json_resp->{'email'};
                }
            }
        }
        else {
            $data->{'server_timeout'} = 1;
        }

        $self->set_cache( 'name' => $key, 'data' => $data );
    }

    return $data;
}

=pod

=head2 I<< Cpanel::Easy::Utils::CloudLinux::cloudlinux_cagefs_updatefs() >>

When the Linux environment is updated after running EasyApache, it's important
that these changes are reflected in the virtual cagefs environment provided by
CloudLinux.  This basically ensures that libraries, scripts, and various
dependencies are removed, updated, or added to each user's cagefs environment.

This simply executes: cagefsctl --update --force-update

  Input:
    N/A

  Output:
    ARRAY -- On success, 2 element array with 0th element being 1.
             On failure, 2 element array with 0th element being 0,
               and the 1th element containing an error message.

  Note:
    This routine returns success (thus emulating a NOOP) if it determines
    nothing needs to occur for the device type.  For example, this can
    occur if the command is executed on a non-CloudLinux machine, or
    the current installation of CageFS hasn't been initialized yet.
=cut

sub cloudlinux_cagefs_updatefs {
    my $self = shift;
    my @ret;

    # despite cloudlinux_cagefs_initialized() telling us, an additional check
    # is placed below since we know the actual output that's being displayed.  In
    # the event it's never executed but an error is returned, that's great.. we're
    # doing the correct job.
    if ( $self->has_cloudlinux_support() && $self->cloudlinux_cagefs_initialized() ) {
        $self->print_alert(q{Updating the CloudLinux CageFS virtual filesystem});

        @ret = $self->run_system_cmd_errors( $CAGEFSCTL, qw/--update --force-update/ );

        if ( $ret[0] ) {    # non-zero == cmd error
            if ( $ret[1] =~ /does\s+NOT\s+exist\s+or\s+is\s+empty/im ) {
                @ret = ( 1, q{CageFS hasn't been initialized yet; skipping update} );
            }
            else {
                $ret[0] = 0;    # return standard boolean error
            }
        }
        else {
            @ret = qw( 1 Ok );
        }
    }
    else {
        @ret = ( 1, q{CloudLinux must be installed, and CageFS initialized to update environment} );
    }
}

=pod

=head2 I<< Cpanel::Easy::Utils::CloudLinux::cloudlinux_post_confgen_command() >>

Wrapper routine called by main EasyApache run-time to ensure CloudLinux
instructions during the 'post-confgen-command' action are executed, but
actually resides in the same module of related code.

  Input: N/A

  Output: N/A

  Note: Should only be called once!

=cut

sub cloudlinux_post_confgen_command {
    my $code = sub {
        my $self = shift;
        $self->cloudlinux_cagefs_updatefs();
    };

    return $code;
}

1;

__END__

