#!/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;
