#!/usr/bin/perl # # DW::Controller::Admin::Invites # # Management tasks related to invite codes. # Requires finduser:codetrace, siteadmin:invites, or payments privileges. # # Authors: # Jen Griffin # # 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 "$reason"; }; 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;