#!/usr/bin/perl # # DW::Controller::Entry # # This controller is for creating and managing entries # # Authors: # Afuna # # 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 = '' . ( LJ::ehtml( $page->description ) || $subject || $page->url ) . "\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{
} . LJ::Lang::ml('/poll/create.bml.error.accttype2') . qq{
}; }; $event =~ s//$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 { "" . LJ::ehtml($_) . "" } @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} .= '\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{ }; 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;