#!/usr/bin/perl
#
# DW::Controller
#
# Not actually a controller, but contains methods that help other controllers.
#
# Authors:
#      Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2009-2022 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#

package DW::Controller;

use strict;
use v5.10;
use Log::Log4perl;
my $log = Log::Log4perl->get_logger(__PACKAGE__);

use Exporter;
use URI;

use DW::Captcha;
use DW::Template;

our ( @ISA, @EXPORT );
@ISA    = qw/ Exporter /;
@EXPORT = qw/ needlogin error_ml success_ml controller /;

# redirects the user to the login page to handle that eventuality
sub needlogin {
    my $r = DW::Request->get;

    my $uri = $r->uri;
    if ( my $qs = $r->query_string ) {
        $uri .= '?' . $qs;
    }
    $uri = LJ::eurl($uri);

    $r->header_out( Location => "$LJ::SITEROOT/?returnto=$uri" );
    return $r->REDIRECT;
}

# returns an error page using a language string
sub error_ml {
    return DW::Template->render_template( 'error.tt',
        { message => LJ::Lang::ml( $_[0], $_[1] ), opts => $_[2] } );
}

# return a success page using a language string
sub success_ml {
    return DW::Template->render_template( 'success.tt',
        { message => LJ::Lang::ml( $_[0], $_[1] ), links => $_[2] } );
}

# return a success page, takes the following arguments:
#   - a scope page in the form of `page-name.tt', in the form that DW::Controller->render_template expects
#       this scope's corresponding .tt.text should have a ".success.message" and ".success.title"
#   - a hashref of arguments to ".success.message", if needed
#   - a list of links, with each link being in the form of { text_ml => ".success.link.x", url => LJ::create_url( "..." ) }
sub render_success {
    return DW::Template->render_template(
        'success-page.tt',
        {
            scope             => "/" . $_[1],
            message_arguments => $_[2],
            links             => $_[3],
        }
    );
}

# helper controller.  give it a few arguments and it does nice things for you.
#
# Supported arguments: (1 stands for any true value, 0 for any false value)
# - anonymous => 1 -- lets anonymous (not logged in) visitors view the page
# - anonymous => 0 -- doesn't (default)
# - authas => 1  or { args } -- allows ?authas= in URL, generates authas form
#                              (not permitted if anonymous => 1 specified)
# - authas => 0 -- doesn't (default)
# - specify_user => 1 -- allows ?user= in URL (Note: requesting both authas and
#                        specify_user is allowed, but probably not a good idea)
# - specify_user => 0 -- doesn't (default)
# - privcheck => $privs -- user must be logged in and have at least one priv of
#                          the ones in this arrayref.
#                          Example: [ "faqedit:guides", "faqcat", "admin:*" ]
# - skip_domsess => 1 -- (for user domains) don't redirect if there is no
#                         domain login cookie
# - skip_domsess => 0 -- (for user domains) do redirect for the user domain
#                        cookie (default)
# - form_auth => 0 -- Do not automatically check form auth ( current default )
# - form_auth => 1 -- Automatically check form auth ( planned to be future default )
#    On any new controller, please try and pass "form_auth => 0" if you are checking
#      the form auth yourself, or if the automatic check will cause problems.
#      Thank you.
# - skip_captcha => 0 -- (default) Controller should do normal captcha logic
#                        and may redirect to a captcha URL.
# - skip_captcha => 1 -- (DANGEROUS) do not ever captcha on this endpoint.
#
# Returns one of:
# - 0, $error_response (if there's an error)
# - 1, $hashref (if everything looks good)
#
# Returned hashref can be passed to DW::Template->render_template as the 2nd
# argument, and has the following keys:
# - remote -- the remote user object or undef (LJ::get_remote())
# - u -- user object for username in ?user= or ?authas= if present and valid,
#        otherwise same as remote
# - authas_html -- HTML for the "switch user" form
sub controller {
    my (%args) = @_;

    my $vars = {};
    my $fail = sub { return ( 0, $_[0] ); };
    my $ok   = sub { return ( 1, $vars ); };

    # some argument combinations are invalid, so just die.  this is something that should
    # be caught in development...
    die "Invalid usage of controller, check your calling arguments.\n"
        if ( $args{authas} && $args{specify_user} )
        || ( $args{authas} && $args{anonymous} )
        || ( $args{privcheck} && $args{anonymous} );

    $args{form_auth}    //= 0;
    $args{skip_captcha} //= 0;

    # 'anonymous' pages must declare themselves, else we assume that a remote is
    # necessary as most pages require a user
    $vars->{u} = $vars->{remote} = LJ::get_remote();

    my $r = DW::Request->get;
    $vars->{r} = $r;

    # check to see if we need to do a bounce to set the domain cookie
    unless ( $r->did_post || $args{skip_domsess} ) {
        my $burl = LJ::remote_bounce_url();
        if ($burl) {
            $r->err_header_out( "Cache-Control" => "no-cache" );
            return $fail->( $r->redirect($burl) );
        }
    }

    unless ( $args{anonymous} ) {
        $vars->{remote}
            or return $fail->( needlogin() );
    }

    # see if we should captcha this user
    unless ( $r->did_post || $args{skip_captcha} ) {
        if ( DW::Captcha->should_captcha_view( $vars->{remote} ) ) {
            return $fail->( $r->redirect( DW::Captcha->redirect_url ) );
        }
    }

    # if they can specify a user argument, try to load that
    if ( $args{specify_user} ) {

        # use 'user' argument if specified, default to remote
        $vars->{u} = LJ::load_user( $r->get_args->{user} ) || $vars->{remote}
            or return $fail->( error_ml('error.invaliduser') );
    }

    # if a page allows authas it must declare it.  authas can only happen if we are
    # requiring the user to be logged in.
    if ( $args{authas} ) {
        $vars->{u} = LJ::get_authas_user( $r->get_args->{authas} || $vars->{remote}->user )
            or return $fail->( error_ml('error.invalidauth') );

        my $authas_args = $args{authas} == 1 ? {} : $args{authas};

        # older pages
        $vars->{authas_html} =
            LJ::make_authas_select( $vars->{remote}, { authas => $vars->{u}->user } );

        # foundation pages
        $vars->{authas_form} =
              "<form action='"
            . LJ::create_url()
            . "' method='get'>"
            . LJ::make_authas_select( $vars->{remote},
            { authas => $vars->{u}->user, foundation => 1, %{ $authas_args || {} } } )
            . "</form>";
    }

    # check user is suitably privved
    if ( my $privs = $args{privcheck} ) {

        # if they just gave us a string, throw it in an array
        $privs = [$privs] unless ref $privs eq 'ARRAY';

        # now iterate over the array and check.  the user must have ANY
        # of the privs to pass the test.
        my $has_one = 0;
        my @privnames;
        foreach my $priv (@$privs) {

            # if priv is a string, assign the priv having to has_one and stop searching
            if ( not ref($priv) ) {
                if ( $vars->{remote}->has_priv($priv) ) {
                    $has_one = 1;
                    last;
                }
                else {
                    push @privnames, $priv;
                }
            }
            elsif ( ref($priv) eq "CODE" ) {    # if priv is a function, get the result and name
                my ( $result, $name ) = $priv->( $vars->{remote} );
                if ($result) {
                    $has_one = 1;
                    last;
                }
                else {
                    push @privnames, $name;
                }
            }
            else {
                die "Malformed priv in privcheck!";
            }
        }

        # now if they have none, throw an error message
        return $fail->(
            error_ml(
                'admin.noprivserror',
                {
                    numprivs  => scalar @$privs,
                    needprivs => join( ', ', sort @privnames )
                }
            )
        ) unless $has_one;
    }

    if ( $r->did_post && $args{form_auth} ) {
        my $post_args = $r->post_args || {};
        return $fail->( error_ml('error.invalidform') )
            unless LJ::check_form_auth( $post_args->{lj_form_auth} );
        $vars->{post_args} = $post_args;
    }

    # everything good... let the caller know they can continue
    return $ok->();
}

# checks a URL to make sure it's ok to redirect to it.
#
# note that this checks on the host name of the given URL, so it's important
# to make sure to pass in a full, absolute URL rather than a relative URI.
sub validate_redirect_url {
    my $url = $_[0];

    return 0 unless $url;

    # Redirect to offsite uri if allowed, and not an internal LJ redirect.
    my $parsed_uri = URI->new($url);

    # if the given URI isn't valid, the URI module doesn't even give the
    # returned object a host method
    my $redir_host = eval { $parsed_uri->host } || "";

    return $LJ::REDIRECT_ALLOWED{$redir_host} || $redir_host =~ m#${LJ::DOMAIN}$#i;
}

1;
