mourningdove/cgi-bin/DW/Controller/Entry.pm

2060 lines
65 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
#
# DW::Controller::Entry
#
# This controller is for creating and managing entries
#
# Authors:
# Afuna <coder.dw@afunamatata.com>
#
# Copyright (c) 2011-2014 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::Controller::Entry;
use strict;
use Storable;
use LJ::Global::Constants;
use DW::Controller;
use DW::Routing;
use DW::Template;
use DW::FormErrors;
use DW::Formats;
use Hash::MultiValue;
use HTTP::Status qw( :constants );
use LJ::JSON;
use DW::External::Account;
use DW::External::Site;
my %form_to_props = (
# currents / metadata
current_mood => "current_moodid",
current_mood_other => "current_mood",
current_music => "current_music",
current_location => "current_location",
);
my @modules = qw(
tags displaydate slug
currents comments age_restriction
icons crosspost sticky
);
my @sites = DW::External::Site->get_sites;
my @sitevalues;
foreach my $site ( sort { $a->{sitename} cmp $b->{sitename} } @sites ) {
push @sitevalues, { domain => $site->{domain}, sitename => $site->{sitename} };
}
=head1 NAME
DW::Controller::Entry - Controller which handles posting and editing entries
=head1 Controller API
Handlers for creating and editing entries
=cut
DW::Routing->register_string( '/entry/new', \&new_handler, app => 1 );
DW::Routing->register_regex( '^/entry/([^/]+)/new$', \&new_handler, app => 1 );
DW::Routing->register_string(
'/entry/preview', \&preview_handler,
app => 1,
methods => { POST => 1 }
);
DW::Routing->register_string( '/__rpc_draft', \&draft_rpc_handler, app => 1, format => 'json' );
DW::Routing->register_string( '/entry/options', \&options_handler, app => 1 );
DW::Routing->register_string( '/__rpc_entryoptions', \&options_rpc_handler, app => 1 );
DW::Routing->register_string(
'/__rpc_entryformcollapse', \&collapse_rpc_handler,
app => 1,
methods => { GET => 1 },
format => 'json'
);
# /entry/username/ditemid/edit
DW::Routing->register_regex( '^/entry/(?:(.+)/)?(\d+)/edit$', \&edit_handler, app => 1 );
DW::Routing->register_string( '/entry/new', \&_new_handler_userspace, user => 1 );
# redirect to app-space
sub _user_to_app_role {
my ($path) = @_;
return DW::Request->get->redirect( LJ::create_url( $path, host => $LJ::DOMAIN_WEB ) );
}
sub _new_handler_userspace { return _user_to_app_role("/entry/$_[0]->{username}/new") }
=head2 C<< DW::Controller::Entry::new_handler( ) >>
Handles posting a new entry
=cut
sub new_handler {
my ( $call_opts, $usejournal ) = @_;
my ( $ok, $rv ) = controller( anonymous => 1 );
return $rv unless $ok;
my $r = DW::Request->get;
my $remote = $rv->{remote};
# these kinds of errors prevent us from initializing the form at all
# so abort and return it without the form
if ($remote) {
return error_ml( "/entry/form.tt.error.nonusercantpost", { sitename => $LJ::SITENAME } )
if $remote->is_identity;
return error_ml("/entry/form.tt.error.cantpost")
unless $remote->can_post;
}
my $errors = DW::FormErrors->new;
my $warnings = DW::FormErrors->new;
my $post = $r->did_post ? $r->post_args : undef;
# figure out times
my $datetime;
my $trust_datetime_value = 0;
if ( $post && $post->{entrytime_date} && $post->{entrytime_time} ) {
$datetime = "$post->{entrytime_date} $post->{entrytime_time}";
$trust_datetime_value = 1;
}
else {
my $now = DateTime->now;
# if user has timezone, use it!
if ( $remote && $remote->prop("timezone") ) {
my $tz = $remote->prop("timezone");
$tz = $tz ? eval { DateTime::TimeZone->new( name => $tz ); } : undef;
$now = eval { DateTime->from_epoch( epoch => time(), time_zone => $tz ); }
if $tz;
}
$datetime = $now->strftime("%F %R"),
$trust_datetime_value = 0; # may want to override with client-side JS
}
# crosspost account selected?
my %crosspost;
if ( !$r->did_post && $remote ) {
%crosspost = map { $_->acctid => $_->xpostbydefault }
DW::External::Account->get_external_accounts($remote);
}
my $get = $r->get_args;
$usejournal ||= $get->{usejournal};
my $vars = _init(
{
usejournal => $usejournal,
remote => $remote,
datetime => $datetime || "",
trust_datetime_value => $trust_datetime_value,
crosspost => \%crosspost,
},
@_
);
# now look for errors that we still want to recover from
$errors->add( undef, ".error.invalidusejournal" )
if defined $usejournal && !$vars->{usejournal};
if ( $r->did_post ) {
my $mode_preview = $post->{"action:preview"} ? 1 : 0;
$errors->add( undef, 'bml.badinput.body1' )
unless LJ::text_in($post);
my $okay_formauth = !$remote || LJ::check_form_auth( $post->{lj_form_auth} );
$errors->add( undef, "error.invalidform" )
unless $okay_formauth;
if ($mode_preview) {
# do nothing
}
elsif ( $okay_formauth && $post->{showform} )
{ # some other form posted content to us, which the user will want to edit further
}
elsif ($okay_formauth) {
my $flags = {};
my %auth = _auth( $flags, $post, $remote );
my $uj = $auth{journal};
$errors->add_string( undef, $LJ::MSG_READONLY_USER )
if $uj && $uj->readonly;
# do a login action to check if we can authenticate as unverified_username
# and to display any important messages connected to your account
{
# build a clientversion string
my $clientversion = "Web/3.0.0";
# build a request object
my %login_req = (
ver => $LJ::PROTOCOL_VER,
clientversion => $clientversion,
username => $auth{unverified_username},
);
my $err;
my $login_res = LJ::Protocol::do_request( "login", \%login_req, \$err, $flags );
unless ($login_res) {
$errors->add( undef, ".error.login",
{ error => LJ::Protocol::error_message($err) } );
}
# e.g. not validated
$warnings->add_string( undef,
LJ::auto_linkify( LJ::ehtml( $login_res->{message} ) ) )
if $login_res->{message};
}
my $form_req = {};
_form_to_backend( $form_req, $post, errors => $errors );
# check for spam domains
LJ::Hooks::run_hooks( 'spam_check', $auth{poster}, $form_req, 'entry' );
# if we didn't have any errors with decoding the form, proceed to post
unless ( $errors->exist ) {
my %post_res = _do_post( $form_req, $flags, \%auth, warnings => $warnings );
return $post_res{render} if $post_res{status} eq "ok";
# oops errors when posting: show error, fall through to show form
$errors->add_string( undef, $post_res{errors} ) if $post_res{errors};
}
}
}
# this is an error in the user-submitted data, so regenerate the form with the error message and previous values
$vars->{errors} = $errors;
$vars->{warnings} = $warnings;
# prepopulate if we haven't been through this form already
$vars->{formdata} = $post || _prepopulate($get);
# Had to wait for formdata before figuring out the editors list -- if we
# have a WIP form submission with errors, we want to reuse what the user
# already chose.
$vars->{editors} = DW::Formats::select_items(
current => $vars->{formdata}->{editor},
preferred => $remote ? $remote->prop('entry_editor2') : '',
);
$vars->{formdata}->{editor} = $vars->{editors}->{selected};
# Set up info for the icon select/preview/browse components
$vars->{current_icon_kw} = $vars->{formdata}->{prop_picture_keyword};
$vars->{current_icon} = LJ::Userpic->new_from_keyword( $remote, $vars->{current_icon_kw} );
$vars->{editable} = { map { $_ => 1 } @modules };
$vars->{action} = { url => LJ::create_url( undef, keep_args => 1 ), };
$vars->{js_for_rte} = LJ::rte_js_vars();
$vars->{sitevalues} = to_json( \@sitevalues );
# Set up vars for drafts
my $draft = '""';
my %draft_properties;
my $draft_subject_raw = "";
if ($remote) {
# Here we get the value of the userprop 'draft_properties', containing
# a frozen Storable string, which we then thaw into a hash by the same
# name.
$draft = $remote->prop('entry_draft');
%draft_properties =
$remote->prop('draft_properties')
? %{ Storable::thaw( $remote->prop('draft_properties') ) }
: ();
# store raw for later use; will be escaped later
$draft_subject_raw = $draft_properties{subject};
}
my $initDraft = 'null';
if ( $remote && LJ::is_enabled('update_draft') ) {
# While transforms aren't considered posts, we don't want to
# prompt the user to restore from a draft on a transform
if ( !LJ::did_post() ) {
$initDraft = 'true';
}
else {
$initDraft = 'false';
}
}
$vars->{init_draft} = $initDraft;
$vars->{draft} = $draft;
$vars->{draft_properties} = \%draft_properties;
$vars->{draft_subject_raw} = $draft_subject_raw;
$vars->{autosave_interval} = $LJ::AUTOSAVE_DRAFT_INTERVAL;
return DW::Template->render_template( 'entry/form.tt', $vars );
}
# Initializes entry form values.
# Can be used when posting a new entry or editing an old entry.
# Arguments:
# * form_opts: options for initializing the form
# usejournal string: username of the journal we're posting to (if not provided,
# use journal of the user we're posting as)
# datetime string: display date of the entry in format "$year-$mon-$mday $hour:$min" (already taking into account timezones)
# * call_opts: instance of DW::Routing::CallInfo (currently unused)
sub _init {
my ( $form_opts, $call_opts ) = @_;
my $u = $form_opts->{remote};
my $vars = {};
my %moodtheme;
my @moodlist;
my $moods = DW::Mood->get_moods;
# we check whether the user can actually post to this journal on form submission
# journal we explicitly say we want to post to
my $usejournal = LJ::load_user( $form_opts->{usejournal} );
my @journallist;
push @journallist, $usejournal if LJ::isu($usejournal);
# the journal we are actually posting to (whether implicitly or overriden by usejournal)
my $journalu = LJ::isu($usejournal) ? $usejournal : $u;
my @crosspost_list;
my $crosspost_main = 0;
my %crosspost_selected = %{ $form_opts->{crosspost} || {} };
my $panels;
my $formwidth;
my $min_animation;
my $displaydate_check;
if ($u) {
# moods
my $theme = DW::Mood->new( $u->{moodthemeid} );
if ($theme) {
$moodtheme{id} = $theme->id;
foreach my $mood ( values %$moods ) {
$theme->get_picture( $mood->{id}, \my %pic );
next unless keys %pic;
$moodtheme{pics}->{ $mood->{id} }->{pic} = $pic{pic};
$moodtheme{pics}->{ $mood->{id} }->{width} = $pic{w};
$moodtheme{pics}->{ $mood->{id} }->{height} = $pic{h};
$moodtheme{pics}->{ $mood->{id} }->{name} = $mood->{name};
}
}
@journallist = ( $u, $u->posting_access_list )
unless $usejournal;
# crosspost
my @accounts = DW::External::Account->get_external_accounts($u);
if ( scalar @accounts ) {
foreach my $acct (@accounts) {
my $id = $acct->acctid;
my $selected = $crosspost_selected{$id};
push @crosspost_list,
{
id => $id,
name => $acct->displayname,
selected => $selected,
need_password => $acct->password ? 0 : 1,
};
$crosspost_main = 1 if $selected;
}
}
$panels = $u->entryform_panels;
$formwidth = $u->entryform_width;
$min_animation = $u->prop("js_animations_minimal") ? 1 : 0;
$displaydate_check =
( $u->displaydate_check && not $form_opts->{trust_datetime_value} ) ? 1 : 0;
}
else {
$panels = LJ::User::default_entryform_panels( anonymous => 1 );
}
@moodlist = ( { id => "", name => LJ::Lang::ml("entryform.mood.noneother") } );
push @moodlist, { id => $_, name => $moods->{$_}->{name} }
foreach sort { $moods->{$a}->{name} cmp $moods->{$b}->{name} } keys %$moods;
my %security_options = (
"public" => {
value => "public",
label => ".public.label",
format => ".public.format",
},
"private" => {
value => "private",
label => ".private.label",
format => ".private.format",
image => $LJ::Img::img{"security-private"},
},
"admin" => {
value => "private",
label => ".admin.label",
format => ".private.format",
image => $LJ::Img::img{"security-private"},
},
"access" => {
value => "access",
label => ".access.label",
format => ".access.format",
image => $LJ::Img::img{"security-protected"},
},
"members" => {
value => "access",
label => ".members.label",
format => ".members.format",
image => $LJ::Img::img{"security-protected"},
},
"custom" => {
value => "custom",
label => ".custom.label",
format => ".custom.format",
image => $LJ::Img::img{"security-groups"},
}
);
foreach my $data ( values %security_options ) {
my $prefix = ".select.security";
$data->{label} = $prefix . $data->{label};
$data->{format} = $prefix . $data->{format};
}
my $is_community = $journalu && $journalu->is_community;
my @security = $is_community ? qw( public members admin ) : qw( public access private );
my @custom_groups;
if ( $u && !$is_community ) {
@custom_groups =
map { { value => $_->{groupnum}, label => $_->{groupname} } } $u->trust_groups;
push @security, "custom" if @custom_groups;
}
@security = map { $security_options{$_} } @security;
my ( $year, $mon, $mday, $hour, $min ) = split( /\D/, $form_opts->{datetime} || "" );
my %displaydate;
$displaydate{year} = $year;
$displaydate{month} = $mon;
$displaydate{day} = $mday;
$displaydate{hour} = $hour;
$displaydate{minute} = $min;
$displaydate{trust_initial} = $form_opts->{trust_datetime_value};
# TODO:
# # JavaScript sets this value, so we know that the time we get is correct
# # but always trust the time if we've been through the form already
# my $date_diff = ($opts->{'mode'} eq "edit") ? 1 : 0;
$vars = {
remote => $u,
moodtheme => \%moodtheme,
moods => \@moodlist,
journallist => \@journallist,
usejournal => $usejournal,
security => \@security,
customgroups => \@custom_groups,
security_options => \%security_options,
journalu => $journalu,
crosspost_entry => $crosspost_main,
crosspostlist => \@crosspost_list,
crosspost_url => "$LJ::SITEROOT/manage/settings/?cat=othersites",
sticky_url => "$LJ::SITEROOT/manage/settings/?cat=display#DW__Setting__StickyEntry_",
sticky_entry => $form_opts->{sticky_entry},
displaydate => \%displaydate,
displaydate_check => $displaydate_check,
panels => $panels,
formwidth => $formwidth && $formwidth eq "P" ? "narrow" : "wide",
min_animation => $min_animation ? 1 : 0,
limits => {
subject_length => LJ::CMAX_SUBJECT,
},
# TODO: Remove this when beta is over
betacommunity => LJ::load_user("dw_beta"),
};
return $vars;
}
=head2 C<< DW::Controller::Entry::edit_handler( ) >>
Handles generating the form for, and handling the actual edit of an entry
=cut
sub edit_handler {
return _edit(@_);
}
sub _edit {
my ( $opts, $username, $ditemid ) = @_;
my ( $ok, $rv ) = controller();
return $rv unless $ok;
my $r = DW::Request->get;
my $remote = $rv->{remote};
my $journal = defined $username ? LJ::load_user($username) : $remote;
return error_ml('error.invalidauth') unless $journal;
my $errors = DW::FormErrors->new;
my $warnings = DW::FormErrors->new;
my $post;
if ( $r->did_post ) {
$post = $r->post_args;
# no difference because we rely on the entry info, but let's get rid of this
# just to make sure it doesn't trip us up in the future...
$post->remove('poster_remote');
$post->remove('usejournal');
my $mode_preview = $post->{"action:preview"} ? 1 : 0;
my $mode_delete = $post->{"action:delete"} ? 1 : 0;
$errors->add( undef, 'bml.badinput.body1' )
unless LJ::text_in($post);
my $okay_formauth = LJ::check_form_auth( $post->{lj_form_auth} );
$errors->add( undef, "error.invalidform" )
unless $okay_formauth;
if ($mode_preview) {
# do nothing
}
elsif ($okay_formauth) {
$errors->add_string( undef, $LJ::MSG_READONLY_USER )
if $journal && $journal->readonly;
my $form_req = {};
_form_to_backend(
$form_req, $post,
allow_empty => $mode_delete,
errors => $errors
);
# check for spam domains
LJ::Hooks::run_hooks( 'spam_check', $remote, $form_req, 'entry' );
# if we didn't have any errors with decoding the form, proceed to post
unless ( $errors->exist ) {
if ($mode_delete) {
$form_req->{event} = "";
# now log the event created above
$journal->log_event(
'delete_entry',
{
remote => $remote,
actiontarget => $ditemid,
method => 'web',
}
);
}
my %edit_res = _do_edit(
$ditemid, $form_req,
{ poster => $remote, journal => $journal },
warnings => $warnings,
);
return $edit_res{render} if $edit_res{status} eq "ok";
# oops errors when posting: show error, fall through to show form
$errors->add_string( undef, $edit_res{errors} ) if $edit_res{errors};
}
}
}
# we can always trust this value:
# it either came straight from the entry
# or it's from the user's POST
my $trust_datetime_value = 1;
my $entry_obj = LJ::Entry->new( $journal, ditemid => $ditemid );
# are you authorized to view this entry
# and does the entry we got match the provided ditemid exactly?
my $anum = $ditemid % 256;
my $itemid = $ditemid >> 8;
return error_ml("/entry/form.tt.error.nofind")
unless $entry_obj->editable_by($remote)
&& $anum == $entry_obj->anum
&& $itemid == $entry_obj->jitemid;
# so at this point, we know that we are authorized to edit this entry
# but we need to handle things differently if we're an admin
# FIXME: handle communities
return error_ml('IS AN ADMIN') unless $entry_obj->poster->equals($remote);
my %crosspost;
if ( !$r->did_post && ( my $xpost = $entry_obj->prop("xpostdetail") ) ) {
my $xposthash = DW::External::Account->xpost_string_to_hash($xpost);
%crosspost = map { $_ => 1 } keys %{ $xposthash || {} };
}
my $vars = _init(
{
usejournal => $journal->username,
remote => $remote,
datetime => $entry_obj->eventtime_mysql,
trust_datetime_value => $trust_datetime_value,
crosspost => \%crosspost,
sticky_entry => $journal->sticky_entries_lookup->{$ditemid},
},
@_
);
# now look for errors that we still want to recover from
my $get = $r->get_args;
$errors->add( undef, ".error.invalidusejournal" )
if defined $get->{usejournal} && !$vars->{usejournal};
# this is an error in the user-submitted data, so regenerate the form with the error message and previous values
$vars->{errors} = $errors;
$vars->{warnings} = $warnings;
$vars->{formdata} = $post || _backend_to_form($entry_obj);
# Now that we have {formdata}->{editor}, we can get the list of available
# editors.
$vars->{editors} = DW::Formats::select_items(
current => $vars->{formdata}->{editor},
preferred => $remote->prop('entry_editor2'),
);
# The template helper uses "formdata" to set the default values for fields,
# so we'll update it in place with what DW::Formats thinks we should use.
$vars->{formdata}->{editor} = $vars->{editors}->{selected};
# Set up info for the icon select/preview/browse components
$vars->{current_icon_kw} = $vars->{formdata}->{prop_picture_keyword};
$vars->{current_icon} = LJ::Userpic->new_from_keyword( $remote, $vars->{current_icon_kw} );
my %editable = map { $_ => 1 } @modules;
$vars->{editable} = \%editable;
# this can't be edited after posting
delete $editable{journal};
$vars->{action} = {
edit => 1,
url => LJ::create_url( undef, keep_args => 1 ),
};
$vars->{js_for_rte} = LJ::rte_js_vars();
$vars->{sitevalues} = to_json( \@sitevalues );
return DW::Template->render_template( 'entry/form.tt', $vars );
}
# returns:
# poster: user object that contains the poster of the entry. may be the current remote user,
# or may be someone logging in via the login form on the entry
# journal: user object for the journal the entry is being posted to. may be the same as the
# poster, or may be a community
# unverified_username: username that current remote is trying to post as; remote may not
# actually have access to this journal so don't treat as trusted
#
# modifies/sets:
# flags: hashref of flags for the protocol
# noauth = 1 if the user is the same as remote or has authenticated successfully
# u = user we're posting as
sub _auth {
my ( $flags, $post, $remote, $referer ) = @_;
# referer only should be passed in if outside web context, such as when running tests
my %auth;
foreach (qw( username chal response password )) {
$auth{$_} = $post->{$_} || "";
}
my %ret;
if (
$auth{username} # user argument given
&& !$remote
)
{ # user not logged in
my $u = LJ::load_user( $auth{username} );
# verify entered password, if it is present
my $ok = LJ::auth_okay( $u, $auth{password} );
if ($ok) {
$flags->{noauth} = 1;
$flags->{u} = $u;
$ret{poster} = $u;
$ret{journal} = $post->{usejournal} ? LJ::load_user( $post->{usejournal} ) : $u;
}
}
elsif ( $remote && LJ::check_referer( undef, $referer ) ) {
$flags->{noauth} = 1;
$flags->{u} = $remote;
$ret{poster} = $remote;
$ret{journal} = $post->{usejournal} ? LJ::load_user( $post->{usejournal} ) : $remote;
}
$ret{unverified_username} = $ret{poster} ? $ret{poster}->username : $auth{username};
return %ret;
}
# decodes the posted form into a hash suitable for use with the protocol
# $post is expected to be an instance of Hash::MultiValue
sub _form_to_backend {
my ( $req, $post, %opts ) = @_;
my $errors = $opts{errors};
# handle event subject and body
$req->{subject} = $post->{subject};
$req->{event} = $post->{event} || "";
$errors->add( undef, ".error.noentry" )
if $errors && $req->{event} eq "" && !$opts{allow_empty};
# warn the user of any bad markup errors
my $clean_event = $post->{event};
my $errref;
my $editor = undef;
my $verbose_err;
LJ::CleanHTML::clean_event( \$clean_event,
{ errref => \$errref, editor => $editor, verbose_err => \$verbose_err } );
if ( $errors && $verbose_err ) {
if ( ref($verbose_err) eq 'HASH' ) {
$errors->add( undef, $verbose_err->{error}, $verbose_err->{opts} );
}
else {
$errors->add( undef, $verbose_err );
}
}
# initialize props hash
$req->{props} ||= {};
my $props = $req->{props};
while ( my ( $formname, $propname ) = each %form_to_props ) {
$props->{$propname} = $post->{$formname} // '';
}
$props->{taglist} = $post->{taglist} if defined $post->{taglist};
$props->{picture_keyword} = $post->{prop_picture_keyword}
if defined $post->{prop_picture_keyword};
$props->{opt_backdated} = $post->{entrytime_outoforder} ? 1 : 0;
# This form always uses the editor prop instead of opt_preformatted.
$props->{opt_preformatted} = 0;
$props->{editor} = DW::Formats::validate( $post->{editor} );
# old implementation of comments
# FIXME: remove this before taking the page out of beta
$props->{opt_screening} = $post->{opt_screening};
$props->{opt_nocomments} =
$post->{comment_settings} && $post->{comment_settings} eq "nocomments" ? 1 : 0;
$props->{opt_noemail} =
$post->{comment_settings} && $post->{comment_settings} eq "noemail" ? 1 : 0;
# see if an "other" mood they typed in has an equivalent moodid
if ( $props->{current_mood} ) {
if ( my $moodid = DW::Mood->mood_id( $props->{current_mood} ) ) {
$props->{current_moodid} = $moodid;
$props->{current_mood} = '';
}
}
# nuke taglists that are just blank
$props->{taglist} = "" unless $props->{taglist} && $props->{taglist} =~ /\S/;
if ( LJ::is_enabled('adult_content') ) {
my $restriction_key = $post->{age_restriction} || '';
$props->{adult_content} = {
'' => '',
'none' => 'none',
'discretion' => 'concepts',
'restricted' => 'explicit',
}->{$restriction_key}
|| "";
$props->{adult_content_reason} = $post->{age_restriction_reason} || "";
}
# Set entry slug if it's been specified
$req->{slug} = LJ::canonicalize_slug( $post->{entry_slug} // '' );
# Check if this is a community.
$props->{admin_post} = $post->{flags_adminpost} || 0;
# entry security
my $sec = "public";
my $amask = 0;
{
my $security = $post->{security} || "";
if ( $security eq "private" ) {
$sec = "private";
}
elsif ( $security eq "access" ) {
$sec = "usemask";
$amask = 1;
}
elsif ( $security eq "custom" ) {
$sec = "usemask";
foreach my $bit ( $post->get_all("custom_bit") ) {
$amask |= ( 1 << $bit );
}
}
}
$req->{security} = $sec;
$req->{allowmask} = $amask;
# date/time
my ( $year, $month, $day ) = split( /\D/, $post->{entrytime_date} || "" );
my ( $hour, $min ) = split( /\D/, $post->{entrytime_time} || "" );
# if we trust_datetime, it's because we either are in a mode where we've saved the datetime before (e.g., edit)
# or we have run the JS that syncs the datetime with the user's current time
# we also have to trust the datetime when the user has JS disabled, because otherwise we won't have any fallback value
if ( $post->{trust_datetime} || $post->{nojs} ) {
delete $req->{tz};
$req->{year} = $year;
$req->{mon} = $month;
$req->{day} = $day;
$req->{hour} = $hour;
$req->{min} = $min;
}
$req->{update_displaydate} = $post->{update_displaydate};
# crosspost
$req->{crosspost_entry} = $post->{crosspost_entry} ? 1 : 0;
if ( $req->{crosspost_entry} ) {
foreach my $acctid ( $post->get_all("crosspost") ) {
$req->{crosspost}->{$acctid} = {
id => $acctid,
password => $post->{"crosspost_password_$acctid"},
chal => $post->{"crosspost_chal_$acctid"},
resp => $post->{"crosspost_resp_$acctid"},
};
}
}
$req->{sticky_entry} = $post->{sticky_entry};
$req->{sticky_select} = $post->{sticky_select};
return 1;
}
# given an LJ::Entry object, returns a hashref populated with data suitable for use in generating the form
sub _backend_to_form {
my ($entry) = @_;
# my $entry = {
# 'usejournal' => $usejournal,
# 'auth' => $auth,
# 'richtext' => LJ::is_enabled('richtext'),
# 'suspended' => $suspend_msg,
# };
# direct translation of prop values to the form
my $event = $entry->event_raw;
# Look up formatting for newer entries...
my $editor = $entry->prop('editor');
# ...or, figure out formatting when editing old entries.
# TODO: This duplicates some logic from LJ::CleanHTML for guessing an editor
# value for old posts. Would be nice to centralize it in the Entry class,
# except that if we're detecting old-style !markdown, we DO want to also
# mutate the body text, which makes it hairy.
unless ($editor) {
if ( LJ::CleanHTML::legacy_markdown( \$event ) ) { # mutates $event
$editor = 'markdown0';
}
elsif ( $entry->prop('used_rte') ) {
$editor = 'rte0';
}
elsif ( $entry->prop('opt_preformatted') ) {
$editor = 'html_raw0';
}
elsif ( $entry->prop('import_source') ) {
$editor = 'html_casual0';
}
elsif ( $entry->logtime_mysql lt '2019-05' ) {
$editor = 'html_casual0';
}
else {
$editor = 'html_casual1'; # For accurate state when editing posts.
}
}
my %formprops = map { $_ => $entry->prop( $form_to_props{$_} ) } keys %form_to_props;
# some properties aren't in the hash above, so go through them manually
my %otherprops = (
taglist => join( ', ', $entry->tags ),
entrytime_outoforder => $entry->prop("opt_backdated"),
age_restriction => {
'' => '',
'none' => 'none',
'concepts' => 'discretion',
'explicit' => 'restricted',
}->{ $entry->prop("adult_content") || '' },
age_restriction_reason => $entry->prop("adult_content_reason"),
entry_slug => $entry->slug,
flags_adminpost => $entry->prop("admin_post"),
# At this point we know enough to get the full list of editors (and
# selection state) for the dropdown, but because of how the template
# variables are laid out, we shouldn't really do that from here. (This
# function's whole return value becomes 'formdata' in the template
# vars.) So we'll pass this along, and the caller (currently just _edit)
# will use it to get the list for the dropdown.
editor => DW::Formats::validate($editor),
# FIXME: remove before taking the page out of beta
opt_screening => $entry->prop("opt_screening"),
comment_settings => $entry->prop("opt_nocomments") ? "nocomments"
: $entry->prop("opt_noemail") ? "noemail"
: undef,
);
my $security = $entry->security || "";
my @custom_groups;
if ( $security eq "usemask" ) {
my $amask = $entry->allowmask;
if ( $amask == 1 ) {
$security = "access";
}
else {
$security = "custom";
@custom_groups = grep { $amask & ( 1 << $_ ) } 1 .. 60;
}
}
# allow editing of embedded content
my $ju = $entry->journal;
LJ::EmbedModule->parse_module_embed( $ju, \$event, edit => 1 );
return {
subject => $entry->subject_raw,
event => $event,
prop_picture_keyword => $entry->userpic_kw,
security => $security,
custom_bit => \@custom_groups,
is_sticky => $entry->journal->sticky_entries_lookup->{ $entry->ditemid },
%formprops,
%otherprops,
};
}
sub _queue_crosspost {
my ( $form_req, %opts ) = @_;
my $u = delete $opts{remote};
my $ju = delete $opts{journal};
my $deleted = delete $opts{deleted};
my $editurl = delete $opts{editurl};
my $ditemid = delete $opts{ditemid};
my @crossposts;
if ( $u->equals($ju) && $form_req->{crosspost_entry} ) {
my $user_crosspost = $form_req->{crosspost};
my ( $xpost_successes, $xpost_errors ) = LJ::Protocol::schedule_xposts(
$u, $ditemid, $deleted,
sub {
my $submitted = $user_crosspost->{ $_[0]->acctid } || {};
# first argument is true if user checked the box
# false otherwise
return (
$submitted->{id} ? 1 : 0,
{
password => $submitted->{password},
auth_challenge => $submitted->{chal},
auth_response => $submitted->{resp},
}
);
}
);
foreach my $crosspost ( @{ $xpost_successes || [] } ) {
push @crossposts,
{
text => LJ::Lang::ml(
"xpost.request.success2",
{
account => $crosspost->displayname,
sitenameshort => $LJ::SITENAMESHORT,
}
),
status => "ok",
};
}
foreach my $crosspost ( @{ $xpost_errors || [] } ) {
push @crossposts,
{
text => LJ::Lang::ml(
'xpost.request.failed',
{
account => $crosspost->displayname,
editurl => $editurl,
}
),
status => "error",
};
}
}
return @crossposts;
}
sub _save_new_entry {
my ( $form_req, $flags, $auth ) = @_;
my $req = {
ver => $LJ::PROTOCOL_VER,
username => $auth->{poster} ? $auth->{poster}->user : undef,
usejournal => $auth->{journal} ? $auth->{journal}->user : undef,
tz => 'guess',
xpost => '0', # don't crosspost by default; we handle this ourselves later
%$form_req
};
my $err = 0;
my $res = LJ::Protocol::do_request( "postevent", $req, \$err, $flags );
return { errors => LJ::Protocol::error_message($err) } unless $res;
return $res;
}
# helper sub for printing success messages when posting or editing
sub _get_extradata {
my ( $form_req, $journal ) = @_;
my $extradata = {
security_ml => "",
filters => "",
};
# use the HTML cleaner on the entry subject if one exists
my $subject = $form_req->{subject};
LJ::CleanHTML::clean_subject( \$subject ) if $subject;
$extradata->{subject} = $subject;
my $c_or_p = $journal->is_community ? 'c' : 'p';
if ( $form_req->{security} eq "usemask" ) {
if ( $form_req->{allowmask} == 1 ) { # access list
$extradata->{security_ml} = "post.security.access.$c_or_p";
}
elsif ( $form_req->{allowmask} > 1 ) { # custom group
$extradata->{security_ml} = "post.security.custom";
$extradata->{filters} = $journal->security_group_display( $form_req->{allowmask} );
}
else { # custom security with no group - essentially private
$extradata->{security_ml} = "post.security.private.$c_or_p";
}
}
elsif ( $form_req->{security} eq "private" ) {
$extradata->{security_ml} = "post.security.private.$c_or_p";
}
else { #public
$extradata->{security_ml} = "post.security.public";
}
# Figure out whether we should offer to update their default formatting.
my $remote = LJ::get_remote();
if ( $remote
&& DW::Formats::is_active( $form_req->{props}->{editor} )
&& $form_req->{props}->{editor} ne $remote->entry_editor2 )
{
$extradata->{format} = $DW::Formats::formats{ $form_req->{props}->{editor} };
}
return $extradata;
}
sub _do_post {
my ( $form_req, $flags, $auth, %opts ) = @_;
my $res = _save_new_entry( $form_req, $flags, $auth );
return %$res if $res->{errors};
# post succeeded, time to do some housecleaning
_persist_props( $auth->{poster}, $form_req, 0 );
# Clear out a draft
if ( $auth->{poster} ) {
$auth->{poster}->set_prop( 'entry_draft', '' );
$auth->{poster}->set_prop( 'draft_properties', '' );
}
my $render_ret;
my @links;
# we may have warnings generated by previous parts of the process
my $warnings = $opts{warnings} || DW::FormErrors->new;
# special-case moderated: no itemid, but have a message
if ( !defined $res->{itemid} && $res->{message} ) {
$render_ret = DW::Template->render_template(
'entry/success.tt',
{
moderated_message => $res->{message},
}
);
}
else {
# e.g., bad HTML in the entry
$warnings->add_string( undef, LJ::auto_linkify( LJ::ehtml( $res->{message} ) ) )
if $res->{message};
my $u = $auth->{poster};
my $journal = $auth->{journal};
# we updated successfully! Now tell the user
my $poststatus = {
status => 'posted',
ml_string => $journal->is_community ? ".new.community" : ".new.journal",
url => $journal->journal_base . "/",
};
# bunch of helpful links
my $juser = $journal->user;
my $ditemid = $res->{itemid} * 256 + $res->{anum};
my $itemlink = $res->{url};
my $edititemlink = "$LJ::SITEROOT/entry/$juser/$ditemid/edit";
my @links = (
{
url => $itemlink,
ml_string => ".links.viewentry"
},
{
url => $edititemlink,
ml_string => ".links.editentry"
},
{
url => "$LJ::SITEROOT/edittags?journal=$juser&itemid=$ditemid",
ml_string => ".links.tags"
},
);
push @links,
{
url => $journal->journal_base . "?poster=" . $auth->{poster}->user,
ml_string => ".links.myentries"
}
if $journal->is_community;
push @links,
(
{
url => "$LJ::SITEROOT/tools/memadd?journal=$juser&itemid=$ditemid",
ml_string => ".links.memories"
},
{
url => "$LJ::SITEROOT/editjournal",
ml_string => '.links.manageentries',
},
{
url => "$LJ::SITEROOT/logout",
ml_string => '.links.logout',
}
);
# crosspost!
my @crossposts = _queue_crosspost(
$form_req,
remote => $u,
journal => $journal,
deleted => 0,
editurl => $edititemlink,
ditemid => $ditemid,
);
# set sticky
if ( $form_req->{sticky_entry} && $u->can_manage($journal) ) {
my $added_sticky = $journal->sticky_entry_new($ditemid);
$warnings->add( '', '.sticky.max', { limit => $u->count_max_stickies } )
unless $added_sticky;
}
$render_ret = DW::Template->render_template(
'entry/success.tt',
{
poststatus => $poststatus, # did the update succeed or fail?
warnings => $warnings, # warnings about the entry or your account
crossposts => \@crossposts, # crosspost status list
links => \@links,
links_header => ".links",
entry_url => $itemlink,
extradata => _get_extradata( $form_req, $journal ),
}
);
}
return ( status => "ok", render => $render_ret );
}
sub _save_editted_entry {
my ( $ditemid, $form_req, $auth ) = @_;
my $req = {
ver => $LJ::PROTOCOL_VER,
username => $auth->{poster} ? $auth->{poster}->user : undef,
usejournal => $auth->{journal} ? $auth->{journal}->user : undef,
xpost => '0', # don't crosspost by default; we handle this ourselves later
itemid => $ditemid >> 8,
%$form_req
};
my $err = 0;
my $res = LJ::Protocol::do_request(
"editevent",
$req,
\$err,
{
noauth => 1,
u => $auth->{poster},
}
);
return { errors => LJ::Protocol::error_message($err) } unless $res;
return $res;
}
sub _do_edit {
my ( $ditemid, $form_req, $auth, %opts ) = @_;
my $res = _save_editted_entry( $ditemid, $form_req, $auth );
return %$res if $res->{errors};
my $remote = $auth->{poster};
my $journal = $auth->{journal};
my $deleted = $form_req->{event} ? 0 : 1;
# post succeeded, time to do some housecleaning
_persist_props( $remote, $form_req, 1 );
my $poststatus_ml;
my $render_ret;
my @links;
# we may have warnings generated by previous parts of the process
my $warnings = $opts{warnings} || DW::FormErrors->new;
# e.g., bad HTML in the entry
$warnings->add_string( undef,
LJ::auto_linkify( LJ::html_newlines( LJ::ehtml( $res->{message} ) ) ) )
if $res->{message};
# bunch of helpful links:
my $juser = $journal->user;
my $comm_modifier = $journal->is_community ? '.comm' : '';
my $entry_url = $res->{url};
my $edit_url = "$LJ::SITEROOT/entry/$juser/$ditemid/edit";
my $is_sticky_entry = $journal->sticky_entries_lookup->{$ditemid};
if ( $remote->can_manage($journal) ) {
if ( $form_req->{sticky_entry} ) {
$journal->sticky_entry_new($ditemid)
unless $is_sticky_entry;
}
elsif ( $form_req->{sticky_select} ) {
$journal->sticky_entry_remove($ditemid)
if $is_sticky_entry;
}
}
if ($deleted) {
$poststatus_ml = ".edit.delete2$comm_modifier";
$journal->sticky_entry_remove($ditemid)
if $is_sticky_entry && $remote->can_manage($journal);
push @links,
{
url => $journal->journal_base . "?poster=" . $auth->{poster}->user,
ml_string => ".links.myentries"
}
if $journal->is_community;
push @links,
{
url => "$LJ::SITEROOT/editjournal",
ml_string => '.links.manageentries',
},
{
url => "$LJ::SITEROOT/logout",
ml_string => '.links.logout',
};
}
else {
$poststatus_ml = ".edit.edited2$comm_modifier";
push @links,
(
{
url => $entry_url,
ml_string => ".links.viewentry",
},
{
url => $edit_url,
ml_string => ".links.editentry",
},
{
url => "$LJ::SITEROOT/edittags?journal=$juser&itemid=$ditemid",
ml_string => ".links.tags"
},
);
push @links,
{
url => $journal->journal_base . "?poster=" . $auth->{poster}->user,
ml_string => ".links.myentries"
}
if $journal->is_community;
push @links,
(
{
url => "$LJ::SITEROOT/tools/memadd?journal=$juser&itemid=$ditemid",
ml_string => ".links.memories"
},
{
url => "$LJ::SITEROOT/editjournal",
ml_string => '.links.manageentries',
},
{
url => "$LJ::SITEROOT/logout",
ml_string => '.links.logout',
}
);
}
my @crossposts = _queue_crosspost(
$form_req,
remote => $remote,
journal => $journal,
deleted => $deleted,
ditemid => $ditemid,
editurl => $edit_url,
);
my $poststatus = {
status => $deleted ? 'deleted' : 'edited',
ml_string => $poststatus_ml,
url => $journal->journal_base . "/",
};
$render_ret = DW::Template->render_template(
'entry/success.tt',
{
poststatus => $poststatus, # did the update succeed or fail?
warnings => $warnings, # warnings about the entry or your account
crossposts => \@crossposts, # crosspost status list
links => \@links,
links_header => '.links',
entry_url => $entry_url,
extradata => _get_extradata( $form_req, $journal ),
}
);
return ( status => "ok", render => $render_ret );
}
# remember value of properties, to use the next time the user makes a post
sub _persist_props {
my ( $u, $form, $is_edit ) = @_;
return unless $u;
$u->displaydate_check( $form->{update_displaydate} ? 1 : 0 ) unless $is_edit;
}
sub _prepopulate {
my $get = $_[0];
my $subject = $get->{subject};
my $event = $get->{event};
my $tags = $get->{tags};
# if a share url was passed in, fill in the fields with the appropriate text
if ( $get->{share} ) {
eval "use DW::External::Page; 1;";
if ( !$@ && ( my $page = DW::External::Page->new( url => $get->{share} ) ) ) {
$subject = LJ::ehtml( $page->title );
$event =
'<a href="'
. $page->url . '">'
. ( LJ::ehtml( $page->description ) || $subject || $page->url )
. "</a>\n\n";
}
}
return {
subject => $subject,
event => $event,
taglist => $tags,
};
}
=head2 C<< DW::Controller::Entry::preview_handler( ) >>
Shows a preview of this entry
=cut
sub preview_handler {
my $r = DW::Request->get;
my $remote = LJ::get_remote();
# Can't count on template to handle resource group, since we might go
# through S2 instead.
LJ::set_active_resource_group('foundation');
my $post = $r->post_args;
my $styleid;
my $siteskinned = 1;
my $username = $remote ? $remote->username : $post->{username};
my $usejournal = $post->{usejournal};
# figure out poster/journal
my ( $u, $up );
if ($usejournal) {
$u = LJ::load_user($usejournal);
$up = $username ? LJ::load_user($username) : $remote;
}
elsif ( !$remote && $username ) {
$u = LJ::load_user($username);
}
else {
$u = $remote;
}
$up ||= $u;
# set up preview variables
my ( $ditemid, $anum, $itemid );
my $form_req = {};
_form_to_backend( $form_req, $post );
# check for spam domains
LJ::Hooks::run_hooks( 'spam_check', $up, $form_req, 'entry' );
my ( $event, $subject ) = ( $form_req->{event}, $form_req->{subject} );
LJ::CleanHTML::clean_subject( \$subject );
# preview poll
if ( LJ::Poll->contains_new_poll( \$event ) ) {
my $error;
my @polls = LJ::Poll->new_from_html(
\$event,
\$error,
{
'journalid' => $u->userid,
'posterid' => $up->userid,
}
);
my $can_create_poll = $up->can_create_polls || ( $u->is_community && $u->can_create_polls );
my $poll_preview = sub {
my $poll = shift @polls;
return '' unless $poll;
return $can_create_poll
? $poll->preview
: qq{<div class="highlight-box">}
. LJ::Lang::ml('/poll/create.bml.error.accttype2')
. qq{</div>};
};
$event =~ s/<poll-placeholder>/$poll_preview->()/eg;
}
# expand existing polls (for editing, or when transferring polls to another entry)
LJ::Poll->expand_entry( \$event );
# parse out embed tags from the RTE
$event = LJ::EmbedModule->transform_rte_post($event);
# do first expand_embedded pass with the preview flag to extract
# embedded content before cleaning and replace with tags
# the cleaner won't eat
LJ::EmbedModule->parse_module_embed( $u, \$event, preview => 1 );
my $editor = $form_req->{props}->{editor};
# clean content normally
LJ::CleanHTML::clean_event(
\$event,
{
preformatted => $form_req->{props}->{opt_preformatted},
editor => $editor,
}
);
# expand the embedded content for real
LJ::EmbedModule->expand_entry( $u, \$event, preview => 1 );
my $ctx;
if ( $u && $up ) {
$r->note( "_journal" => $u->{user} );
$r->note( "journalid" => $u->{userid} );
# load necessary props
$u->preload_props(qw( s2_style journaltitle journalsubtitle ));
# determine style system to preview with
$ctx = LJ::S2::s2_context( $u->{s2_style} );
my $view_entry_disabled = !LJ::S2::use_journalstyle_entry_page( $u, $ctx );
if ($view_entry_disabled) {
# force site-skinned
( $siteskinned, $styleid ) = ( 1, 0 );
}
else {
( $siteskinned, $styleid ) = ( 0, $u->{s2_style} );
}
}
else {
( $siteskinned, $styleid ) = ( 1, 0 );
}
# Include helper CSS/JS for highest fidelity previews
LJ::Talk::init_s2journal_js( noqr => 1, siteskin => $siteskinned );
if ($siteskinned) {
my $vars = {
event => $event,
subject => $subject,
journal => $u,
poster => $up,
};
my $pic = LJ::Userpic->new_from_keyword( $up, $form_req->{props}->{picture_keyword} );
$vars->{icon} = $pic ? $pic->imgtag : undef;
my $date = "$form_req->{year}-$form_req->{mon}-$form_req->{day}";
my $etime = $u ? LJ::date_to_view_links( $u, $date ) : $date;
my $hour = sprintf( "%02d", $form_req->{hour} );
my $min = sprintf( "%02d", $form_req->{min} );
$vars->{displaydate} = "$etime $hour:$min:00";
my %current = LJ::currents( $form_req->{props}, $up );
if ($u) {
$current{Groups} = $u->security_group_display( $form_req->{allowmask} );
delete $current{Groups} unless $current{Groups};
}
my @taglist = ();
LJ::Tags::is_valid_tagstring( $form_req->{props}->{taglist}, \@taglist );
if (@taglist) {
my $base = $u ? $u->journal_base : "";
$current{Tags} = join( ', ',
map { "<a href='$base/tag/" . LJ::eurl($_) . "'>" . LJ::ehtml($_) . "</a>" }
@taglist );
}
$vars->{currents} = LJ::currents_div(%current);
my $security = "";
if ( $form_req->{security} eq "private" ) {
$security = $LJ::Img::img{"security-private"};
}
elsif ( $form_req->{security} eq "usemask" ) {
$security =
$form_req->{allowmask} > 1
? $LJ::Img::img{"security-groups"}
: $LJ::Img::img{"security-protected"};
}
$vars->{security} = $security;
return DW::Template->render_template( 'entry/preview.tt', $vars );
}
else {
my $ret = "";
my $opts = {};
LJ::need_res( { priority => $LJ::LIB_RES_PRIORITY, group => 'foundation' },
"stc/css/foundation/foundation_minimal.css" );
$LJ::S2::ret_ref = \$ret;
$opts->{r} = $r;
$u->{_s2styleid} = ( $styleid || 0 ) + 0;
$u->{_journalbase} = $u->journal_base;
$LJ::S2::CURR_CTX = $ctx;
my $p = LJ::S2::Page( $u, $opts );
$p->{_type} = "EntryPreviewPage";
$p->{view} = "entry";
# Mock up entry from form data
my $userlite_journal = LJ::S2::UserLite($u);
my $userlite_poster = LJ::S2::UserLite($up);
my $userpic = LJ::S2::Image_userpic( $up, 0, $form_req->{props}->{picture_keyword} );
my $comments = LJ::S2::CommentInfo(
{
read_url => "#",
post_url => "#",
permalink_url => "#",
count => "0",
maxcomments => 0,
enabled =>
( $u->{opt_showtalklinks} eq "Y" && !$form_req->{props}->{opt_nocomments} )
? 1
: 0,
screened => 0,
}
);
# build tag objects, faking kwid as '-1'
# * invalid tags will be stripped by is_valid_tagstring()
my @taglist = ();
LJ::Tags::is_valid_tagstring( $form_req->{props}->{taglist}, \@taglist );
@taglist = map { LJ::S2::Tag( $u, -1, $_ ) } @taglist;
# custom friends groups
my $group_names = $u ? $u->security_group_display( $form_req->{allowmask} ) : undef;
# format it
my $raw_subj = $form_req->{subject};
my $s2entry = LJ::S2::Entry(
$u,
{
subject => $subject,
text => $event,
dateparts =>
"$form_req->{year} $form_req->{mon} $form_req->{day} $form_req->{hour} $form_req->{min} 00 ",
security => $form_req->{security},
allowmask => $form_req->{allowmask},
props => $form_req->{props},
itemid => -1,
comments => $comments,
journal => $userlite_journal,
poster => $userlite_poster,
new_day => 0,
end_day => 0,
tags => \@taglist,
userpic => $userpic,
permalink_url => "#",
adult_content_level => $form_req->{props}->{adult_content},
group_names => $group_names,
}
);
my $copts;
$copts->{out_pages} = $copts->{out_page} = 1;
$copts->{out_items} = 0;
$copts->{out_itemfirst} = $copts->{out_itemlast} = undef;
$p->{comment_pages} = LJ::S2::ItemRange(
{
all_subitems_displayed => ( $copts->{out_pages} == 1 ),
current => $copts->{out_page},
from_subitem => $copts->{out_itemfirst},
num_subitems_displayed => 0,
to_subitem => $copts->{out_itemlast},
total => $copts->{out_pages},
total_subitems => $copts->{out_items},
_url_of => sub { return "#"; },
}
);
$p->{entry} = $s2entry;
$p->{comments} = [];
$p->{preview_warn_text} = LJ::Lang::ml('/entry/preview.tt.entry.preview_warn_text');
$p->{viewing_thread} = 0;
$p->{multiform_on} = 0;
# page display settings
if ( $u->should_block_robots ) {
$p->{head_content} .= LJ::robot_meta_tags();
}
my $charset = $opts->{saycharset} // '';
$p->{head_content} .=
'<meta http-equiv="Content-Type" content="text/html; charset=' . $charset . "\" />\n";
# Include required CSS and really fundamental JS like Site object (most
# other JS gets loaded at end of page by s2_run)
$p->{head_content} .= LJ::res_includes_head();
# Don't show the navigation strip or invisible content
$p->{head_content} .= qq{
<style type="text/css">
html body {
padding-top: 0 !important;
}
#lj_controlstrip {
display: none !important;
}
.invisible {
position: absolute;
left: -10000px;
top: auto;
}
.highlight-box {
border: 1px solid #c1272c;
background-color: #ffd8d8;
color: #000;
}
</style>
};
LJ::S2::s2_run( $r, $ctx, $opts, "EntryPage::print()", $p );
$r->print($ret);
return $r->OK;
}
}
=head2 C<< DW::Controller::Entry::options_handler( ) >>
Show the entry options page in a separate page
=cut
sub options_handler {
my ( $ok, $rv ) = controller();
return $rv unless $ok;
return DW::Template->render_template( 'entry/options.tt', _options( $rv->{remote} ) );
}
=head2 C<< DW::Controller::Entry::options_rpc_handler( ) >>
Show the entry options page in a form suitable for loading via JS
=cut
sub options_rpc_handler {
my ( $ok, $rv ) = controller();
return $rv unless $ok;
my $vars = _options( $rv->{remote} );
$vars->{use_js} = 1;
my $r = DW::Request->get;
$r->status( $vars->{errors} && $vars->{errors}->exist ? HTTP_BAD_REQUEST : HTTP_OK );
return DW::Template->render_template( 'entry/options.tt', $vars, { fragment => 1 } );
}
=head2 C<< DW::Controller::Entry::collapse_rpc_handler( ) >>
Load or save entry form module header settings
=cut
sub collapse_rpc_handler {
my ( $ok, $rv ) = controller();
return $rv unless $ok;
my $u = $rv->{remote};
my $r = DW::Request->get;
my $args = $r->get_args;
my $module = $args->{id} || "";
my $expand = $args->{expand} && $args->{expand} eq "true" ? 1 : 0;
my $show = sub {
$r->print( to_json( $u->entryform_panels_collapsed ) );
return $r->OK;
};
if ($module) {
my $is_collapsed = $u->entryform_panels_collapsed;
# no further action needed
return $show->() if $is_collapsed->{$module} && !$expand;
return $show->() if !$is_collapsed->{$module} && $expand;
if ($expand) {
delete $is_collapsed->{$module};
}
else {
$is_collapsed->{$module} = 1;
}
$u->entryform_panels_collapsed($is_collapsed);
return $show->();
}
else {
# just view
return $show->();
}
}
sub _load_visible_panels {
my $u = $_[0];
my $user_panels = $u->entryform_panels;
my @panels;
foreach my $panel_group ( @{ $user_panels->{order} } ) {
foreach my $panel (@$panel_group) {
push @panels, $panel if $user_panels->{show}->{$panel};
}
}
return \@panels;
}
sub _options {
my $u = $_[0];
my $panel_element_name = "visible_panels";
my @panel_options = map +{
label_ml => "/entry/module-$_.tt.header",
panel_name => $_,
id => "panel_$_",
name => $panel_element_name,
}, @modules;
my $vars = { panels => \@panel_options };
my $r = DW::Request->get;
my $errors = DW::FormErrors->new;
if ( $r->did_post ) {
my $post = $r->post_args;
$vars->{formdata} = $post;
if ( LJ::check_form_auth( $post->{lj_form_auth} ) ) {
if ( $post->{reset_panels} ) {
$vars->{formdata}->remove("reset_panels");
$u->set_prop( "entryform_panels" => undef );
$vars->{formdata}
->set( $panel_element_name => @{ _load_visible_panels($u) || [] } );
}
else {
$u->set_prop( entryform_width => $post->{entry_field_width} );
my %panels;
my %post_panels = map { $_ => 1 } $post->get_all($panel_element_name);
foreach my $panel (@panel_options) {
my $name = $panel->{panel_name};
$panels{$name} = $post_panels{$name} ? 1 : 0;
}
$u->entryform_panels_visibility( \%panels );
my @columns;
my $didpost_order = 0;
foreach my $column_index ( 0 ... 2 ) {
my @col;
foreach ( $post->get_all("column_$column_index") ) {
my ( $order, $panel ) = m/(\d+):(.+)/;
$col[$order] = $panel;
$didpost_order = 1;
}
# remove any in-betweens in case we managed to skip a number in the order somehow
$columns[$column_index] = [ grep { $_ } @col ];
}
$u->entryform_panels_order( \@columns ) if $didpost_order;
}
$u->set_prop( js_animations_minimal => $post->{minimal_animations} );
}
else {
$errors->add( undef, "error.invalidform" );
}
$vars->{errors} = $errors;
}
else {
my $default = {
entry_field_width => $u->entryform_width,
minimal_animations => $u->prop("js_animations_minimal") ? 1 : 0,
};
$default->{$panel_element_name} = _load_visible_panels($u);
$vars->{formdata} = $default;
}
return $vars;
}
sub draft_rpc_handler {
my ( $ok, $rv ) = controller();
return $rv unless $ok;
my $u = $rv->{remote};
my $r = DW::Request->get;
my $GET = $r->get_args;
my $POST = $r->post_args;
my $err = sub {
my $msg = shift;
return to_json(
{
'alert' => $msg,
}
);
};
my $ret = {};
# This property thaws the contents of the userprop 'draft_properties' and
# sends them back as a JS object.
if ( defined $GET->{getProperties} ) {
my $ret =
$u->prop('draft_properties') ? Storable::thaw( $u->prop('draft_properties') ) : {};
$r->print( to_json($ret) );
return $r->OK;
}
# This property clears out all the fields of the user's draft, except the
# draft body itself.
if ( defined $POST->{clearProperties} ) {
$u->clear_prop('draft_properties');
}
# If even one property of the draft was changed, this property saves them
# all into a new draft (in order to avoid multiple HTTP posts which would
# decrease performance considerably).
# This is set up as a long if statement to avoid tying draft property saving to
# the draft body save logic, so that users won't have to change their
# draft body every time they want to get their properties saved.
if ( ( defined $POST->{saveSubject} )
|| ( defined $POST->{saveEditor} )
|| ( defined $POST->{saveUserpic} )
|| ( defined $POST->{saveTaglist} )
|| ( defined $POST->{saveMoodID} )
|| ( defined $POST->{saveMood} )
|| ( defined $POST->{saveLocation} )
|| ( defined $POST->{saveMusic} )
|| ( defined $POST->{saveAdultReason} )
|| ( defined $POST->{saveCommentSet} )
|| ( defined $POST->{saveCommentScr} )
|| ( defined $POST->{saveAdultCnt} ) )
{
my %properties = (
subject => $POST->{saveSubject},
editor => $POST->{saveEditor},
userpic => $POST->{saveUserpic},
taglist => $POST->{saveTaglist},
moodid => $POST->{saveMoodID},
mood => $POST->{saveMood},
location1 => $POST->{saveLocation},
music => $POST->{saveMusic},
adultreason => $POST->{saveAdultReason},
commentset => $POST->{saveCommentSet},
commentscr => $POST->{saveCommentScr},
adultcnt => $POST->{saveAdultCnt}
);
# If the property is null, a default menu selection or a JS undefined
# value, we don't want to save it.
foreach my $key ( keys(%properties) ) {
if ( !defined $properties{$key}
|| ( $properties{$key} =~ /^$/ )
|| ( $properties{$key} =~ /^0$/ )
|| ( $properties{$key} =~ /^undefined$/ ) )
{
delete $properties{$key};
}
}
# Freeze the hash into a frozen storable string. If the hash is not empty
# save it to the userprop. If it is, delete it.
my $frozen_properties = Storable::nfreeze( \%properties );
if ( $frozen_properties =~ /\w/ ) {
$u->set_prop( 'draft_properties', $frozen_properties );
}
else {
$u->clear_prop('draft_properties');
}
}
# This property saves the main body of the draft.
if ( defined $POST->{'saveDraft'} ) {
$u->set_draft_text( $POST->{'saveDraft'} );
# This property clears out the main body of the draft.
}
elsif ( $POST->{'clearDraft'} ) {
$u->set_draft_text('');
}
else {
$ret->{draft} = $u->draft_text;
}
$r->print( to_json($ret) );
return $r->OK;
}
1;