#!/usr/bin/perl # package LJ::Support; use strict; use Digest::MD5 qw(md5_hex); require "$ENV{'LJHOME'}/cgi-bin/sysban.pl"; ## 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) = @_; my $base = $sp->{_cat}->{'basepoints'}; $secs = int($secs / (3600*6)); my $total = ($base + $secs); if ($total > 10) { $total = 10; } $total ||= 1; return $total; } sub init_remote { my $remote = shift; return unless $remote; LJ::load_user_privs($remote, qw(supportclose supporthelp supportdelete supportread supportviewinternal supportmakeinternal supportmovetouch supportviewscreened supportchangesummary)); } # given all the categories, maps a catkey into a cat sub get_cat_by_key { my ($cats, $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 is_poster { my ($sp, $remote, $auth) = @_; # special case with non-logged in requesters that use miniauth if ($auth && $auth eq mini_auth($sp)) { return 1; } return 0 unless $remote; if ($sp->{'reqtype'} eq "email") { if ($remote->{'email'} eq $sp->{'reqemail'} && $remote->{'status'} eq "A") { return 1; } } elsif ($sp->{'reqtype'} eq "user") { if ($remote->{'userid'} eq $sp->{'requserid'}) { return 1; } } return 0; } sub can_see_helper { my ($sp, $remote) = @_; if ($sp->{_cat}->{'hide_helpers'}) { if (can_help($sp, $remote)) { return 1; } if (LJ::check_priv($remote, "supportviewinternal", $sp->{_cat}->{'catkey'})) { return 1; } if (LJ::check_priv($remote, "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'} || LJ::check_priv($remote, "supportread", $cat->{'catkey'})); } sub can_bounce { my ($sp, $remote) = @_; if ($sp->{_cat}->{'public_read'}) { if (LJ::check_priv($remote, "supportclose", "")) { return 1; } } my $catkey = $sp->{_cat}->{'catkey'}; if (LJ::check_priv($remote, "supportclose", $catkey)) { return 1; } return 0; } sub can_lock { my ($sp, $remote) = @_; return 1 if $sp->{_cat}->{public_read} && LJ::check_priv($remote, 'supportclose', ''); return 1 if LJ::check_priv($remote, 'supportclose', $sp->{_cat}->{catkey}); return 0; } sub can_close { my ($sp, $remote, $auth) = @_; if (is_poster($sp, $remote, $auth)) { return 1; } if ($sp->{_cat}->{'public_read'}) { if (LJ::check_priv($remote, "supportclose", "")) { return 1; } } my $catkey = $sp->{_cat}->{'catkey'}; if (LJ::check_priv($remote, "supportclose", $catkey)) { return 1; } return 0; } sub can_append { my ($sp, $remote, $auth) = @_; if (is_poster($sp, $remote, $auth)) { return 1; } return 0 unless $remote; return 0 unless $remote->{'statusvis'} eq "V"; 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 LJ::check_priv($remote, $priv, '') && $sp->{_cat}->{public_read}; return 1 if LJ::check_priv($remote, $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 LJ::check_priv($remote, "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_perform_actions { return LJ::Support::support_check_priv(@_, 'supportmovetouch'); } sub can_change_summary { return LJ::Support::support_check_priv(@_, 'supportchangesummary'); } sub can_help { my ($sp, $remote) = @_; if ($sp->{_cat}->{'public_read'}) { if ($sp->{_cat}->{'public_help'}) { return 1; } if (LJ::check_priv($remote, "supporthelp", "")) { return 1; } } my $catkey = $sp->{_cat}->{'catkey'}; if (LJ::check_priv($remote, "supporthelp", $catkey)) { return 1; } 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; } # $loadreq is used by /abuse/report.bml and # ljcmdbuffer.pl 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; } sub load_response { my $splid = shift; my $sth; $splid += 0; # load the support request 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", "More information"); return @ans_type; } if (can_help($sp, $remote)) { push @ans_type, ("screened" => "Screened Response", "answer" => "Answer", "comment" => "Comment or Question"); } elsif ($sp->{_cat}->{'allow_screened'}) { push @ans_type, ("screened" => "Screened Response"); } if (can_make_internal($sp, $remote) && ! $sp->{_cat}->{'public_help'}) { push @ans_type, ("internal" => "Internal Comment / Action"); } if (can_bounce($sp, $remote)) { push @ans_type, ("bounce" => "Bounce to Email & Close"); } return @ans_type; } sub file_request { my $errors = shift; my $o = shift; my $email = $o->{'reqtype'} eq "email" ? $o->{'reqemail'} : ""; 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'}; if (LJ::sysban_check('support_user', $u->{'user'})) { return LJ::sysban_block($userid, "Support request blocked based on user", $log); } $email = $u->{'email'}; } } 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'}); unless ($reqsubject) { push @$errors, "You must enter a problem summary."; } unless ($reqbody) { push @$errors, "You did not enter a support request."; } my $cats = LJ::Support::load_cats(); push @$errors, "Invalid support category" unless $cats->{$o->{'spcatid'}+0}; if (@$errors) { return 0; } 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"); $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())"); $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)"; }; $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.bml?id=$spid"; $urlauth = "$url&auth=$miniauth"; $body = "Your $LJ::SITENAME support request regarding \"$o->{'subject'}\" has been filed and will be answered as soon as possible. Your request tracking number is $spid.\n\n"; $body .= "You can track your request's progress or add information here:\n\n "; $body .= $urlauth; $body .= "\n\nIf you figure out the problem before somebody gets back to you, please cancel your request by clicking this:\n\n "; $body .= "$LJ::SITEROOT/support/act.bml?close;$spid;$authcode"; unless ($scat->{'no_autoreply'}) { LJ::send_mail({ 'to' => $email, 'from' => $LJ::BOGUS_EMAIL, 'fromname' => "$LJ::SITENAME Support", 'charset' => 'utf-8', 'subject' => "Support Request \#$spid", 'body' => $body }); } # attempt to buffer job to send email (but don't care if it fails) LJ::do_to_cluster(sub { # first parameter is cluster id return LJ::cmd_buffer_add(shift(@_), 0, '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) 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'}; 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'})) { 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'}+0; my $quserid = $posterid+0; my $spid = $sp->{'spid'}+0; my $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'}; 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); } } # attempt to buffer job to send email (but don't care if it fails) LJ::do_to_cluster(sub { # first parameter is cluster id return LJ::cmd_buffer_add(shift(@_), 0, '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; } 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()". " WHERE spid=?", undef, $spid) or return 0; set_points($spid, undef, 0); return 1; } sub mail_response_to_user { my $sp = shift; my $splid = shift; $splid += 0; my $res = load_response($splid); my $email; if ($sp->{'reqtype'} eq "email") { $email = $sp->{'reqemail'}; } else { my $u = LJ::load_userid($sp->{'requserid'}); $email = $u->{'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 $body = ""; my $dbh = LJ::get_db_writer(); my $what = $type eq "answer" ? "an answer to" : "a comment on"; $body .= "Below is $what your support question regarding \"$sp->{'subject'}\"\n"; my $miniauth = mini_auth($sp); $body .= "($LJ::SITEROOT/support/see_request.bml?id=$spid&auth=$miniauth).\n\n"; $body .= "="x70 . "\n\n"; if ($faqid) { my $faqname = ""; my $sth = $dbh->prepare("SELECT question FROM faq WHERE faqid=$faqid"); $sth->execute; ($faqname) = $sth->fetchrow_array; if ($faqname) { $body .= "FAQ REFERENCE: $faqname\n"; $body .= "$LJ::SITEROOT/support/faqbrowse.bml?faqid=$faqid"; $body .= "\n\n"; } } $body .= "$res->{'message'}\n\nDid this answer your question?\nYES:\n"; $body .= "$LJ::SITEROOT/support/act.bml?close;$spid;$sp->{'authcode'}"; $body .= ";$splid" if $type eq "answer"; $body .= "\nNO:\n$LJ::SITEROOT/support/see_request.bml?id=$spid&auth=$miniauth\n\n"; $body .= "If you are having problems using any of the links in this email, please try copying and pasting the *entire* link into your browser's address bar rather than clicking on it."; my $fromemail = $LJ::BOGUS_EMAIL; 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/; } LJ::send_mail({ 'to' => $email, 'from' => $fromemail, 'fromname' => "$LJ::SITENAME Support", 'charset' => 'utf-8', 'subject' => "Re: $sp->{'subject'}", 'body' => $body }); if ($type eq "answer") { $dbh->do("UPDATE support SET timelasthelp=UNIX_TIMESTAMP() WHERE spid=$spid"); } } sub mini_auth { my $sp = shift; return substr($sp->{'authcode'}, 0, 4); } 1;