mourningdove/cgi-bin/DW/Controller/Admin/Invites.pm

478 lines
15 KiB
Perl
Raw Permalink Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
#
# DW::Controller::Admin::Invites
#
# Management tasks related to invite codes.
# Requires finduser:codetrace, siteadmin:invites, or payments privileges.
#
# Authors:
# Jen Griffin <kareila@livejournal.com>
#
# Copyright (c) 2020 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::Admin::Invites;
use strict;
use DW::Controller;
use DW::Controller::Admin;
use DW::Routing;
use DW::Task::DistributeInvites;
use DW::Template;
use DW::FormErrors;
use DW::InviteCodes;
use DW::InviteCodes::Promo;
use DW::InviteCodeRequests;
use DW::Pay;
my $all_invite_privs =
[ 'finduser:codetrace', 'finduser:*', 'payments', 'siteadmin:invites', 'siteadmin:*' ];
my $light_invite_privs = [ 'payments', 'siteadmin:invites', 'siteadmin:*' ];
DW::Routing->register_string( "/admin/invites", \&index_controller, app => 1 );
DW::Controller::Admin->register_admin_page(
'/',
path => 'invites',
ml_scope => '/admin/invites/index.tt',
privs => $all_invite_privs
);
DW::Routing->register_string( "/admin/invites/codetrace", \&codetrace_controller, app => 1 );
DW::Routing->register_string( "/admin/invites/distribute", \&distribute_controller, app => 1 );
DW::Routing->register_string( "/admin/invites/requests", \&requests_controller, app => 1 );
DW::Routing->register_string( "/admin/invites/review", \&review_controller, app => 1 );
DW::Routing->register_string( "/admin/invites/promo", \&promo_controller, app => 1 );
sub index_controller {
my ( $ok, $rv ) = controller( privcheck => $all_invite_privs );
return $rv unless $ok;
my $remote = $rv->{remote};
# we show links to various subpages depending on which privs the remote has;
# can_manage_invites_light consists of "payments" or "siteadmin:invites"
my $vars = {
has_payments => $remote->has_priv("payments"),
has_finduser => $remote->has_priv( "finduser", "codetrace" ),
has_invites => $remote->can_manage_invites_light,
};
return DW::Template->render_template( 'admin/invites/index.tt', $vars );
}
sub codetrace_controller {
my ( $ok, $rv ) = controller( privcheck => [ 'finduser:codetrace', 'finduser:*' ] );
return $rv unless $ok;
my $scope = '/admin/invites/codetrace.tt';
my $r = DW::Request->get;
my $form_args = $r->get_args;
my $vars = {};
my $account;
if ( $form_args->{code} ) {
if ( my $code = DW::InviteCodes->new( code => $form_args->{code} ) ) {
$account = $rv->{remote};
$vars->{display_codes} = [$code];
}
else {
$vars->{display_error} =
LJ::Lang::ml( "$scope.error.invalidcode", { code => $form_args->{code} } );
}
}
elsif ( $form_args->{account} ) {
if ( $account = LJ::load_user( $form_args->{account} ) ) {
my @used = DW::InviteCodes->by_recipient( userid => $account->id );
my @owned = DW::InviteCodes->by_owner( userid => $account->id );
if ( @used or @owned ) {
$vars->{display_codes} = [ @used, @owned ];
}
else {
$vars->{display_error} =
LJ::Lang::ml( "$scope.error.nocodes", { account => $account->ljuser_display } );
}
}
else {
$vars->{display_error} =
LJ::Lang::ml( "$scope.error.invaliduser", { user => $form_args->{account} } );
}
}
$vars->{maxlength_code} = DW::InviteCodes::CODE_LEN;
$vars->{maxlength_user} = $LJ::USERNAME_MAXLENGTH;
$vars->{time_to_http} = sub { return LJ::time_to_http( $_[0] ) };
$vars->{load_code_owner} = sub {
return $_[0]->owner == $account->id ? $account : LJ::load_userid( $_[0]->owner );
};
$vars->{load_code_recipient} = sub {
return $_[0]->is_used ? LJ::load_userid( $_[0]->recipient ) : undef;
};
return DW::Template->render_template( 'admin/invites/codetrace.tt', $vars );
}
sub distribute_controller {
my ( $ok, $rv ) = controller( form_auth => 1, privcheck => $light_invite_privs );
return $rv unless $ok;
my $scope = '/admin/invites/distribute.tt';
my $r = DW::Request->get;
my $post_args = $r->post_args;
my $vars = {};
my $classes = DW::BusinessRules::InviteCodes::user_classes();
# we can't just splat this hashref into an arrayref because
# the order of the list will be randomized every time we load it
# which is bad UX for a dropdown menu
$vars->{classes} = [];
foreach ( sort { $classes->{$a} cmp $classes->{$b} } keys %$classes ) {
push @{ $vars->{classes} }, $_, $classes->{$_};
}
if ( $r->did_post ) {
# sanitize the number of invites
my $num_invites_requested = $post_args->{num_invites};
$num_invites_requested =~ s/[^0-9]//g;
$num_invites_requested += 0;
if ($num_invites_requested) {
# sanitize selected user class
my $selected_user_class = $post_args->{user_class};
if ( exists $classes->{$selected_user_class} ) {
$vars->{dispatch} = DW::TaskQueue->dispatch(
DW::Task::DistributeInvites->new(
{
requester => $rv->{remote}->userid,
searchclass => $selected_user_class,
invites => $num_invites_requested,
reason => $post_args->{reason}
}
)
);
$vars->{display_error} = LJ::Lang::ml("$scope.error.cantinsertjob")
unless $vars->{dispatch};
}
else {
$vars->{display_error} =
LJ::Lang::ml( "$scope.error.nosuchclass", { class => $selected_user_class } );
}
}
else {
$vars->{display_error} = LJ::Lang::ml("$scope.error.noinvites");
}
}
return DW::Template->render_template( 'admin/invites/distribute.tt', $vars );
}
sub requests_controller {
my ( $ok, $rv ) = controller( form_auth => 1, privcheck => $light_invite_privs );
return $rv unless $ok;
my $scope = '/admin/invites/requests.tt';
my $r = DW::Request->get;
my $vars = {};
# we do the post processing in the template (radical!)
$vars->{r} = $r;
# get outstanding invites
my @outstanding = DW::InviteCodeRequests->outstanding;
$vars->{outstanding} = \@outstanding;
# load user objects
my $users = LJ::load_userids( map { $_->userid } @outstanding );
$vars->{users} = $users;
# count invites the user has
$vars->{counts} = {};
foreach my $u ( values %$users ) {
$vars->{counts}->{ $u->id } =
DW::InviteCodes->unused_count( userid => $u->id );
}
# subroutine for counting accounts registered to user's email.
$vars->{pc_accts} = sub {
my ($u) = @_;
if ( my $acctids = $u->accounts_by_email ) {
my $us = LJ::load_userids(@$acctids);
my ( $pct, $cct ) = ( 0, 0 );
foreach (@$acctids) {
next unless $us->{$_};
$pct++ if $us->{$_}->is_personal;
$cct++ if $us->{$_}->is_comm;
}
return "$pct/$cct";
}
else {
return "N/A";
}
};
# subroutine to check whether the user is sysbanned
$vars->{sysbanned} = sub { DW::InviteCodeRequests->invite_sysbanned( user => $_[0] ) };
$vars->{time_to_http} = sub { return LJ::time_to_http( $_[0] ) };
$vars->{reason_text} = sub { $_[0]->reason || LJ::Lang::ml("$scope.noreason") };
$vars->{reason_link} = sub {
my ( $u, $reason ) = @_;
return $reason unless $rv->{remote}->has_priv("payments");
return "<a href='review?user=$u->{user}'>$reason</a>";
};
return DW::Template->render_template( 'admin/invites/requests.tt', $vars );
}
sub review_controller {
my ( $ok, $rv ) = controller( form_auth => 1, privcheck => ['payments'] );
return $rv unless $ok;
my $r = DW::Request->get;
my $vars = {};
# we do the post processing in the template (radical!)
$vars->{r} = $r;
$vars->{getuser} = $r->get_args->{user};
$vars->{u} = LJ::load_user( $vars->{getuser} )
if defined $vars->{getuser};
$vars->{load_req} = sub { DW::InviteCodeRequests->new( reqid => $_[0] ) };
$vars->{list_req} = sub { [ DW::InviteCodeRequests->by_user( userid => $_[0]->id ) ] };
$vars->{unused_count} = sub { DW::InviteCodes->unused_count( userid => $_[0]->id ) };
$vars->{usercodes} = sub { [ DW::InviteCodes->by_owner( userid => $_[0]->id ) ] };
$vars->{load_recipient} = sub { LJ::load_userid( $_[0]->recipient ) };
$vars->{time_to_http} = sub { LJ::time_to_http( $_[0] ) };
$vars->{paid_status} = sub { defined DW::Pay::get_paid_status( $_[0] ) };
$vars->{get_oldest} = sub {
# being tyrannical, and forcing the earliest outstanding
# request to be the one which is processed
my ($requests) = @_;
return ( grep { $_->{status} eq 'outstanding' } @$requests )[0];
};
return DW::Template->render_template( 'admin/invites/review.tt', $vars );
}
sub promo_controller {
my ( $ok, $rv ) = controller( form_auth => 1, privcheck => $light_invite_privs );
return $rv unless $ok;
my $r = DW::Request->get;
my $errors = DW::FormErrors->new;
my $vars = {};
$vars->{code} = $r->get_args->{code} || "";
$vars->{state} = lc( $r->get_args->{state} || "" );
$vars->{load_suggest_u} = sub {
my ($data) = @_;
return unless $data && $data->{suggest_journalid};
return LJ::load_userid( $data->{suggest_journalid} );
};
$vars->{mysql_date} = sub { $_[0] ? LJ::mysql_date( $_[0] ) : "" };
if ( $r->did_post ) {
$vars->{code} = $r->post_args->{code} || "";
$vars->{state} = lc( $r->post_args->{state} || "" );
my $post = $r->post_args;
my $valid = 1;
my $info;
my $data = {
active => defined( $post->{active} ) ? 1 : 0,
code => $vars->{code},
current_count => 0,
max_count => $post->{max_count} || 0,
suggest_journal => $post->{suggest_journal},
paid_class => $post->{paid_class} || '',
paid_months => $post->{paid_months} || undef,
expiry_date_unedited => $post->{expiry_date_unedited} || 0,
expiry_date => $post->{expiry_date} || 0,
expiry_months => $post->{expiry_months} || 0,
expiry_days => $post->{expiry_days} || 0,
};
if ( !$vars->{code} ) {
$errors->add( 'code', '.error.code.missing' );
$valid = 0;
}
elsif ( $vars->{state} eq 'create' ) {
if ( $vars->{code} !~ /^[a-z0-9]+$/i ) {
$errors->add( 'code', '.error.code.invalid_character' );
$valid = 0;
}
elsif ( DW::InviteCodes::Promo->is_promo_code( code => $vars->{code} ) ) {
$errors->add( 'code', '.error.code.exists' );
$valid = 0;
}
}
elsif ( !ref( $info = DW::InviteCodes::Promo->load( code => $vars->{code} ) ) ) {
$errors->add( 'code', '.error.code.invalid' );
$valid = 0;
}
else {
$data->{current_count} = $info->{current_count};
}
if ( $post->{max_count} < 0 ) {
$errors->add( 'max_count', '.error.count.negative' );
}
if ( $post->{suggest_journal} ) {
if ( my $user = LJ::load_user( $post->{suggest_journal} ) ) {
$data->{suggest_journalid} = $user->userid;
}
else {
$errors->add( 'suggest_journal', '.error.suggest_journal.invalid' );
$valid = 0;
}
}
else {
$data->{suggest_journal} = undef;
}
if ( $data->{paid_class} !~ /^(paid|premium)$/ ) {
$data->{paid_class} = undef;
$data->{paid_months} = undef;
}
if ( $data->{expiry_date} ne $data->{expiry_date_unedited} ) {
if ( $data->{expiry_days} || $data->{expiry_months} ) {
$errors->add( 'expiry_date', '.error.date.double_specified' );
$valid = 0;
}
$data->{expiry_db} = LJ::mysqldate_to_time( $data->{expiry_date} );
}
else {
if ( $data->{expiry_days} < 0 ) {
$errors->add( 'expiry_date', '.error.days.negative' );
$valid = 0;
}
if ( $data->{expiry_months} < 0 ) {
$errors->add( 'expiry_date', '.error.months.negative' );
$valid = 0;
}
$data->{expiry_months} = 0 unless $data->{expiry_months};
$data->{expiry_days} = 0 unless $data->{expiry_days};
if ( my $length = $data->{expiry_months} * 30 + $data->{expiry_days} ) {
$data->{expiry_db} = time() + ( $length * 86400 );
}
else {
$data->{expiry_db} = 0;
}
}
if ($valid) {
my $dbh = LJ::get_db_writer();
if ( $vars->{state} eq 'create' ) {
$dbh->do(
"INSERT INTO acctcode_promo (code, max_count, active, suggest_journalid, paid_class, paid_months, expiry_date) VALUES (?, ?, ?, ?, ?, ?, ?)",
undef,
$data->{code},
$data->{max_count},
$data->{active},
$data->{suggest_journalid},
$data->{paid_class},
$data->{paid_months},
$data->{expiry_db}
) or die $dbh->errstr;
delete $vars->{state};
}
else {
$dbh->do(
"UPDATE acctcode_promo SET max_count = ?, active = ?, suggest_journalid = ?, paid_class = ?, paid_months = ?, expiry_date =? WHERE code = ?",
undef,
$data->{max_count},
$data->{active},
$data->{suggest_journalid},
$data->{paid_class},
$data->{paid_months},
$data->{expiry_db},
$data->{code}
) or die $dbh->errstr;
}
}
else {
$vars->{errors} = $errors;
$vars->{formdata} = $data;
return DW::Template->render_template( 'admin/invites/promo-edit.tt', $vars );
}
delete $vars->{code};
} # end if did_post
return DW::Template->render_template( 'admin/invites/promo-edit.tt', $vars )
if $vars->{state} && $vars->{state} eq 'create';
if ( DW::InviteCodes::Promo->is_promo_code( code => $vars->{code} ) ) {
$vars->{formdata} = DW::InviteCodes::Promo->load( code => $vars->{code} );
return DW::Template->render_template( 'admin/invites/promo-edit.tt', $vars );
}
# variables only used in promo.tt
$vars->{codelist} = DW::InviteCodes::Promo->load_bulk( state => $vars->{state} );
return DW::Template->render_template( 'admin/invites/promo.tt', $vars );
}
1;