#!/usr/bin/perl # # Authors: # Afuna # # Copyright (c) 2013 by Dreamwidth Studios, LLC. # # This program is free software; you may redistribute it and/or modify it under # the same terms as Perl itself. For a copy of the license, please reference # 'perldoc perlartistic' or 'perldoc perlgpl'. package DW::Entry::Moderated; use strict; =head1 NAME DW::Entry::Moderated - Entry in the moderation queue =head1 SYNOPSIS =cut sub new { my ( $class, $cu, $modid ) = @_; # use dbcm to read to minimize collisions between moderators due to replication lag my $dbcm = LJ::get_cluster_master($cu); my ( $posterid, $frozen ) = $dbcm->selectrow_array( "SELECT l.posterid, b.request_stor FROM modlog l, modblob b " . "WHERE l.journalid=? AND l.modid=? AND l.journalid=b.journalid AND l.modid=b.modid", undef, $cu->userid, $modid ); # not in modlog return unless $posterid; # there's no entry. maybe there was a modlog row, but not a modblob row # for whatever reason. let's lazy-clean. don't care if it returns a value # or not, because they may have legitimately just raced another moderator unless ($frozen) { my $sth = $dbcm->prepare("DELETE FROM modlog WHERE journalid=? AND modid=?"); $sth->execute( $cu->userid, $modid ); return; } my $raw_data = Storable::thaw($frozen); my $self = bless { _raw_data => $raw_data, id => $modid, journal => $cu, poster => LJ::load_userid($posterid), }; return $self; } =head2 C<< $self->id >> Returns the modid. =cut sub id { my ($self) = @_; return $self->{id}; } =head2 C<< $self->event >> Returns the event text cleaned, with anything that should be expanded also processed (e.g., polls) =cut sub event { my ($self) = @_; my $raw_data = $self->{_raw_data}; # cleaning my $event = $raw_data->{event}; $event =~ s/^\s+//; $event =~ s/\s+$//; if ( ( $raw_data->{lineendings} || "" ) eq "mac" ) { $event =~ s/\r/\n/g; } else { $event =~ s/\r//g; } my $error; my @polls = LJ::Poll->new_from_html( \$event, \$error, { journalid => $self->journal->userid, posterid => $self->poster->userid, } ); my $poll_preview = sub { my $poll = shift @polls; return '' unless $poll; return $poll->preview; }; $event =~ s//$poll_preview->()/eg; LJ::CleanHTML::clean_event( \$event, { preformatted => $raw_data->{props}->{opt_preformatted}, editor => $raw_data->{props}->{editor}, cutpreview => 1, cuturl => '#', } ); # create iframe from tag LJ::EmbedModule->expand_entry( $self->journal, \$event ); return $event; } =head2 C<< $self->subject >> Returns the cleaned subject for this moderated entry. =cut sub subject { my ($self) = @_; my $subject = $self->{_raw_data}->{subject}; LJ::CleanHTML::clean_subject( \$subject ); return $subject; } =head2 C<< $self->time >> Returns the date / time, already formatted =cut sub time { my ( $self, %opts ) = @_; my $raw_data = $self->{_raw_data}; my $datestr = sprintf( "%04d-%02d-%02d", $raw_data->{year}, $raw_data->{mon}, $raw_data->{day} ); my $etime = sprintf( "%s %02d:%02d", $opts{linkify} ? LJ::date_to_view_links( $self->journal, $datestr ) : $datestr, $raw_data->{hour}, $raw_data->{min} ); return $etime; } =head2 C<< $self->journal >> The journal this entry was posted in. LJ::User object. =cut sub journal { my ($self) = @_; return $self->{journal}; } =head2 C<< $self->poster >> The poster of this entry. LJ::User object. =cut sub poster { my ($self) = @_; return $self->{poster}; } =head2 C<< $self->props >> Returns all props =cut sub props { my ($self) = @_; return $self->{_raw_data}->{props}; } =head2 C<< $self->icon >> Returns the icon used for this entry =cut sub icon { my ($self) = @_; my $kw = $self->{props}->{picture_keyword}; my $icon = LJ::Userpic->new_from_keyword( $self->poster, $kw ); return $icon; } =head2 C<< $self->currents_html >> Return HTML for the currents (should see if we can move this view-related code out of here) =cut sub currents_html { my ($self) = @_; my $props = $self->props; my %current = LJ::currents( $props, $self->poster ); $current{Tags} = join( ", ", sort split( /\s*,\s*/, $props->{taglist} ) ) if $props->{taglist}; return LJ::currents_table(%current); } =head2 C<< $self->security_html >> Return HTML for the security icon (should see if we can move this view-related code out of here) =cut sub security_html { my ($self) = @_; my $security = $self->{_raw_data}->{security}; return LJ::img("security-private") if $security eq "private"; return LJ::img("security-protected") if $security eq "usemask"; return ""; } =head2 C<< $self->age_restriction_html >> Returns HTML for age restrictions icon (should see if we can move this view-related code out of here) =cut sub age_restriction_html { my ($self) = @_; my $age_restriction = $self->props->{adult_content}; return LJ::img("adult-18") if $age_restriction eq "explicit"; return LJ::img("adult-nsfw") if $age_restriction eq "concepts"; return ""; } =head2 C<< $self->age_restriction_reason >> Returns the reason provided for the age restriction status =cut sub age_restriction_reason { my ($self) = @_; return $self->props->{adult_content_reason}; } =head2 C<< $self->authcode >> Return the authcode for this moderated entry =cut sub auth { my ($self) = @_; return $self->{_raw_data}->{_moderate}->{authcode}; } =head2 C<< $self->request_data >> Hash of the entry text / metadata. Can be used with postevent =cut sub request_data { my ($self) = @_; my %req = %{ $self->{_raw_data} }; my $poster = $self->poster; # in case the user renamed while the submission was in the queue # we need to fix up the username based on the userid we stored $req{user} = $poster->user; $req{username} = $poster->user; return %req; } =head2 C<< $self->approve >> Approves the moderated entry and posts it to the community. Returns ( 1, $entry_url ) on success, ( 0, $error ) on failure. =cut sub approve { my ($self) = @_; my $req = { $self->request_data }; # allow all system logprops # we've already made sure that the original user didn't provide any system ones my $protocol_error; my $res = LJ::Protocol::do_request( 'postevent', $req, \$protocol_error, { nomod => 1, noauth => 1, allow_system => 1, } ); if ($res) { $self->delete_from_queue; my $entry = LJ::Entry->new( $self->journal, jitemid => $res->{itemid}, anum => $res->{anum} ); return ( 1, $entry->url ); } my $error = ""; $error = LJ::Protocol::error_message($protocol_error) if $protocol_error; return ( 0, $error ); } =head2 C<< $self->reject >> Reject the moderated entry. Returns 1 on success, 0 on failure. =cut sub reject { my ($self) = @_; $self->delete_from_queue; return 1; } =head2 C<< $self->reject_as_spam >> Reject the moderated entry as spam. Returns 1 on success, 0 on failure. =cut sub reject_as_spam { my ($self) = @_; my $did_reject = LJ::reject_entry_as_spam( $self->journal->userid, $self->id ) ? 1 : 0; $self->delete_from_queue if $did_reject; return $did_reject; } =head2 C<< $self->notify_poster( $msg_ml ) >> Sends an email to the poster of the moderated entry. Returns 1 if it was sent. =cut sub notify_poster { my ( $self, $status, %opts ) = @_; my $poster = $self->poster; return unless $poster->is_validated && $poster->is_visible; my $journal = $self->journal; my $props = $self->props; my $raw_data = $self->{_raw_data}; my $ml_scope = "/communities/queue/entries/edit.tt"; my @metadata; push @metadata, LJ::Lang::ml( "$ml_scope.email.submission.music", { music => $props->{current_music} } ) if $props->{current_music}; push @metadata, LJ::Lang::ml( "$ml_scope.email.submission.mood", { mood => $props->{current_mood} } ) if $props->{current_mood}; push @metadata, LJ::Lang::ml( "$ml_scope.email.submission.icon", { icon => $props->{picture_keyword} } ) if $props->{picture_keyword}; my $subject = LJ::Lang::ml( "$ml_scope.email.submission.subject", { subject => $raw_data->{subject} } ); my $time = LJ::Lang::ml( "$ml_scope.email.submission.time", { time => $self->time } ); # this is all ugly because we only conditionally include metadata if it's available. So we can't just do this as one string # the trailing spaces are unfortunately necessary to introduce line breaks... my $submission_text = LJ::Lang::ml( "$ml_scope.email.submission", { time => "> $time ", subject => "> $subject ", metadata => join( "\n", map { "> $_ " } @metadata ), text => LJ::markdown_blockquote( $raw_data->{event} ), } ); my $email_body = ""; if ( $status eq 'approved' ) { $email_body = LJ::Lang::ml( "$ml_scope.email.body.approved", { comm => "@" . $journal->user, entry_url => $opts{entry_url}, } ); $email_body .= "\n\n" . LJ::Lang::ml( "$ml_scope.email.body.approved.message", { message => $opts{message} } ) if $opts{message}; } elsif ( $status eq 'error' ) { $email_body = LJ::Lang::ml( "$ml_scope.email.body.error", { comm => "@" . $journal->user, error => $opts{error}, } ); } elsif ( $status eq 'rejected' ) { $email_body = $opts{message} ? LJ::Lang::ml( "$ml_scope.email.body.rejected_with_message", { comm => "@" . $journal->user, message => $opts{message} } ) : LJ::Lang::ml( "$ml_scope.email.body.rejected", { comm => "@" . $journal->user, } ); } $email_body .= "\n\n" . $submission_text; LJ::send_formatted_mail( to => $poster->email_raw, greeting_user => $poster->user, from => $LJ::BOGUS_EMAIL, fromname => qq{"$LJ::SITENAME"}, subject => LJ::Lang::ml("$ml_scope.email.subject"), body => $email_body, charset => 'utf-8', ); return 1; } =head2 C<< $self->delete_from_queue >> Delete from the moderation queue. It's been handled now =cut sub delete_from_queue { my ($self) = @_; my $modid = $self->id; my $journal = $self->journal; # Delete this moderated entry from the list $journal->do( "DELETE FROM modlog WHERE journalid=? AND modid=?", undef, $journal->userid, $modid ); $journal->do( "DELETE FROM modblob WHERE journalid=? AND modid=?", undef, $journal->userid, $modid ); # expire mod_queue_count memcache $journal->memc_delete('mqcount'); } 1;