#!/usr/bin/perl # # DW::Controller::Admin::VirtualGift # # Management pages for virtual gifts in the shop. # # 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::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;