#!/usr/bin/perl # # DW::Controller::Admin::SpamReports # # Manage reports of unsolicited messages and comments. # Requires siteadmin:spamreports or siteadmin:* privileges. # # Authors: # Jen Griffin # # Copyright (c) 2015 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::SpamReports; use strict; use DW::Controller; use DW::Controller::Admin; use DW::Routing; use DW::Template; use LJ::Sysban; DW::Routing->register_string( "/admin/spamreports", \&main_controller, app => 1 ); DW::Controller::Admin->register_admin_page( '/', path => 'spamreports', ml_scope => '/admin/spamreports/index.tt', privs => [ 'siteadmin:spamreports', 'siteadmin:*' ] ); sub main_controller { my ( $ok, $rv ) = controller( form_auth => 1, privcheck => [ 'siteadmin:spamreports', 'siteadmin:*' ] ); return $rv unless $ok; my $remote = $rv->{remote}; my $r = DW::Request->get; my $scope = '/admin/spamreports/index.tt'; # determine view mode; 'get' takes priority over 'post' my $mode = lc( $r->get_args->{mode} || $r->post_args->{mode} || '' ); $mode = '' if $mode =~ /^del/ && !$r->did_post && !LJ::check_referer('/admin/spamreports'); # check mode suffix for users only (u), anon only(a), or combined (c) my $view = $mode =~ /_([cua])$/ ? $1 : 'c'; # default is combined view $mode =~ s/_[cua]$//; # strip out viewing option my %extrawhere = ( c => '1', u => 'posterid > 0', a => 'posterid = 0' ); $rv->{mode} = $mode; $rv->{view} = $view; # helper function for constructing links to view reports my $viewlink = sub { my ( $by, $what, $state, $reporttime, $etext ) = @_; $reporttime = LJ::mysql_time($reporttime) if defined $reporttime; my $linktext = $etext // $reporttime // '[error: no text]'; ( $by, $what, $state ) = map { LJ::eurl($_) } ( $by, $what, $state ); return "$linktext"; }; # helper function for constructing buttons to close reports my $closeform = sub { my ( $srids, $text ) = @_; return LJ::html_hidden( mode => 'del' ) . LJ::html_hidden( srids => join( ',', @$srids ) ) . LJ::html_hidden( ret => LJ::create_url( undef, keep_args => 1 ) ) . LJ::html_submit( submit => $text ); }; # retrieve and display the requested rows from the database my $dbr = LJ::get_db_reader(); return error_ml("$scope.error.db.noread") unless $dbr; my @rows; if ( $mode eq 'top10ip' ) { # top 10 by ip my $res = $dbr->selectall_arrayref( "SELECT COUNT(ip) AS num, ip, MAX(reporttime) FROM spamreports" . " WHERE state = 'open' AND ip IS NOT NULL" . " GROUP BY ip ORDER BY num DESC LIMIT 10" ); foreach (@$res) { push @rows, [ $_->[0], $_->[1], $viewlink->( 'ip', $_->[1], 'open', $_->[2] ) ]; } $rv->{rows} = \@rows; $rv->{count} = scalar @rows; $rv->{headers} = [ qw( .header.numreports .header.ipaddress .header.mostrecentreport ) ]; return DW::Template->render_template( 'admin/spamreports/toptable.tt', $rv ); } elsif ( $mode eq 'top10user' ) { # top 10 by user my $res = $dbr->selectall_arrayref( "SELECT COUNT(posterid) AS num, posterid, MAX(reporttime) FROM spamreports" . " WHERE state = 'open' AND posterid > 0" . " GROUP BY posterid ORDER BY num DESC LIMIT 10" ); foreach (@$res) { my $u = LJ::load_userid( $_->[1] ); push @rows, [ $_->[0], LJ::ljuser($u), $viewlink->( 'posterid', $_->[1], 'open', $_->[2] ) ]; } $rv->{rows} = \@rows; $rv->{count} = scalar @rows; $rv->{headers} = [ qw( .header.numreports .header.postedbyuser .header.mostrecentreport ) ]; return DW::Template->render_template( 'admin/spamreports/toptable.tt', $rv ); } elsif ( $mode eq 'tlast10' ) { # most recent 10 reports my $res = $dbr->selectall_arrayref( "SELECT posterid, ip, journalid, reporttime FROM spamreports" . " WHERE state = 'open' AND $extrawhere{$view}" . " ORDER BY reporttime DESC LIMIT 10" ); foreach (@$res) { my $ju = LJ::load_userid( $_->[2] ); if ( $_->[0] > 0 ) { # user report my $u = LJ::load_userid( $_->[0] ); push @rows, [ LJ::ljuser($u), LJ::ljuser($ju), $viewlink->( 'posterid', $_->[0], 'open', $_->[3] ) ]; } else { # anonymous report push @rows, [ $_->[1], LJ::ljuser($ju), $viewlink->( 'ip', $_->[1], 'open', $_->[3] ) ]; } } $rv->{rows} = \@rows; $rv->{count} = scalar @rows; $rv->{headers} = [ qw( .header.postedby .header.postedin .header.reporttime ) ]; return DW::Template->render_template( 'admin/spamreports/toptable.tt', $rv ); } elsif ( $mode =~ /^last(\d+)hr$/ ) { # reports in last X hours my $hours = $1 + 0; my $secs = $hours * 3600; # seconds in an hour $rv->{hours} = $hours; $rv->{mode} = 'tlasthrs'; # now that we know the number of hours my $res = $dbr->selectall_arrayref( "SELECT ip, posterid, reporttime FROM spamreports" . " WHERE $extrawhere{$view} AND reporttime > (UNIX_TIMESTAMP() - $secs)" . " LIMIT 1000" ); # count up items and their most recent report my %hits; my %times; foreach (@$res) { my ( $ip, $posterid, $reporttime ) = @$_; my $key; if ( $posterid > 0 ) { my $u = LJ::load_userid($posterid); $key = $u->userid if $u; } else { $key = $ip if $ip; } if ( defined $key ) { $hits{$key}++; $times{$key} = $reporttime unless defined $times{$key} && $times{$key} gt $reporttime; } } # now reverse %hits to number => item(s) list my %revhits; foreach ( keys %hits ) { my $num = $hits{$_}; $revhits{$num} ||= []; push @{ $revhits{$num} }, $_; } # now push them onto @rows foreach ( sort { $b <=> $a } keys %revhits ) { my $r = $revhits{$_}; foreach (@$r) { if (/^\d+$/) { # userid my $u = LJ::load_userid($_); push @rows, [ $hits{$_}, LJ::ljuser($u), $viewlink->( 'posterid', $_, 'open', $times{$_} ) ]; } else { # assumed to be IP address push @rows, [ $hits{$_}, $_, $viewlink->( 'ip', $_, 'open', $times{$_} ) ]; } } } $rv->{rows} = \@rows; $rv->{count} = scalar @rows; $rv->{headers} = [ qw( .tlasthrs.numreports .tlasthrs.postedby .tlasthrs.reporttime ) ]; return DW::Template->render_template( 'admin/spamreports/toptable.tt', $rv ); } elsif ( $mode eq 'view' ) { # view a particular report my $get = $r->get_args; my ( $by, $what, $state ) = ( lc( $get->{by} || '' ), $get->{what}, lc( $get->{state} || '' ) ); $by = '' unless $by =~ /^(?:ip|journal|poster(?:id)?)$/; $state = 'open' unless $state =~ /^(?:open|closed)$/; # check to see whether the viewer requested a posttime sort instead my $sort = lc( $get->{sort} || '' ); $sort = 'reporttime' unless $sort =~ /^(?:reporttime|posttime)$/; $rv->{view_by} = $by; $rv->{view_what} = $what; $rv->{view_state} = $state; $rv->{view_sort} = $sort; my %flip = ( reporttime => 'posttime', posttime => 'reporttime' ); $rv->{sorturl} = LJ::create_url( undef, keep_args => 1, args => { sort => $flip{$sort} } ); if ( $state eq 'open' ) { $rv->{statelink} = $viewlink->( $by, $what, 'closed', undef, LJ::Lang::ml("$scope.view.closedreports") ); } else { $rv->{statelink} = $viewlink->( $by, $what, 'open', undef, LJ::Lang::ml("$scope.view.openreports") ); } if ( $by eq 'posterid' ) { $what += 0 if defined $what; my $u = LJ::load_userid($what); return error_ml("$scope.error.noposterid") unless $u; $rv->{view_u} = $u; $rv->{show_posted} = LJ::is_enabled('show-talkleft') && $u->is_individual; } elsif ( $by eq 'poster' ) { my $u = LJ::load_user($what); return error_ml("$scope.error.nouser") unless $u; # Now just pretend that user used 'posterid' $by = 'posterid'; $what = $u->userid; $rv->{view_u} = $u; } elsif ( $by eq 'journal' ) { my $u = LJ::load_user($what); return error_ml("$scope.error.nouser") unless $u; # Now just pretend that user used 'journalid' $by = 'journalid'; $what = $u->userid; $rv->{view_u} = $u; } elsif ( $by eq 'ip' ) { # don't worry about IP format, just do a length check # if the IP is in the results table, we consider it valid # if it's not, we'll get a noreports error later, so it's all good return error_ml("$scope.error.noip") if length $what > 45; $rv->{reason} = LJ::Sysban::populate_full_by_value( $what, 'talk_ip_test' ); $rv->{is_ipv4} = ( $what =~ /^\d+\.\d+\.\d+\.\d+$/ ) ? 1 : 0; $rv->{timestr} = LJ::mysql_time(); } # now the general info gathering my $res = $by ? $dbr->selectall_arrayref( "SELECT reporttime, journalid, subject, body, posttime, report_type," . " srid, client, posterid FROM spamreports WHERE state=? AND $by=?" . " ORDER BY ? DESC LIMIT 1000", undef, $state, $what, $sort ) : []; my @srids; foreach (@$res) { my $reporttime = LJ::mysql_time( $_->[0] ); my $ju = LJ::load_userid( $_->[1] ); my $comment_subject = $_->[2]; my $comment_body = $_->[3]; LJ::text_uncompress( \$comment_body ); my $posttime = $_->[4] ? LJ::mysql_time( $_->[4] ) : undef; my $spamlocation = ucfirst $_->[5]; my $srid = $_->[6]; my $client = $_->[7] || ''; my $pu = LJ::load_userid( $_->[8] ); push @srids, $srid; push @rows, { srid => $srid, spamloc => $spamlocation, journal => $ju, poster => $pu, reporttime => $reporttime, posttime => $posttime, client => $client, subject => $comment_subject, body => $comment_body }; } $rv->{srids} = \@srids; $rv->{rows} = \@rows; $rv->{count} = scalar @rows; $rv->{commafy} = sub { LJ::commafy( $_[0] ) }; $rv->{closeform} = sub { $closeform->(@_) }; return DW::Template->render_template( 'admin/spamreports/view.tt', $rv ); } # if deletion was requested, do post processing if ( $mode eq 'del' ) { my $dbh = LJ::get_db_writer(); return error_ml("$scope.error.db.nowrite") unless $dbh; my $post = $r->post_args; # split srid argument at commas and reconstruct using quotes my @srids = split( ',', $post->{srids} ); my $in = join( "','", map { $_ + 0 } @srids ); $in = "'$in'"; if ( $post->{sysban_ip} && $remote->has_priv( 'sysban', "talk_ip_test" ) && !LJ::Sysban::validate( "talk_ip_test", $post->{sysban_ip} ) ) { LJ::Sysban::create( what => 'talk_ip_test', value => $post->{sysban_ip}, bandays => 0, note => $post->{sysban_note} || LJ::Lang::ml("$scope.reports.individual.sysban.anon") ); } my $count = $dbh->do( "UPDATE spamreports SET state='closed'" . " WHERE srid IN ($in) AND state='open'" ); return error_ml( "$scope.error.db.failure", { dberr => $dbh->errstr } ) if $dbh->err; $rv->{count} = $count + 0; $rv->{ret} = $post->{ret}; # already escaped return DW::Template->render_template( 'admin/spamreports/closed.tt', $rv ); } else { # no valid mode requested - show default page of links $rv->{modes} = { top10user => '.mode.top10user', top10ip => '.mode.top10ip', tlast10 => '.mode.tlast10', last01hr => '.mode.tlasthrs.01', last06hr => '.mode.tlasthrs.06', last24hr => '.mode.tlasthrs.24', }; $rv->{useronly} = sub { "spamreports?mode=${_[0]}_u" }; $rv->{anononly} = sub { "spamreports?mode=${_[0]}_a" }; return DW::Template->render_template( 'admin/spamreports/index.tt', $rv ); } } 1;