The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
package Apache::TestConfig; #not TestConfigParse on purpose

#dont really want/need a full-blown parser
#but do want something somewhat generic

use strict;
use warnings FATAL => 'all';

use Apache::TestTrace;

use File::Spec::Functions qw(rel2abs splitdir file_name_is_absolute);
use File::Basename qw(dirname basename);

sub strip_quotes {
    local $_ = shift || $_;
    s/^\"//; s/\"$//; $_;
}

my %wanted_config = (
    TAKE1 => {map { $_, 1 } qw(ServerRoot ServerAdmin TypesConfig DocumentRoot)},
    TAKE2 => {map { $_, 1 } qw(LoadModule LoadFile)},
);

my %spec_init = (
    TAKE1 => sub { shift->{+shift} = "" },
    TAKE2 => sub { shift->{+shift} = [] },
);

my %spec_apply = (
    TypesConfig => \&inherit_server_file,
    ServerRoot  => sub {}, #dont override $self->{vars}->{serverroot}
    DocumentRoot => \&inherit_directive_var,
    LoadModule  => \&inherit_load_module,
    LoadFile    => \&inherit_load_file,
);

#where to add config, default is preamble
my %spec_postamble = map { $_, 'postamble' } qw(TypesConfig);

# need to enclose the following directives into <IfModule
# mod_foo.c>..</IfModule>, since mod_foo might be unavailable
my %ifmodule = (
    TypesConfig => 'mod_mime.c',
);

sub spec_add_config {
    my($self, $directive, $val) = @_;

    my $where = $spec_postamble{$directive} || 'preamble';

    if (my $ifmodule = $ifmodule{TypesConfig}) {
        $self->postamble(<<EOI);
<IfModule $ifmodule>
    $directive $val
</IfModule>
EOI
    }
    else {
        $self->$where($directive => $val);
    }
}

# resolve relative files like Apache->server_root_relative
# this function doesn't test whether the resolved file exists
sub server_file_rel2abs {
    my($self, $file, $base) = @_;

    my ($serverroot, $result) = ();

    # order search sequence
    my @tries = ([ $base,
                       'user-supplied $base' ],
                 [ $self->{inherit_config}->{ServerRoot},
                       'httpd.conf inherited ServerRoot' ],
                 [ $self->apxs('PREFIX'),
                       'apxs-derived ServerRoot' ]);

    # remove surrounding quotes if any
    # e.g. Include "/tmp/foo.html"
    $file =~ s/^\s*["']?//;
    $file =~ s/["']?\s*$//;

    if (file_name_is_absolute($file)) {
        debug "$file is already absolute";
        $result = $file;
    }
    else {
        foreach my $try (@tries) {
            next unless defined $try->[0];

            if (-d $try->[0]) {
                $serverroot = $try->[0];
                debug "using $try->[1] to resolve $file";
                last;
            }
        }

        if ($serverroot) {
            $result = rel2abs $file, $serverroot;
        }
        else {
            warning "unable to resolve $file - cannot find a suitable ServerRoot";
            warning "please specify a ServerRoot in your httpd.conf or use apxs";

            # return early, skipping file test below
            return $file;
        }
    }

    my $dir = dirname $result;
    # $file might not exist (e.g. if it's a glob pattern like
    # "conf/*.conf" but what we care about here is to check whether
    # the base dir was successfully resolved. we don't check whether
    # the file exists at all. it's the responsibility of the caller to
    # do this check
    if (defined $dir && -e $dir && -d _) {
        if (-e $result) {
            debug "$file successfully resolved to existing file $result";
        }
        else {
            debug "base dir of '$file' successfully resolved to $dir";
        }

    }
    else {
        $dir ||= '';
        warning "dir '$dir' does not exist (while resolving '$file')";

        # old behavior was to return the resolved but non-existent
        # file.  preserve that behavior and return $result anyway.
    }

    return $result;
}

sub server_file {
    my $f = shift->server_file_rel2abs(@_);
    return qq("$f");
}

sub inherit_directive_var {
    my($self, $c, $directive) = @_;

    $self->{vars}->{"inherit_\L$directive"} = $c->{$directive};
}

sub inherit_server_file {
    my($self, $c, $directive) = @_;

    $self->spec_add_config($directive,
                           $self->server_file($c->{$directive}));
}

#so we have the same names if these modules are linked static or shared
my %modname_alias = (
    'mod_pop.c'            => 'pop_core.c',
    'mod_proxy_ajp.c'      => 'proxy_ajp.c',
    'mod_proxy_http.c'     => 'proxy_http.c',
    'mod_proxy_ftp.c'      => 'proxy_ftp.c',
    'mod_proxy_balancer.c' => 'proxy_balancer.c',
    'mod_proxy_connect.c'  => 'proxy_connect.c',
    'mod_modperl.c'        => 'mod_perl.c',
);

# Block modules which inhibit testing:
# - mod_jk requires JkWorkerFile or JkWorker to be configured
#   skip it for now, tomcat has its own test suite anyhow.
# - mod_casp2 requires other settings in addition to LoadModule
# - mod_bwshare and mod_evasive20 block fast requests that tests are doing
# - mod_fcgid causes https://rt.cpan.org/Public/Bug/Display.html?id=54476
# - mod_modnss.c and mod_rev.c require further configuration
my @autoconfig_skip_module = qw(mod_jk.c mod_casp2.c mod_bwshare.c
    mod_fcgid.c mod_evasive20.c mod_modnss.c mod_rev.c);

# add modules to be not inherited from the existing config.
# e.g. prevent from LoadModule perl_module to be included twice, when
# mod_perl already configures LoadModule and it's certainly found in
# the existing httpd.conf installed system-wide.
sub autoconfig_skip_module_add {
    push @autoconfig_skip_module, @_;
}

sub should_skip_module {
    my($self, $name) = @_;

    for (@autoconfig_skip_module) {
        if (UNIVERSAL::isa($_, 'Regexp')) {
            return 1 if $name =~ /$_/;
        }
        else {
            return 1 if $name eq $_;
        }
    }
    return 0;
}

#inherit LoadModule
sub inherit_load_module {
    my($self, $c, $directive) = @_;

    for my $args (@{ $c->{$directive} }) {
        my $modname = $args->[0];
        my $file = $self->server_file_rel2abs($args->[1]);

        unless (-e $file) {
            debug "$file does not exist, skipping LoadModule";
            next;
        }

        my $name = basename $args->[1];
        $name =~ s/\.(s[ol]|dll)$/.c/;  #mod_info.so => mod_info.c
        $name =~ s/^lib/mod_/; #libphp4.so => mod_php4.c

        $name = $modname_alias{$name} if $modname_alias{$name};

        # remember all found modules
        $self->{modules}->{$name} = $file;
        debug "Found: $modname => $name";

        if ($self->should_skip_module($name)) {
            debug "Skipping LoadModule of $name";
            next;
        }

        debug "LoadModule $modname $name";

        # sometimes people have broken system-wide httpd.conf files,
        # which include LoadModule of modules, which are built-in, but
        # won't be skipped above if they are found in the modules/
        # directory. this usually happens when httpd is built once
        # with its modules built as shared objects and then again with
        # static ones: the old httpd.conf still has the LoadModule
        # directives, even though the modules are now built-in
        # so we try to workaround this problem using <IfModule>
        $self->preamble(IfModule => "!$name",
                        qq{LoadModule $modname "$file"\n});
    }
}

#inherit LoadFile
sub inherit_load_file {
    my($self, $c, $directive) = @_;

    for my $args (@{ $c->{$directive} }) {
        my $file = $self->server_file_rel2abs($args->[0]);

        unless (-e $file) {
            debug "$file does not exist, skipping LoadFile";
            next;
        }

        if ($self->should_skip_module($args->[0])) {
            debug "Skipping LoadFile of $args->[0]";
            next;
        }

        # remember all found modules
        push @{$self->{load_file}}, $file;

        debug "LoadFile $file";

        $self->preamble_first(qq{LoadFile "$file"\n});
    }
}

sub parse_take1 {
    my($self, $c, $directive) = @_;
    $c->{$directive} = strip_quotes;
}

sub parse_take2 {
    my($self, $c, $directive) = @_;
    push @{ $c->{$directive} }, [map { strip_quotes } split];
}

sub apply_take1 {
    my($self, $c, $directive) = @_;

    if (exists $self->{vars}->{lc $directive}) {
        #override replacement @Variables@
        $self->{vars}->{lc $directive} = $c->{$directive};
    }
    else {
        $self->spec_add_config($directive, qq("$c->{$directive}"));
    }
}

sub apply_take2 {
    my($self, $c, $directive) = @_;

    for my $args (@{ $c->{$directive} }) {
        $self->spec_add_config($directive => [map { qq("$_") } @$args]);
    }
}

sub inherit_config_file_or_directory {
    my ($self, $item) = @_;

    if (-d $item) {
        my $dir = $item;
        debug "descending config directory: $dir";

        for my $entry (glob "$dir/*") {
            $self->inherit_config_file_or_directory($entry);
        }
        return;
    }

    my $file = $item;
    debug "inheriting config file: $file";

    my $fh = Symbol::gensym();
    open($fh, $file) or return;

    my $c = $self->{inherit_config};
    while (<$fh>) {
        s/^\s*//; s/\s*$//; s/^\#.*//;
        next if /^$/;

        # support continuous config lines (which use \ to break the line)
        while (s/\\$//) {
            my $cont = <$fh>;
            $cont =~ s/^\s*//;
            $cont =~ s/\s*$//;
            $_ .= $cont;
        }

        (my $directive, $_) = split /\s+/, $_, 2;

        if ($directive eq "Include") {
            foreach my $include (glob($self->server_file_rel2abs($_))) {
                $self->inherit_config_file_or_directory($include);
            }
        }

        #parse what we want
        while (my($spec, $wanted) = each %wanted_config) {
            next unless $wanted->{$directive};
            my $method = "parse_\L$spec";
            $self->$method($c, $directive);
        }
    }

    close $fh;
}

sub inherit_config {
    my $self = shift;

    $self->get_httpd_static_modules;
    $self->get_httpd_defines;

    #may change after parsing httpd.conf
    $self->{vars}->{inherit_documentroot} =
      catfile $self->{httpd_basedir}, 'htdocs';

    my $file = $self->{vars}->{httpd_conf};
    my $extra_file = $self->{vars}->{httpd_conf_extra};

    unless ($file and -e $file) {
        if (my $base = $self->{httpd_basedir}) {
            my $default_conf = $self->{httpd_defines}->{SERVER_CONFIG_FILE};
            $default_conf ||= catfile qw(conf httpd.conf);
            $file = catfile $base, $default_conf;

            # SERVER_CONFIG_FILE might be an absolute path
            unless (-e $file) {
                if (-e $default_conf) {
                    $file = $default_conf;
                }
                else {
                    # try a little harder
                    if (my $root = $self->{httpd_defines}->{HTTPD_ROOT}) {
                        debug "using HTTPD_ROOT to resolve $default_conf";
                        $file = catfile $root, $default_conf;
                    }
                }
            }
        }
    }

    unless ($extra_file and -e $extra_file) {
        if ($extra_file and my $base = $self->{httpd_basedir}) {
            my $default_conf = catfile qw(conf $extra_file);
            $extra_file = catfile $base, $default_conf;
            # SERVER_CONFIG_FILE might be an absolute path
            $extra_file = $default_conf if !-e $extra_file and -e $default_conf;
        }
    }

    return unless $file or $extra_file;

    my $c = $self->{inherit_config};

    #initialize array refs and such
    while (my($spec, $wanted) = each %wanted_config) {
        for my $directive (keys %$wanted) {
            $spec_init{$spec}->($c, $directive);
        }
    }

    $self->inherit_config_file_or_directory($file) if $file;
    $self->inherit_config_file_or_directory($extra_file) if $extra_file;

    #apply what we parsed
    while (my($spec, $wanted) = each %wanted_config) {
        for my $directive (keys %$wanted) {
            next unless $c->{$directive};
            my $cv = $spec_apply{$directive} ||
                     $self->can("apply_\L$directive") ||
                     $self->can("apply_\L$spec");
            $cv->($self, $c, $directive);
        }
    }
}

sub get_httpd_static_modules {
    my $self = shift;

    my $httpd = $self->{vars}->{httpd};
    return unless $httpd;

    $httpd = shell_ready($httpd);
    my $cmd = "$httpd -l";
    my $list = $self->open_cmd($cmd);

    while (<$list>) {
        s/\s+$//;
        next unless /\.c$/;
        chomp;
        s/^\s+//;
        $self->{modules}->{$_} = 1;
    }

    close $list;
}

sub get_httpd_defines {
    my $self = shift;

    my $httpd = $self->{vars}->{httpd};
    return unless $httpd;

    $httpd = shell_ready($httpd);
    my $cmd = "$httpd -V";
    my $proc = $self->open_cmd($cmd);

    while (<$proc>) {
        chomp;
        if( s/^\s*-D\s*//) {
            s/\s+$//;
            my($key, $val) = split '=', $_, 2;
            $self->{httpd_defines}->{$key} = $val ? strip_quotes($val) : 1;
            debug "isolated httpd_defines $key = " . $self->{httpd_defines}->{$key};
        }
        elsif (/(version|built|module magic number|server mpm):\s+(.*)/i) {
            my $val = $2;
            (my $key = uc $1) =~ s/\s/_/g;
            $self->{httpd_info}->{$key} = $val;
            debug "isolated httpd_info $key = " . $val;
        }
    }

    close $proc;

    if (my $mmn = $self->{httpd_info}->{MODULE_MAGIC_NUMBER}) {
        @{ $self->{httpd_info} }
          {qw(MODULE_MAGIC_NUMBER_MAJOR
              MODULE_MAGIC_NUMBER_MINOR)} = split ':', $mmn;
    }

    # get the mpm information where available
    # lowercase for consistency across the two extraction methods
    # XXX or maybe consider making have_apache_mpm() case-insensitive?
    if (my $mpm = $self->{httpd_info}->{SERVER_MPM}) {
        # 2.1
        $self->{mpm} = lc $mpm;
    }
    elsif (my $mpm_dir = $self->{httpd_defines}->{APACHE_MPM_DIR}) {
        # 2.0
        $self->{mpm} = lc basename $mpm_dir;
    }
    else {
        # Apache 1.3 - no mpm to speak of
        $self->{mpm} = '';
    }

    my $version = $self->{httpd_info}->{VERSION} || '';

    if ($version =~ qr,Apache/2,) {
        # PHP 4.x on httpd-2.x needs a special modname alias:
        $modname_alias{'mod_php4.c'} = 'sapi_apache2.c';
    }

    unless ($version =~ qr,Apache/(2.0|1.3),) {
        # for 2.1 and later, mod_proxy_* are really called mod_proxy_*
        delete @modname_alias{grep {/^mod_proxy_/} keys %modname_alias};
    }
}

sub httpd_version {
    my $self = shift;

    my $httpd = $self->{vars}->{httpd};
    return unless $httpd;

    my $version;
    $httpd = shell_ready($httpd);
    my $cmd = "$httpd -v";

    my $v = $self->open_cmd($cmd);

    local $_;
    while (<$v>) {
        next unless s/^Server\s+version:\s*//i;
        chomp;
        my @parts = split;
        foreach (@parts) {
            next unless /^Apache\//;
            $version = $_;
            last;
        }
        $version ||= $parts[0];
        last;
    }

    close $v;

    return $version;
}

sub httpd_mpm {
    return shift->{mpm};
}

1;