mourningdove/cgi-bin/DW/Controller/Admin/VirtualGift.pm
2026-05-24 01:03:05 +00:00

673 lines
23 KiB
Perl

#!/usr/bin/perl
#
# DW::Controller::Admin::VirtualGift
#
# Management pages for virtual gifts in the shop.
#
# 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::VirtualGift;
use strict;
use DW::Controller;
use DW::Controller::Admin;
use DW::Routing;
use DW::Template;
use DW::VirtualGift;
use DW::FormErrors;
use Image::Size;
my $vgift_privs = [
'vgifts',
'siteadmin:vgifts',
sub {
return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml("/admin/index.tt.devserver") );
}
];
DW::Controller::Admin->register_admin_page(
'/',
path => 'vgifts',
ml_scope => '/admin/vgifts/index.tt',
privs => $vgift_privs
);
DW::Routing->register_string( "/admin/vgifts/index", \&index_controller, app => 1 );
DW::Routing->register_string( "/admin/vgifts/inactive", \&inactive_controller, app => 1 );
DW::Routing->register_string( "/admin/vgifts/tags", \&tags_controller, app => 1 );
sub _loose_refer {
my $baseuri = $_[0];
my $refer = DW::Request->get->header_in('Referer');
return 1 unless $refer;
# annoyingly, we get different results for /index vs /
if ( $baseuri =~ m(/$) ) {
return 1 if LJ::check_referer("${baseuri}index");
}
return LJ::check_referer($baseuri);
}
sub _strict_refer {
# make sure we have a referer header. check_referer doesn't care.
my $ret = DW::Request->get->header_in('Referer') && _loose_refer( $_[0] );
return $ret;
}
sub _check_id {
my $err_ml = $_[1] // \undef;
if ( my $id = $_[0] ) {
my $vgift = DW::VirtualGift->new($id);
return $vgift if $vgift && $vgift->name;
$$err_ml = "vgift.error.badid";
}
else {
$$err_ml = "error.invalidform";
}
}
sub index_controller {
my ( $ok, $rv ) = controller( form_auth => 0, privcheck => $vgift_privs );
return $rv unless $ok;
my $r = $rv->{r};
my $scope = '/admin/vgifts/index.tt';
my $vars = {};
my $remote = $rv->{remote};
my $siteadmin = $remote->has_priv( 'siteadmin', 'vgifts' ) || $LJ::IS_DEV_SERVER;
my $form_args = $r->did_post ? $r->post_args : $r->get_args;
# process multipart form
if ( $r->did_post && !%$form_args ) {
my $size = $r->header_in("Content-Length");
return error_ml("$scope.error.upload.noheader") unless $size;
my $uploads = eval { $r->uploads };
return error_ml( "$scope.error.upload.content", { err => $@ } ) if $@;
foreach my $h (@$uploads) {
$form_args->{ $h->{name} } = $h->{body};
}
# now uploaded data is in $form_args, we can continue
}
my $checkid = sub { return _check_id( $form_args->{id}, $_[0] ) };
my $mode = lc( $form_args->{mode} || $r->get_args->{mode} || '' );
# process post request, but only if we have a mode
if ( $r->did_post && $mode ) {
# check auth manually in case we had a multipart form
return error_ml("error.invalidform")
unless LJ::check_form_auth( $form_args->{lj_form_auth} );
$mode = '' unless _loose_refer("/admin/vgifts/");
my $errors = DW::FormErrors->new;
my $loadpic = sub {
my ($id) = @_;
my $imgposted = length( $form_args->{"data_$id"} ) || length( $form_args->{"url_$id"} );
return undef unless $imgposted;
my $data;
if ( $form_args->{$id} eq 'url' ) {
$data = $form_args->{"url_$id"};
if ( length($data) == 0 ) {
$errors->add( '', "$scope.error.upload.nourl" );
}
elsif ( $data !~ m!^https?://! ) {
$errors->add( '', "$scope.error.upload.badurl" );
}
else {
my $ua = LJ::get_useragent( role => 'vgift' );
my $res = $ua->get($data);
if ( $res && $res->is_success ) {
$data = $res->content;
}
else {
$errors->add( '', "$scope.error.upload.urlerror" );
}
}
}
elsif ( $form_args->{$id} eq 'file' ) {
$data = $form_args->{"data_$id"};
$errors->add( '', "$scope.error.upload.nofile" )
unless length($data);
}
else {
$errors->add( '', 'error.invalidform' );
}
return undef if $errors->exist;
# further processing
my ( $width, $height, $filetype ) = Image::Size::imgsize( \$data );
unless ( $width && $height ) {
$errors->add( '', "$scope.error.upload.badtype", { filetype => $filetype } );
}
elsif ( ( $width > 100 || $height > 100 ) && $id eq 'img_small' ) {
$errors->add(
'',
"$scope.error.upload.dimstoolarge",
{ imagesize => "${width}x$height", maxsize => "100x100" }
);
}
elsif ( ( $width > 300 || $height > 300 ) && $id eq 'img_large' ) {
$errors->add(
'',
"$scope.error.upload.dimstoolarge",
{ imagesize => "${width}x$height", maxsize => "300x300" }
);
}
elsif ( length($data) > 250 * 1024 ) { # 250KB (arbitrary)
$errors->add( '', "$scope.error.upload.filetoolarge", { maxsize => "250" } );
}
else {
# data should be good, return a reference to it
return \$data;
}
# check $errors to see what went wrong
return undef;
};
my ( $redirect_args, $err_ml, $errmsg );
if ( $mode eq 'create' ) {
return error_ml( "$scope.error.denied", { action => $mode } )
unless $remote->has_priv('vgifts') || $siteadmin;
$errors->add( 'name', "$scope.error.create.noname" ) unless $form_args->{name};
$errors->add( 'desc', "$scope.error.create.nodesc" ) unless $form_args->{desc};
my $creatorid;
if ( $form_args->{creator} && $siteadmin ) {
my $u = LJ::load_user_or_identity( $form_args->{creator} );
if ( $u && $u->is_individual ) {
$creatorid = $u->id;
}
else {
$errors->add(
'creator',
"$scope.error.create.badusername",
{ name => $form_args->{creator} }
);
}
}
my ( $img_small, $img_large );
$img_small = $loadpic->('img_small') unless $errors->exist;
$img_large = $loadpic->('img_large') unless $errors->exist;
unless ( $errors->exist ) {
my $vgift = DW::VirtualGift->create(
error => \$errmsg,
name => $form_args->{name},
description => $form_args->{desc},
img_small => $img_small,
img_large => $img_large,
creatorid => $creatorid
);
return error_ml( "$scope.error.create.failure", { err => $errmsg } )
unless $vgift;
# hallelujah, the vgift was created.
$redirect_args = { mode => 'view', title => 'created', id => $vgift->id };
}
# return template below if there were correctable errors
$vars->{mode} = '';
}
elsif ( $mode eq 'edit' ) {
return error_ml($err_ml) unless my $vgift = $checkid->( \$err_ml );
return error_ml( "$scope.error.denied", { action => $mode } )
unless $vgift->can_be_edited_by($remote);
my ( $img_small, $img_large );
$img_small = $loadpic->('img_small') unless $errors->exist;
$img_large = $loadpic->('img_large') unless $errors->exist;
# Don't honor null attributes.
delete $form_args->{name} unless length $form_args->{name};
delete $form_args->{desc} unless length $form_args->{desc};
# Note: this resets any existing approval status.
unless ( $errors->exist ) {
my $ok = $vgift->edit(
error => \$errmsg,
approved => '',
name => $form_args->{name},
description => $form_args->{desc},
img_small => $img_small,
img_large => $img_large
);
return error_ml( "$scope.error.edit.failure", { err => $errmsg } )
unless $ok;
$redirect_args = { mode => 'view', title => 'edited', id => $vgift->id };
}
# return template below if there were correctable errors
$vars->{mode} = 'view';
}
elsif ( $mode eq 'approve' ) {
return error_ml($err_ml) unless my $vgift = $checkid->( \$err_ml );
return error_ml( "$scope.error.denied", { action => $mode } )
unless $vgift->can_be_approved_by($remote);
my $id = $vgift->id;
return error_ml("$scope.error.changed")
if $form_args->{"${id}_chksum"} ne $vgift->checksum;
if ( exists $form_args->{"${id}_approve"} ) {
if ( $form_args->{"${id}_approve"} ) {
my $ok = $vgift->edit(
error => \$errmsg,
approved => $form_args->{"${id}_approve"},
approved_why => $form_args->{"${id}_comment"},
approved_by => $remote->userid
);
return error_ml( "$scope.error.edit.failure", { err => $errmsg } )
unless $ok;
$vgift->notify_approved;
}
else {
# this error isn't fatal
$errors->add( "${id}_approve", "$scope.error.yn" );
}
}
if ( $form_args->{"${id}_featured"} || $form_args->{"${id}_cost"} ) {
my %opts;
foreach my $k (qw( featured cost )) {
$opts{$k} = $form_args->{"${id}_$k"} if $form_args->{"${id}_$k"};
}
my $ok = $vgift->edit( error => \$errmsg, %opts );
return error_ml( "$scope.error.edit.failure", { err => $errmsg } )
unless $ok;
}
if ( $form_args->{"${id}_tags"} ) {
my $ok = $vgift->tags(
$form_args->{"${id}_tags"},
error => \$errmsg,
autovivify => $siteadmin
);
return error_ml( "$scope.error.edit.failure", { err => $errmsg } )
unless $ok;
}
unless ( $errors->exist ) {
return $r->redirect("/admin/vgifts/inactive")
if $form_args->{activation};
# return to review page for item
$redirect_args = { mode => 'review', title => 'approved', id => $id };
my $days = $form_args->{days} ? $form_args->{days} + 0 : 0;
$redirect_args->{days} = $days if $days;
}
# return template below if there were correctable errors
$vars->{mode} = 'review';
}
elsif ( $mode eq 'confirm' ) {
return error_ml($err_ml) unless my $vgift = $checkid->( \$err_ml );
my $ok = $vgift->delete($remote);
return error_ml("$scope.error.delete") unless $ok;
my $re_mode = $remote->userid == $vgift->creatorid ? 'view' : 'review';
$redirect_args = { mode => $re_mode, title => 'deleted' };
}
else {
# if we get here, check_referer failed or something weird happened
return $r->redirect("$LJ::SITEROOT/admin/vgifts/");
}
if ( defined $redirect_args ) {
return $r->redirect( LJ::create_url( undef, args => $redirect_args ) );
}
else {
$vars->{errors} = $errors;
$vars->{formdata} = $form_args;
# fall through to template
}
} # end did_post
# transform get arguments into template variables (id -> vgift; user -> vu)
if ( $form_args->{title} && _strict_refer("/admin/vgifts/") ) {
$vars->{title} = $form_args->{title};
}
$vars->{mode} //= $form_args->{mode};
if ( $form_args->{id} ) {
return error_ml("$scope.error.badid") unless $vars->{vgift} = $checkid->();
}
if ( $form_args->{user} ) {
$vars->{vu} = LJ::load_user( $form_args->{user} );
return error_ml( "$scope.error.baduser", { user => $form_args->{user} } )
unless LJ::isu( $vars->{vu} );
}
$vars->{remote} = $remote;
$vars->{siteadmin} = $siteadmin;
$vars->{inactive} = $form_args->{title} ? $form_args->{title} eq 'inactive' : 0;
$vars->{days} = $form_args->{days} ? $form_args->{days} + 0 : 0;
$vars->{review_list} = sub {
my $days = $vars->{days};
return [ DW::VirtualGift->list_recent($days) ] if $days;
return [ DW::VirtualGift->list_queued() ];
};
$vars->{display_creatorlist} = sub { DW::VirtualGift->display_creatorlist( $_[0] ) };
$vars->{list_created_by} = sub { [ DW::VirtualGift->list_created_by( $_[0] ) ] };
return DW::Template->render_template( 'admin/vgifts/index.tt', $vars );
}
sub inactive_controller {
my $privs = [ $vgift_privs->[1], $vgift_privs->[2] ];
my ( $ok, $rv ) = controller( privcheck => $privs );
return $rv unless $ok;
my $r = $rv->{r};
my $scope = '/admin/vgifts/inactive.tt';
my $vars = {};
my $remote = $rv->{remote};
my $form_args = $r->did_post ? $r->post_args : $r->get_args;
my $mode = lc( $form_args->{mode} || $r->get_args->{mode} || '' );
# process post request, but only if we have a mode
if ( $r->did_post && $mode ) {
$mode = '' unless _loose_refer("/admin/vgifts/inactive");
if ( $mode eq 'activate' ) {
my @vgs;
foreach ( keys %$form_args ) {
my ($id) = ( $_ =~ /^(\d+)_activate$/ );
next unless $id;
my $err_ml;
my $vg = _check_id( $id, $err_ml );
return error_ml($err_ml) unless $vg;
next if $vg->is_active; # already active
return error_ml( "$scope.error.notags", { name => $vg->name_ehtml } )
if $vg->is_untagged;
return error_ml( "$scope.error.changed", { name => $vg->name_ehtml } )
if $form_args->{"${id}_chksum"} ne $vg->checksum;
push @vgs, $vg;
}
# now that we're clear of possible errors, do the activation
$_->mark_active foreach @vgs;
my $ids = join ', ', map { $_->id } @vgs;
LJ::statushistory_add( 0, $remote, 'vgifts', "Activated: $ids" )
if $ids;
# go back to where we were
return $r->redirect( $r->header_in('Referer') );
}
else {
# if we get here, check_referer failed or something weird happened
return $r->redirect("$LJ::SITEROOT/admin/vgifts/inactive");
}
} # end did_post
my @vgs;
if ( $mode eq 'tags' ) {
my $tag = $form_args->{tag} || '';
if ($tag) {
return error_ml("$scope.error.badid")
unless DW::VirtualGift->get_tagid($tag);
$vars->{tag} = $tag;
$vars->{count} = scalar grep { $_->is_approved } DW::VirtualGift->list_untagged;
@vgs = DW::VirtualGift->list_tagged_with($tag);
# list_tagged_with includes active gifts
@vgs = grep { $_->is_inactive } @vgs;
}
else {
@vgs = DW::VirtualGift->list_untagged;
}
my $app = DW::VirtualGift->fetch_tagcounts_approved;
my $act = DW::VirtualGift->fetch_tagcounts_active;
$vars->{approved_inactive} = {};
$vars->{approved_inactive}->{$_} = $app->{$_} - ( $act->{$_} // 0 ) foreach keys %$app;
}
else {
# DEFAULT PAGE DISPLAY
@vgs = DW::VirtualGift->list_inactive;
}
# don't include queued or rejected gifts
@vgs = grep { $_->is_approved } @vgs;
$vars->{feat} = [ grep { $_->is_featured } @vgs ];
$vars->{nonfeat} = [ grep { !$_->is_featured } @vgs ];
$vars->{nonpriv} = { map { $_ => 1 } DW::VirtualGift->list_nonpriv_tags };
$vars->{mode} = $mode;
$vars->{modes} = [ '', 'tags' ]; # ordered
$vars->{tabs} = {
'' => '.tab.default',
'tags' => '.tab.tags',
};
return DW::Template->render_template( 'admin/vgifts/inactive.tt', $vars );
}
sub tags_controller {
my $privs = [ $vgift_privs->[1], $vgift_privs->[2] ];
my ( $ok, $rv ) = controller( privcheck => $privs );
return $rv unless $ok;
my $r = $rv->{r};
my $scope = '/admin/vgifts/tags.tt';
my $vars = {};
my $form_args = $r->did_post ? $r->post_args : $r->get_args;
my $redirect_args;
my $mode = lc( $form_args->{mode} || $r->get_args->{mode} || '' );
# process post request, but only if we have a mode
if ( $r->did_post && $mode ) {
$mode = '' unless _loose_refer("/admin/vgifts/tags");
my $errors = DW::FormErrors->new;
if ( $mode eq 'edit' ) {
my $tagid = $form_args->{id};
return error_ml("$scope.error.badid") unless $tagid;
my $tag = DW::VirtualGift->get_tagname($tagid);
return error_ml("$scope.error.badid") unless $tag;
my $priv = $form_args->{"${tagid}_addpriv"};
my $arg = $form_args->{"${tagid}_privarg"};
$errors->add( "${tagid}_addpriv", "$scope.error.needpriv" ) if $arg && !$priv;
if ($priv) {
my $e_priv = LJ::ehtml($priv);
my $e_arg = LJ::ehtml($arg);
# make sure the new priv is valid
$errors->add( "${tagid}_addpriv", "$scope.error.badpriv", { priv => $e_priv } )
unless DW::VirtualGift->validate_priv($priv);
# also validate arg if given
if ( $arg && $arg ne '*' ) {
my $valid_args = LJ::list_valid_args($priv);
$errors->add( "${tagid}_privarg", "$scope.error.badarg",
{ priv => $e_priv, arg => $e_arg } )
unless $valid_args && $valid_args->{$arg};
}
}
# process rename first
if ( my $newtag = $form_args->{"${tagid}_rename"} ) {
if ( DW::VirtualGift->alter_tag( $tag, $newtag ) ) {
$tag = $newtag; # subsequent changes target $newtag
}
else {
$errors->add( "${tagid}_rename", "$scope.error.badtagname",
{ tag => LJ::ehtml($newtag) } );
}
}
unless ( $errors->exist ) {
# process new privilege
if ($priv) {
my $e_privarg = LJ::ehtml($priv) . ':' . LJ::ehtml($arg);
DW::VirtualGift->add_priv_to_tag( $tag, $priv, $arg )
or return error_ml( "$scope.error.privarg", { privarg => $e_privarg } );
}
# process old privileges
if ( my $maxprivnum = $form_args->{"${tagid}_maxprivnum"} ) {
# only remove existing privs if not renamed (can resubmit)
unless ( $form_args->{"${tagid}_rename"} ) {
DW::VirtualGift->remove_priv_from_tag( $tag, $_->[0], $_->[1] )
foreach DW::VirtualGift->list_tagprivs($tag);
}
# add back selected privs
foreach my $i ( 0 .. $maxprivnum ) {
my $priv_i = $form_args->{"${tagid}_priv$i"};
next unless $priv_i;
my ( $p, $a ) = ( $priv_i =~ /^([^:]+):(.*)$/ );
next unless $p;
DW::VirtualGift->add_priv_to_tag( $tag, $p, $a )
or return error_ml( "$scope.error.privarg",
{ privarg => LJ::ehtml("$p:$a") } );
}
}
$redirect_args =
{ mode => 'view', title => 'edited', id => DW::VirtualGift->get_tagid($tag) };
}
}
elsif ( $mode eq 'confirm' ) {
my $tag = DW::VirtualGift->get_tagname( $form_args->{id} );
return error_ml("$scope.error.badid") unless $tag;
DW::VirtualGift->alter_tag($tag); # deletes the tag
}
if ( $errors->exist ) {
$vars->{errors} = $errors;
$vars->{formdata} = $form_args;
# fall through to template
}
else {
return $r->redirect( LJ::create_url( undef, args => $redirect_args ) );
}
} # end did_post
# non post processing stuff (check for gets)
my $tag = LJ::durl( $form_args->{tag} || '' );
my $id;
if ($mode) {
return $r->redirect( LJ::create_url() ) unless $tag;
$id = DW::VirtualGift->get_tagid($tag);
return error_ml("$scope.error.badid") unless $id;
}
if ( $mode eq 'remove' ) {
my $err_ml;
my $vg = _check_id( $form_args->{vg}, $err_ml );
return error_ml($err_ml) unless $vg;
# quickly process and return
$vg->remove_tag_by_id($id);
$redirect_args = { mode => 'view', tag => LJ::eurl($tag) };
return $r->redirect( LJ::create_url( undef, args => $redirect_args ) );
}
if ( $form_args->{title} && _strict_refer("/admin/vgifts/tags") ) {
$vars->{title} = $form_args->{title};
}
$vars->{mode} = $mode;
$vars->{tag} = $tag;
$vars->{id} = $id;
$vars->{list_tagprivs} = sub { [ DW::VirtualGift->list_tagprivs( $_[0] ) ] };
$vars->{tagged_with} = sub { [ DW::VirtualGift->list_tagged_with( $_[0] ) ] };
unless ($mode) {
my $counts = DW::VirtualGift->fetch_tagcounts_approved;
my %nonpriv = map { $_ => $counts->{$_} } DW::VirtualGift->list_nonpriv_tags;
delete @$counts{ keys %nonpriv }; # leaving only privileged data
$vars->{haspriv} = $counts;
$vars->{nonpriv} = \%nonpriv;
}
return DW::Template->render_template( 'admin/vgifts/tags.tt', $vars );
}
1;