# 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/ ); # # 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. # 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, "Database error: (report this)
$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, "Database error: (report this)
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{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 : "" . LJ::Lang::ml("support.email.update.unknown_username") . ""; $user_url = $u ? $u->journal_base : "" . LJ::Lang::ml("support.email.update.unknown_username") . ""; 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;