mourningdove/cgi-bin/DW/Controller.pm

268 lines
9.2 KiB
Perl
Raw Permalink Normal View History

2026-05-24 01:03:05 +00:00
#!/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;