#!/usr/bin/perl # # DW::Controller::Support::Request # # This controller is for the Support Request submission page. # # Authors: # Ruth Hatch # Jen Griffin # # Copyright (c) 2020 by Dreamwidth Studios, LLC. # # This is based on code originally implemented on LiveJournal. # # 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::Support::Request; use strict; use warnings; use DW::Controller; use DW::Routing; use DW::Template; DW::Routing->register_string( '/support/see_request', \&see_request_handler, app => 1 ); sub see_request_handler { my $r = DW::Request->get; my ( $ok, $rv ) = controller( anonymous => 1 ); return $rv unless $ok; my $scope = '/support/see_request.tt'; my $dbr = LJ::get_db_reader(); my $remote = $rv->{remote}; my $GET = $r->get_args; my $vars = {}; my $spid = $GET->{'id'} ? $GET->{'id'} + 0 : 0; my $sp = LJ::Support::load_request($spid); my $props = LJ::Support::load_props($spid); my $cats = LJ::Support::load_cats(); LJ::Support::init_remote($remote); $vars->{remote} = $remote; $vars->{sp} = $sp; $vars->{spid} = $spid; $vars->{uniq} = $props->{'uniq'}; if ( $GET->{'find'} ) { my $find = $GET->{'find'}; my $op = '<'; my $sort = 'DESC'; if ( $find eq 'next' || $find eq 'cnext' || $find eq 'first' ) { $op = '>'; $sort = 'ASC'; } my $spcatand = ''; if ( $sp && ( $find eq 'cnext' || $find eq 'cprev' ) ) { my $spcatid = $sp->{_cat}->{'spcatid'} + 0; $spcatand = "AND spcatid=$spcatid"; } else { my @filter_cats = LJ::Support::filter_cats( $remote, $cats ); return error_ml("$scope.error.text1") unless @filter_cats; my $cats_in = join( ",", map { $_->{'spcatid'} } @filter_cats ); $spcatand = "AND spcatid IN ($cats_in)"; } my $clause = ""; $clause = "AND spid$op$spid" unless ( $find eq 'first' || $find eq 'last' ); my ($foundspid) = $dbr->selectrow_array( "SELECT spid FROM support WHERE state='open' " . "$spcatand $clause ORDER BY spid $sort LIMIT 1" ); if ($foundspid) { return $r->redirect("see_request?id=$foundspid"); } else { my $extra = $find eq "cnext" || $find eq "cprev" ? "_cat" : ""; my $text = $find eq 'next' || $find eq 'cnext' ? LJ::Lang::ml( "$scope.error.nonext" . $extra ) : LJ::Lang::ml( "$scope.error.noprev" . $extra ); my $goback = $sp ? LJ::Lang::ml( "$scope.goback.text", { request_link => "href='see_request?id=$spid'", spid => $spid } ) : ''; return DW::Template->render_template( 'error.tt', { message => "$text$goback" } ); } $vars->{find} = 1; } return error_ml("$scope.unknownumber") unless $sp; $vars->{robot_meta_tags} = LJ::robot_meta_tags(); my $sth; my $user; my $user_url; my $auth = $GET->{'auth'}; my $email = $sp->{'reqemail'}; # Get remote username and journal URL, or example user's username and journal URL if ($remote) { $user = $remote->user; $user_url = $remote->journal_base; } else { my $exampleu = LJ::load_user($LJ::EXAMPLE_USER_ACCOUNT); $user = $exampleu ? $exampleu->user : "[Unknown or undefined example username]"; $user_url = $exampleu ? $exampleu->journal_base : "[Unknown or undefined example username]"; } my $u; my $clusterdown = 0; if ( $sp->{'reqtype'} eq "user" && $sp->{'requserid'} ) { $u = LJ::load_userid( $sp->{'requserid'} ); unless ($u) { warn "Error: user '$sp->{requserid}' not found in request #$spid"; return DW::Template->render_template( 'error.tt', { message => "Unknown user" } ); } # now do a check for a down cluster? $clusterdown = 1 unless LJ::get_cluster_reader($u); $email = $u->email_raw if $u->email_raw; $u->preload_props( "stylesys", "s2_style", "schemepref" ) unless $clusterdown; $vars->{u} = $u; $vars->{clusterdown} = $clusterdown; } my $winner; # who closed it? if ( $sp->{'state'} eq "closed" ) { $sth = $dbr->prepare( "SELECT u.user, sp.points FROM useridmap u, supportpoints sp " . "WHERE u.userid=sp.userid AND sp.spid=?" ); $sth->execute($spid); $winner = $sth->fetchrow_hashref; } # get all replies my @replies; $sth = $dbr->prepare( "SELECT splid, timelogged, UNIX_TIMESTAMP()-timelogged AS 'age', type, faqid, userid, message " . "FROM supportlog WHERE spid=? ORDER BY timelogged" ); $sth->execute($spid); while ( my $le = $sth->fetchrow_hashref ) { push @replies, $le; } # load category this request is in $vars->{problemarea} = $sp->{_cat}->{'catname'}; $vars->{catkey} = $sp->{_cat}->{'catkey'}; unless ( LJ::Support::can_read( $sp, $remote, $auth ) ) { return error_ml("$scope.nothaveprivilege"); } # helper variables for commonly called methods my $can_close = LJ::Support::can_close( $sp, $remote, $auth ) ? 1 : 0; my $can_reopen = LJ::Support::can_reopen( $sp, $remote, $auth ) ? 1 : 0; my $helper_mode = LJ::Support::can_help( $sp, $remote ) ? 1 : 0; my $stock_mode = LJ::Support::can_see_stocks( $sp, $remote ) ? 1 : 0; my $is_poster = LJ::Support::is_poster( $sp, $remote, $auth ) ? 1 : 0; $vars->{can_close} = $can_close; $vars->{can_reopen} = $can_reopen; $vars->{helper_mode} = $helper_mode; $vars->{stock_mode} = $stock_mode; $vars->{is_poster} = $is_poster; # fix up the subject if needed eval { if ( $sp->{'subject'} =~ /^=\?(utf-8)?/i ) { my @subj_data; require MIME::Words; @subj_data = MIME::Words::decode_mimewords( $sp->{'subject'} ); if ( scalar(@subj_data) ) { if ( !$1 ) { $sp->{'subject'} = Unicode::MapUTF8::to_utf8( { -string => $subj_data[0][0], -charset => $subj_data[0][1] } ); } else { $sp->{'subject'} = $subj_data[0][0]; } } } }; if ( $u->{'defaultpicid'} && !$u->is_suspended ) { my $userpic_obj = $u->userpic; my $user_img = ''; $user_img .= ""; $user_img .= $userpic_obj->imgtag; $user_img .= ""; $vars->{user_img} = $user_img; } my $display_name; # show requester name + email { my $visemail = $email; $visemail =~ s/^.+\@/********\@/; my $ename = $sp->{'reqtype'} eq 'user' ? LJ::ljuser($u) : LJ::ehtml( $sp->{reqname} ); # we show links to the history page if the user is a helper since # helpers can always find this information anyway just by taking # more steps. Show email history link if they have finduser and # thus once again could get this information anyway. my $has_sh = $remote && $remote->has_priv('supporthelp'); my $has_fu = $remote && $remote->has_priv('finduser'); my $has_vs = $remote && $remote->has_priv('supportviewscreened'); my %show_history = ( user => $has_sh, email => ( $has_fu || ( $has_sh && !$sp->{_cat}->{public_read} ) ), ); if ( $show_history{user} || $show_history{email} ) { $display_name = $sp->{reqtype} eq 'user' && $show_history{user} ? "$ename {user}\">" . LJ::ehtml( $u->{name} ) . "" : "$ename"; my $email_string = $has_vs || $has_sh ? " ($visemail)" : ""; $email_string = " ($email)" if $show_history{email}; $display_name .= $email_string; } else { # default view $display_name = $ename; $display_name .= " ($visemail)" if $has_vs || $has_sh; } } $vars->{display_name} = $display_name; $vars->{accounttype} = LJ::Capabilities::name_caps( $u->{caps} ) || "" . LJ::Lang::ml("$scope.unknown") . ""; my $ustyle = ''; if ( $u->{'stylesys'} == 2 ) { $ustyle .= "(S2) "; if ( $u->{'s2_style'} ) { my $s2style = LJ::S2::load_style( $u->{'s2_style'} ); my $pub = LJ::S2::get_public_layers(); # cached foreach my $lay ( sort { $a cmp $b } keys %{ $s2style->{'layer'} } ) { my $lid = $s2style->{'layer'}->{$lay}; unless ($lid) { next if $lay eq 'i18n'; # do we even support style langcodes? next if $lay eq 'i18nc'; # do we even support style langcodes? $ustyle .= "$lay: none, "; next; } $ustyle .= "$lay: "; $ustyle .= ( defined $pub->{$lid} ? 'public' : 'custom' ) . ", "; } } else { $ustyle .= LJ::Lang::ml("$scope.none"); } } else { $ustyle .= "(User on S1; why?) "; } $ustyle =~ s/,\s*$//; $vars->{ustyle} = $ustyle; # if the user has siteadmin:users or siteadmin:* show them link to resend validation email? my $extraval = sub { return '' unless $remote && $remote->has_priv( 'siteadmin', 'users' ); return " (" . LJ::Lang::ml("$scope.resend.validation.email") . ")"; }; my $email_status; if ( $u->{'status'} eq "A" ) { $email_status = "" . LJ::Lang::ml("$scope.yes") . ""; } if ( $u->{'status'} eq "N" ) { $email_status = "" . LJ::Lang::ml("$scope.no") . "" . $extraval->(); } if ( $u->{'status'} eq "T" ) { $email_status = LJ::Lang::ml("$scope.transitioning") . $extraval->(); } $vars->{email_status} = $email_status; $vars->{cluster_info} = LJ::DB::get_cluster_description( $u->{clusterid} ) if $u->{clusterid}; if ( LJ::isu($u) && $u->is_personal ) { # only personal accounts can upload images my $media_usage = DW::Media->get_usage_for_user($u); my $media_quota = DW::Media->get_quota_for_user($u); my $megabytes = sprintf( "%0.3f MB", $media_usage / 1024 / 1024 ); my $percentage = ( $media_quota != 0 ) ? sprintf( "%0.1f%%", $media_usage / $media_quota * 100 ) : LJ::Lang::ml("$scope.noquota"); $vars->{media_usage} = "$megabytes ($percentage)"; } $vars->{view_history} = $remote && $remote->has_priv('historyview'); $vars->{view_userlog} = $remote && $remote->has_priv( 'canview', 'userlog' ); if ( %LJ::BETA_FEATURES && LJ::Support::has_any_support_priv($remote) ) { $vars->{show_beta} = 1; $vars->{betafeatures} = LJ::isu($u) ? join( ", ", $u->prop( LJ::BetaFeatures->prop_name ) ) // '' : ''; } $vars->{show_cat_links} = LJ::Support::can_read_cat( $sp->{_cat}, $remote ); $vars->{timecreate} = LJ::time_to_http( $sp->{timecreate} ); $vars->{age} = LJ::diff_ago_text( $sp->{timecreate} ); my $state = $sp->{'state'}; if ( $state eq "open" ) { # check if it's still open or needing help or what if ( $sp->{'timelasthelp'} > ( $sp->{'timetouched'} + 5 ) ) { # open, answered $state = LJ::Lang::ml("$scope.answered"); } elsif ( $sp->{'timelasthelp'} && $sp->{'timetouched'} > $sp->{'timelasthelp'} + 5 ) { # open, still needs help $state = LJ::Lang::ml("$scope.answered.need.help"); } else { # default $state = "" . LJ::Lang::ml("$scope.open") . ""; } } if ( $state eq "closed" && $winner && LJ::Support::can_see_helper( $sp, $remote ) ) { my $s = $winner->{'points'} > 1 ? "s" : ""; my $wuser = $winner->{'user'}; $state .= " ($winner->{'points'} point$s to "; $state .= LJ::ljuser( $wuser, { 'full' => 1 } ) . ")"; } if ( $can_close || $can_reopen ) { if ( $sp->{'state'} eq "open" && $can_close ) { $state .= ", {'authcode'}'>" . LJ::Lang::ml("$scope.close.without.credit") . ""; } elsif ( $sp->{state} eq 'closed' ) { my $permastatus = LJ::Support::is_locked($sp); $state .= $sp->{'state'} eq "closed" && !$permastatus ? ", {'authcode'}'>" . LJ::Lang::ml("$scope.reopen.this.request") . "" : ""; if ( LJ::Support::can_lock( $sp, $remote ) ) { $state .= $permastatus ? ", " . LJ::Lang::ml("$scope.unlock.request") . "" : ", " . LJ::Lang::ml("$scope.lock.request") . ""; } } } $vars->{state} = $state; $vars->{private_req} = ( !$sp->{_cat}->{public_read} && $is_poster ) ? 1 : 0; my @screened; my @cleaned_replies; my $curlang = LJ::Lang::get_effective_lang(); ### reply loop foreach my $le (@replies) { my $reply = {}; my $up = LJ::load_userid( $le->{userid} ); my $remote_is_up = $remote && $remote->equals($up); next if $le->{type} eq "internal" && !( LJ::Support::can_read_internal( $sp, $remote ) || $remote_is_up ); next if $le->{type} eq "screened" && !( LJ::Support::can_read_screened( $sp, $remote ) || $remote_is_up ); next if $le->{type} eq "screened" && $up && !$up->is_visible; push @screened, $le if $le->{type} eq "screened"; my $message = $le->{message}; my %url; my $urlN = 0; $message = LJ::trim( LJ::ehtml($message) ); $message =~ s/\n( +)/"\n" . "  " x length($1) /eg; $message = LJ::html_newlines($message); $message = LJ::auto_linkify($message); # special case: original request if ( $le->{'type'} eq "req" ) { # insert support diagnostics from props if ( $props->{useragent} ) { $message .= sprintf( "
%s %s", LJ::Lang::ml("$scope.diagnostics"), LJ::ehtml( $props->{useragent} ) ); } $reply->{msg} = $message; $reply->{orig} = 1; push @cleaned_replies, $reply; next; } $reply->{msg} = $message; $reply->{id} = $le->{splid}; $reply->{type} = $le->{type}; # reply header my $header = ""; $reply->{show_helper} = LJ::Support::can_see_helper( $sp, $remote ); if ( $up && $reply->{show_helper} ) { my $picid = $up->get_picid_from_keyword('_support') || $up->{defaultpicid}; my $icon = $picid ? LJ::Userpic->new( $up, $picid ) : undef; $reply->{poster} = $up; $reply->{icon} = $icon; } my $what = '.answer'; if ( $le->{'type'} eq "internal" ) { $what = '.internal.comment'; } elsif ( $le->{'type'} eq "comment" ) { $what = ".comment"; } elsif ( $le->{'type'} eq "screened" ) { $what = '.screened.response'; } $reply->{type_title} = $what; $reply->{timehelped} = LJ::time_to_http( $le->{'timelogged'} ); $reply->{age} = LJ::ago_text( $le->{'age'} ); if ( $can_close && $sp->{'state'} eq "open" && $le->{'type'} eq "answer" ) { $reply->{show_close} = 1; } if ( $helper_mode && $le->{type} eq "screened" ) { $reply->{show_approve} = 1; } my $bordercolor = "default"; if ( $le->{'type'} eq "internal" ) { $bordercolor = "internal"; } if ( $le->{'type'} eq "answer" ) { $bordercolor = "answer"; } if ( $le->{'type'} eq "screened" ) { $bordercolor = "screened"; } $reply->{bordercolor} = $bordercolor; if ( $le->{faqid} ) { $reply->{faqid} = $le->{faqid}; my $faq = LJ::Faq->load( $le->{faqid}, lang => $curlang ); $faq->render_in_place; $reply->{faq} = $faq; } push @cleaned_replies, $reply; } $vars->{replies} = \@cleaned_replies; my @ans_type = LJ::Support::get_answer_types( $sp, $remote, $auth ); my %ans_type = @ans_type; $vars->{can_append} = LJ::Support::can_append( $sp, $remote, $auth ); $vars->{show_note} = !LJ::Support::can_read_internal( $sp, $remote ) && ( $ans_type{'answer'} || $ans_type{'screened'} ); # FAQ reference my @faqlist; if ( $ans_type{'answer'} || $ans_type{'screened'} ) { my %faqcat; my %faqq; # FIXME: must refactor that somewhere my $deflang = BML::get_language_default(); my $mll = LJ::Lang::get_lang($curlang); my $mld = LJ::Lang::get_dom("faq"); my $altlang = $deflang ne $curlang; $altlang = 0 unless $mld and $mll; if ($altlang) { my $sql = qq{SELECT fc.faqcat, t.text as faqcatname, fc.catorder FROM faqcat fc, ml_text t, ml_latest l, ml_items i WHERE t.dmid=$mld->{'dmid'} AND l.dmid=$mld->{'dmid'} AND i.dmid=$mld->{'dmid'} AND l.lnid=$mll->{'lnid'} AND l.itid=i.itid AND i.itcode=CONCAT('cat.', fc.faqcat) AND l.txtid=t.txtid AND fc.faqcat<>'int-abuse'}; $sth = $dbr->prepare($sql); } else { $sth = $dbr->prepare( "SELECT faqcat, faqcatname, catorder FROM faqcat WHERE faqcat<>'int-abuse'"); } $sth->execute; while ( $_ = $sth->fetchrow_hashref ) { $faqcat{ $_->{'faqcat'} } = $_; } foreach my $f ( LJ::Faq->load_all( lang => $curlang ) ) { $f->render_in_place( { user => $user, url => $user_url } ); push @{ $faqq{ $f->faqcat } ||= [] }, $f; } @faqlist = ( '0', "(don't reference FAQ)" ); foreach my $faqcat ( sort { $faqcat{$a}->{'catorder'} <=> $faqcat{$b}->{'catorder'} } keys %faqcat ) { push @faqlist, ( '0', "[ $faqcat{$faqcat}->{'faqcatname'} ]" ); foreach my $faq ( sort { $a->sortorder <=> $b->sortorder } @{ $faqq{$faqcat} || [] } ) { my $q = $faq->question_raw; next unless $q; $q = "... $q"; $q =~ s/^\s+//; $q =~ s/\s+$//; $q =~ s/\n/ /g; $q = substr( $q, 0, 75 ) . "..." if length($q) > 75; push @faqlist, ( $faq->faqid, $q ); } } $vars->{faqlist} = \@faqlist; } # Prefill an e-mail validation reminder, if needed. if ( ( $u->{status} eq "N" || $u->{status} eq "T" ) && !$u->is_identity && !$is_poster ) { my $reminder = LJ::load_include('validationreminder'); $vars->{reminder} = "\n\n$reminder" if $reminder; } # add in canned answers if there are any for this category and the user can use them my $stocks_html = ""; if ( $stock_mode && !$is_poster ) { # if one category's stock answers exactly matches another's my $stock_spcatid = $LJ::SUPPORT_STOCKS_OVERRIDE{ $sp->{_cat}->{catkey} } || $sp->{_cat}->{spcatid}; my $rows = $dbr->selectall_arrayref( 'SELECT subject, body FROM support_answers WHERE spcatid = ? ORDER BY subject', undef, $stock_spcatid ); if ( $rows && @$rows ) { $stocks_html .= "\n"; $stocks_html .= "\n"; } } $vars->{stock_answers} = $stocks_html; my $can_move_touch = LJ::Support::can_perform_actions( $sp, $remote ) && !$is_poster; $vars->{can_move_touch} = $can_move_touch; $vars->{catlist} = [ ( '', $sp->{'_cat'}->{'catname'} ), map { $_->{'spcatid'}, "---> $_->{'catname'}" } LJ::Support::sorted_cats($cats) ]; $vars->{screenedlist} = [ ( '', '' ), map { $_->{'splid'}, "\#$_->{'splid'} (" . LJ::get_username( $_->{'userid'} ) . ")" } @screened ]; $vars->{userfacing_actions_list} = [ map { $ans_type{$_} ? ( $_ => $ans_type{$_} ) : () } qw(screened answer comment) ]; $vars->{internal_actions_list} = [ map { $ans_type{$_} ? ( $_ => $ans_type{$_} ) : () } qw(internal bounce) ]; $vars->{approve_actions_list} = [ "answer" => "as answer", "comment" => "as comment" ]; $vars->{can} = { do_internal_actions => LJ::Support::can_make_internal( $sp, $remote ) && !$is_poster, use_stock_answers => 1, #$stock_mode && ! $is_poster && $stocks_html, approve_answers => @screened && $helper_mode, change_category => $can_move_touch, put_in_queue => $can_move_touch && $sp->{timelasthelp} > ( $sp->{timetouched} + 5 ), take_out_of_queue => $can_move_touch && $sp->{timelasthelp} <= ( $sp->{timetouched} + 5 ), change_summary => LJ::Support::can_change_summary( $sp, $remote ), }; return DW::Template->render_template( 'support/see_request.tt', $vars ); } 1;