#!/usr/bin/perl
#
# DW::Controller::Admin::SpamReports
#
# Manage reports of unsolicited messages and comments.
# Requires siteadmin:spamreports or siteadmin:* privileges.
#
# Authors:
#      Jen Griffin <kareila@livejournal.com>
#
# 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
"<a href=\"spamreports?mode=view&amp;by=$by&amp;what=$what&amp;state=$state\">$linktext</a>";
    };

    # 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;
