package Cpanel::Easy::Utils::PHP;

# cpanel - Cpanel/Easy/Utils/PHP.pm               Copyright(c) 2015 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;
no warnings qw(redefine);

use Cpanel::Logger           ();
use Cpanel::Encoder::Tiny    ();
use Cpanel::FileUtils        ();
use Cpanel::PHPINI           ();
use Cpanel::PHPConfig        ();
use Cpanel::SafeRun          ();
use Cpanel::SafeFile         ();
use Cpanel::LangMods         ();
use Cpanel::Version::Compare ();

use IO::Handle ();

sub is_top_ns_version {
    my ( $self, $top_ns ) = @_;
    return 1 if $top_ns =~ m{^\d+(?:\_+\d+)*$};    # 2, 2_2, 2_2_2, 2__2__2__2
    return;
}

# Update php.ini for syntax changes across PHP versions
sub update_ini_file {
    my $self = shift;

    return ( 1, 'ok' ) if $self->get_param('makecpphp');    # the changes this method makes is not applicable to cpphp's php.ini

    my $php_version = shift || $self->{'__'}{'meta'}{'vers'};
    my $php_prefix  = shift || $self->{'__'}{'meta'}{'apache_prefix'}[0];

    my $v5_3_or_later = Cpanel::Version::Compare::compare( $php_version, '>=', '5.3' );
    my $v5_4_or_later = Cpanel::Version::Compare::compare( $php_version, '>=', '5.4' );

    my @directives_removed_in_v53 = qw(
      zend_extension_ts
      extension_ts
      zend_extension_debug
      zend_extension_debug_ts
      zend.ze1_compatibility_mode
    );
    my $directives_removed_in_v53 = join( '|', map( "\Q$_\E", @directives_removed_in_v53 ) );

    my @directives_removed_in_v54 = qw(
      allow_call_time_pass_reference
      define_syslog_variables
      highlight.bg
      magic_quotes_gpc
      magic_quotes_runtime
      magic_quotes_sybase
      register_globals
      register_long_arrays
      safe_mode
      safe_mode_gid
      safe_mode_include_dir
      safe_mode_exec_dir
      safe_mode_allowed_env_vars
      safe_mode_protected_env_vars
      session.bug_compat_42
      session.bug_compat_warn
      y2k_compliance
    );
    my $directives_removed_in_v54 = join( '|', map( "\Q$_\E", @directives_removed_in_v54 ) );

    my $ruid2_enabled = !!$self->get_ns_value_from_profile( 'Cpanel::Easy::ModRuid2', $self->{'working_profile'} );

    my $error_reporting_fixed = -e '/var/cpanel/version/php-5_3-error_reporting' ? 1 : 0;

    my $open = 0;
    for my $path (qw(lib etc)) {
        next if !-e $php_prefix . "/$path/php.ini";

        $self->print_alert( q{Editing “[_1]” ... }, $php_prefix . "/$path/php.ini" );

        my $ini_fh = IO::Handle->new();
        my $lock = Cpanel::SafeFile::safeopen( $ini_fh, '+<', $php_prefix . "/$path/php.ini" );
        if ($lock) {
            $open++;
            my @ini_file = readline($ini_fh);
            seek( $ini_fh, 0, 0 );
            foreach my $line (@ini_file) {
                if ($v5_3_or_later) {
                    if ( $line =~ /^\s*(?:$directives_removed_in_v53)\s*=/ ) {

                        # Obsolete in PHP 5.3+
                        print $ini_fh ';' . $line;
                        chomp $line;
                        $self->print_alert( q{Commenting out line: [_1]}, $line );
                        next;
                    }

                    elsif ( $v5_4_or_later && $line =~ /^\s*(?:$directives_removed_in_v54)\s*=/s ) {

                        # Obsolete in PHP 5.4+
                        print $ini_fh ';' . $line;
                        chomp $line;
                        $self->print_alert( q{Commenting out line: [_1]}, $line );
                        next;
                    }
                    elsif ( !$error_reporting_fixed && $line =~ /^\s*error_reporting\s*=\s*['"]?\s*(\S.*?)\s*['"]?\s*$/ ) {
                        my $current_val = $1;
                        if ( $current_val =~ /E_ALL/ && $current_val !~ /(\||E_DEPRECATED)/ ) {
                            $self->print_alert(q{Masking E_DEPRECATED from error_reporting setting});
                            $current_val .= ' & ~E_DEPRECATED';
                        }
                        print $ini_fh 'error_reporting = ' . $current_val . "\n";
                        if ( open my $flag_fh, '>', '/var/cpanel/version/php-5_3-error_reporting' ) {
                            print $flag_fh time() . "\n";
                            close $flag_fh;
                        }
                        next;
                    }
                    elsif ( $v5_4_or_later && $line =~ /^\s*allow_call_time_pass_reference/s ) {
                        $self->print_alert(q{Removing obsolete allow_call_time_pass_reference setting});
                        print $ini_fh '; ' . $line;
                        next;
                    }
                    elsif ( $line =~ /^\s*extension\s*=\s*['"]?\s*(\S*)\.so\b/ ) {
                        my $extension = $1;
                        my $comment = sprintf( '; %s was removed by EasyApache v%s on %s', $extension, $self->{'version'}, scalar localtime() );
                        if ( $v5_4_or_later && $extension eq 'sqlite' ) {
                            $self->print_alert(q{Removing sqlite extension from php.ini});
                            print $ini_fh "$comment (PHP v5.4.x incompatibility)\n; $line";
                            next;
                        }
                        elsif ( $ruid2_enabled && $extension eq 'eio' ) {
                            $self->print_alert(q{Mod Ruid2 is enabled; Removing the PHP Pecl 'eio' extension from php.ini});
                            print $ini_fh "$comment (Mod Ruid2 incompatibility)\n; $line";
                            next;
                        }
                        elsif ( $ruid2_enabled && $extension eq 'dio' ) {
                            $self->print_alert(q{Mod Ruid2 is enabled. Removing the PHP Pecl 'dio' extension from php.ini});
                            print $ini_fh "$comment (Mod Ruid2 incompatibility)\n; $line";
                            next;
                        }
                    }
                }
                elsif ( $php_prefix eq '/usr/local' ) {

                    # Versions prior to 5.3 will not understand the E_DEPRECATED setting
                    # This should only remove that setting when the php.ini file is in the normal PHP 5 install path
                    if ( $error_reporting_fixed && $line =~ /^\s*error_reporting\s*=\s*['"]?\s*(\S.*?)\s*['"]?\s*$/ ) {
                        my $error_reporting = $1;
                        unlink '/var/cpanel/version/php-5_3-error_reporting';
                        if ( $error_reporting =~ s/\s*(\||\^|&\s*~)\s*E_DEPRECATED//g ) {
                            $self->print_alert(q{Removing E_DEPRECATED from error_reporting setting});
                            print $ini_fh 'error_reporting = ' . $error_reporting . "\n";
                            next;
                        }
                    }
                }
                print $ini_fh $line;
            }

            truncate( $ini_fh, tell($ini_fh) );
            Cpanel::SafeFile::safeclose( $ini_fh, $lock );
        }
    }

    return ( 1, 'ok' ) if $open;
    return ( 0, 'Failed to open “[_1]” for read/write: [_2]', "$php_prefix/[lib|etc]/php.ini", $! );
}

# Update php.ini for timezonedb
#
# Sometimes when installing a new PHP, the builtin timezonedb is current,
# so there is no need for an external (PEAR/PECL) timezonedb,
# but the extension=timezonedb.so is still in php.ini, causing a warning each time PHP is run.
sub update_timezonedb_ini {
    my $self = shift;

    my $php_version = shift || $self->{'__'}{'meta'}{'vers'};
    my $php_prefix  = shift || $self->{'__'}{'meta'}{'apache_prefix'}[0];

    # Determine whether PHP is producing a missing .so error for timezonedb.so
    my @php_info = Cpanel::SafeRun::saferunallerrors( "${php_prefix}/bin/php", '-i' );
    my $timezonedb_error = ( grep /Unable to load.*timezonedb/, @php_info ) ? 1 : 0;

    return ( 1, 'ok' ) if !$timezonedb_error;

    my $open = 0;
    for my $path (qw(lib etc)) {
        next if !-e $php_prefix . "/$path/php.ini";

        $self->print_alert( q{Editing “[_1]” ... }, $php_prefix . "/$path/php.ini" );

        my $ini_fh = IO::Handle->new();
        my $lock = Cpanel::SafeFile::safeopen( $ini_fh, '+<', $php_prefix . "/$path/php.ini" );
        if ($lock) {
            $open++;
            my @ini_file = readline($ini_fh);
            seek( $ini_fh, 0, 0 );
            foreach my $line (@ini_file) {
                if ( $line =~ /^\s*extension\s*=.*timezonedb[.]so/ ) {
                    next;
                }
                print $ini_fh $line;
            }
            truncate( $ini_fh, tell($ini_fh) );
            Cpanel::SafeFile::safeclose( $ini_fh, $lock );
        }
    }

    return ( 1, 'ok' ) if $open;
    return ( 0, 'Failed to open “[_1]” for read/write: [_2]', "$php_prefix/[lib|etc]/php.ini", $! );
}

# Determine current selected PHP version
sub get_php_version {
    my ($self)      = @_;
    my $php_version = '';
    my $php_spec    = '';
    my $pns         = "Cpanel::Easy::PHP5";
    if ( $self->{'working_profile'}{$pns} ) {
        eval qq{require $pns;};
        if ( !$@ ) {
            for my $spec ( $pns->versions() ) {
                if ( $self->{'working_profile'}{ $pns . '::' . $spec } ) {
                    $php_spec    = $spec;
                    $php_version = "5.$spec";
                    $php_version =~ s{\_}{\.}g;
                    next;
                }
            }
        }
    }
    return $php_version if !wantarray;
    return ( $php_version, $php_spec );
}

sub strip_php_directives {
    my ($self) = @_;

    eval 'require Cpanel::FileUtils;';
    return ( 0, q{Could not load '[_1]': [_2]}, 'Cpanel::FileUtils', $@ ) if $@;

    my %err;

    my $rc = Cpanel::FileUtils::regex_rep_file(
        $self->_get_main_httpd_conf(),
        {
            qr{^([\s]*LoadModule[\s]*concurrent_php_module[\s]*libexec/concurrent_php[.]so)} => q{# Removed php directive: $1},
            qr{^([\s]*LoadModule[\s]*concurrent_php_module[\s]*modules/concurrent_php[.]so)} => q{# Removed php directive: $1},
            qr{^([\s]*LoadModule[\s]*suphp_module[\s]*libexec/mod_suphp[.]so)}               => q{# Removed php directive: $1},
            qr{^([\s]*LoadModule[\s]*suphp_module[\s]*modules/mod_suphp[.]so)}               => q{# Removed php directive: $1},
            qr{^([\s]*LoadModule[\s]*fcgid_module[\s]*modules/mod_fcgid[.]so)}               => q{# Removed php directive: $1},
            qr{^([\s]*suPHP.*$)}                                                             => q{# Removed php directive: $1},
            qr{^([\s]*suPHP.*$)}                                                             => q{# Removed php directive: $1},
            qr{^([\s]*AddHandler.*x-httpd-php.*$)}                                           => q{# Removed php directive: $1},
            qr{^([\s]*AddType.*x-httpd-php.*$)}                                              => q{# Removed php directive: $1},
            qr{^([\s]*LoadModule[\s]*php[\d]_module[\s]*libexec/libphp[\d][.]so)}            => q{# Removed php directive: $1},
            qr{^([\s]*LoadModule[\s]*php[\d]_module[\s]*libexec/libphp[\d][.]so)}            => q{# Removed php directive: $1},
            qr{^([\s]*LoadModule[\s]*php[\d]_module[\s]*modules/libphp[\d][.]so)}            => q{# Removed php directive: $1},
            qr{^([\s]*AddModule[\s]*mod_php[\d][.]c)}                                        => q{# Removed php directive: $1},
        },
        \%err,
    );

    return $rc = ( 1, 'ok' ) if $rc;
    return ( 0, 'Could not remove LoadModule from httpd.conf' );
}

sub setup_php_include {
    my $self = shift;

    my $php5 = $self->get_ns_value_from_profile( 'Cpanel::Easy::PHP5', $self->{'working_profile'} );

    #TODO: Remove the 'php4' key once Cpanel::PHPConfig no longer enables PHP4 by default (Cases 60118, 60131)
    my $rc = Cpanel::PHPConfig::setup_default_phpconf(
        {
            '_easyapache_setting' => {
                'php4' => 0,
                'php5' => $php5,
            },
        }
    );

    return ( 0, q{'[_1]' did not return true}, 'Cpanel::::PHPConfig::setup_phpconf_link()' ) if !$rc;

    $self->setup_php_symlinks() if ( $Cpanel::PHPConfig::VERSION eq '1.0' );

    return $self->include_directive_handler(
        {
            'include_path' => '/usr/local/apache/conf/php.conf',
            'addmodule'    => 'mod_bwlimited',
            'loadmodule'   => 'bwlimited_module',
        }
    );
}

# Temporary workaround until changes to PHPConfig.pm are published
sub setup_php_symlinks {
    my $self                   = shift;
    my $installed_php_binaries = Cpanel::PHPConfig::_check_installed_php_binaries();
    my $php_symlink_base       = '/usr/bin/php';
  PHP_VERSION_LOOP:
    foreach my $php_version ( '', '4', '5' ) {
        unlink $php_symlink_base . $php_version . '-cli' if ( -l $php_symlink_base . $php_version . '-cli' || -e _ );
        unlink $php_symlink_base . $php_version . '-cgi' if ( -l $php_symlink_base . $php_version . '-cgi' || -e _ );
        next unless $php_version;
        unlink $php_symlink_base . $php_version if ( -l $php_symlink_base . $php_version || -e _ );
        foreach my $install_position ( 'D', 'A' ) {
            if ( $installed_php_binaries->{$install_position}{'version'} eq $php_version ) {
                my $cgi_path = $installed_php_binaries->{$install_position}{'path'};
                my $cli_path = $cgi_path;
                $cli_path =~ s{^/usr/}{/usr/local/};
                symlink $cgi_path, $php_symlink_base . $php_version . '-cgi';
                symlink $cli_path, $php_symlink_base . $php_version . '-cli';
                symlink $cli_path, $php_symlink_base . $php_version;

                if ( $install_position eq 'D' ) {
                    symlink $cgi_path, $php_symlink_base . '-cgi';
                    symlink $cli_path, $php_symlink_base . '-cli';
                }
                next PHP_VERSION_LOOP;
            }
        }
    }
}

sub get_php_easyconfig {
    my ( $self, $root, $vers ) = @_;

    my $v5_4_or_later = Cpanel::Version::Compare::compare( $vers, '>=', '5.4' );

    my $ec = {
        'name'      => "PHP $vers support",
        'note'      => 'Be sure to "harden" your PHP since PHP has many security issues',
        'src_cd2'   => "php-$vers",
        'hastargz'  => 1,
        'haspatch'  => 1,
        'ensurepkg' => ['flex'],
        'url'       => 'http://php.net/',
        'modself'   => sub {
            my $self    = shift;
            my $self_hr = shift;

            if ( Cpanel::Version::Compare::compare( $vers, '>=', '5.5' ) ) {
                $self_hr->{'implies'}{'Cpanel::Easy::EAccelerator'} = 0;    # Unsupported as of 7/8/13
            }
        },
        'meta' => {
            'root'          => '5',
            'vers'          => $vers,
            'apache_prefix' => ['/usr/local'],
            'system_prefix' => ['/usr']
        },
        'configure' => { '--with-apxs' => ['/usr/local/apache/bin/apxs'], },
        'step'      => {
            '0.1' => {

                # Must get PECL list *before* we run the php build in case the build changes
                # the PECL environment or whatever.
                'name'    => 'Get list of pecl extensions',
                'command' => sub {
                    my $self = shift;
                    $self->{'_'}{'have_prior_php'} = $self->have_prior_php;
                    $self->get_pecl_extensions if $self->{'_'}{'have_prior_php'};
                    return ( 1, 'Ok' );
                },
            },
            '0.2' => {
                'name'    => 'finding libxml2 path',
                'command' => sub {
                    my ($self) = @_;
                    my $path = $self->get_path_installed('Cpanel::Easy::OptLib::libxml2');

                    $path
                      ? $self->add_to_configure( { '--enable-libxml' => '', '--with-libxml-dir' => [$path] }, "Cpanel::Easy::PHP5" )
                      : $self->add_to_configure( { '--disable-libxml' => '' }, "Cpanel::Easy::PHP5" );

                    return ( 1, 'ok' );
                },
            },
            '0.5' => {
                'name'    => 'finding PCRE path',
                'command' => sub {
                    my ($self) = @_;
                    if ( my $path = $self->get_path_installed('Cpanel::Easy::OptLib::pcre') ) {
                        $self->add_to_configure( { '--with-pcre-regex' => [$path] }, "Cpanel::Easy::PHP5" );
                    }
                    return ( 1, 'ok' );
                },
            },
            '1' => {
                'name'    => 'determining extra configure options',
                'command' => sub {
                    my ($self) = @_;
                    $self->add_to_configure( { '--with-libdir' => 'lib64' }, "Cpanel::Easy::PHP5" ) if ( $self->{'cpu_bits'} eq '64' );
                    $self->add_to_configure( { '--with-pic'    => '' },      "Cpanel::Easy::PHP5" ) if ( $self->{'cpu_bits'} eq '64' );
                    $self->add_to_configure( $self->get_raw_opts_if_any("all_php5"),  "Cpanel::Easy::PHP5" ) if !$self->get_param('makecpphp');
                    $self->add_to_configure( $self->get_raw_opts_if_any("PHP-$vers"), "Cpanel::Easy::PHP5" ) if !$self->get_param('makecpphp');

                    return ( 1, 'ok' );
                },
            },
            '2' => {
                'name'    => 'patching main/php_config.h',
                'command' => sub {
                    my ($self) = @_;

                    open( my $php_config_fh, ">>", "main/php_config.h" );
                    print {$php_config_fh} qq{#undef COMPILE_DL_ZLIB\n};
                    print {$php_config_fh} qq{#define HAVE_SENDMAIL 1\n};
                    close($php_config_fh);

                    return ( 1, 'ok' );
                },
            },
            '3' => {
                'name'    => 'applying compatability patches',
                'command' => sub {
                    my ($self) = @_;

                    # Standard patchset
                    my @patches = map { "../cppatch/php5-$_.patch" } qw(
                      binary-rename
                      iconv-ldflags
                      pqfreemem
                      imap-utf8_mime2text
                      javac
                      openssl-EVP-cleanup
                      gcc295
                      cve-2008-5498
                      bug-48518-regression
                      exif-segfault
                      bug49503
                      bug49611
                      bug49572
                      bug50251
                      rc-version-fix
                      png_check_sig
                      openssl-1.0
                      pdo_sqlite
                      bug53516
                      bug-53632
                      CVE-2011-4885
                      CVE-2012-0830
                      conftest-tmpnam
                      libxml
                      3_29_intl_extension
                      mariadb-10.2-compatability
                    );

                    # System specific patches
                    if ( $self->{'cpu_bits'} eq '64' ) {
                        unshift @patches, "../cppatch/php5-fixed-lib64.patch";
                    }

                    foreach my $patch (@patches) {
                        if ( -e $patch ) {

                            my @rc = $self->apply_patch($patch);
                            return @rc unless $rc[0];
                        }
                    }
                    return ( 1, 'ok' );
                },
            },
            '4' => {
                'name'    => 'setting LDFLAGS env',
                'command' => sub {
                    my ($self) = @_;
                    $vers =~ /^(\d+\.\d+)/;
                    my $short_version = $1;
                    $self->{'__'}{'env_ldflags'} = $ENV{'LDFLAGS'};
                    $self->{'__'}{'env_ldflags'} .= ( $self->{'cpu_bits'} eq '64' ? ' -L/usr/X11R6/lib64' : '' ) . ( $short_version >= 5.3 ? '' : ' -lstdc++' );
                    $self->{'__'}{'env_ldflags'} .= ' -static' if ( $self->get_param('makecpphp') && $self->get_param('makecpphp') eq 'buildbox' );
                    return ( 1, 'ok' );
                },
            },
            '4.5' => {
                'name'    => 'checking for prefork apache MPM',
                'command' => sub {
                    my ($self) = @_;

                    if ( !$self->get_param('makecpphp') ) {
                        my $res = $self->get_configured_mpm();

                        if ($res) {

                            # mod_php
                            if ( $res ne 'prefork' && $res ne 'itk' ) {

                                # Don't generate CGI/FCGI binary on first pass (unless PHP 5.4 or later)
                                $self->add_to_configure( { '--disable-cgi' => '' }, "Cpanel::Easy::PHP5" ) unless $v5_4_or_later;

                                # Don't generate mod_php; we don't support Zend Thread Safety (ZTS)
                                $self->delete_from_configure( { '--with-apxs' => undef, '--with-apxs2' => undef }, "Cpanel::Easy::PHP5" );
                            }
                        }
                        else {
                            return ( 0, $res );
                        }
                    }
                    return ( 1, 'ok' );
                },
            },
            '5' => {
                'name'    => 'configuring php for apache and cli',
                'command' => sub {
                    my ($self) = @_;
                    $self->replace_in_configure( { '--prefix' => $self->{'__'}{'meta'}{'apache_prefix'} }, "Cpanel::Easy::PHP5" );
                    $self->print_configure("Cpanel::Easy::PHP5");

                    local $ENV{'LDFLAGS'}                 = $self->{'__'}{'env_ldflags'};
                    local $ENV{'php_ldflags_add_usr_lib'} = 'yes';
                    $self->hide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    my @return = $self->run_system_cmd_returnable( [ './configure', $self->get_configure_as_array("Cpanel::Easy::PHP5") ], 1 );
                    $self->unhide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    if ( !$return[0] ) {
                        push @{ $self->{'attach_files_to_report'} }, $self->cwd() . '/config.log';
                    }
                    return @return;
                },
            },
            '6' => {
                'name'    => 'make php for apache and cli',
                'command' => sub {
                    my ($self) = @_;
                    local $ENV{'LDFLAGS'} = $self->{'__'}{'env_ldflags'};
                    $self->hide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    my @cmd = ( 'make', @{ $self->get_make_options() } );
                    my @return = $self->run_system_cmd_returnable( [@cmd], 1 );
                    $self->unhide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    return @return;
                },
            },
            '7' => {
                'name'    => 'make install php for apache and cli',
                'command' => sub {
                    my ($self) = @_;
                    local $ENV{'LDFLAGS'} = $self->{'__'}{'env_ldflags'};
                    $self->hide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    my @cmd = qw( make install );
                    my @return = $self->run_system_cmd_returnable( [@cmd], 1 );
                    $self->unhide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    return @return;
                },
            },
            '8' => {
                'name'    => 'setup php.ini',
                'command' => sub {
                    my ($self) = @_;
                    my $target = $self->{'__'}{'meta'}{'apache_prefix'}[0] . '/lib/php.ini';
                    my $source = '/usr/local/cpanel/scripts/php.ini';
                    unless ( -e $target ) {
                        if ( -e $source ) {
                            $self->copy_file( $source, $target );
                        }
                        return ( 0, q{Copy '[_1]' to '[_2]' failed: [_3]}, $source, $target, $! ) if !-e $target;
                        my $apache_include = $self->{'__'}{'meta'}{'apache_prefix'}[0] . '/lib/php';
                        my $system_include = $self->{'__'}{'meta'}{'system_prefix'}[0] . '/lib/php';
                        my %err;
                        my $rc = Cpanel::FileUtils::regex_rep_file( $target, { qr{^[\s]*include_path[\s]*=.*$} => qq{include_path = ".:$system_include:$apache_include"}, }, \%err, );

                        $self->print_alert( q{Could not change '[_1]' in php.ini'}, 'include_path' ) if !$rc;
                    }
                    return ( 1, 'Ok' );
                },
            },
            '9' => {
                'name'    => 'make clean in prep for system PHP',
                'command' => sub {
                    my ($self) = @_;
                    local $ENV{'LDFLAGS'} = $self->{'__'}{'env_ldflags'};
                    return $self->run_system_cmd_returnable( [qw(make clean)], 1 );
                },
            },
            '10' => {
                'name'    => 'configuring for system PHP',
                'command' => sub {
                    return ( 1, 'Skipped for PHP 5.4 and later' ) if $v5_4_or_later;
                    my ($self) = @_;
                    $self->delete_from_configure( { '--with-apxs' => undef, '--with-apxs2' => undef }, "Cpanel::Easy::PHP5" );
                    unless ( $self->{'working_profile'}{"Cpanel::Easy::PHP5::CGI"} ) {
                        $self->delete_from_configure( { '--disable-cgi' => undef }, "Cpanel::Easy::PHP5" );
                    }

                    $self->replace_in_configure( { '--prefix' => $self->{'__'}{'meta'}{'system_prefix'} }, "Cpanel::Easy::PHP5" );
                    $self->print_configure("Cpanel::Easy::PHP5");

                    local $ENV{'LDFLAGS'}                 = $self->{'__'}{'env_ldflags'};
                    local $ENV{'php_ldflags_add_usr_lib'} = 'yes';

                    $self->hide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    my @return = $self->run_system_cmd_returnable( [ './configure', $self->get_configure_as_array("Cpanel::Easy::PHP5") ], 1 );
                    $self->unhide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    return @return;
                },
            },
            '11' => {
                'name'    => 'make for system PHP',
                'command' => sub {
                    return ( 1, 'Skipped for PHP 5.4 and later' ) if $v5_4_or_later;
                    my ($self) = @_;
                    local $ENV{'LDFLAGS'} = $self->{'__'}{'env_ldflags'};
                    $self->hide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    my @cmd = ( 'make', @{ $self->get_make_options() } );
                    my @return = $self->run_system_cmd_returnable( [@cmd], 1 );
                    $self->unhide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    return @return;
                },
            },
            '12' => {
                'name'    => 'make install for system PHP',
                'command' => sub {
                    return ( 1, 'Skipped for PHP 5.4 and later' ) if $v5_4_or_later;
                    my ($self) = @_;
                    local $ENV{'LDFLAGS'} = $self->{'__'}{'env_ldflags'};
                    $self->hide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    my @cmd = qw( make install );
                    my @return = $self->run_system_cmd_returnable( [@cmd], 1 );
                    $self->unhide_imap_libraries() if ( $self->{'_'}{'hide_imap_libraries'} );
                    return @return;
                },
            },
            '13' => {
                'name'    => 'symlink php.ini',
                'command' => sub {
                    my ($self) = @_;
                    my $source = $self->{'__'}{'meta'}{'apache_prefix'}[0] . '/lib/php.ini';
                    my $target = $self->{'__'}{'meta'}{'system_prefix'}[0] . '/lib/php.ini';
                    unless ( -e $target && -l $target ) {
                        if ( -e $source ) {
                            unlink $target;
                            symlink( $source, $target );
                        }
                        return ( 0, q{Symlink '[_1]' to '[_2]' failed: [_3]}, $source, $target, $! ) unless ( -e $target && -l $target );
                    }
                    return ( 1, 'Ok' );
                },
            },
            '13.4' => {
                'name'    => 'set php_ini for PECL and PEAR',
                'command' => sub {
                    my ($self) = @_;

                    # See https://fogbugz.cpanel.net/default.asp?W1947 for background

                    # Delete the .pearrc configuration file, which stores user-layer config settings
                    # These are used by both user PHP and makecpphp, and will thus cause conflicts.
                    my $pearrc = $ENV{'HOME'} . '/.pearrc';
                    unlink $pearrc if -e $pearrc;

                    # Select the binary and INI file based on whether we are using internal or user installs of PHP
                    my $bin_dir = $self->get_param('makecpphp') ? '/usr/local/cpanel/3rdparty/bin'         : '/usr/local/bin';
                    my $php_ini = $self->get_param('makecpphp') ? '/usr/local/cpanel/3rdparty/etc/php.ini' : '/usr/local/lib/php.ini';

                    # Set the configurations for both PEAR and PECL channels
                    for my $bin ( "$bin_dir/pear", "$bin_dir/pecl" ) {
                        next unless -e $bin;

                        # Set the system php_ini, since makecpphp and user PECL/PEAR each have their own system config files
                        my ( $status, @messages ) = $self->run_system_cmd_returnable( [ $bin, 'config-set', 'php_ini', $php_ini, 'system' ], 1 );
                        if ( !$status ) {
                            $self->print_alert( join( ' ', 'Could not set php_ini in system layer.', @messages ) );
                        }
                    }
                    return ( 1, 'Ok' );
                },
            },
            '13.5' => {
                'name'    => 'set extension_dir in php.ini',
                'command' => sub {
                    my ($self) = @_;
                    local %ENV = ();
                    Cpanel::PHPINI::set_extension_dir( $self->{'__'}{'meta'}{'apache_prefix'}[0] );
                    return ( 1, 'Ok' );
                },
            },
            '14' => {
                'name'    => 'PHP httpd.conf setup',
                'command' => sub {
                    my ($self) = @_;

                    Cpanel::FileUtils::touchfile('/usr/local/apache/conf/php.conf');

                    return ( 1, 'Ok' );
                },
            },

            # Step 15 is used for PHP extensions: SuHosin <15.0x>,
            # EAccelerator <15.1x>, Zend Optimizer <15.2x>, IonCube
            # Loader <15.3x>, PDO <15.4x>, Opcache <15.5x>
            '16' => {
                'name'    => 'Update PHP magicloader if necessary',
                'command' => sub {
                    my ($self) = @_;
                    my $start = $self->cwd();

                    my $cpconf_ref = Cpanel::Config::loadcpconf();
                    if ( defined $cpconf_ref->{'magicloader_php-pear'} && $cpconf_ref->{'magicloader_php-pear'} eq '1' ) {
                        chdir '/usr/local/cpanel/src/userphp' || return ( 0, q{Could not chdir '[_1]': [_2]}, '/usr/local/cpanel/src/userphp', $! );
                        $self->run_system_cmd( [ '/usr/local/cpanel/src/userphp/install', $self->{'__'}{'meta'}{'apache_prefix'}[0] ] );
                        chdir $start || return ( 0, q{Could not chdir '[_1]': [_2]}, $start, $! );
                    }
                    return ( 1, 'Ok' );
                },
            },
            '17' => {
                'name'    => 'Update PHP version files',
                'command' => sub {
                    my ($self) = @_;

                    my @files = ("/usr/local/apache/conf/php5.version");

                    # ??? do this only/also in php configurator ???
                    # push php.version on if this is the one we want
                    my $php5 = $self->get_ns_value_from_profile( 'Cpanel::Easy::PHP5', $self->{'working_profile'} );
                    push @files, '/usr/local/apache/conf/php.version';

                    for my $vfile (@files) {
                        if ( open my $ver_fh, '>', $vfile ) {
                            print {$ver_fh} $self->{'__'}{'meta'}{'vers'};
                            close $ver_fh;
                            $self->print_alert( q{'[_1]' has been updated to '[_2]'}, $vfile, $self->{'__'}{'meta'}{'vers'} );
                        }
                        else {
                            $self->print_alert( q{Could not open '[_1]' for writing: [_2]}, $vfile, $! );
                        }
                    }

                    return ( 1, 'Ok' );
                },
            },
            '18' => {
                'name'    => 'Reinstall pecl extensions',
                'command' => sub {
                    my $self = shift;
                    if ( $self->{'_'}{'have_prior_php'} ) {
                        $self->reinstall_pecl_extensions;
                    }
                    else {
                        $self->print_alert("No prior PHP installation, therefore no pre-existing PECL extensions to install.");
                    }

                    return ( 1, 'Ok' );
                },
            },
            '19' => {
                'name'    => 'Check for php.ini compatability',
                'command' => sub {
                    my $self = shift;
                    system("cp -f /usr/local/lib/php.ini /root/php.ini.orig");
                    my $ret = $self->update_ini_file();
                    system("cp -f /usr/local/lib/php.ini /root/php.ini.new");
                    return $ret;
                },
            },
            '20' => {
                'name'    => 'Updating Timezone Database',
                'command' => sub {
                    my $self = shift;

                    if ( $self->{'_'}{'have_prior_php'} ) {

                        if ( !$self->get_ns_value_from_profile( 'Cpanel::Easy::PHP5::SysTimezone', $self->{'working_profile'} ) ) {

                            # If we are downgrading from a PHP with a current timezonedb builtin,
                            # or upgrading from PHP4 to a PHP5 with an out-of-date timezonedb builtin,
                            # we need to update the timezonedb.so
                            Cpanel::SafeRun::saferunallerrors('/usr/local/cpanel/scripts/update_php_timezonedb');
                        }

                        # If there is a superfluous extensions=timezonedb entry in php.ini, remove it
                        return $self->update_timezonedb_ini();
                    }

                    return ( 1, 'Ok' );
                },
            },
        },
    };

    # on new cPanel installs, this 'version' key should be set to a
    # default during profile building by get_ns_value_from_profile()
    delete $ec->{'configure'}{'--with-apxs'};
    $ec->{'configure'}{'--with-apxs2'} = ['/usr/local/apache/bin/apxs'];

    if ( $self->get_param('makecpphp') ) {
        if ( $self->get_param('profile') ne $self->determine_profile() ) {
            die q{'} . $self->get_param('profile') . q{' could not be used as profile, aborting};
        }

        $self->set_param( 'skip-re-saveas', 1 );

        delete $ec->{'configure'}{$_} for qw(--with-apxs --with-apxs2);
        delete $ec->{'step'}{$_} for $self->step_numbers_between( { 'easyconfig_hr' => $ec, 'start' => 8, 'stop' => 17, 'exclusive' => 0, } );
        for my $id ( $self->step_numbers_between( { 'easyconfig_hr' => $ec, 'start' => 5, 'stop' => 7, 'exclusive' => 0, } ) ) {
            $ec->{'step'}{$id}{'name'} =~ s{apache}{cPanel};
        }

        my $cpphp_prefix = $self->get_param('makecpphp') eq 'buildbox' ? '/usr/local/cpanel' : '/var/cpanel';
        $ec->{'configure'}{'--enable-static'}         = [];
        $ec->{'configure'}{'--with-config-file-path'} = ['/usr/local/cpanel/3rdparty/etc/'];
        $ec->{'meta'}{'apache_prefix'}                = ["$cpphp_prefix/3rdparty/"];
        mkdir "$cpphp_prefix/3rdparty/" if !-d "$cpphp_prefix/3rdparty/";
    }

    return $ec;
}

sub merge_in_cpphp_opts_if_applicable {
    my ($self) = @_;

    if ( $self->get_param('makecpphp') ) {

        # allow users to turn on (but not off) options for their cpanel internal PHP
        my $custom_cpphp = '/var/cpanel/easy/apache/profile/makecpphp.profile.yaml.local';

        # only do this once, only do this if we've loaded up $self->{'working_profile'}
        if ( !$self->{'_'}{'custom_cpphp'} && -e $custom_cpphp && exists $self->{'working_profile'} && ref( $self->{'working_profile'} ) eq 'HASH' ) {
            $self->{'_'}{'custom_cpphp'}++;

            my $hr = $self->deserialize($custom_cpphp);
            for my $key ( keys %{$hr} ) {
                next if ref $hr->{$key};

                my $ec = $self->get_easyconfig_hr_from_ns_variations($key);
                if ( $ec->{'reverse'} && $hr->{$key} eq '0' ) {

                    # reinstate this once ea3 is using the actual Cpanel::Locale object
                    # $self->print_alert_color( 'blue', "[boolean,_1,Enabling,Disabling] “[_2]” as per [_3].", 0, $key, $custom_cpphp );
                    $self->print_alert_color( 'blue', "Disabling “[_2]” as per [_3].", 0, $key, $custom_cpphp );
                    $self->{'working_profile'}{$key} = 0;
                }
                elsif ( $hr->{$key} eq '1' ) {

                    # reinstate this once ea3 is using the actual Cpanel::Locale object
                    # $self->print_alert_color( 'blue', "[boolean,_1,Enabling,Disabling] “[_2]” as per [_3].", 1, $key, $custom_cpphp );
                    $self->print_alert_color( 'blue', "Enabling “[_2]” as per [_3].", 1, $key, $custom_cpphp );
                    $self->{'working_profile'}{$key} = 1;
                }
                else {
                    $self->print_alert_color( 'yellow', "Invalid value for “[_1]” in [_2].", $key, $custom_cpphp );
                }
            }
        }
    }
}

sub set_phpini_object_key_from {
    my ( $self, $file ) = @_;
    return if !-e $file;    # warn/log ?

    $self->{'_'}{'php_ini'} = {};

    $self->{'_'}{'php_ini'} = Cpanel::PHPINI::get_phpini_hashref($file);

    if ( !$self->{'_'}{'php_ini'} ) {
        delete $self->{'_'}{'php_ini'};
        $self->print_alert_color( 'red', q{Could not open() file '[_1]' for reading: [_2]}, $file, $! );
    }
}

sub get_pecl_extensions {
    my ($self) = @_;
    local $ENV{'TERM'} = 'console';
    my $list = Cpanel::LangMods::doaction( 'php-pecl', 'list_installed' ) || [];
    my @exts = map { $_->{module} } @{$list};
    $self->{'_'}{'pecl_exts'} = \@exts;

    my $str = @exts ? "@exts" : '(None)';
    print "Found the following PHP PECL extensions: $str\n";
}

sub reinstall_pecl_extension {
    my ( $self, $ext, $buf ) = @_;
    my $quiet = 1;
    Cpanel::LangMods::doaction( 'php-pecl', 'uninstall', $ext, $quiet );
    $$buf = Cpanel::LangMods::doaction( 'php-pecl', 'install', $ext, $quiet );
    $$buf =~ s/\<.*?\>//g;    # Cpanel::Locale thinks this is non-interactive, so strip out HTML.
    chomp $$buf;
    return $$buf =~ /^build process completed successfully/mi ? 1 : 0;
}

sub reinstall_pecl_extensions {
    my ($self) = @_;

    my @exts = @{ $self->{'_'}{'pecl_exts'} };

    return unless @exts;

    # Installing FFMPEG carries with it severe patent or copyright infringement penalties
    my $num_exts = scalar @exts;
    @exts = grep { !/ffmpe?g/i } @exts;

    if ( @exts != $num_exts ) {
        $self->print_alert_color( 'red', 'Bypassing Pecl FFMPEG extension. Please reinstall your FFMPEG manually.' );
    }

    my $successes = 0;
    my $count     = scalar @exts;
    for my $ext (@exts) {
        my $buf;
        print "Reinstalling PHP PECL extension: $ext ... ";
        my $ret = $self->reinstall_pecl_extension( $ext, \$buf );

        if ($ret) {
            print "Success\n";
            ++$successes;
        }
        else {
            print "***FAILURE***\n";
            $buf =~ s/\n/\n\t/gm;    # indent this error message to make it stand out in log
            print "\t", $buf, "\n";
        }
    }

    my $failures = $count - $successes;

    if ( !$failures ) {
        $self->print_alert_color( 'yellow', 'All [_1] of your PECL extensions have been reinstalled successfully, but some might be incompatible with your new PHP build. Please test each PECL extension thoroughly, to assure that all extensions work correctly and are compatible with your new PHP build.', $count );
    }
    else {
        $self->print_alert_color( 'red', 'Warning: [_1] of your [_2] PECL extensions could not be reinstalled.  But, even successfully installed PECL extensions may incompatible with your new PHP build. Please test each PECL extension thoroughly, to assure that all extensions work correctly and are compatible with your new PHP build.', $failures, $count );
    }
}

sub have_prior_php {
    my $self = shift;
    return -e '/usr/local/lib/php.ini';
}

sub php4_with_dso {
    my $self = shift;

    if ( open my $fh_conf, '<', '/usr/local/apache/conf/php.conf.yaml' ) {
        my @php4_dso = grep /php4: dso/, (<$fh_conf>);
        close $fh_conf;
        return 1 if (@php4_dso);
    }

    return;
}

sub profile_specified_with_php4 {
    my ($self) = @_;

    if ( $self->get_param('build') ) {
        my $profile       = $self->determine_profile();
        my $profile_setup = $self->deserialize($profile);
        return 1 if $profile_setup && keys %$profile_setup && $profile_setup->{'Cpanel::Easy::PHP4'};
    }

    return;
}

sub php4_conflict_check {
    my ($self) = @_;

    return if ( !$self->php4_with_dso() );
    if ( $self->profile_specified_with_php4() ) {
        $self->_header();
        $self->print_alert_color( 'red', "*** WARNING ***" );
        print "<pre>\n" if ref $self->{'ui_obj'} eq 'Cpanel::Easy::Apache::UI::HTML';
        print "PHP4 is currently configured to run with the DSO handler.\n";
        print "This version of EasyApache does not provide PHP4.\n";
        print "In order to continue, please change your PHP handler using WHM:\n";
        if ( ref $self->{'ui_obj'} eq 'Cpanel::Easy::Apache::UI::HTML' ) {
            print "</pre>\n";
            print qq{<p><a href="/scripts2/phpandsuexecconf">Click here to reconfigure PHP</a>.</p>};
        }
        else {
            print "( Main >> Service Configuration >> Configure PHP and SuExec )\n";
        }
        $self->_footer();
        exit;
    }
}

sub get_php_mysql_support_type {
    my $self    = shift;
    my $db_type = shift;    # e.g. mariadb or mysql
    my $version = shift;    # e.g. 5.5
    my $type;
    return if !defined $db_type;

    if ( $db_type eq 'mariadb' && Cpanel::Version::Compare::compare( $version, '>=', 10.0 ) ) {
        $type = 'mysqlnd';
    }
    elsif ( $db_type eq 'mysql' && Cpanel::Version::Compare::compare( $version, '>=', 5.6 ) ) {
        $type = 'mysqlnd';
    }
    else {
        my $sys   = $self->get_ns_value_from_profile('Cpanel::Easy::PHP5::MysqlOfSystem');
        my $cpphp = $self->get_ns_value_from_profile('Cpanel::Easy::PHP5::cPPHPOpts');

        $type = ( $sys || $cpphp ) ? 'libmysqlclient' : 'mysqlnd';
    }

    return $type;
}

1;
