#!/usr/bin/perl
#
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.

package Apache::LiveJournal;

use strict;
no warnings 'uninitialized';

use Apache2::Const qw/ :common REDIRECT HTTP_NOT_MODIFIED
    HTTP_MOVED_PERMANENTLY HTTP_MOVED_TEMPORARILY
    M_TRACE M_OPTIONS /;
use Carp qw/ croak confess /;
use Compress::Zlib;
use Cwd qw/abs_path/;
use Fcntl ':mode';

use Apache::LiveJournal::PalImg;
use LJ::ModuleCheck;
use LJ::PageStats;
use LJ::Protocol;
use LJ::S2;
use LJ::URI;

use DW::Auth;
use DW::BlobStore;
use DW::Captcha;
use DW::Routing;
use DW::Template;
use DW::VirtualGift;
use DW::RateLimit;

BEGIN {
    require "ljlib.pl";
}

my %RQ;    # per-request data
my %REDIR;
my ( $TOR_UPDATE_TIME, %TOR_EXITS );

my %FILE_LOOKUP_CACHE;

# Mapping of MIME types to image types understood by the blob functions.
my %MimeTypeMapd6 = (
    'G' => 'gif',
    'J' => 'jpg',
    'P' => 'png',
);

# redirect data.
foreach my $file ( 'redirect.dat', 'redirect-local.dat' ) {
    open( REDIR, "$LJ::HOME/cgi-bin/$file" ) or next;
    while (<REDIR>) {
        next unless (/^(\S+)\s+(\S+)/);
        my ( $src, $dest ) = ( $1, $2 );
        $REDIR{$src} = $dest;
    }
    close REDIR;
}

my @req_hosts;    # client IP, and/or all proxies, real or claimed

# init handler (PostReadRequest)
sub handler {
    my $apache_r = shift;

    # only perform this once in case of internal redirects
    if ( $apache_r->is_initial_req ) {
        $apache_r->push_handlers( PerlCleanupHandler => sub { %RQ = () } );
        $apache_r->push_handlers( PerlCleanupHandler => "LJ::end_request" );

        if ($LJ::TRUST_X_HEADERS) {

            # if we're behind a lite mod_proxy front-end, we need to trick future handlers
            # into thinking they know the real remote IP address.  problem is, it's complicated
            # by the fact that mod_proxy did nothing, requiring mod_proxy_add_forward, then
            # decided to do X-Forwarded-For, then did X-Forwarded-Host, so we have to deal
            # with all permutations of versions, hence all the ugliness.  Furthermore, we might
            # be behind other types of proxies and loadbalancers that we trust, but the user
            # might have their own, so we should distinguish trusted proxies from other IPs.
            @req_hosts = ( $apache_r->connection->client_ip );
            if ( my $forward = $apache_r->headers_in->{'X-Forwarded-For'} ) {
                my @hosts = split( /\s*,\s*/, $forward );
                if (@hosts) {

                    # Completely ignore original client ip here, since it may belong to
                    # the previous pipelined request on this connection to this Apache worker.
                    @req_hosts = keys %{ { map { $_ => 1 } @hosts } };

                    my $real;
                    if ( ref $LJ::IS_TRUSTED_PROXY eq 'CODE' ) {

                        # Find last IP in X-Forwarded-For that isn't a trusted proxy.
                        do {
                            $real = pop @hosts;
                        } while ( @hosts && $LJ::IS_TRUSTED_PROXY->($real) );
                    }
                    else {
                        # Trust everything by default, real client IP is first.
                        $real  = shift @hosts;
                        @hosts = ();
                    }
                    $apache_r->connection->client_ip($real);
                }
                $apache_r->headers_in->{'X-Forwarded-For'} = join( ", ", @hosts );
            }

            # and now, deal with getting the right Host header
            if ( $_ = $apache_r->headers_in->{'X-Host'} ) {
                $apache_r->headers_in->{'Host'} = $_;
            }
            elsif ( $_ = $apache_r->headers_in->{'X-Forwarded-Host'} ) {
                $apache_r->headers_in->{'Host'} = $_;
            }
        }

        # reload libraries that might've changed
        if ( $LJ::IS_DEV_SERVER && LJ::is_enabled('module_reload') ) {
            my %to_reload;
            while ( my ( $file, $mod ) = each %LJ::LIB_MOD_TIME ) {
                my $cur_mod = ( stat($file) )[9];
                next if $cur_mod == $mod;
                $to_reload{$file} = 1;
            }
            foreach my $key ( keys %INC ) {
                my $file = $INC{$key};
                delete $INC{$key} if $to_reload{$file};
            }

            foreach my $file ( keys %to_reload ) {
                print STDERR "[$$] Reloading file: $file.\n";
                my %reloaded;
                local $SIG{__WARN__} = sub {
                    if ( $_[0] =~ m/^Subroutine (\S+) redefined at / ) {
                        warn @_ if ( $reloaded{$1}++ );
                    }
                    else {
                        warn(@_);
                    }
                };
                my $good = do $file;
                if ($good) {
                    $LJ::LIB_MOD_TIME{$file} = ( stat($file) )[9];
                }
                else {
                    die "Failed to reload module [$file] due to error: $@\n";
                }
            }
        }
    }

    $apache_r->set_handlers( PerlTransHandler => [ \&trans ] );

    return OK;
}

sub redir {
    my ( $apache_r, $url, $code ) = @_;

    $apache_r->content_type("text/html");
    $apache_r->headers_out->{Location} = $url;

    return $code || REDIRECT;
}

# send the user to the URL for them to get their domain session cookie
sub remote_domsess_bounce {
    return redir( BML::get_request(), LJ::remote_bounce_url(), HTTP_MOVED_TEMPORARILY );
}

sub send_concat_res_response {
    my $apache_r = $_[0];
    my $args     = $apache_r->args;
    my $uri      = $apache_r->uri;

    my $dir    = ( $LJ::STATDOCS // $LJ::HTDOCS ) . $uri;
    my $maxdir = ( $LJ::STATDOCS // $LJ::HTDOCS ) . '/max' . $uri;
    return 404
        unless -d $dir || -d $maxdir;

    # Might contain cache buster "?v=3234234234" at the end;
    # plus possibly other unique args (caught by the .*)
    $args =~ s/\?v=.*$//;

    # Collect each file
    my ( $body, $size, $mtime, $mime ) = ( '', 0, 0, undef );
    foreach my $file ( split /,/, substr( $args, 1 ) ) {
        my $res = load_file_for_concat("$dir$file") // load_file_for_concat("$maxdir$file");
        return 404
            unless defined $res;
        $body .= $res->[0];
        $size += $res->[1];
        $mtime = $res->[2]
            if $res->[2] > $mtime;
        $mime //= $res->[3];

        # And catch if they've mixed filetypes, we can't do that
        return 404
            if $mime ne $res->[3];
    }

    # No files? Something went wrong
    return 404
        unless $body;

    # Got a bunch of files, let's concatenate them into the output
    $apache_r->status(200);
    $apache_r->status_line("200 OK");
    $apache_r->content_type($mime);
    $apache_r->headers_out->{"Content-length"} = $size;
    $apache_r->headers_out->{"Last-Modified"}  = LJ::time_to_http($mtime);
    if ( $apache_r->method eq 'GET' ) {
        $apache_r->print($body);
    }
    return OK;
}

sub blocked_bot {
    my $apache_r = shift;

    $apache_r->status(403);
    $apache_r->status_line("403 Denied");
    $apache_r->content_type("text/html");
    my $subject = $LJ::BLOCKED_BOT_SUBJECT || "403 Denied";
    my $message = $LJ::BLOCKED_BOT_MESSAGE || "You don't have permission to view this page.";

    if ($LJ::BLOCKED_BOT_INFO) {
        my $ip   = LJ::get_remote_ip();
        my $uniq = LJ::UniqCookie->current_uniq;
        $message .= " $uniq @ $ip";
    }

    $apache_r->print("<h1>$subject</h1>$message");
    return OK;
}

sub blocked_anon {
    my $apache_r = shift;
    $apache_r->status(403);
    $apache_r->status_line("403 Denied");
    $apache_r->content_type("text/html");

    my $subject = "403 Denied";
    my $message =
"You don't have permission to access $LJ::SITENAME. Please first <a href='$LJ::SITEROOT/login.bml?skin=lynx'>log in</a>.";

    $apache_r->print("<html><head><title>$subject</title></head><body>");
    $apache_r->print("<h1>$subject</h1> $message");
    $apache_r->print("</body></html>");
    return OK;
}

sub resolve_path_for_uri {
    my ( $apache_r, $orig_uri, %opts ) = @_;

    my $uri          = $orig_uri;
    my $redirect_ref = $opts{redirect_url_ref};

    if ( $uri !~ m!(\.\.|\%|\.\/)! ) {
        if ( exists $FILE_LOOKUP_CACHE{$orig_uri} ) {
            return @{ $FILE_LOOKUP_CACHE{$orig_uri} };
        }

        foreach my $dir ( LJ::get_all_directories('htdocs') ) {

            # main page
            my $file = "$dir/$uri";
            if ( -e "$file/index.bml" && $uri eq '/' ) {
                $file .= "index.bml";
                $uri  .= "/index.bml";
            }

            # /blah/file => /blah/file.bml
            if ( -e "$file.bml" ) {
                $file .= ".bml";
                $uri  .= ".bml";
            }

            # /foo  => /foo/
            # /foo/ => /foo/index.bml
            if ( -d $file && -e "$file/index.bml" ) {
                unless ( $uri =~ m!/$! ) {
                    $$redirect_ref = LJ::create_url( $uri . "/" );
                    return;
                }
                $file .= "index.bml";
                $uri  .= "index.bml";
            }
            next unless -f $file;

            $file = abs_path($file);
            if ($file) {
                $uri =~ s!^/+!/!;
                $FILE_LOOKUP_CACHE{$orig_uri} = [ $uri, $file ];
                return @{ $FILE_LOOKUP_CACHE{$orig_uri} };
            }
        }
    }
    return undef;
}

# trans does all the things. This is the initial entrypoint for all requests.
sub trans {
    my $apache_r = $_[0];

    # include some modern security headers for best practices; we put these on everything
    # so that no matter how a user reaches us, they get the configs
    $apache_r->headers_out->{'X-Content-Type-Options'} = 'nosniff';
    $apache_r->headers_out->{'Referrer-Policy'}        = 'same-origin';
    if ( $LJ::PROTOCOL eq 'https' ) {

        # TODO: Raise HSTS timer here, but I want it low while I test to make sure that
        # the world doesn't implode; suggested value is 31536000.
        $apache_r->headers_out->{'Strict-Transport-Security'} = 'max-age=300; includeSubDomains';
    }

    # don't deal with subrequests or OPTIONS
    return DECLINED
        if defined $apache_r->main || $apache_r->method_number == M_OPTIONS;

    my $uri      = $apache_r->uri;
    my $args     = $apache_r->args;
    my $args_wq  = $args ? "?$args" : "";
    my $host     = lc( $apache_r->headers_in->{"Host"} );
    my $hostport = ( $host =~ s/(:\d+)$// ) ? $1 : "";

    # Allow hosts ending in . to work properly.
    $host =~ s/\.$//;

    # disable TRACE (so scripts on non-LJ domains can't invoke
    # a trace to get the LJ cookies in the echo)
    return FORBIDDEN if $apache_r->method_number == M_TRACE;

    # If the configuration says to log statistics and GTop is available, mark
    # values before the request runs so it can be turned into a delta later
    if ( my $gtop = LJ::gtop() ) {
        $apache_r->pnotes->{gtop_cpu} = $gtop->cpu;
        $apache_r->pnotes->{gtop_mem} = $gtop->proc_mem($$);
    }

    LJ::start_request();
    S2::set_domain('LJ');

    # Apply rate limiting (after start_request so get_remote returns fresh data)
    my $remote = LJ::get_remote();
    my $ip     = LJ::get_remote_ip();

    # Get the appropriate rate limit based on whether user is logged in
    my $limit;
    if ($remote) {
        $limit = DW::RateLimit->get(
            "authenticated_requests",
            rate => "100/60s"    # 100 requests per minute
        );
    }
    else {
        $limit = DW::RateLimit->get(
            "anonymous_requests",
            rate => "30/60s"     # 30 requests per minute
        );
    }

    # Check if rate limit is exceeded
    if ($limit) {
        my $result = $limit->check(
            userid => $remote ? $remote->userid : undef,
            ip     => $remote ? undef           : $ip
        );

        if ( $result->{exceeded} ) {
            $apache_r->status(429);
            $apache_r->status_line("429 Too Many Requests");
            $apache_r->content_type("text/html");

            my $retry_after = $result->{time_remaining};
            $apache_r->headers_out->{'Retry-After'} = $retry_after;

            $apache_r->print("<h1>429 Too Many Requests</h1>");
            $apache_r->print("<p>You have made too many requests. Please try again later.</p>");
            if ($retry_after) {
                $apache_r->print("<p>Please wait $retry_after seconds before trying again.</p>");
            }
            return OK;
        }
    }

    my $lang = $LJ::DEFAULT_LANG || $LJ::LANGS[0];
    BML::set_language( $lang, \&LJ::Lang::get_text );

    if ( $apache_r->is_initial_req ) {

        # handle uniq cookies
        # this will ensure that we have a correct cookie value
        # and also add it to $apache_r->notes
        LJ::UniqCookie->ensure_cookie_value;

        # apply sysban block if applicable
        if ( LJ::UniqCookie->sysban_should_block ) {
            $apache_r->handler("perl-script");
            $apache_r->push_handlers( PerlResponseHandler => \&blocked_bot );
            return OK;
        }

    }
    else {    # not is_initial_req
        if ( $apache_r->status == 404 ) {
            my $ret = DW::Routing->call( uri => "/internal/local/404" );
            $ret //= DW::Routing->call( uri => "/internal/404" );
            return $ret if defined $ret;
        }
    }

    # let foo.com still work, but redirect to www.foo.com
    if (   $LJ::DOMAIN_WEB
        && $apache_r->method eq "GET"
        && $host eq $LJ::DOMAIN
        && $LJ::DOMAIN_WEB ne $LJ::DOMAIN )
    {
        my $url = "$LJ::SITEROOT$uri";
        $url .= "?" . $args if $args;
        return redir( $apache_r, $url );
    }

    # handle alternate domains
    if (   $host ne $LJ::DOMAIN
        && $host ne $LJ::DOMAIN_WEB
        && !( $LJ::EMBED_MODULE_DOMAIN && $host =~ /$LJ::EMBED_MODULE_DOMAIN$/ ) )
    {
        my $which_alternate_domain = undef;
        foreach my $other_host (@LJ::ALTERNATE_DOMAINS) {
            $which_alternate_domain = $other_host
                if $host =~ m/\Q$other_host\E$/i;
        }

        if ( defined $which_alternate_domain ) {
            my $root = "$LJ::PROTOCOL://";
            $host =~ s/\Q$which_alternate_domain\E$/$LJ::DOMAIN/i;

            # do $LJ::DOMAIN -> $LJ::DOMAIN_WEB here, to save a redirect.
            if ( $LJ::DOMAIN_WEB && $host eq $LJ::DOMAIN ) {
                $host = $LJ::DOMAIN_WEB;
            }
            $root .= "$host";

            if ( $apache_r->method eq "GET" ) {
                my $url = "$root$uri";
                $url .= "?" . $args if $args;
                return redir( $apache_r, $url );
            }
            else {
                return redir( $apache_r, $root );
            }
        }
    }

    $remote = LJ::get_remote();

    # block on IP address for anonymous users but allow users to log in,
    # and logged in users to go through

    # we're not logged in, and we're not in the middle of logging in
    unless ( $remote || LJ::remote_bounce_url() ) {

        # allow the user to go through login and subdomain cookie checking paths
        unless ( $uri =~ m!^(?:/login|/__setdomsess|/misc/get_domain_session)! ) {

            foreach my $ip (@req_hosts) {
                if ( LJ::sysban_check( 'noanon_ip', $ip ) ) {
                    $apache_r->handler("perl-script");
                    $apache_r->push_handlers( PerlResponseHandler => \&blocked_anon );
                    return OK;
                }
            }
        }
    }

    # check for sysbans on ip address, and block the ip address completely
    unless ( $LJ::BLOCKED_BOT_URI && index( $uri, $LJ::BLOCKED_BOT_URI ) == 0 ) {
        foreach my $ip (@req_hosts) {
            if ( LJ::sysban_check( 'ip', $ip ) ) {
                $apache_r->handler("perl-script");
                $apache_r->push_handlers( PerlResponseHandler => \&blocked_bot );
                return OK;
            }
        }

        if ( LJ::Sysban::tempban_check( ip => \@req_hosts ) ) {
            $apache_r->handler("perl-script");
            $apache_r->push_handlers( PerlResponseHandler => \&blocked_bot );
            return OK;
        }

        if ( LJ::Hooks::run_hook( "forbid_request", $apache_r ) ) {
            $apache_r->handler("perl-script");
            $apache_r->push_handlers( PerlResponseHandler => \&blocked_bot );
            return OK;
        }
    }

    # See if this is a concatenated static file request. If so, the args will start with a '?'
    # which means the original URL was of the form '/foo/??bar'.
    if ( $args =~ /^\?/ ) {
        $apache_r->handler("perl-script");
        $apache_r->push_handlers( PerlResponseHandler => \&send_concat_res_response );
        return OK;
    }

    # Normal request processing
    my %GET = LJ::parse_args( $apache_r->args );

    if ( $LJ::IS_DEV_SERVER && $GET{'as'} =~ /^\w{1,25}$/ ) {
        my $ru = LJ::load_user( $GET{'as'} );
        LJ::set_remote($ru);    # might be undef, to allow for "view as logged out"
    }

    # is this the embed module host
    if ( $LJ::EMBED_MODULE_DOMAIN && $host =~ /$LJ::EMBED_MODULE_DOMAIN$/ ) {
        return DW::Routing->call( uri => '/journal/embedcontent' );
    }

    my $journal_view = sub {
        my $opts = shift;
        $opts ||= {};

        my $orig_user = $opts->{'user'};
        $opts->{'user'} = LJ::canonical_username( $opts->{'user'} );

        my $remote = LJ::get_remote();
        my $u      = LJ::load_user($orig_user);

        # do redirects:
        # -- uppercase usernames
        # -- users with hyphens/underscores, except users from external domains
        if (   $orig_user ne lc($orig_user)
            || $orig_user =~ /[_-]/
            && $u
            && $u->journal_base !~ m!^$LJ::PROTOCOL://$host!i
            && $opts->{'vhost'} !~ /^other:/ )
        {
            my $newurl = $uri;

            # if we came through $opts->{vhost} eq "users" path above, then
            # the s/// below will not match and there will be a leading /,
            # so the s/// leaves a leading slash as well so that $newurl is
            # consistent for the concatenation before redirect
            $newurl =~ s!^/(users/|community/|~)\Q$orig_user\E!/!;
            $newurl = $u->journal_base . "$newurl$args_wq" if $u;
            return redir( $apache_r, $newurl );
        }

        # check if this entry or journal contains adult content
        if ( LJ::is_enabled('adult_content') ) {

            # force remote to be checked
            my $burl = LJ::remote_bounce_url();
            return remote_domsess_bounce() if LJ::remote_bounce_url();

            my $entry = $opts->{ljentry};
            my $poster;

            my $adult_content = "none";
            if ( $u && $entry ) {
                $adult_content = $entry->adult_content_calculated || $u->adult_content_calculated;
                $poster        = $entry->poster;
            }
            elsif ($u) {
                $adult_content = $u->adult_content_calculated;
            }

            # we should show the page (no interstitial) if:
            # the viewed user is deleted / suspended OR
            # the entry is specified but invalid OR
            # the remote user owns the journal we're viewing OR
            # the remote user posted the entry we're viewing
            my $should_show_page =
                   ( $u && !$u->is_visible )
                || ( $entry && !$entry->valid )
                || ( $remote
                && ( $remote->can_manage($u) || ( $entry && $remote->equals($poster) ) ) );

            my %journal_pages = (
                read    => 1,
                archive => 1,
                month   => 1,
                day     => 1,
                tag     => 1,
                entry   => 1,
                reply   => 1,
                lastn   => 1,
            );
            my $is_journal_page = !$opts->{mode} || $journal_pages{ $opts->{mode} };

            if ( $adult_content ne "none" && $is_journal_page && !$should_show_page ) {
                my $returl = "$LJ::PROTOCOL://$host" . $apache_r->uri . "$args_wq";

                LJ::set_active_journal($u);
                $apache_r->pnotes->{user}  = $u;
                $apache_r->pnotes->{entry} = $entry if $entry;
                $apache_r->notes->{returl} = $returl;

                unless (
                    DW::Logic::AdultContent->user_confirmed_page(
                        user          => $remote,
                        journal       => $u,
                        entry         => $entry,
                        adult_content => $adult_content
                    )
                    )
                {
                    my $adult_content_handler = sub {
                        $apache_r->handler("perl-script");
                        $apache_r->notes->{adult_content_type} = $_[0];
                        $apache_r->push_handlers(
                            PerlHandler => sub { adult_interstitial( $_[0] ) }, );
                        return OK;
                    };

# logged in users with a defined age of under 18 are blocked from explicit adult content
# logged in users with a defined age of under 18 are given a confirmation page for adult concepts depending on their settings
# logged in users with a defined age of 18 or older are given confirmation pages for adult content depending on their settings
# logged in users without defined ages and logged out users are given confirmation pages for all adult content
                    if ( $adult_content eq "explicit" && $remote && $remote->is_minor ) {
                        return $adult_content_handler->(
                            DW::Logic::AdultContent->adult_interstitial_path(
                                type => 'explicit_blocked'
                            )
                        );
                    }
                    else {
                        my $hide_adult_content = $remote ? $remote->hide_adult_content : "concepts";
                        if ( $adult_content eq "explicit" && $hide_adult_content ne "none" ) {
                            return $adult_content_handler->(
                                DW::Logic::AdultContent->adult_interstitial_path(
                                    type => 'explicit'
                                )
                            );
                        }
                        elsif ( $adult_content eq "concepts" && $hide_adult_content eq "concepts" )
                        {
                            return $adult_content_handler->(
                                DW::Logic::AdultContent->adult_interstitial_path(
                                    type => 'concepts'
                                )
                            );
                        }
                    }
                }
            }
        }

        if ( $opts->{'mode'} eq "info" ) {
            my $u = LJ::load_user( $opts->{user} )
                or return 404;
            my $mode = $GET{mode} eq 'full' ? '?mode=full' : '';
            return redir( $apache_r, $u->profile_url . $mode );
        }

        if ( $opts->{'mode'} eq "profile" ) {

            $apache_r->notes->{_journal} = $opts->{user};

            # this is the notes field that all other s1/s2 pages use.
            # so be consistent for people wanting to read it.
            # _journal above is kinda deprecated, but we'll carry on
            # its behavior of meaning "whatever the user typed" to be
            # passed to the profile BML page, whereas this one only
            # works if journalid exists.
            if ( my $u = LJ::load_user( $opts->{user} ) ) {
                $apache_r->notes->{journalid} = $u->{userid};
            }

            return DW::Routing->call( uri => '/profile' );
        }

        if ( $opts->{'mode'} eq "update" ) {
            my $u = LJ::load_user( $opts->{user} )
                or return 404;

            return redir( $apache_r, "$LJ::SITEROOT/update.bml?usejournal=" . $u->{'user'} );
        }

        %RQ = %$opts;

        if ( $opts->{mode} eq "data" && $opts->{pathextra} =~ m!^/(\w+)(/.*)?! ) {
            my $remote = LJ::get_remote();
            my $burl   = LJ::remote_bounce_url();
            return remote_domsess_bounce() if LJ::remote_bounce_url();

            my ( $mode, $path ) = ( $1, $2 );

            if ( my $handler = LJ::Hooks::run_hook( "data_handler:$mode", $RQ{'user'}, $path ) ) {
                $apache_r->handler("perl-script");
                $apache_r->push_handlers( PerlResponseHandler => $handler );
                return OK;
            }
        }

        $apache_r->handler("perl-script");
        $apache_r->push_handlers( PerlResponseHandler => \&journal_content );
        return OK;
    };

    my $determine_view = sub {
        my ( $user, $vhost, $uuri ) = @_;
        my $mode = undef;
        my $pe;
        my $ljentry;

        # if favicon, let filesystem handle it, for now, until
        # we have per-user favicons.
        if ( $uuri eq "/favicon.ico" ) {
            $apache_r->filename( LJ::resolve_file("htdocs/$uuri") );
            return OK;
        }

        # see if there is a modular handler for this URI
        my $ret = LJ::URI->handle( $uuri, $apache_r );
        $ret = DW::Routing->call( username => $user ) unless defined $ret;
        return $ret if defined $ret;

        if ( $uuri =~ m#^/tags(.*)# ) {
            return redir( $apache_r, "/tag$1" );
        }

        if ( $uuri eq "/__setdomsess" ) {
            return redir( $apache_r, LJ::Session->setdomsess_handler );
        }

        if ( $uuri =~ m#^/calendar(.*)# ) {
            return redir( $apache_r, "/archive$1" );
        }

        if ( $uuri =~ m#^/(\d+)(\.html?)$#i ) {
            return redir( $apache_r, "/$1.html$args_wq" )
                unless $2 eq '.html';

            my $u = LJ::load_user($user)
                or return 404;

            $ljentry = LJ::Entry->new( $u, ditemid => $1 );
            if ( $GET{'mode'} eq "reply" || $GET{'replyto'} || $GET{'edit'} ) {
                $mode = "reply";
            }
            else {
                $mode = "entry";
            }
        }
        elsif ( $uuri =~ m#^/(\d\d\d\d/\d\d/\d\d)/([a-z0-9_-]+)\.html$# ) {
            my $u = LJ::load_user($user)
                or return 404;

            # This hack validates that the YYYY/MM/DD given to us is correct.
            my $date = $1;
            $ljentry = LJ::Entry->new( $u, slug => $2 );
            if ( defined $ljentry ) {
                my $dt = join( '/', split( '-', substr( $ljentry->eventtime_mysql, 0, 10 ) ) );
                return 404 unless $dt eq $date;
            }

            if ( $GET{'mode'} eq "reply" || $GET{'replyto'} || $GET{'edit'} ) {
                $mode = "reply";
            }
            else {
                $mode = "entry";
            }
        }
        elsif ( $uuri =~ m#^/(\d\d\d\d)(?:/(\d\d)(?:/(\d\d))?)?(/?)$# ) {
            my ( $year, $mon, $day, $slash ) = ( $1, $2, $3, $4 );
            unless ($slash) {
                my $u = LJ::load_user($user)
                    or return 404;
                my $proper = $u->journal_base . "/$year";
                $proper .= "/$mon" if defined $mon;
                $proper .= "/$day" if defined $day;
                $proper .= "/";
                return redir( $apache_r, $proper );
            }

            # the S1 ljviews code looks at $opts->{'pathextra'}, because
            # that's how it used to do it, when the pathextra was /day[/yyyy/mm/dd]
            $pe = $uuri;

            if ( defined $day ) {
                $mode = "day";
            }
            elsif ( defined $mon ) {
                $mode = "month";
            }
            else {
                $mode = "archive";
            }

        }
        elsif (
            $uuri =~ m!
                 /([a-z\_]+)?           # optional /<viewname>
                 (.*)                   # path extra: /ReadingFilter, for example
                 !x && ( $1 eq "" || defined $LJ::viewinfo{$1} )
            )
        {
            ( $mode, $pe ) = ( $1, $2 );
            $mode ||= "" unless length $pe;    # if no pathextra, then imply 'lastn'

            # redirect old-style URLs to new versions:
            if ( $mode =~ /^day|calendar$/ && $pe =~ m!^/\d\d\d\d! ) {
                my $newuri = $uri;
                $newuri =~ s!$mode/(\d\d\d\d)!$1!;
                return redir( $apache_r, LJ::journal_base($user) . $newuri );
            }
            elsif ( $mode eq 'rss' ) {

                # code 301: moved permanently, update your links.
                return redir( $apache_r, LJ::journal_base($user) . "/data/rss$args_wq", 301 );
            }
            elsif ( $mode eq 'tag' ) {

                # tailing slash on here to prevent a second redirect after this one
                return redir( $apache_r, LJ::journal_base($user) . "$uri/" ) unless $pe;
                if ( $pe eq '/' ) {

                    # tag list page
                    $mode = 'tag';
                    $pe   = undef;
                }
                else {
                    # filtered lastn page
                    $mode = 'lastn';

                    # prepend /tag so that lastn knows to do tag filtering
                    $pe = "/tag$pe";
                }
            }
            elsif ( $mode eq 'security' ) {

                # tailing slash on here to prevent a second redirect after this one
                return redir( $apache_r, LJ::journal_base($user) . "$uri/" ) unless $pe;

                # filtered lastn page
                $mode = 'lastn';

                # prepend /security so that lastn knows to do security filtering
                $pe = "/security$pe";

            }
        }
        elsif ( ( $vhost eq "users" || $vhost =~ /^other:/ )
            && $uuri eq "/robots.txt" )
        {
            $mode = "robots_txt";
        }
        else {
            my $key = $uuri;
            $key =~ s!^/!!;
            my $u = LJ::load_user($user)
                or return 404;
        }

        return undef unless defined $mode;

        # Now that we know ourselves to be at a sensible URI, redirect renamed
        # journals. This ensures redirects work sensibly for all valid paths
        # under a given username, without sprinkling redirects everywhere.
        my $u = LJ::load_user($user);
        if ( $u && $u->is_redirect && $u->is_renamed ) {
            my $renamedto = $u->prop('renamedto');
            if ( $renamedto ne '' ) {
                my $redirect_url =
                    ( $renamedto =~ m!^$LJ::PROTOCOL?://! )
                    ? $renamedto
                    : LJ::journal_base( $renamedto, vhost => $vhost ) . $uuri . $args_wq;
                return redir( $apache_r, $redirect_url, 301 );
            }
        }

        return $journal_view->(
            {
                'vhost'     => $vhost,
                'mode'      => $mode,
                'args'      => $args,
                'pathextra' => $pe,
                'user'      => $user,
                'ljentry'   => $ljentry,
            }
        );
    };

    # user or shop domains
    if (
           $host =~ /^(www\.)?([\w\-]{1,25})\.\Q$LJ::USER_DOMAIN\E$/
        && $2 ne "www"
        &&

        # 1xx: info, 2xx: success, 3xx: redirect, 4xx: client err, 5xx: server err
        # let the main server handle any errors
        $apache_r->status < 400
        )
    {
        # Per bug 3734: users sometimes type 'www.username.USER_DOMAIN'.
        return redir( $apache_r, "$LJ::PROTOCOL://$2.$LJ::USER_DOMAIN$uri$args_wq" )
            if $1 eq 'www.';

        my $user = $2;

        # see if the "user" is really functional code
        my $func = $LJ::SUBDOMAIN_FUNCTION{$user};
        if ( $func eq "normal" ) {

            # site admin wants this domain to be ignored and treated as if it
            # were "www"

        }
        elsif ( $func eq "cssproxy" ) {

            return DW::Routing->call( uri => '/extcss' );

        }
        elsif ( $func eq 'support' ) {
            return redir( $apache_r, "$LJ::SITEROOT/support/" );

        }
        elsif ( $func eq 'shop' ) {

            return redir( $apache_r, "$LJ::SITEROOT/shop$uri" );

        }
        elsif ( $func eq 'mobile' ) {

            return redir( $apache_r, "$LJ::SITEROOT/mobile$uri" );

        }
        elsif ( ref $func eq "ARRAY" && $func->[0] eq "changehost" ) {

            return redir( $apache_r, "$LJ::PROTOCOL://$func->[1]$uri$args_wq" );

        }
        elsif ( $uri =~ m!^/(?:talkscreen|delcomment)\.bml! ) {

            # these URLs need to always work for the javascript comment management code
            # (JavaScript can't do cross-domain XMLHttpRequest calls)
            return DECLINED;

        }
        elsif ( $func eq "journal" ) {
            unless ( $uri =~ m!^/(\w{1,25})(/.*)?$! ) {
                if ( $uri eq "/favicon.ico" ) {
                    $apache_r->filename( LJ::resolve_file("htdocs/$uri") );
                    return OK;
                }

                my $redir = LJ::Hooks::run_hook( "journal_subdomain_redirect_url", $host, $uri );
                return redir( $apache_r, $redir ) if $redir;
                return 404;
            }
            ( $user, $uri ) = ( $1, $2 );
            $uri ||= "/";

            # redirect them to their canonical URL if on wrong host/prefix
            if ( my $u = LJ::load_user($user) ) {
                my $canon_url = $u->journal_base;
                unless ( $canon_url =~ m!^$LJ::PROTOCOL://$host!i ) {
                    return redir( $apache_r, "$canon_url$uri$args_wq" );
                }
            }

            my $view = $determine_view->( $user, "safevhost", $uri );
            return $view if defined $view;

        }
        elsif ($func) {
            my $code = {
                'userpics' => \&userpic_trans,
                'files'    => \&files_trans,
            };
            return $code->{$func}->($apache_r) if $code->{$func};
            return 404;    # bogus ljconfig
        }
        else {
            # if this is the shop, manage domain cookie if needed, else just render
            if ( $user eq 'shop' ) {
                if ( $uri eq '/__setdomsess' ) {
                    return redir( $apache_r, LJ::Session->setdomsess_handler );
                }
                else {
                    $uri =~ s/\/$//;
                    $uri = "/shop$uri";
                }
            }
            else {
                my $view = $determine_view->( $user, "users", $uri );
                return $view if defined $view;
                return 404;
            }
        }
    }

    # userpic
    return userpic_trans($apache_r) if $uri =~ m!^/userpic/!;

    return vgift_trans($apache_r) if $uri =~ m!^/vgift/!;

    # Attempt to handle a URI given the old-style LJ handler, falling back to
    # the new style Dreamwidth routing system.
    my $ret = LJ::URI->handle( $uri, $apache_r ) // DW::Routing->call( uri => $uri );
    return $ret if defined $ret;

    # API role
    if ( $uri =~ m!^/api/v(\d+)(/.+)$! ) {
        my $ver = $1 + 0;
        $ret = DW::Routing->call(
            apiver => $ver,
            uri    => "/v$ver$2",
            role   => 'api'
        );
        $ret //= DW::Routing->call( uri => "/internal/api/404" );
        return $ret if defined $ret;
    }

    # now check for BML pages
    my $redirect_url;
    my ( $alt_uri, $alt_path ) =
        resolve_path_for_uri( $apache_r, $uri, redirect_url_ref => \$redirect_url );

    return redir( $apache_r, $redirect_url ) if $redirect_url;

    if ($alt_path) {
        $apache_r->uri($alt_uri);
        $apache_r->filename($alt_path);
        return OK;
    }

    # normal (non-domain) journal view
    if (
        $uri =~ m!
        ^/(users\/|community\/|\~)  # users/community/tilde
        ([^/]+)                     # potential username
        (.*)?                       # rest
        !x && $uri !~ /\.bml/
        )
    {
        my ( $part1, $user, $rest ) = ( $1, $2, $3 );

        # get what the username should be
        my $cuser = LJ::canonical_username($user);
        return DECLINED unless length($cuser);

        my $srest = $rest || '/';

        # FIXME: skip two redirects and send them right to __setdomsess with the right
        #        cookie-to-be-set arguments.  below is the easy/slow route.
        my $u = LJ::load_user($cuser)
            or return 404;
        my $base = $u->journal_base;

        # If journal_base is path-based on this host (dev container), handle
        # the request here instead of redirecting to ourselves in a loop.
        if (   $LJ::IS_DEV_SERVER
            && $LJ::IS_DEV_CONTAINER
            && $base =~ m!^$LJ::PROTOCOL://$host\Q$hostport\E/~! )
        {
            my $view = $determine_view->( $cuser, "users", $srest );
            return $view if defined $view;
            return 404;
        }

        return redir( $apache_r, "$base$srest$args_wq", correct_url_redirect_code() );
    }

    if ( $uri =~ m!^/palimg/! ) {
        Apache::LiveJournal::PalImg->load;
        $apache_r->handler("perl-script");
        $apache_r->push_handlers( PerlResponseHandler => \&Apache::LiveJournal::PalImg::handler );
        return OK;
    }

    # redirected resources
    if ( $REDIR{$uri} ) {
        my $new = $REDIR{$uri};
        if ( $apache_r->args ) {
            $new .= ( $new =~ /\?/ ? "&" : "?" );
            $new .= $apache_r->args;
        }
        return redir( $apache_r, $new, HTTP_MOVED_PERMANENTLY );
    }

    return FORBIDDEN if $uri =~ m!^/userpics!;

    return DECLINED;
}

sub userpic_trans {
    my $apache_r = shift;
    return 404 unless $apache_r->uri =~ m!^/(?:userpic/)?(\d+)/(\d+)$!;
    my ( $picid, $userid ) = ( $1, $2 );

    # we can safely do this without checking since we never re-use
    # picture IDs and don't let the contents get modified
    return HTTP_NOT_MODIFIED if $apache_r->headers_in->{'If-Modified-Since'};

    $RQ{'picid'}      = $picid;
    $RQ{'pic-userid'} = $userid;

    $apache_r->handler("perl-script");
    $apache_r->push_handlers( PerlResponseHandler => \&userpic_content );
    return OK;
}

sub userpic_content {
    my $apache_r = $_[0];

    # Load the user object and pic and make sure the picture is viewable
    my $u   = LJ::load_userid( $RQ{'pic-userid'} );
    my $pic = LJ::Userpic->get( $u, $RQ{'picid'}, { no_expunged => 1 } )
        or return NOT_FOUND;

    # Must have contents by now, or return 404
    my $data = $pic->imagedata;
    return NOT_FOUND unless $data;

    # Everything looks good, send it
    $apache_r->content_type( $pic->mimetype );
    $apache_r->headers_out->{"Content-length"} = length $data;
    $apache_r->headers_out->{"Cache-Control"}  = "no-transform";
    $apache_r->headers_out->{"Last-Modified"}  = LJ::time_to_http( $pic->pictime );
    $apache_r->print($data)
        unless $apache_r->header_only;
    return OK;
}

sub files_trans {
    my $apache_r = shift;
    return 404 unless $apache_r->uri =~ m!^/(\w{1,25})/(\w+)(/\S+)!;
    my ( $user, $domain, $rest ) = ( $1, $2, $3 );

    if ( my $handler = LJ::Hooks::run_hook( "files_handler:$domain", $user, $rest ) ) {
        $apache_r->handler("perl-script");
        $apache_r->push_handlers( PerlResponseHandler => $handler );
        return OK;
    }
    return 404;
}

sub vgift_trans {
    my $apache_r = shift;
    return 404 unless $apache_r->uri =~ m!^/vgift/(\d+)/(\w+)$!;
    my ( $picid, $picsize ) = ( $1, $2 );
    return 404 unless $picsize =~ /^(?:small|large)$/;

    # we can safely do this without checking
    # unless we're using the admin interface
    return HTTP_NOT_MODIFIED
        if $apache_r->headers_in->{'If-Modified-Since'}
        && $apache_r->headers_in->{'Referer'} !~ m!^\Q$LJ::SITEROOT\E$/admin/!;

    $RQ{picid}   = $picid;
    $RQ{picsize} = $picsize;

    $apache_r->handler("perl-script");
    $apache_r->push_handlers( PerlResponseHandler => \&vgift_content );
    return OK;
}

sub vgift_content {
    my $apache_r = $_[0];
    my $picid    = $RQ{picid};
    my $picsize  = $RQ{picsize};

    my $vg   = DW::VirtualGift->new($picid);
    my $mime = $vg->mime_type($picsize);
    return NOT_FOUND unless $mime;

    my $size;
    my $send_headers = sub {
        $size = $_[0] if @_;
        $size ||= 0;
        $apache_r->content_type($mime);
        $apache_r->headers_out->{"Content-length"} = $size + 0;
        $apache_r->headers_out->{"Cache-Control"}  = "no-transform";
    };

    my $key  = $vg->img_mogkey($picsize);
    my $data = DW::BlobStore->retrieve( vgifts => $key );
    return NOT_FOUND unless $data;

    $send_headers->( length $$data );
    $apache_r->print($$data)
        unless $apache_r->header_only;
    return OK;
}

# load_file_for_concat will retrieve the given file from the disk and return:
# (contents, size, mtime, mime). If the file doesn't exist, undef is returned.
sub load_file_for_concat {
    my $fn = $_[0];

    # No path traversal
    return undef if $fn =~ /\.\./;

    # Specific types only -- we only support loading files for concatenation if
    # their extension is in this list
    my $mime;
    if ( $fn =~ /\.([a-z]+)$/ ) {
        $mime = {
            css => 'text/css; charset=utf-8',
            js  => 'application/javascript; charset=utf-8',
        }->{$1};
    }
    return undef unless $mime;

    # Verify exists and is regular file
    my @stat = stat($fn);
    return undef
        unless scalar @stat > 0
        && S_ISREG( $stat[2] );

    my $contents;
    open FILE, "<$fn"
        or return undef;
    { local $/ = undef; $contents = <FILE>; }
    close FILE;

    # Remove UTF-8 byte-order mark, in case someone set us up the BOM. (It's not
    # valid after the first position in a file, and can screw up syntax when
    # concatenating.)
    $contents =~ s/\A\x{ef}\x{bb}\x{bf}//;

    # Add a newline, juuuust to be safe.
    $contents .= "\n";

    # If we were handling unicode properly, we'd need to encode before checking
    # length... but we aren't.
    my $size = length($contents);

    return [ $contents, $size, $stat[9], $mime ];
}

sub adult_interstitial {
    my ( $apache_r, %opts ) = @_;
    my $int_redir = DW::Routing->call( uri => $apache_r->notes->{adult_content_type}, );

    if ( defined $int_redir ) {

        # we got a match; clear the request cache and return DECLINED.
        LJ::start_request();
        return DECLINED;
    }
}

sub journal_content {
    my $apache_r = shift;
    my $uri      = $apache_r->uri;
    my %GET      = LJ::parse_args( $apache_r->args );

    if ( $RQ{'mode'} eq "robots_txt" ) {
        my $u = LJ::load_user( $RQ{'user'} );
        return 404 unless $u;

        $u->preload_props( "opt_blockrobots", "adult_content" );
        $apache_r->content_type("text/plain");
        my @extra = LJ::Hooks::run_hook( "robots_txt_extra", $u ), ();
        $apache_r->print($_) foreach @extra;
        $apache_r->print("User-Agent: *\n");
        if ( $u->should_block_robots ) {
            $apache_r->print("Disallow: /\n");
        }
        return OK;
    }

    # handle HTTP digest authentication
    if (   $GET{'auth'} eq 'digest'
        || $apache_r->headers_in->{"Authorization"} =~ /^Digest/ )
    {
        my ($res) = DW::Auth->authenticate( digest => 1 );
        unless ($res) {
            $apache_r->status(401);
            $apache_r->status_line("401 Authentication required");
            $apache_r->content_type("text/html");
            $apache_r->print("<b>Digest authentication failed.</b>");
            return OK;
        }
    }

    my $criterr = 0;

    my $remote = LJ::get_remote(
        {
            criterr => \$criterr,
        }
    );

    return remote_domsess_bounce() if LJ::remote_bounce_url();

    # check for faked cookies here, since this is pretty central.
    if ($criterr) {
        $apache_r->status(500);
        $apache_r->status_line("500 Invalid Cookies");
        $apache_r->content_type("text/html");

        # reset all cookies
        foreach my $dom ( "", $LJ::DOMAIN, $LJ::COOKIE_DOMAIN ) {
            DW::Request->get->add_cookie(
                name     => 'ljsession',
                expires  => LJ::time_to_cookie(1),
                domain   => $dom ? $dom : undef,
                path     => '/',
                httponly => 1
            );
        }

        $apache_r->print(
"Invalid cookies.  Try <a href='$LJ::SITEROOT/logout'>logging out</a> and then logging back in.\n"
        );
        $apache_r->print("<!-- xxxxxxxxxxxxxxxxxxxxxxxx -->\n") for ( 0 .. 100 );
        return OK;
    }

    # Before we actually make a journal, let's potentially bounce this user
    # off to get captchaed
    if ( DW::Captcha->should_captcha_view($remote) ) {
        return redir( $apache_r, DW::Captcha->redirect_url );
    }

    # LJ::make_journal() will set this flag for pages that should use the site
    # skin and therefore need to be processed by siteviews (e.g., style=site)
    my $handle_with_siteviews = 0;

    my %headers = ();
    my $opts    = {
        'r'         => $apache_r,
        'headers'   => \%headers,
        'args'      => $RQ{'args'},
        'vhost'     => $RQ{'vhost'},
        'pathextra' => $RQ{'pathextra'},
        'header'    => {
            'If-Modified-Since' => $apache_r->headers_in->{"If-Modified-Since"},
        },
        'handle_with_siteviews_ref' => \$handle_with_siteviews,
        'siteviews_extra_content'   => {},
        'ljentry'                   => $RQ{'ljentry'},
    };

    $apache_r->notes->{view} = $RQ{mode};
    my $user = $RQ{'user'};

    my $html = LJ::make_journal( $user, $RQ{'mode'}, $remote, $opts );

    # Allow to add extra http-header or even modify html
    LJ::Hooks::run_hooks( "after_journal_content_created", $opts, \$html )
        unless $handle_with_siteviews;

    # check for redirects
    if ( $opts->{internal_redir} ) {
        my $int_redir = DW::Routing->call( uri => $opts->{internal_redir} );
        if ( defined $int_redir ) {

            # we got a match; clear the request cache and return DECLINED.
            LJ::start_request();
            return DECLINED;
        }
    }
    return redir( $apache_r, $opts->{'redir'} ) if $opts->{'redir'};
    return $opts->{'handler_return'} if defined $opts->{'handler_return'};

    # if LJ::make_journal() indicated it can't handle the request:
    # only if HTML is set, otherwise leave it alone so the user
    # gets "messed up template definition", cause something went wrong.
    return DW::Template->render_string( $html, $opts->{siteviews_extra_content} )
        if $handle_with_siteviews && $html;

    my $status = $opts->{'status'} || "200 OK";
    $opts->{'contenttype'} ||= $opts->{'contenttype'} = "text/html";
    if (   $opts->{'contenttype'} =~ m!^text/!
        && $opts->{'contenttype'} !~ /charset=/ )
    {
        $opts->{'contenttype'} .= "; charset=utf-8";
    }

    # Set to 1 if the code should generate junk to help IE
    # display a more meaningful error message.
    my $generate_iejunk = 0;

    if ( $opts->{'badargs'} ) {

        # No special information to give to the user, so just let
        # Apache handle the 404
        return 404;
    }
    elsif ( $opts->{'badfriendgroup'} ) {

        # give a real 404 to the journal owner
        if ( $remote && $remote->{'user'} eq $user ) {
            return 404;

            # otherwise be vague with a 403
        }
        else {
            $status = "403 Forbidden";
            $html =
"<h1>Invalid Filter</h1><p>Either this reading filter doesn't exist or you are not authorized to view it. Try <a href='$LJ::SITEROOT/login'>checking that you are logged in</a> if you're sure you have the name right.</p>";
        }

        $generate_iejunk = 1;

    }
    elsif ( $opts->{'suspendeduser'} ) {
        $status = "403 User suspended";
        $html =
            "<h1>Suspended User</h1>" . "<p>The content at this URL is from a suspended user.</p>";

        $generate_iejunk = 1;

    }
    elsif ( $opts->{'suspendedentry'} ) {
        $status = "403 Entry suspended";
        $html   = "<h1>Suspended Entry</h1>"
            . "<p>The entry at this URL is suspended.  You cannot reply to it.</p>";

        $generate_iejunk = 1;

    }
    elsif ( $opts->{'readonlyremote'} || $opts->{'readonlyjournal'} ) {
        $status = "403 Read-only user";
        $html   = "<h1>Read-Only User</h1>";
        $html .=
            $opts->{'readonlyremote'}
            ? "<p>You are read-only.  You cannot post comments.</p>"
            : "<p>This journal is read-only.  You cannot comment in it.</p>";

        $generate_iejunk = 1;
    }

    unless ($html) {
        $status = "500 Bad Template";
        $html =
"<h1>Error</h1><p>User <b>$user</b> has messed up their journal template definition.</p>";
        $generate_iejunk = 1;
    }

    $apache_r->status( $status =~ m/^(\d+)/ );
    $apache_r->status_line($status);
    foreach my $hname ( keys %headers ) {
        if ( ref( $headers{$hname} ) && ref( $headers{$hname} ) eq "ARRAY" ) {
            foreach ( @{ $headers{$hname} } ) {
                $apache_r->headers_out->{$hname} = $_;
            }
        }
        else {
            $apache_r->headers_out->{$hname} = $headers{$hname};
        }
    }

    $apache_r->content_type( $opts->{'contenttype'} );
    $apache_r->headers_out->{"Cache-Control"} = "private, proxy-revalidate";

    $html .= ( "<!-- xxxxxxxxxxxxxxxxxxxxxxxxxxxx -->\n" x 100 ) if $generate_iejunk;

    # Parse the page content for any temporary matches
    # defined in local config
    if ( my $cb = $LJ::TEMP_PARSE_MAKE_JOURNAL ) {
        $cb->( \$html );
    }

    # add stuff before </body>
    my $before_body_close = "";
    LJ::Hooks::run_hooks( "insert_html_before_body_close",            \$before_body_close );
    LJ::Hooks::run_hooks( "insert_html_before_journalctx_body_close", \$before_body_close );

    # Insert pagestats HTML and Javascript
    $before_body_close .= LJ::PageStats->new->render('journal');

    $html =~ s!</body>!$before_body_close</body>!i if $before_body_close;

    my $ctbase = $opts->{'contenttype'};
    $ctbase =~ s/;.*//;
    my $do_gzip = 1;
    $do_gzip = 0 unless $LJ::GZIP_OKAY{$ctbase};
    $do_gzip = 0 if $apache_r->headers_in->{"Accept-Encoding"} !~ /gzip/;

    my $length = length($html);
    $do_gzip = 0 if $length < 500;

    if ($do_gzip) {
        my $pre_len = $length;
        $apache_r->notes->{bytes_pregzip}            = $pre_len;
        $html                                        = Compress::Zlib::memGzip($html);
        $length                                      = length($html);
        $apache_r->headers_out->{'Content-Encoding'} = 'gzip';
    }

    # Let caches know that Accept-Encoding will change content
    $apache_r->headers_out->{'Vary'} = 'Accept-Encoding';

    $apache_r->headers_out->{"Content-length"} = $length;
    $apache_r->print($html) unless $apache_r->header_only;

    return OK;
}

sub correct_url_redirect_code {
    if ($LJ::CORRECT_URL_PERM_REDIRECT) {
        return HTTP_MOVED_PERMANENTLY;
    }
    return REDIRECT;
}

1;
