mourningdove/cgi-bin/LJ/Support.pm

1292 lines
39 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.
package LJ::Support;
use strict;
use v5.10;
use Log::Log4perl;
my $log = Log::Log4perl->get_logger(__PACKAGE__);
use DW::Task::SupportNotify;
use Digest::MD5 qw(md5_hex);
use LJ::Sysban;
use LJ::Faq;
# Constants
my $SECONDS_IN_DAY = 3600 * 24;
our @SUPPORT_PRIVS = (
qw/supportclose
supporthelp
supportread
supportviewinternal
supportmakeinternal
supportmovetouch
supportviewscreened
supportviewstocks
supportchangesummary/
);
# <LJFUNC>
# name: LJ::Support::slow_query_dbh
# des: Retrieve a database handle to be used for support-related
# slow queries... defaults to 'slow' role but can be
# overriden by [ljconfig[support_slow_roles]].
# args: none
# returns: master database handle.
# </LJFUNC>
sub slow_query_dbh {
return LJ::get_dbh(@LJ::SUPPORT_SLOW_ROLES);
}
# basic function to add or update a support category.
# args: hashref corresponding to row values of supportcat table
# returns: spcatid on success, undef on failure
sub define_cat {
my ($opts) = @_;
if ( $opts->{catkey} ) {
# see if this category is already defined (catkey is unique)
my $cat = get_cat_by_key( load_cats(), $opts->{catkey} );
if ($cat) {
# use the existing category id
$opts->{spcatid} = $cat->{spcatid};
delete $opts->{catkey};
}
}
my @columns = qw/ catkey catname sortorder basepoints is_selectable
public_read public_help allow_screened hide_helpers
user_closeable replyaddress no_autoreply scope /;
my %row;
foreach (@columns) {
$row{$_} = $opts->{$_} if exists $opts->{$_};
delete $opts->{$_};
}
my $id = delete $opts->{spcatid};
return $id unless %row;
# if we have any $opts remaining here, they're invalid
my $invalid = join ', ', keys %$opts;
die "Invalid opts passed to LJ::Support::define_cat: $invalid"
if $invalid;
my $dbh = LJ::get_db_writer() or return;
if ($id) { # update path
my ( @cols, @vals );
while ( my ( $col, $val ) = each %row ) {
push @cols, "$col=?";
push @vals, $val;
}
my $bind = join ', ', @cols;
$dbh->do( "UPDATE supportcat SET $bind WHERE spcatid=?", undef, @vals, $id );
}
else { # insert path
my @cols = keys %row;
my @vals = @row{@cols};
my $colbind = join ',', map { $_ } @cols;
my $valbind = join ',', map { '?' } @vals;
$dbh->do( "INSERT INTO supportcat ($colbind) VALUES ($valbind)", undef, @vals );
$id = $dbh->{mysql_insertid};
}
die $dbh->errstr if $dbh->err;
return $id;
}
sub delete_cat {
my ($id) = @_;
my $dbh = LJ::get_db_writer() or return;
$dbh->do( "DELETE FROM supportcat WHERE spcatid=?", undef, $id );
die $dbh->errstr if $dbh->err;
return 1; # regardless of whether the id was in the table
}
## pass $id of zero or blank to get all categories
sub load_cats {
my ($id) = @_;
my $hashref = {};
$id += 0;
my $where = $id ? "WHERE spcatid=$id" : "";
my $dbr = LJ::get_db_reader();
my $sth = $dbr->prepare("SELECT * FROM supportcat $where");
$sth->execute;
$hashref->{ $_->{'spcatid'} } = $_ while ( $_ = $sth->fetchrow_hashref );
return $hashref;
}
sub load_email_to_cat_map {
my $map = {};
my $dbr = LJ::get_db_reader();
my $sth = $dbr->prepare("SELECT * FROM supportcat ORDER BY sortorder DESC");
$sth->execute;
while ( my $sp = $sth->fetchrow_hashref ) {
next unless ( $sp->{'replyaddress'} );
$map->{ $sp->{'replyaddress'} } = $sp;
}
return $map;
}
sub calc_points {
my ( $sp, $secs, $spcat ) = @_;
$spcat ||= $sp->{_cat};
my $base = $spcat->{basepoints} || 1;
$secs = int( $secs / ( 3600 * 6 ) );
my $total = ( $base + $secs );
$total = 10 if $total > 10;
return $total;
}
sub init_remote {
my $remote = shift;
return unless $remote;
$remote->load_user_privs(@SUPPORT_PRIVS);
}
sub has_any_support_priv {
my $u = shift;
return 0 unless $u;
foreach my $support_priv (@SUPPORT_PRIVS) {
return 1 if $u->has_priv($support_priv);
}
return 0;
}
# given all the categories, maps a catkey into a cat
sub get_cat_by_key {
my ( $cats, $cat ) = @_;
$cat ||= '';
foreach ( keys %$cats ) {
if ( $cats->{$_}->{'catkey'} eq $cat ) {
return $cats->{$_};
}
}
return undef;
}
sub filter_cats {
my $remote = shift;
my $cats = shift;
return grep { can_read_cat( $_, $remote ); } sorted_cats($cats);
}
sub sorted_cats {
my $cats = shift;
return sort { $a->{'catname'} cmp $b->{'catname'} } values %$cats;
}
# takes raw support request record and puts category info in it
# so it can be used in other functions like can_*
sub fill_request_with_cat {
my ( $sp, $cats ) = @_;
$sp->{_cat} = $cats->{ $sp->{'spcatid'} };
}
sub open_request_status {
my ( $timetouched, $timelasthelp ) = @_;
my $status;
if ( $timelasthelp > $timetouched + 5 ) {
$status = "awaiting close";
}
elsif ($timelasthelp
&& $timetouched > $timelasthelp + 5 )
{
$status = "still needs help";
}
else {
$status = "open";
}
return $status;
}
sub is_poster {
my ( $sp, $remote, $auth ) = @_;
if ( $sp->{'reqtype'} eq "user" ) {
return 1 if $remote && $remote->id == $sp->{'requserid'};
}
else {
if ($remote) {
return 1 if lc( $remote->email_raw ) eq lc( $sp->{'reqemail'} );
}
else {
return 1 if $auth && $auth eq mini_auth($sp);
}
}
return 0;
}
sub can_see_helper {
my ( $sp, $remote ) = @_;
if ( $sp->{_cat}->{'hide_helpers'} ) {
if ( can_help( $sp, $remote ) ) {
return 1;
}
if ( $remote && $remote->has_priv( "supportviewinternal", $sp->{_cat}->{'catkey'} ) ) {
return 1;
}
if ( $remote && $remote->has_priv( "supportviewscreened", $sp->{_cat}->{'catkey'} ) ) {
return 1;
}
return 0;
}
return 1;
}
sub can_read {
my ( $sp, $remote, $auth ) = @_;
return ( is_poster( $sp, $remote, $auth ) || can_read_cat( $sp->{_cat}, $remote ) );
}
sub can_read_cat {
my ( $cat, $remote ) = @_;
return unless ($cat);
return ( $cat->{'public_read'}
|| ( $remote && $remote->has_priv( "supportread", $cat->{'catkey'} ) ) );
}
*can_bounce = \&can_close_cat;
*can_lock = \&can_close_cat;
# if they can close in this category
sub can_close_cat {
my ( $sp, $remote ) = @_;
return 1 if $sp->{_cat}->{public_read} && $remote && $remote->has_priv( 'supportclose', '' );
return 1 if $remote && $remote->has_priv( 'supportclose', $sp->{_cat}->{catkey} );
return 0;
}
# if they can close this particular request
sub can_close {
my ( $sp, $remote, $auth ) = @_;
return 1 if $sp->{_cat}->{user_closeable} && is_poster( $sp, $remote, $auth );
return can_close_cat( $sp, $remote );
}
# if they can reopen a request
sub can_reopen {
my ( $sp, $remote, $auth ) = @_;
return 1 if is_poster( $sp, $remote, $auth );
return can_close_cat( $sp, $remote );
}
sub can_append {
my ( $sp, $remote, $auth ) = @_;
if ( is_poster( $sp, $remote, $auth ) ) { return 1; }
return 0 unless $remote;
return 0 unless $remote->is_visible;
if ( $sp->{_cat}->{'allow_screened'} ) { return 1; }
if ( can_help( $sp, $remote ) ) { return 1; }
return 0;
}
sub is_locked {
my $sp = shift;
my $spid = ref $sp ? $sp->{spid} : $sp + 0;
return undef unless $spid;
my $props = LJ::Support::load_props($spid);
return $props->{locked} ? 1 : 0;
}
sub lock {
my $sp = shift;
my $spid = ref $sp ? $sp->{spid} : $sp + 0;
return undef unless $spid;
my $dbh = LJ::get_db_writer();
$dbh->do( "REPLACE INTO supportprop (spid, prop, value) VALUES (?, 'locked', 1)", undef,
$spid );
}
sub unlock {
my $sp = shift;
my $spid = ref $sp ? $sp->{spid} : $sp + 0;
return undef unless $spid;
my $dbh = LJ::get_db_writer();
$dbh->do( "DELETE FROM supportprop WHERE spid = ? AND prop = 'locked'", undef, $spid );
}
# privilege policy:
# supporthelp with no argument gives you all abilities in all public_read categories
# supporthelp with a catkey arg gives you all abilities in that non-public_read category
# supportread with a catkey arg is required to view requests in a non-public_read category
# all other privs work like:
# no argument = global, where category is public_read or user has supportread on that category
# argument = local, priv applies in that category only if it's public or user has supportread
sub support_check_priv {
my ( $sp, $remote, $priv ) = @_;
return 1 if can_help( $sp, $remote );
return 0 unless can_read_cat( $sp->{_cat}, $remote );
return 1 if $remote && $remote->has_priv( $priv, '' ) && $sp->{_cat}->{public_read};
return 1 if $remote && $remote->has_priv( $priv, $sp->{_cat}->{catkey} );
return 0;
}
# can they read internal comments? if they're a helper or have
# extended supportread (with a plus sign at the end of the category key)
sub can_read_internal {
my ( $sp, $remote ) = @_;
return 1 if LJ::Support::support_check_priv( $sp, $remote, 'supportviewinternal' );
return 1 if $remote && $remote->has_priv( "supportread", $sp->{_cat}->{catkey} . "+" );
return 0;
}
sub can_make_internal {
return LJ::Support::support_check_priv( @_, 'supportmakeinternal' );
}
sub can_read_screened {
return LJ::Support::support_check_priv( @_, 'supportviewscreened' );
}
sub can_read_response {
my ( $sp, $u, $rtype, $posterid ) = @_;
return 1 if $posterid == $u->id;
return 0
if $rtype eq 'screened'
&& !LJ::Support::can_read_screened( $sp, $u );
return 0
if $rtype eq 'internal'
&& !LJ::Support::can_read_internal( $sp, $u );
return 1;
}
sub can_perform_actions {
return LJ::Support::support_check_priv( @_, 'supportmovetouch' );
}
sub can_change_summary {
return LJ::Support::support_check_priv( @_, 'supportchangesummary' );
}
sub can_see_stocks {
return LJ::Support::support_check_priv( @_, 'supportviewstocks' );
}
sub can_help {
my ( $sp, $remote ) = @_;
if ( $sp->{_cat}->{'public_read'} ) {
return 1 if $sp->{_cat}->{'public_help'};
return 1 if $remote && $remote->has_priv( "supporthelp", "" );
}
my $catkey = $sp->{_cat}->{'catkey'};
return 1 if $remote && $remote->has_priv( "supporthelp", $catkey );
return 0;
}
sub load_props {
my $spid = shift;
return unless $spid;
my %props = (); # prop => value
my $dbr = LJ::get_db_reader();
my $sth = $dbr->prepare("SELECT prop, value FROM supportprop WHERE spid=?");
$sth->execute($spid);
while ( my ( $prop, $value ) = $sth->fetchrow_array ) {
$props{$prop} = $value;
}
return \%props;
}
sub prop {
my ( $spid, $propname ) = @_;
my $props = LJ::Support::load_props($spid);
return $props->{$propname} || undef;
}
sub set_prop {
my ( $spid, $propname, $propval ) = @_;
# TODO:
# -- delete on 'undef' propval
# -- allow setting of multiple
my $dbh = LJ::get_db_writer()
or die "couldn't contact global master";
$dbh->do( "REPLACE INTO supportprop (spid, prop, value) VALUES (?,?,?)",
undef, $spid, $propname, $propval );
die $dbh->errstr if $dbh->err;
return 1;
}
# $loadreq is used by /abuse/report.bml and
# to signify that the full request
# should not be loaded. To simplify code going live,
# Whitaker and I decided to not try and merge it
# into the new $opts hash.
# $opts->{'db_force'} loads the request from a
# global master. Needed to prevent a race condition
# where the request may not have replicated to slaves
# in the time needed to load an auth code.
sub load_request {
my ( $spid, $loadreq, $opts ) = @_;
my $sth;
$spid += 0;
# load the support request
my $db = $opts->{'db_force'} ? LJ::get_db_writer() : LJ::get_db_reader();
$sth = $db->prepare("SELECT * FROM support WHERE spid=$spid");
$sth->execute;
my $sp = $sth->fetchrow_hashref;
return undef unless $sp;
# load the category the support requst is in
$sth = $db->prepare("SELECT * FROM supportcat WHERE spcatid=$sp->{'spcatid'}");
$sth->execute;
$sp->{_cat} = $sth->fetchrow_hashref;
# now load the user's request text, if necessary
if ($loadreq) {
$sp->{body} =
$db->selectrow_array( "SELECT message FROM supportlog WHERE spid = ? AND type = 'req'",
undef, $sp->{spid} );
}
return $sp;
}
# load_requests:
# Given an arrayref, fetches information about the requests
# with these spid's; unlike load_request(), it doesn't fetch information
# about supportcats.
sub load_requests {
my ($spids) = @_;
my $dbr = LJ::get_db_reader() or return;
my $list = join( ',', map { '?' } @$spids );
my $requests = $dbr->selectall_arrayref(
"SELECT spid, reqtype, requserid, reqname, reqemail, state,"
. " authcode, spcatid, subject, timecreate, timetouched, timeclosed,"
. " timelasthelp, timemodified FROM support WHERE spid IN ($list)",
{ Slice => {} },
map { $_ + 0 } @$spids
);
die $dbr->errstr if $dbr->err;
return $requests;
}
sub load_response {
my $splid = shift;
my $sth;
$splid += 0;
# load the support request. we hit the master because we generally
# only invoke this when we want the freshest version of the row.
# (ie, approving a response changes its type from screened to
# answer ... then we fetch the row again and make decisions on its type.
# so we want the authoritative version)
my $dbh = LJ::get_db_writer();
$sth = $dbh->prepare("SELECT * FROM supportlog WHERE splid=$splid");
$sth->execute;
my $res = $sth->fetchrow_hashref;
return $res;
}
sub get_answer_types {
my ( $sp, $remote, $auth ) = @_;
my @ans_type;
if ( is_poster( $sp, $remote, $auth ) ) {
push @ans_type, ( "comment", LJ::Lang::ml("support.answertype.moreinfo") );
return @ans_type;
}
if ( can_help( $sp, $remote ) ) {
push @ans_type,
(
"screened" => LJ::Lang::ml("support.answertype.screened"),
"answer" => LJ::Lang::ml("support.answertype.answer"),
"comment" => LJ::Lang::ml("support.answertype.comment")
);
}
elsif ( $sp->{_cat}->{'allow_screened'} ) {
push @ans_type, ( "screened" => LJ::Lang::ml("support.answertype.screened") );
}
if ( can_make_internal( $sp, $remote )
&& !$sp->{_cat}->{'public_help'} )
{
push @ans_type, ( "internal" => LJ::Lang::ml("support.answertype.internal") );
}
if ( can_bounce( $sp, $remote ) ) {
push @ans_type, ( "bounce" => LJ::Lang::ml("support.answertype.bounce") );
}
return @ans_type;
}
sub file_request {
my $errors = shift;
my $o = shift;
my $email = $o->{'reqtype'} eq "email" ? $o->{'reqemail'} : "";
unless ( LJ::is_enabled('loggedout_support_requests') || !$email ) {
push @$errors, LJ::Lang::ml("error.support.mustbeloggedin");
}
my $log = {
'uniq' => $o->{'uniq'},
'email' => $email
};
my $userid = 0;
unless ($email) {
if ( $o->{'reqtype'} eq "user" ) {
my $u = LJ::load_userid( $o->{'requserid'} );
$userid = $u->{'userid'};
$log->{'user'} = $u->user;
$log->{'email'} = $u->email_raw;
unless ( $u->is_person || $u->is_identity ) {
push @$errors, LJ::Lang::ml("error.support.nonuser");
}
if ( LJ::sysban_check( 'support_user', $u->{'user'} ) ) {
return LJ::Sysban::block( $userid, "Support request blocked based on user", $log );
}
$email = $u->email_raw || $o->{'reqemail'};
}
}
if ( LJ::sysban_check( 'support_email', $email ) ) {
return LJ::Sysban::block( $userid, "Support request blocked based on email", $log );
}
if ( LJ::sysban_check( 'support_uniq', $o->{'uniq'} ) ) {
return LJ::Sysban::block( $userid, "Support request blocked based on uniq", $log );
}
my $reqsubject = LJ::trim( $o->{'subject'} );
my $reqbody = LJ::trim( $o->{'body'} );
# remove the auth portion of any see_request links
$reqbody = LJ::strip_request_auth($reqbody);
unless ($reqsubject) {
push @$errors, LJ::Lang::ml("error.support.nosummary");
}
unless ($reqbody) {
push @$errors, LJ::Lang::ml("error.support.norequest");
}
my $cats = LJ::Support::load_cats();
push @$errors, LJ::Lang::ml { "error.support.invalid_category" }
unless $cats->{ $o->{'spcatid'} + 0 };
if (@$errors) { return 0; }
if ( LJ::is_enabled("support_request_language") ) {
$o->{'language'} = undef unless grep { $o->{'language'} eq $_ } ( @LJ::LANGS, "xx" );
$reqsubject = "[$o->{'language'}] $reqsubject"
if $o->{'language'} && $o->{'language'} !~ /^en_/;
}
my $dbh = LJ::get_db_writer();
my $dup_id = 0;
my $qsubject = $dbh->quote($reqsubject);
my $qbody = $dbh->quote($reqbody);
my $qreqtype = $dbh->quote( $o->{'reqtype'} );
my $qrequserid = $o->{'requserid'} + 0;
my $qreqname = $dbh->quote( $o->{'reqname'} );
my $qreqemail = $dbh->quote( $o->{'reqemail'} );
my $qspcatid = $o->{'spcatid'} + 0;
my $scat = $cats->{$qspcatid};
# make the authcode
my $authcode = LJ::make_auth_code(15);
my $qauthcode = $dbh->quote($authcode);
my $md5 = md5_hex("$qreqname$qreqemail$qsubject$qbody");
my $sth;
$dbh->do("LOCK TABLES support WRITE, duplock WRITE");
unless ( $o->{ignore_dup_check} ) {
$sth = $dbh->prepare(
"SELECT dupid FROM duplock WHERE realm='support' AND reid=0 AND userid=$qrequserid AND digest='$md5'"
);
$sth->execute;
($dup_id) = $sth->fetchrow_array;
if ($dup_id) {
$dbh->do("UNLOCK TABLES");
return $dup_id;
}
}
my ( $urlauth, $url, $spid ); # used at the bottom
my $sql =
"INSERT INTO support (spid, reqtype, requserid, reqname, reqemail, state, authcode, spcatid, subject, timecreate, timetouched, timeclosed, timelasthelp) VALUES (NULL, $qreqtype, $qrequserid, $qreqname, $qreqemail, 'open', $qauthcode, $qspcatid, $qsubject, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 0)";
$sth = $dbh->prepare($sql);
$sth->execute;
if ( $dbh->err ) {
my $error = $dbh->errstr;
$dbh->do("UNLOCK TABLES");
push @$errors, "<b>Database error:</b> (report this)<br>$error";
return 0;
}
$spid = $dbh->{'mysql_insertid'};
$dbh->do(
"INSERT INTO duplock (realm, reid, userid, digest, dupid, instime) VALUES ('support', 0, $qrequserid, '$md5', $spid, NOW())"
) unless $o->{ignore_dup_check};
$dbh->do("UNLOCK TABLES");
unless ($spid) {
push @$errors, "<b>Database error:</b> (report this)<br>Didn't get a spid.";
return 0;
}
# save meta-data for this request
my @data;
my $add_data = sub {
my $q = $dbh->quote( $_[1] );
return unless $q && $q ne 'NULL';
push @data, "($spid, '$_[0]', $q)";
};
if ( LJ::is_enabled("support_request_language") && $o->{language} ne "xx" ) {
$add_data->( $_, $o->{$_} ) foreach qw(uniq useragent language);
}
else {
$add_data->( $_, $o->{$_} ) foreach qw(uniq useragent);
}
$dbh->do( "INSERT INTO supportprop (spid, prop, value) VALUES " . join( ',', @data ) );
$dbh->do( "INSERT INTO supportlog (splid, spid, timelogged, type, faqid, userid, message) "
. "VALUES (NULL, $spid, UNIX_TIMESTAMP(), 'req', 0, $qrequserid, $qbody)" );
my $body;
my $miniauth = mini_auth( { 'authcode' => $authcode } );
$url = "$LJ::SITEROOT/support/see_request?id=$spid";
$urlauth = "$url&auth=$miniauth";
$body = LJ::Lang::ml(
"support.email.confirmation.body",
{
sitename => $LJ::SITENAME,
subject => $o->{'subject'},
number => $spid,
url => $urlauth
}
);
if ( $scat->{user_closeable} ) {
$body .= "\n\n" . LJ::Lang::ml("support.email.confirmation.close") . "\n\n";
$body .= "$LJ::SITEROOT/support/act?close;$spid;$authcode";
}
# disable auto-replies for the entire category, or per request
unless ( $scat->{'no_autoreply'} || $o->{'no_autoreply'} ) {
LJ::send_mail(
{
'to' => $email,
'from' => $LJ::BOGUS_EMAIL,
'fromname' =>
LJ::Lang::ml( "support.email.fromname", { sitename => $LJ::SITENAME } ),
'charset' => 'utf-8',
'subject' => LJ::Lang::ml( "support.email.subject", { number => $spid } ),
'body' => $body
}
);
}
support_notify( { spid => $spid, type => 'new' } );
# and we're done
return $spid;
}
sub append_request {
my $sp = shift; # support request to be appended to.
my $re = shift; # hashref of attributes of response to be appended
my $sth;
# $re->{'body'}
# $re->{'type'} (req, answer, comment, internal, screened)
# $re->{'faqid'}
# $re->{'remote'} (remote if known)
# $re->{'uniq'} (uniq of remote)
# $re->{'tier'} (tier of response if type is answer or internal)
my $remote = $re->{'remote'};
my $posterid = $remote ? $remote->{'userid'} : 0;
# check for a sysban
my $log = { 'uniq' => $re->{'uniq'} };
if ($remote) {
$log->{'user'} = $remote->user;
$log->{'email'} = $remote->email_raw;
if ( LJ::sysban_check( 'support_user', $remote->{'user'} ) ) {
return LJ::Sysban::block( $remote->{userid}, "Support request blocked based on user",
$log );
}
if ( LJ::sysban_check( 'support_email', $remote->email_raw ) ) {
return LJ::Sysban::block( $remote->{userid}, "Support request blocked based on email",
$log );
}
}
if ( LJ::sysban_check( 'support_uniq', $re->{'uniq'} ) ) {
my $userid = $remote ? $remote->{'userid'} : 0;
return LJ::Sysban::block( $userid, "Support request blocked based on uniq", $log );
}
my $message = $re->{'body'};
$message =~ s/^\s+//;
$message =~ s/\s+$//;
my $dbh = LJ::get_db_writer();
my $qmessage = $dbh->quote($message);
my $qtype = $dbh->quote( $re->{'type'} );
my $qfaqid = $re->{'faqid'} ? $re->{'faqid'} + 0 : 0;
my $quserid = $posterid + 0;
my $spid = $sp->{'spid'} + 0;
my $qtier = $re->{'tier'} ? ( $re->{'tier'} + 0 ) . "0" : "NULL";
my $sql;
if ( LJ::is_enabled("support_response_tier") ) {
$sql =
"INSERT INTO supportlog (splid, spid, timelogged, type, faqid, userid, message, tier) VALUES (NULL, $spid, UNIX_TIMESTAMP(), $qtype, $qfaqid, $quserid, $qmessage, $qtier)";
}
else {
$sql =
"INSERT INTO supportlog (splid, spid, timelogged, type, faqid, userid, message) VALUES (NULL, $spid, UNIX_TIMESTAMP(), $qtype, $qfaqid, $quserid, $qmessage)";
}
$dbh->do($sql);
my $splid = $dbh->{'mysql_insertid'};
# mark this as an interesting update
$dbh->do( 'UPDATE support SET timemodified=UNIX_TIMESTAMP() WHERE spid=?', undef, $spid );
if ($posterid) {
# add to our index of recently replied to support requests per-user.
$dbh->do( "INSERT IGNORE INTO support_youreplied (userid, spid) VALUES (?, ?)",
undef, $posterid, $spid );
die $dbh->errstr if $dbh->err;
# and also lazily clean out old stuff:
$sth =
$dbh->prepare( "SELECT s.spid FROM support s, support_youreplied yr "
. "WHERE yr.userid=? AND yr.spid=s.spid AND s.state='closed' "
. "AND s.timeclosed < UNIX_TIMESTAMP() - 3600*72" );
$sth->execute($posterid);
my @to_del;
push @to_del, $_ while ($_) = $sth->fetchrow_array;
if (@to_del) {
my $in = join( ", ", map { $_ + 0 } @to_del );
$dbh->do( "DELETE FROM support_youreplied WHERE userid=? AND spid IN ($in)",
undef, $posterid );
}
}
support_notify( { spid => $spid, splid => $splid, type => 'update' } );
return $splid;
}
# userid may be undef/0 in the setting to zero case
sub set_points {
my ( $spid, $userid, $points ) = @_;
my $dbh = LJ::get_db_writer();
if ($points) {
$dbh->do( "REPLACE INTO supportpoints (spid, userid, points) " . "VALUES (?,?,?)",
undef, $spid, $userid, $points );
}
else {
$userid ||=
$dbh->selectrow_array( "SELECT userid FROM supportpoints WHERE spid=?", undef, $spid );
$dbh->do( "DELETE FROM supportpoints WHERE spid=?", undef, $spid );
}
$dbh->do(
"REPLACE INTO supportpointsum (userid, totpoints, lastupdate) "
. "SELECT userid, SUM(points), UNIX_TIMESTAMP() FROM supportpoints "
. "WHERE userid=? GROUP BY 1",
undef, $userid
) if $userid;
# clear caches
if ($userid) {
my $u = LJ::load_userid($userid);
delete $u->{_supportpointsum} if $u;
my $memkey = [ $userid, "supportpointsum:$userid" ];
LJ::MemCache::delete($memkey);
}
}
# closes request, assigning points for the last response left to the request
sub close_request_with_points {
my ( $sp, $spcat, $remote ) = @_;
my $spid = $sp->{spid} + 0;
my $dbh = LJ::get_db_writer() or return;
# close the request
$dbh->do(
'UPDATE support SET state="closed", '
. 'timeclosed=UNIX_TIMESTAMP(), timemodified=UNIX_TIMESTAMP() WHERE spid=?',
undef, $spid
);
die $dbh->errstr if $dbh->err;
# check to see who should get the points
my $response = $dbh->selectrow_hashref(
'SELECT splid, timelogged, userid FROM supportlog '
. 'WHERE spid=? AND type="answer" '
. 'ORDER BY timelogged DESC LIMIT 1',
undef, $spid
);
die $dbh->errstr if $dbh->err;
# deliberately not using LJ::Support::append_request
# to avoid sysban checks etc.; this sub is supposed to be fast.
my $sth =
$dbh->prepare( 'INSERT INTO supportlog '
. '(spid, timelogged, type, userid, message) VALUES '
. '(?, UNIX_TIMESTAMP(), "internal", ?, ?)' );
unless ( defined $response ) {
# no points awarded
$sth->execute(
$spid,
LJ::want_userid($remote),
"(Request has been closed as part of mass closure)"
);
die $sth->errstr if $sth->err;
return 1;
}
# award the points
my $userid = $response->{userid};
my $points =
LJ::Support::calc_points( $sp, $response->{timelogged} - $sp->{timecreate}, $spcat );
LJ::Support::set_points( $spid, $userid, $points );
my $username = LJ::want_user($userid)->display_name;
$sth->execute( $spid, LJ::want_userid($remote),
"(Request has been closed as part of mass closure, "
. "granting $points points to $username for response #"
. $response->{splid}
. ")" );
die $sth->errstr if $sth->err;
return 1;
}
sub touch_request {
my ($spid) = @_;
# no touching if the request is locked
return 0 if LJ::Support::is_locked($spid);
my $dbh = LJ::get_db_writer();
$dbh->do(
"UPDATE support"
. " SET state='open', timeclosed=0, timetouched=UNIX_TIMESTAMP(), timemodified=UNIX_TIMESTAMP()"
. " WHERE spid=?",
undef, $spid
) or return 0;
set_points( $spid, undef, 0 );
return 1;
}
# Extra email addresses are stored as support properties
# - nb_extra_addresses: number of extra addresses (if not present, 0)
# - extra_address_$n: extra address $n (0<=$n<nb_extra_addresses)
sub add_email_address {
my ( $sp, $address ) = @_;
# Already present?
return if grep { $_ eq $address } all_email_addresses($sp);
# Add
my $props = load_props( $sp->{spid} + 0 );
my $nb_extra_addresses = $props->{nb_extra_addresses} || 0;
set_prop( $sp->{spid}, 'nb_extra_addresses', $nb_extra_addresses + 1 );
set_prop( $sp->{spid}, "extra_address_$nb_extra_addresses", $address );
}
sub all_email_addresses {
my ($sp) = @_;
my $props = load_props( $sp->{spid} + 0 );
my @emails =
map { $props->{"extra_address_$_"} } 0 .. ( ( $props->{nb_extra_addresses} || 0 ) - 1 );
if ( $sp->{reqtype} eq 'email' ) {
push @emails, $sp->{reqemail};
}
else {
my $u = LJ::load_userid( $sp->{requserid} );
push @emails, ( $u->email_raw || $sp->{reqemail} );
}
return @emails;
}
sub mail_response_to_user {
my $sp = shift;
my $splid = shift;
$splid += 0;
my $res = load_response($splid);
my $u;
$u = LJ::load_userid( $sp->{requserid} ) if $sp->{reqtype} ne 'email';
my $spid = $sp->{'spid'} + 0;
my $faqid = $res->{'faqid'} + 0;
my $type = $res->{'type'};
# don't mail internal comments (user shouldn't see) or
# screened responses (have to wait for somebody to approve it first)
return if ( $type eq "internal" || $type eq "screened" );
# the only way it can be zero is if it's a reply to an email, so it's
# problem the person replying to their own request, so we don't want
# to mail them:
return unless ( $res->{'userid'} );
# also, don't send them their own replies:
return if ( $sp->{'requserid'} == $res->{'userid'} );
my $lang;
$lang = LJ::Support::prop( $spid, 'language' )
if LJ::is_enabled('support_request_language');
$lang ||= $LJ::DEFAULT_LANG;
my $body = "";
my $dbh = LJ::get_db_writer();
$body .=
$type eq "answer"
? LJ::Lang::ml( "support.email.update.body_a", { subject => $sp->{'subject'} } )
: LJ::Lang::ml( "support.email.update.body_c", { subject => $sp->{'subject'} } );
$body .= "\n";
my $miniauth = mini_auth($sp);
$body .= "($LJ::SITEROOT/support/see_request?id=$spid&auth=$miniauth).\n\n";
$body .= "=" x 70 . "\n\n";
if ($faqid) {
# Get requesting username and journal URL, or example user's username
# and journal URL
my ( $user, $user_url );
$u ||= LJ::load_user($LJ::EXAMPLE_USER_ACCOUNT);
$user =
$u ? $u->user : "<b>" . LJ::Lang::ml("support.email.update.unknown_username") . "</b>";
$user_url =
$u
? $u->journal_base
: "<b>" . LJ::Lang::ml("support.email.update.unknown_username") . "</b>";
my $faq = LJ::Faq->load( $faqid, lang => $lang );
if ($faq) {
$faq->render_in_place;
$body .= LJ::Lang::ml("support.email.update.faqref") . " " . $faq->question_raw . "\n";
$body .= $faq->url_full;
$body .= "\n\n";
}
}
$body .= "$res->{'message'}\n\n";
if ( $sp->{_cat}->{user_closeable} ) {
my $closeurl = "$LJ::SITEROOT/support/act?close;$spid;$sp->{'authcode'}"
. ( $type eq "answer" ? ";$splid" : "" );
$body .= LJ::Lang::ml(
"support.email.update.close",
{
close => $closeurl,
reply => "$LJ::SITEROOT/support/see_request?id=$spid&auth=$miniauth"
}
);
$body .= "\n\n";
}
$body .= LJ::Lang::ml("support.email.update.linkserror");
my $fromemail;
if ( $sp->{_cat}->{'replyaddress'} ) {
my $miniauth = mini_auth($sp);
$fromemail = $sp->{_cat}->{'replyaddress'};
# insert mini-auth stuff:
my $rep = "+${spid}z$miniauth\@";
$fromemail =~ s/\@/$rep/;
}
else {
$fromemail = $LJ::BOGUS_EMAIL;
$body .= "\n\n" . LJ::Lang::ml("support.email.update.noreply");
}
foreach my $email ( all_email_addresses($sp) ) {
LJ::send_mail(
{
to => $email,
from => $fromemail,
fromname => LJ::Lang::ml( 'support.email.fromname', { sitename => $LJ::SITENAME } ),
charset => 'utf-8',
subject =>
LJ::Lang::ml( 'support.email.update.subject', { subject => $sp->{subject} } ),
body => $body
}
);
}
if ( $type eq "answer" ) {
$dbh->do(
"UPDATE support SET timelasthelp=UNIX_TIMESTAMP(), timemodified=UNIX_TIMESTAMP() WHERE spid=$spid"
);
}
}
sub mini_auth {
my $sp = shift;
return substr( $sp->{'authcode'}, 0, 4 );
}
sub support_notify {
my $params = shift;
my $h = DW::TaskQueue->dispatch( DW::Task::SupportNotify->new($params) );
return $h ? 1 : 0;
}
package LJ::Worker::SupportNotify;
use base 'TheSchwartz::Worker';
sub work {
my ( $class, $job ) = @_;
my $a = $job->arg;
# load basic stuff common to both paths
my $type = $a->{type};
my $spid = $a->{spid} + 0;
my $load_body = $type eq 'new' ? 1 : 0;
my $sp = LJ::Support::load_request( $spid, $load_body, { force => 1 } ); # force from master
# we're only going to be reading anyway, but these jobs
# sometimes get processed faster than replication allows,
# causing the message not to load from the reader
my $dbr = LJ::get_db_writer();
# now branch a bit to select the right user information
my $level = $type eq 'new' ? "'new', 'all'" : "'all'";
my $data = $dbr->selectcol_arrayref(
"SELECT userid FROM supportnotify WHERE spcatid=? AND level IN ($level)",
undef, $sp->{_cat}{spcatid} );
my $userids = LJ::load_userids(@$data);
# prepare the email
my $body;
my @emails;
if ( $type eq 'new' ) {
my $show_name = $sp->{reqname};
if ( $sp->{reqtype} eq 'user' ) {
my $u = LJ::load_userid( $sp->{requserid} );
$show_name = $u->display_name if $u;
}
$body = LJ::Lang::ml(
"support.email.notif.new.body2",
{
sitename => $LJ::SITENAMESHORT,
category => $sp->{_cat}{catname},
subject => $sp->{subject},
username => LJ::trim($show_name),
url => "$LJ::SITEROOT/support/see_request?id=$spid",
text => $sp->{body}
}
);
$body .= "\n\n" . "=" x 4 . "\n\n";
$body .= LJ::Lang::ml(
"support.email.notif.new.footer",
{
url => "$LJ::SITEROOT/support/see_request?id=$spid",
setting => "$LJ::SITEROOT/support/changenotify"
}
);
foreach my $u ( values %$userids ) {
next unless $u->should_receive_support_notifications( $sp->{_cat}{spcatid} );
push @emails, $u->email_raw;
}
}
elsif ( $type eq 'update' ) {
# load the response we want to stuff in the email
my ( $resp, $rtype, $posterid, $faqid ) =
$dbr->selectrow_array(
"SELECT message, type, userid, faqid FROM supportlog WHERE spid = ? AND splid = ?",
undef, $sp->{spid}, $a->{splid} + 0 );
# set up $show_name for this environment
my $show_name;
if ($posterid) {
my $u = LJ::load_userid($posterid);
$show_name = $u->display_name if $u;
}
$show_name ||= $sp->{reqname};
# set up $response_type for this environment
my $response_type = {
req => "New Request", # not applicable here
answer => "Answer",
comment => "Comment",
internal => "Internal Comment",
screened => "Screened Answer",
}->{$rtype};
# build body
$body = LJ::Lang::ml(
"support.email.notif.update.body4",
{
sitename => $LJ::SITENAMESHORT,
category => $sp->{_cat}{catname},
subject => $sp->{subject},
username => LJ::trim($show_name),
url => "$LJ::SITEROOT/support/see_request?id=$spid",
type => $response_type
}
);
if ($faqid) {
# need to set up $lang
my ( $lang, $u );
$u = LJ::load_userid($posterid) if $posterid;
$lang = LJ::Support::prop( $spid, 'language' )
if LJ::is_enabled('support_request_language');
$lang ||= $LJ::DEFAULT_LANG;
# now actually get the FAQ
my $faq = LJ::Faq->load( $faqid, lang => $lang );
if ($faq) {
$faq->render_in_place;
my $faqref = $faq->question_raw . " " . $faq->url_full;
# now add it to the e-mail!
$body .= "\n"
. LJ::Lang::ml(
"support.email.notif.update.body.faqref",
{
faqref => $faqref
}
);
$body .= "\n";
}
}
$body .= LJ::Lang::ml(
"support.email.notif.update.body.text",
{
text => $resp
}
);
$body .= "\n\n" . "=" x 4 . "\n\n";
$body .= LJ::Lang::ml(
"support.email.notif.update.footer",
{
url => "$LJ::SITEROOT/support/see_request?id=$spid",
setting => "$LJ::SITEROOT/support/changenotify"
}
);
# now see who this should be sent to
foreach my $u ( values %$userids ) {
next unless $u->should_receive_support_notifications( $sp->{_cat}{spcatid} );
next unless LJ::Support::can_read_response( $sp, $u, $rtype, $posterid );
next if $posterid == $u->id && !$u->prop('opt_getselfsupport');
push @emails, $u->email_raw;
}
}
# send the email
LJ::send_mail(
{
bcc => join( ', ', @emails ),
from => $LJ::BOGUS_EMAIL,
fromname => LJ::Lang::ml( "support.email.fromname", { sitename => $LJ::SITENAME } ),
charset => 'utf-8',
subject => (
$type eq 'update'
? LJ::Lang::ml( "support.email.notif.update.subject", { number => $spid } )
: LJ::Lang::ml( "support.email.subject", { number => $spid } )
),
body => $body,
wrap => 1,
}
) if @emails;
$job->completed;
return 1;
}
sub keep_exit_status_for { 0 }
sub grab_for { 30 }
sub max_retries { 5 }
sub retry_delay {
my ( $class, $fails ) = @_;
return 30;
}
1;