
#
#  This file is part of WebDyne.
#
#  This software is copyright (c) 2026 by Andrew Speer <andrew.speer@isolutions.com.au>.
#
#  This is free software; you can redistribute it and/or modify it under
#  the same terms as the Perl 5 programming language system itself.
#
#  Full license text is available at:
#
#  <http://dev.perl.org/licenses/>
#
package WebDyne::Install::Apache;


#  Compiler Pragma
#
sub BEGIN {$^W=0}
use strict qw(vars);
use vars   qw($VERSION);
use warnings;
no warnings qw(uninitialized);


#  Webmod Modules
#
use WebDyne::Util;


#  External Modules
#
use File::Spec;
use Text::Template;
use IO::File;
use Data::Dumper;


#  Base installer
#
use WebDyne::Install qw(message);


#  Constants
#
use WebDyne::Constant;
use WebDyne::Install::Constant;
use WebDyne::Install::Apache::Constant;


#  Version information
#
$VERSION='3.011';


#  Debug
#
debug("%s loaded, version $VERSION", __PACKAGE__);


#  Uninstaller global
#
my $Uninstall_fg;


#  Init done.
#
1;


#------------------------------------------------------------------------------


#  Uninstall
#
sub uninstall {

    #  Set flag and call install routine
    #
    $Uninstall_fg++;
    shift()->install(@_);

}


#  Install the Apache conf params needed to run the system
#
sub install {


    #  Get class, other paths
    #
    my ($class, $prefix, $installbin_dn, $opt_hr)=@_;
    my $dry_run=$opt_hr->{'dry_run'} || $ENV{'DRY_RUN'};
    local $ENV{'DRY_RUN'}=$ENV{'DRY_RUN'};
    $ENV{'DRY_RUN'}=1 if $dry_run;


    #  Run the base install/uninstall routine to create the cache dir.
    #  In --text mode this module only renders generated config to STDOUT;
    #  cache path discovery still happens below, but no filesystem setup is
    #  performed.
    #
    unless ($Uninstall_fg || $opt_hr->{'text'}) {
        WebDyne::Install->install($prefix, $installbin_dn) ||
            return err();
    }
    elsif ($Uninstall_fg) {
        WebDyne::Install->uninstall($prefix, $installbin_dn) ||
            return err();
    }


    #  Get package file name so we can look up inc for templates
    #
    (my $class_dn=$class . q(.pm))=~s/::/\//g;


    #  Load constants, get full path for class and constants from INC
    #
    $class_dn=$INC{$class_dn} ||
        return err("unable to find location for $class in \%INC");


    #  Get file name component, then cut file out to get directory,
    #  split and reverse class for later use;
    #
    my $class_fn=(File::Spec->splitpath($class_dn))[2];
    $class_dn=~s/\Q$class_fn\E$//;
    my @class=reverse split(/::/, $class);


    #  Get webdyne cache dn;
    #
    my $cache_dn=&WebDyne::Install::cache_dn($prefix);


    #  Get config/constant hash ref
    #
    my %constant=(

        %WebDyne::Constant::Constant,
        %WebDyne::Install::Constant::Constant,
        %WebDyne::Install::Apache::Constant::Constant,
        DIR_INSTALLBIN   => $installbin_dn,
        WEBDYNE_CACHE_DN => $cache_dn,

    );
    my $config_hr=\%constant;
    debug('config_hr %s', Dumper($config_hr));


    #  Cannot do anything without Apache binary
    #
    return err("unable to find apache binary")
        unless $config_hr->{'HTTPD_BIN'};


    #  Get template path name from module install dir.
    #
    my $template_dn=File::Spec->catdir($class_dn, $class[0]);
    my $template_fn=File::Spec->catfile(
        $template_dn, $config_hr->{'FILE_WEBDYNE_CONF_TEMPLATE'});


    #  Open it as a template
    #
    my $template_or=Text::Template->new(

        type   => 'FILE',
        source => $template_fn,

    ) || return err("unable to open template $template_fn, $!");


    #  Fill in with out self ref as a hash
    #
    my $webdyne_conf=$template_or->fill_in(

        HASH       => $config_hr,
        DELIMITERS => ['<!--', '-->'],

    ) || return err("unable to fill in template $template_fn, $Text::Template::ERROR");


    #  Get apache config dir
    #
    my $apache_conf_dn=$config_hr->{'DIR_APACHE_CONF'} ||
        return err('unable to determine Apache config directory');


    #  Work out config file name
    #
    my $webdyne_conf_fn=File::Spec->catfile(
        $apache_conf_dn, $config_hr->{'FILE_WEBDYNE_CONF'});




    #  Remove webdyne config file in uninstall mode. Install mode writes it
    #  after all generated fragments are available, so --text can print the
    #  complete Apache/WebDyne configuration.
    #
    if ($Uninstall_fg) {

        #  In uninstall - get rid of conf file
        #
        if (-f $webdyne_conf_fn) {
            if ($dry_run) {
                message "would remove config file $webdyne_conf_fn";
            }
            else {
                unlink($webdyne_conf_fn) ||
                    return err("unable to remove config file $webdyne_conf_fn, $!");
                message "remove config file $webdyne_conf_fn";
            }
        }

    }
    

    #  Work out constants file name
    #
    my $webdyne_conf_pl_fn=File::Spec->catfile(
        $apache_conf_dn, $config_hr->{'FILE_WEBDYNE_CONF_PL'});
    

    #  Get template path name from module install dir.
    #
    my $template_webdyne_conf_pl_fn=File::Spec->catfile(
        $template_dn, $config_hr->{'FILE_WEBDYNE_CONF_PL_TEMPLATE'});


    #  Open it as a template
    #
    my $template_webdyne_conf_pl_or=Text::Template->new(

        type   => 'FILE',
        source => $template_webdyne_conf_pl_fn

    ) || return err("unable to open template $template_fn, $!");


    #  Fill in with out self ref as a hash
    #
    my $webdyne_conf_pl=$template_webdyne_conf_pl_or->fill_in(

        HASH       => $config_hr,
        DELIMITERS => ['<!--', '-->'],

    ) || return err("unable to fill in template $template_webdyne_conf_pl_fn, $Text::Template::ERROR");


    #  Print complete generated output if requested.
    #
    if ($opt_hr->{'text'} && !$Uninstall_fg) {
        print "# $webdyne_conf_fn\n";
        print $webdyne_conf;
        print "\n# $webdyne_conf_pl_fn\n";
        print $webdyne_conf_pl;
        unless ($config_hr->{'HTTPD_SERVER_CONFIG_SKIP'}) {
            my $apache_template_fn=File::Spec->catfile(
                $template_dn, $config_hr->{'FILE_APACHE_CONF_TEMPLATE'});
            my $apache_template_or=Text::Template->new(
                type   => 'FILE',
                source => $apache_template_fn,
            ) || return err("unable to open template $apache_template_fn, $!");
            my $apache_conf=$apache_template_or->fill_in(
                HASH       => $config_hr,
                DELIMITERS => ['<!--', '-->'],
            ) || return err("unable to fill in template $apache_template_fn, $Text::Template::ERROR");
            print "\n# $apache_template_fn\n";
            print $apache_conf;
        }
        elsif (my $enabled_dn=$config_hr->{'DIR_APACHE_CONF_ENABLED'}) {
            my $enabled_fn=File::Spec->catfile($enabled_dn, $config_hr->{'FILE_WEBDYNE_CONF'});
            print "\n# Enable by linking $enabled_fn to $webdyne_conf_fn\n";
        }
        return \undef;
    }


    #  Open, write webdyne config file unless in uninstall
    #
    unless ($Uninstall_fg) {
        message "writing Apache config file '$webdyne_conf_fn'.";
        if ($dry_run) {
            message "would write Apache config file '$webdyne_conf_fn'.";
        }
        else {
            my $webdyne_conf_fh=IO::File->new($webdyne_conf_fn, O_CREAT | O_WRONLY | O_TRUNC) ||
                return err("unable to open file $webdyne_conf_fn, $!");
            print $webdyne_conf_fh $webdyne_conf ||
                return err("unable to write file $webdyne_conf_fn, $!");
            $webdyne_conf_fh->close() ||
                return err("unable to close file $webdyne_conf_fn, $!");
        }
    }


    #  Copy constants file into conf.d
    #
    unless ($Uninstall_fg) {

        message "writing Webdyne config file '$webdyne_conf_pl_fn'.";
        if ($dry_run) {
            message "would write Webdyne config file '$webdyne_conf_pl_fn'.";
        }
        else {
            my $webdyne_conf_pl_fh=IO::File->new($webdyne_conf_pl_fn, O_CREAT | O_WRONLY | O_TRUNC) ||
                return err("unable to open file $webdyne_conf_pl_fn, $!");
            print $webdyne_conf_pl_fh $webdyne_conf_pl ||
                return err("unable to write file $webdyne_conf_pl_fn, $!");
            $webdyne_conf_pl_fh->close() ||
                return err("unable to close file $webdyne_conf_pl_fn, $!");
        }

    }
    else {
    
        #  In uninstall - get rid of conf file
        #
        if (-f $webdyne_conf_pl_fn) {
            if ($dry_run) {
                message "would remove config file $webdyne_conf_pl_fn";
            }
            else {
                unlink($webdyne_conf_pl_fn) ||
                    return err("unable to remove config file $webdyne_conf_pl_fn, $!");
                message "remove config file $webdyne_conf_pl_fn";
            }
        }

    }


    #  Debian-style Apache installs use conf-available/conf-enabled.
    #
    if (my $enabled_dn=$config_hr->{'DIR_APACHE_CONF_ENABLED'}) {
        my $enabled_fn=File::Spec->catfile($enabled_dn, $config_hr->{'FILE_WEBDYNE_CONF'});
        if ($Uninstall_fg) {
            if (-l $enabled_fn) {
                my $target=readlink($enabled_fn);
                if ($target eq $webdyne_conf_fn) {
                    if ($dry_run) {
                        message "would remove enabled Apache config link $enabled_fn";
                    }
                    else {
                        unlink($enabled_fn) ||
                            return err("unable to remove enabled Apache config link $enabled_fn, $!");
                        message "remove enabled Apache config link $enabled_fn";
                    }
                }
                else {
                    message "not removing enabled Apache config link $enabled_fn; target is $target";
                }
            }
            elsif (-e $enabled_fn) {
                message "not removing enabled Apache config path $enabled_fn; it is not a symlink";
            }
        }
        else {
            unless (-e $enabled_fn) {
                if ($dry_run) {
                    message "would enable Apache config link '$enabled_fn'.";
                }
                else {
                    symlink($webdyne_conf_fn, $enabled_fn) ||
                        return err("unable to enable Apache config $enabled_fn -> $webdyne_conf_fn, $!");
                    message "enabled Apache config link '$enabled_fn'.";
                }
            }
            elsif (-l $enabled_fn) {
                my $target=readlink($enabled_fn);
                return err("Apache config link $enabled_fn points to $target, expected $webdyne_conf_fn")
                    unless $target eq $webdyne_conf_fn;
            }
            else {
                return err("Apache config enable path $enabled_fn already exists and is not a symlink");
            }
        }
    }
        


    #  Only modify config file if no conf.d dir, denoted by var below
    #
    unless ($config_hr->{'HTTPD_SERVER_CONFIG_SKIP'}) {


        #  Get Apache config file, append root if not absolute
        #
        my $apache_conf_fn=$config_hr->{'HTTPD_SERVER_CONFIG_FILE'} ||
            return err("unable to determine main server config file");
        ($apache_conf_fn=~/^\//) || (
            $apache_conf_fn=File::Spec->catfile(
                $config_hr->{'HTTPD_ROOT'}, $apache_conf_fn
            ));
        my $apache_conf_fh=$opt_hr->{'text'} ? *STDOUT : IO::File->new($apache_conf_fn, O_RDONLY) ||
            return err("unable to open file $apache_conf_fn, $!");
        message "Apache config file '$apache_conf_fn'";


        #  Setup delims looking for
        #
        my ($delim, @delim)=$config_hr->{'FILE_APACHE_CONF_DELIM'};


        #  Turn into array, search for delims
        #
        my ($index, @apache_conf);
        while (my $line=<$apache_conf_fh>) {
            push @apache_conf, $line;
            push(@delim, $index) if $line=~/\Q$delim\E/;
            $index++;
        }


        #  Check found right number of delims
        #
        if (@delim != 2 and @delim != 0) {

            return err(
                "found %s '$delim' delimiter%s in $apache_conf_fn at line%s %s, expected exactly 2 delimiters",
                scalar @delim,
                ($#delim ? 's' : '') x 2,
                join(',', @delim)
            );

        }


        #  Check if delim found, if not, make last line
        #
        unless (@delim) {@delim=($index, $index-1)}
        $delim[1]++;


        #  Close
        #
        $apache_conf_fh->close() unless ($apache_conf_fh eq '*main::STDOUT');
        exit if $opt_hr->{'text'};


        #  Splice the lines between the delimiters out
        #
        splice(@apache_conf, $delim[0], $delim[1]-$delim[0]);


        #  Clean up end of conf file, remove excess CR's
        #
        my $lineno=$delim[0]-1;
        for (undef; $lineno > 0; $lineno--) {


            #  We are going backwards through the file, as soon as we
            #  see something we quit
            #
            my $line=$apache_conf[$lineno];
            chomp($line);


            #  Empty
            #
            if ($line=~/^\s*$/) {


                #  Yes, delete and continue
                #
                splice(@apache_conf, $lineno, 1);

            }
            else {


                #  No, quit after rewinding lineno to last val
                #
                $lineno++;
                last;

            }
        }


        #  Only splice back in if not uninstalling
        #
        unless ($Uninstall_fg) {


            #  Get template we want to include in the config file
            #
            $template_fn=File::Spec->catfile(
                $template_dn, $config_hr->{'FILE_APACHE_CONF_TEMPLATE'});


            #  Open it as a template
            #
            $template_or=Text::Template->new(

                type   => 'FILE',
                source => $template_fn,

            ) || return err("unable to open template $template_fn, $!");


            #  Fill in with out self ref as a hash
            #
            my $apache_conf=$template_or->fill_in(

                HASH       => $config_hr,
                DELIMITERS => ['<!--', '-->'],

            ) || return err("unable to fill in template $template_fn, $Text::Template::ERROR");


            #  Splice in now, but write out at end of block
            #
            splice(@apache_conf, $lineno, undef, $apache_conf);


        }


        #  Re-open httpd.conf for write out, unless uninstall and delims not found, which
        #  means nothing was changed.
        #
        unless ($Uninstall_fg && ($delim[0] == $delim[1])) {
            if ($dry_run) {
                message "would update Apache config file '$apache_conf_fn'.";
            }
            else {
                $apache_conf_fh=IO::File->new($apache_conf_fn, O_TRUNC | O_WRONLY) ||
                    return err("unable to open file $apache_conf_fn, $!");
                print $apache_conf_fh join('', @apache_conf) ||
                    return err("unable to write file $apache_conf_fn, $!");
                $apache_conf_fh->close() ||
                    return err("unable to close file $apache_conf_fn, $!");
                message "Apache config file '$apache_conf_fn' updated.";
            }
        }

    }
    else {

        #  No need to update Apache config file - using conf.d dir
        #
        message 'Apache uses conf.d directory - not changing httpd.conf file';

    }


    #  Almost done ..
    #
    unless ($Uninstall_fg) {

        #  Chown cache dir unless it is the system temp dir - then don't mess with it
        #
        if ($cache_dn) {
            unless ($cache_dn eq File::Spec->tmpdir()) {
                message
                    "Granting Apache ($APACHE_UNAME.$APACHE_GNAME) ownership of cache directory '$cache_dn'.";
                if ($dry_run) {
                    message "would chown $cache_dn to $APACHE_UNAME.$APACHE_GNAME";
                }
                else {
                    chown($APACHE_UID, $APACHE_GID, $cache_dn) ||
                        return err("unable to chown $cache_dn to $APACHE_UNAME.$APACHE_GNAME");
                }


                #  Selinx fixup
                #
                if ($SELINUX_ENABLED_BIN && !$dry_run) {


                    #  Run to see if SELinux enabled
                    #
                    if ((system($SELINUX_ENABLED_BIN) >> 8) == 0) {

                        #  SELinux is enabled. chcon first
                        #
                        message("SELinux appears enabled - attempting to set cache directory file contexts appropriately.");
                        if (my $chcon_bin=$SELINUX_CHCON_BIN) {

                            message("Adding SELinux context '$SELINUX_CONTEXT_HTTPD' to cache directory '$cache_dn' via chcon");
                            if (my $rc=system($chcon_bin, '-R', '-t', $SELINUX_CONTEXT_HTTPD, $cache_dn) >> 8) {
                                message("WARNING: SELinux chcon of $cache_dn to $SELINUX_CONTEXT_HTTPD failed with error code $rc\n")
                            }


                            #  Dynaloader files
                            #
                            my @module_so_fn;
                            while (my ($module_so, $module_so_fn)=each %{$SELINUX_SO_CHECK}) {
                                if (eval("require $module_so")) {
                                    foreach my $symbol (keys %::) {
                                        if ($symbol=~/^_<(.*)\/\Q$module_so_fn\E$/) {
                                            push @module_so_fn, File::Spec->catfile($1, $module_so_fn);
                                        }
                                    }
                                }
                            }

                            foreach my $module_so_fn (@module_so_fn) {

                                my $context_ls=qx/ls -lZ $module_so_fn/ ||
                                    message("WARNING: unable to get context of file $module_so_fn");
                                my @context_ls=split(/\s+/, $context_ls);
                                my $context=$context_ls[3];
                                my ($user, $role, $type)=split(/\:/, $context);
                                if (($type ne $SELINUX_CONTEXT_LIB) && !$opt_hr->{'setcontext'}) {
                                    message;
                                    message("WARNING: SELinux context type of '$module_so_fn' is '$type'");
                                    message("WARNING: file may not be loadable by Apache ! Use '$0 --setcontext' to change or fix manually");
                                    message;
                                }
                                elsif (($type ne $SELINUX_CONTEXT_LIB) && $opt_hr->{'setcontext'}) {

                                    message("Adding SELinux context '$SELINUX_CONTEXT_LIB' to module library '$module_so_fn' via chcon");

                                    if (my $rc=system($chcon_bin, '-t', $SELINUX_CONTEXT_LIB, $module_so_fn) >> 8) {
                                        message("WARNING: SELinux chcon of '$module_so_fn' to '$SELINUX_CONTEXT_LIB' failed with error code $rc\n")
                                    }

                                }

                            }

                        }
                        else {
                            message(
                                'WARNING: SELinux appears enabled, but the \'chcon\' command was not found - ' .
                                    "your cache directory may not be writable\n"
                            );
                        }

                        #  Now semanage, if we can find it
                        #
                        if (my $semanage_bin=$SELINUX_SEMANAGE_BIN) {

                            #  List
                            #
                            my $selist=qx/$SELINUX_SEMANAGE_BIN fcontext -l -n/;
                            unless ($selist=~/^\Q$cache_dn\E/m) {

                                #  Context not added yet
                                #
                                message("Adding SELinux context '$SELINUX_CONTEXT_HTTPD' to cache directory '$cache_dn' via semanage");
                                if (my $rc=system($semanage_bin, 'fcontext', '-a', '-t', $SELINUX_CONTEXT_HTTPD, "${cache_dn}(/.*)?") >> 8) {
                                    message("WARNING: SELinux semanage of $cache_dn to $SELINUX_CONTEXT_HTTPD failed with error code $rc\n")
                                }
                            }
                        }
                        else {
                            message('WARNING: SELinux semanage utility not found - chcon changes will be lost if SELinux relabel takes place.')
                        }
                    }
                }


                # Done
                #
                message 'install completed.'

            }
        }
    }
    else {

        message 'uninstall completed'

    }


    #  Finished
    message;
    return \undef;


}
__END__

=begin markdown

# WebDyne::Install::Apache #

# NAME #

WebDyne::Install::Apache - Apache configuration installer for WebDyne

# SYNOPSIS #

```perl
use WebDyne::Install::Apache;

WebDyne::Install::Apache->install($prefix, $installbin_dn, \%options);
WebDyne::Install::Apache->uninstall($prefix, $installbin_dn, \%options);
```

# DESCRIPTION #

`WebDyne::Install::Apache` installs or removes Apache-side configuration files required for serving WebDyne pages under Apache/mod_perl.

The module builds rendered configuration files from bundled templates, merges values from `WebDyne::Constant`, `WebDyne::Install::Constant`, and `WebDyne::Install::Apache::Constant`, ensures the base WebDyne installation step has run, and writes or removes the generated Apache config and WebDyne Perl config files.

# METHODS #

* **install($prefix, $installbin_dn, \%options)**

    Run the Apache installer. This writes rendered configuration files derived from the bundled templates. If `text` is true in the options hash, the complete generated Apache/WebDyne config is printed instead of written to disk. If `dry_run` is true, planned filesystem and ownership changes are reported but not applied.

* **uninstall($prefix, $installbin_dn, \%options)**

    Run the uninstall path, removing generated Apache-side configuration files and delegating cache-directory cleanup to the base installer layer. If `dry_run` is true, planned removals are reported but not applied.

# NOTES #

The module depends on the discovery logic in `WebDyne::Install::Apache::Constant` for Apache binary detection, config directory selection, mod_perl library discovery, and SELinux-related helpers.

# AUTHOR #

Andrew Speer <andrew.speer@isolutions.com.au>

# LICENSE and COPYRIGHT

This file is part of WebDyne.

This software is copyright (c) 2026 by Andrew Speer <andrew.speer@isolutions.com.au>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

Full license text is available at:

<http://dev.perl.org/licenses/>


=end markdown


=head1 WebDyne::Install::Apache


=head1 NAME

WebDyne::Install::Apache - Apache configuration installer for WebDyne


=head1 SYNOPSIS


 use WebDyne::Install::Apache;
 
 WebDyne::Install::Apache->install($prefix, $installbin_dn, \%options);
 WebDyne::Install::Apache->uninstall($prefix, $installbin_dn, \%options);

=head1 DESCRIPTION

C<WebDyne::Install::Apache> installs or removes Apache-side configuration files required for serving WebDyne pages under Apache/mod_perl.

The module builds rendered configuration files from bundled templates, merges values from C<WebDyne::Constant>, C<WebDyne::Install::Constant>, and C<WebDyne::Install::Apache::Constant>, ensures the base WebDyne installation step has run, and writes or removes the generated Apache config and WebDyne Perl config files.


=head1 METHODS

=over

=item *

B<install($prefix, $installbin_dn, \%options)>

Run the Apache installer. This writes rendered configuration files derived from the bundled templates. If C<text> is true in the options hash, the complete generated Apache/WebDyne config is printed instead of written to disk. If C<dry_run> is true, planned filesystem and ownership changes are reported but not applied.



=item *

B<uninstall($prefix, $installbin_dn, \%options)>

Run the uninstall path, removing generated Apache-side configuration files and delegating cache-directory cleanup to the base installer layer. If C<dry_run> is true, planned removals are reported but not applied.



=back


=head1 NOTES

The module depends on the discovery logic in C<WebDyne::Install::Apache::Constant> for Apache binary detection, config directory selection, mod_perl library discovery, and SELinux-related helpers.


=head1 AUTHOR

Andrew Speer L<mailto:andrew.speer@isolutions.com.au>


=head1 LICENSE and COPYRIGHT

This file is part of WebDyne.

This software is copyright (c) 2026 by Andrew Speer L<mailto:andrew.speer@isolutions.com.au>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

Full license text is available at:

L<http://dev.perl.org/licenses/>

=cut
